Files
buzz/desktop/tests/e2e/doctor-states.spec.ts
cls 9dfa06ffee
Docker image / Build (linux/amd64) (push) Has been cancelled
Docker image / Build (linux/arm64) (push) Has been cancelled
Docker image / Merge release multi-arch manifest (push) Has been cancelled
Docker image / Merge debug multi-arch manifest (push) Has been cancelled
Docker image / Build public push gateway (linux/amd64) (push) Has been cancelled
Docker image / Build public push gateway (linux/arm64) (push) Has been cancelled
Docker image / Publish public push gateway image (push) Has been cancelled
Sprig image / Build (linux/amd64) (push) Has been cancelled
Sprig image / Build (linux/arm64) (push) Has been cancelled
Sprig image / Merge multi-arch manifest (push) Has been cancelled
Harbor Buzz Orchestra / Python tests and lint (push) Has been cancelled
CI / Detect Changed Paths (push) Has been cancelled
CI / Rust Lint (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Desktop Core (push) Has been cancelled
CI / Desktop Smoke E2E (1) (push) Has been cancelled
CI / Desktop Smoke E2E (2) (push) Has been cancelled
CI / Desktop Smoke E2E (3) (push) Has been cancelled
CI / Desktop Smoke E2E (4) (push) Has been cancelled
CI / Desktop (push) Has been cancelled
CI / Desktop E2E Relay (push) Has been cancelled
CI / Desktop E2E Integration (1/2) (push) Has been cancelled
CI / Desktop E2E Integration (2/2) (push) Has been cancelled
CI / Desktop E2E Integration (push) Has been cancelled
CI / Backend Integration (relay e2e) (push) Has been cancelled
CI / Relay E2E (push) Has been cancelled
CI / Web (push) Has been cancelled
CI / Mobile (push) Has been cancelled
CI / Security (push) Has been cancelled
CI / Dead Token Reference Guard (push) Has been cancelled
CI / Server Cross-Compile (aarch64-unknown-linux-musl) (push) Has been cancelled
CI / Server Cross-Compile (x86_64-unknown-linux-musl) (push) Has been cancelled
CI / Windows Rust (x86_64-pc-windows-msvc) (push) Has been cancelled
CI / Desktop Build (macOS) (push) Has been cancelled
helm chart / lint + unittest + render matrix (push) Has been cancelled
helm chart / install on kind (gated) (push) Has been cancelled
helm chart / publish chart to GHCR (push) Has been cancelled
Mesh Lifecycle / Relay-Driven Mesh Lifecycle Smoke (push) Has been cancelled
Sprig / Build (aarch64-unknown-linux-musl) (push) Has been cancelled
Sprig / Build (x86_64-unknown-linux-musl) (push) Has been cancelled
Sprig / Publish rolling release (push) Has been cancelled
Sprig / Publish tagged release (push) Has been cancelled
feat: import Chinese-localized Buzz source snapshot
Signed-off-by: cls_宁波本机 <908705107@qq.com>
2026-08-13 18:34:25 +08:00

1074 lines
37 KiB
TypeScript

import { expect, test } from "@playwright/test";
import { installMockBridge } from "../helpers/bridge";
import { waitForAnimations } from "../helpers/animations";
import { openSettings } from "../helpers/settings";
const SHOTS = "test-results/screenshots-doctor";
// ── Shared catalog fixture data ───────────────────────────────────────────────
/**
* A goose runtime that is available and needs no auth step — used as a neutral
* backdrop so the Doctor panel has realistic content beyond the row under test.
*/
const GOOSE_AVAILABLE = {
id: "goose",
label: "Goose",
avatar_url: "",
availability: "available",
command: "goose",
binary_path: "/usr/local/bin/goose",
default_args: ["acp"],
mcp_command: null,
install_hint: "",
install_instructions_url:
"https://goose-docs.ai/docs/getting-started/installation/",
can_auto_install: false,
underlying_cli_path: null,
node_required: false,
auth_status: { status: "not_applicable" },
};
/** buzz-agent is always available and has no auth step. */
const BUZZ_AGENT_AVAILABLE = {
id: "buzz-agent",
label: "Buzz Agent",
avatar_url: "",
availability: "available",
command: "buzz-agent",
binary_path: "/usr/local/bin/buzz-agent",
default_args: [],
mcp_command: "buzz-dev-mcp",
install_hint: "",
install_instructions_url: "https://github.com/block/buzz",
can_auto_install: false,
underlying_cli_path: null,
node_required: false,
auth_status: { status: "not_applicable" },
};
/**
* Claude available and logged in — used as a neutral entry when claude is not
* the runtime under test, and as the base for the auth states being tested.
*/
const CLAUDE_AVAILABLE_LOGGED_IN = {
id: "claude",
label: "Claude Code",
avatar_url: "",
availability: "available",
command: "claude-agent-acp",
binary_path: "/usr/local/bin/claude-agent-acp",
default_args: [],
mcp_command: null,
install_hint: "",
install_instructions_url:
"https://github.com/agentclientprotocol/claude-agent-acp",
can_auto_install: true,
underlying_cli_path: "/usr/local/bin/claude",
node_required: false,
auth_status: { status: "logged_in" },
};
/**
* Codex not-installed base — tweak `availability`, `auth_status`, and
* `node_required` in each test as needed.
*/
const CODEX_NOT_INSTALLED = {
id: "codex",
label: "Codex",
avatar_url: "",
availability: "not_installed",
command: null,
binary_path: null,
default_args: [],
mcp_command: null,
install_hint: "Buzz talks to Codex through the Codex CLI.",
install_instructions_url: "https://developers.openai.com/codex/cli/",
can_auto_install: true,
underlying_cli_path: null,
node_required: false,
auth_status: { status: "unknown" },
};
// ── Tests ─────────────────────────────────────────────────────────────────────
test.describe("Doctor panel state screenshots", () => {
test.use({ viewport: { width: 1280, height: 720 } });
test.beforeEach(async ({ page }) => {
page.on("pageerror", (err) => {
console.error(
"PAGE ERROR:",
err.message,
err.stack?.split("\n").slice(0, 3).join("\n"),
);
});
page.on("console", (msg) => {
if (msg.type() === "error") {
console.error("CONSOLE ERROR:", msg.text().slice(0, 300));
}
});
});
/**
* 00 — the runtime catalog reads as a set of individual status cards rather
* than one continuous table.
*/
test("00-runtime-card-layout", async ({ page }) => {
await installMockBridge(page, {
acpRuntimesCatalog: [
GOOSE_AVAILABLE,
CLAUDE_AVAILABLE_LOGGED_IN,
CODEX_NOT_INSTALLED,
BUZZ_AGENT_AVAILABLE,
],
});
await page.goto("/", { waitUntil: "domcontentloaded" });
await openSettings(page, "agents");
const runtimeList = page.getByTestId("doctor-runtime-list");
await expect(runtimeList).toBeVisible({ timeout: 10_000 });
await expect(page.getByTestId("doctor-runtime-goose")).toBeVisible();
await expect(page.getByTestId("doctor-runtime-codex")).toBeVisible();
await expect(
runtimeList.locator(":scope > [data-testid^='doctor-runtime-']"),
).toHaveCount(4);
expect(
await runtimeList
.locator(":scope > [data-testid^='doctor-runtime-']")
.evaluateAll((rows) =>
rows.map((row) => row.getAttribute("data-testid")),
),
).toEqual([
"doctor-runtime-buzz-agent",
"doctor-runtime-goose",
"doctor-runtime-claude",
"doctor-runtime-codex",
]);
for (const runtimeId of ["goose", "claude", "codex", "buzz-agent"]) {
await expect(
page.getByTestId(`doctor-runtime-logo-${runtimeId}`),
).toBeVisible();
}
const rowHeights = await Promise.all(
["goose", "claude", "codex", "buzz-agent"].map((runtimeId) =>
page
.getByTestId(`doctor-runtime-${runtimeId}`)
.evaluate((element) =>
Math.round(element.getBoundingClientRect().height),
),
),
);
expect(rowHeights[2]).toBeGreaterThan(rowHeights[0]);
const [gooseColors, codexColors] = await Promise.all(
["goose", "codex"].map((runtimeId) =>
page.getByTestId(`doctor-runtime-${runtimeId}`).evaluate((element) => {
const styles = getComputedStyle(element);
return {
backgroundColor: styles.backgroundColor,
borderColor: styles.borderColor,
};
}),
),
);
expect(codexColors).toEqual(gooseColors);
await expect(
page
.getByRole("heading", { name: "Agent runtimes", exact: true })
.locator("..")
.locator(".."),
).toHaveCSS("align-items", "center");
for (const runtimeId of ["goose", "claude", "buzz-agent"]) {
await expect(
page.getByTestId(`doctor-runtime-menu-${runtimeId}`),
).toHaveCount(0);
}
const codexInstallButton = page.getByTestId("doctor-runtime-install-codex");
await expect(codexInstallButton).toBeEnabled();
await expect(codexInstallButton).toHaveText("Install");
await expect(
page.getByRole("menuitem", { name: "CLI setup guide" }),
).toHaveCount(0);
await page.getByTestId("doctor-runtime-menu-codex").click();
await expect(
page.getByRole("menuitem", { name: "CLI setup guide" }),
).toBeVisible();
await waitForAnimations(page);
await page.screenshot({
path: `${SHOTS}/00-runtime-overflow-menu.png`,
});
await page.keyboard.press("Escape");
await expect(page.getByTestId("doctor-runtime-ready-goose")).toHaveText(
"Ready",
);
await expect(page.getByTestId("doctor-runtime-install-goose")).toHaveCount(
0,
);
await expect(page.getByTestId("doctor-runtime-codex")).not.toContainText(
"Not installed",
);
await expect(page.getByTestId("doctor-runtime-status-codex")).toHaveText(
"CLI needed",
);
await expect(
page.getByTestId("doctor-runtime-guidance-codex"),
).toContainText("Buzz talks to Codex through the Codex CLI.");
await expect(
page
.getByTestId("doctor-runtime-guidance-codex")
.getByRole("button", { name: "CLI setup guide" }),
).toBeVisible();
await runtimeList.scrollIntoViewIfNeeded();
await waitForAnimations(page);
await runtimeList.screenshot({
path: `${SHOTS}/00-runtime-card-layout.png`,
});
});
/** 01 — a ready runtime stays compact without redundant status copy. */
test("01-auth-logged-in", async ({ page }) => {
await installMockBridge(page, {
acpRuntimesCatalog: [
GOOSE_AVAILABLE,
CLAUDE_AVAILABLE_LOGGED_IN,
CODEX_NOT_INSTALLED,
BUZZ_AGENT_AVAILABLE,
],
});
await page.goto("/", { waitUntil: "domcontentloaded" });
await openSettings(page, "agents");
const row = page.getByTestId("doctor-runtime-claude");
await expect(row).toBeVisible({ timeout: 10_000 });
await expect(page.getByTestId("doctor-runtime-ready-claude")).toHaveText(
"Ready",
);
await expect(row).not.toContainText("Authenticated");
await expect(row).not.toContainText("Available");
await expect(row).not.toContainText("claude-agent-acp");
await expect(row).not.toContainText("/usr/local/bin");
await expect(page.getByTestId("doctor-runtime-menu-claude")).toHaveCount(0);
await row.scrollIntoViewIfNeeded();
await waitForAnimations(page);
await row.screenshot({ path: `${SHOTS}/01-auth-logged-in.png` });
});
/**
* 02 — an available runtime that needs authentication shows an explicit
* "Sign-in needed" chip on the row face (never a green Ready chip), stays
* the same height as the others, and keeps setup instructions in its
* overflow menu.
*/
test("02-auth-logged-out", async ({ page }) => {
await installMockBridge(page, {
acpRuntimesCatalog: [
GOOSE_AVAILABLE,
CLAUDE_AVAILABLE_LOGGED_IN,
{
...CODEX_NOT_INSTALLED,
availability: "available",
command: "codex-acp",
binary_path: "/usr/local/bin/codex-acp",
underlying_cli_path: "/usr/local/bin/codex",
auth_status: { status: "logged_out" },
login_hint: "Run `codex login` to authenticate.",
},
BUZZ_AGENT_AVAILABLE,
],
});
await page.goto("/", { waitUntil: "domcontentloaded" });
await openSettings(page, "agents");
const row = page.getByTestId("doctor-runtime-codex");
await expect(row).toBeVisible({ timeout: 10_000 });
// Auth-required is an explicit row-face state: amber "Sign-in needed"
// chip (single source: entryStatusLabel), no green Ready chip.
await expect(page.getByTestId("doctor-runtime-status-codex")).toHaveText(
"Sign-in needed",
);
await expect(page.getByTestId("doctor-runtime-ready-codex")).toHaveCount(0);
await expect(row).not.toContainText("Not authenticated");
await expect(row).not.toContainText("Run `codex login` to authenticate.");
await expect(row).toHaveCSS(
"height",
await page
.getByTestId("doctor-runtime-goose")
.evaluate((element) => getComputedStyle(element).height),
);
await page.getByTestId("doctor-runtime-menu-codex").click();
await expect(
page.getByRole("menuitem", { name: "CLI setup guide" }),
).toBeVisible();
await page.keyboard.press("Escape");
await row.scrollIntoViewIfNeeded();
await waitForAnimations(page);
await row.screenshot({ path: `${SHOTS}/02-auth-logged-out.png` });
});
/**
* 03 — a runtime with invalid configuration exposes its diagnostic and keeps
* setup instructions in overflow.
*/
test("03-auth-config-error", async ({ page }) => {
const diagnostic =
"error loading configuration: ~/.claude/settings.json: unknown key foo";
await installMockBridge(page, {
acpRuntimesCatalog: [
GOOSE_AVAILABLE,
{
...CLAUDE_AVAILABLE_LOGGED_IN,
auth_status: { status: "config_invalid", diagnostic },
login_hint: "Run the Claude CLI to complete authentication.",
},
CODEX_NOT_INSTALLED,
BUZZ_AGENT_AVAILABLE,
],
});
await page.goto("/", { waitUntil: "domcontentloaded" });
await openSettings(page, "agents");
const row = page.getByTestId("doctor-runtime-claude");
await expect(row).toBeVisible({ timeout: 10_000 });
await expect(page.getByTestId("doctor-runtime-status-claude")).toHaveText(
"Config error",
);
await expect(
page.getByTestId("doctor-runtime-config-error-claude"),
).toContainText(
"Config error: error loading configuration: ~/.claude/settings.json: unknown key foo",
);
await page.getByTestId("doctor-runtime-menu-claude").click();
await expect(
page.getByRole("menuitem", { name: "CLI setup guide" }),
).toBeVisible();
await page.keyboard.press("Escape");
await row.scrollIntoViewIfNeeded();
await waitForAnimations(page);
await row.screenshot({ path: `${SHOTS}/03-auth-config-error.png` });
});
/**
* 04 — adapter_missing runtime with node_required: true is NOT a
* one-click-ready row: it must be catalog-only (no Install button), and its
* catalog detail must offer the setup guide instead of a one-click Install
* that would fail without Node.
*/
test("04-node-required", async ({ page }) => {
await installMockBridge(page, {
acpRuntimesCatalog: [
GOOSE_AVAILABLE,
CLAUDE_AVAILABLE_LOGGED_IN,
{
...CODEX_NOT_INSTALLED,
availability: "adapter_missing",
underlying_cli_path: "/usr/local/bin/codex",
node_required: true,
install_hint: "Install the Codex ACP adapter via npm.",
install_instructions_url:
"https://github.com/agentclientprotocol/codex-acp",
},
BUZZ_AGENT_AVAILABLE,
],
});
await page.goto("/", { waitUntil: "domcontentloaded" });
await openSettings(page, "agents");
// Node-gated entries never get a Your-harnesses row (and thus never an
// Install button) — setup happens in the catalog.
await expect(page.getByTestId("doctor-runtime-goose")).toBeVisible({
timeout: 10_000,
});
await expect(page.getByTestId("doctor-runtime-codex")).toHaveCount(0);
await expect(page.getByTestId("doctor-runtime-install-codex")).toHaveCount(
0,
);
await expect(page.getByTestId("doctor-runtime-ready-codex")).toHaveCount(0);
await page.getByTestId("harness-add-button").click();
await expect(page.getByTestId("harness-catalog-dialog")).toBeVisible();
await page.getByTestId("harness-catalog-list-item-codex").click();
await expect(page.getByTestId("harness-catalog-status-codex")).toHaveText(
"Adapter needed",
);
// Node gate blocks one-click install; the primary action is the vendor
// setup guide instead — a single pinned bottom-bar CTA
// (harness-catalog-setup-*), not a separate docs button.
await expect(page.getByTestId("harness-catalog-install-codex")).toHaveCount(
0,
);
await expect(page.getByTestId("harness-catalog-setup-codex")).toBeVisible();
await expect(page.getByTestId("harness-catalog-setup-codex")).toHaveText(
/Setup guide/,
);
await expect(page.getByTestId("harness-catalog-docs-codex")).toHaveCount(0);
const detail = page.getByTestId("harness-catalog-detail-pane");
await expect(detail).toContainText("Install the Codex ACP adapter");
await waitForAnimations(page);
await detail.screenshot({ path: `${SHOTS}/04-node-required.png` });
});
/**
* 05 — a failed install brings the Install button back; clicking it again
* retries.
*
* The mock is configured with a two-call sequence:
* call 1 → failure (E404)
* call 2 → installer exit 0, but post-install discovery still cannot find
* the runtime
* This exercises the full retry path: fail state → Install again →
* verified failure without a false installed state.
*/
test("05-retry-after-failure", async ({ page }) => {
await installMockBridge(page, {
acpRuntimesCatalog: [
GOOSE_AVAILABLE,
CLAUDE_AVAILABLE_LOGGED_IN,
{
...CODEX_NOT_INSTALLED,
can_auto_install: true,
node_required: false,
},
BUZZ_AGENT_AVAILABLE,
],
installAcpRuntimeDelayMs: 250,
installAcpRuntimeResults: [
{
success: false,
steps: [
{
step: "adapter",
command: "npm install -g @zed-industries/codex-acp",
success: false,
stdout: "",
stderr: "npm ERR! code E404\nnpm ERR! 404 Not Found",
exit_code: 1,
},
],
},
{
success: false,
steps: [
{
step: "cli",
command: "powershell.exe install codex",
success: true,
stdout: "installed",
stderr: "",
exit_code: 0,
},
{
step: "verify",
command: "discover codex",
success: false,
stdout: "",
stderr:
"The installer finished, but Buzz still could not use codex (observed: NotInstalled).",
exit_code: null,
hint: "Buzz requires the vendor CLI executable, not only its desktop app. If the CLI was installed while Buzz was open, restart Buzz and check again.",
},
],
},
],
});
await page.goto("/", { waitUntil: "domcontentloaded" });
await openSettings(page, "agents");
const row = page.getByTestId("doctor-runtime-codex");
await expect(row).toBeVisible({ timeout: 10_000 });
await expect(row).not.toContainText("Not installed");
// Trigger the first install — the mock returns a failure.
const installButton = page.getByTestId("doctor-runtime-install-codex");
await expect(installButton).toBeEnabled();
await expect(installButton).toHaveText("Install");
await installButton.click();
const loading = page.getByTestId("doctor-runtime-loading-codex");
await expect(loading).toBeVisible();
await expect(loading).toContainText("Codex installing");
await expect(installButton).toHaveCount(0);
// After failure: the Install button returns and the error is visible.
await expect(loading).toHaveCount(0, { timeout: 5_000 });
await expect(installButton).toBeVisible({ timeout: 5_000 });
await expect(installButton).toBeEnabled();
await expect(row).toContainText("Step");
await expect(row).toContainText("failed");
await row.scrollIntoViewIfNeeded();
await waitForAnimations(page);
await row.screenshot({ path: `${SHOTS}/05-retry-after-failure.png` });
// Install again — the install command exits 0, but verification fails.
await installButton.click();
await expect(loading).toBeVisible();
await expect(installButton).toHaveCount(0);
// The runtime remains retryable and never renders a false success state.
await expect(loading).toHaveCount(0, { timeout: 5_000 });
await expect(row).toContainText("desktop app", { timeout: 5_000 });
await expect(row).toContainText('Step "verify" failed');
await expect(row.getByText(/installed\. Checking/)).toHaveCount(0);
await expect(installButton).toBeVisible();
await expect(installButton).toBeEnabled();
await row.scrollIntoViewIfNeeded();
await waitForAnimations(page);
await row.screenshot({ path: `${SHOTS}/05-verification-failure.png` });
});
test("05b-verified-install-enables-runtime", async ({ page }) => {
await installMockBridge(page, {
acpRuntimesCatalog: [
GOOSE_AVAILABLE,
CLAUDE_AVAILABLE_LOGGED_IN,
CODEX_NOT_INSTALLED,
BUZZ_AGENT_AVAILABLE,
],
acpRuntimesCatalogAfterInstall: [
GOOSE_AVAILABLE,
CLAUDE_AVAILABLE_LOGGED_IN,
{
...CODEX_NOT_INSTALLED,
availability: "available",
command: "codex-acp",
binary_path: "/usr/local/bin/codex-acp",
underlying_cli_path: "/usr/local/bin/codex",
auth_status: { status: "logged_in" },
},
BUZZ_AGENT_AVAILABLE,
],
installAcpRuntimeResult: {
success: true,
steps: [
{
step: "adapter",
command: "npm install -g @agentclientprotocol/codex-acp",
success: true,
stdout: "added 1 package",
stderr: "",
exit_code: 0,
},
],
},
});
await page.goto("/", { waitUntil: "domcontentloaded" });
await openSettings(page, "agents");
const installButton = page.getByTestId("doctor-runtime-install-codex");
await expect(installButton).toBeEnabled();
await installButton.click();
await expect(page.getByTestId("doctor-runtime-ready-codex")).toBeVisible({
timeout: 5_000,
});
await expect(installButton).toHaveCount(0);
await expect(page.getByTestId("doctor-runtime-guidance-codex")).toHaveCount(
0,
);
});
/**
* 06 — adapter-provided account methods appear in the overflow menu and
* launch the vendor-owned flow without expanding the runtime row. The
* row face flips "Sign-in needed" → Ready once the connect settles and
* discovery reports logged_in.
*/
test("06-connect-account-methods", async ({ page }) => {
await installMockBridge(page, {
acpRuntimesCatalog: [
GOOSE_AVAILABLE,
CLAUDE_AVAILABLE_LOGGED_IN,
{
...CODEX_NOT_INSTALLED,
availability: "available",
command: "codex-acp",
binary_path: "/usr/local/bin/codex-acp",
underlying_cli_path: "/usr/local/bin/codex",
auth_status: { status: "logged_out" },
login_hint: "Run `codex login` to authenticate.",
},
BUZZ_AGENT_AVAILABLE,
],
// After the mocked connect succeeds, discovery reports logged_in so
// the row face can flip from "Sign-in needed" to Ready.
acpRuntimesCatalogAfterConnect: [
GOOSE_AVAILABLE,
CLAUDE_AVAILABLE_LOGGED_IN,
{
...CODEX_NOT_INSTALLED,
availability: "available",
command: "codex-acp",
binary_path: "/usr/local/bin/codex-acp",
underlying_cli_path: "/usr/local/bin/codex",
auth_status: { status: "logged_in" },
},
BUZZ_AGENT_AVAILABLE,
],
connectAcpRuntimeDelayMs: 250,
acpAuthMethods: {
codex: {
methods: [
{
id: "chat-gpt",
name: "Sign in with ChatGPT",
description: "Use your Codex subscription in the browser.",
type: "browser",
},
],
},
},
});
await page.goto("/", { waitUntil: "domcontentloaded" });
await openSettings(page, "agents");
const row = page.getByTestId("doctor-runtime-codex");
await expect(row).toBeVisible({ timeout: 10_000 });
// Signed-out row face: explicit auth chip, no green Ready chip.
await expect(page.getByTestId("doctor-runtime-status-codex")).toHaveText(
"Sign-in needed",
);
await expect(page.getByTestId("doctor-runtime-ready-codex")).toHaveCount(0);
await expect(row).not.toContainText("Not authenticated");
await expect(row).toHaveCSS(
"height",
await page
.getByTestId("doctor-runtime-goose")
.evaluate((element) => getComputedStyle(element).height),
);
await page.getByTestId("doctor-runtime-menu-codex").click();
await expect(
page.getByRole("menuitem", { name: "Sign in with ChatGPT" }),
).toBeVisible({
timeout: 5_000,
});
await page.getByRole("menuitem", { name: "Sign in with ChatGPT" }).click();
const loading = page.getByTestId("doctor-runtime-loading-codex");
await expect(loading).toBeVisible();
await expect(loading).toContainText("Codex connecting");
await expect(page.getByTestId("doctor-runtime-ready-codex")).toHaveCount(0);
await expect(loading).toHaveCount(0, { timeout: 5_000 });
// Connect settled and discovery reports logged_in — sign-in chip gone,
// Ready chip on.
await expect(page.getByTestId("doctor-runtime-ready-codex")).toBeVisible();
await expect(page.getByTestId("doctor-runtime-status-codex")).toHaveCount(
0,
);
});
/**
* 07 — an adapter with no advertised auth methods shows only its manual
* instructions in overflow and keeps the row compact.
*/
test("07-connect-account-no-methods", async ({ page }) => {
await installMockBridge(page, {
acpRuntimesCatalog: [
GOOSE_AVAILABLE,
{
...CLAUDE_AVAILABLE_LOGGED_IN,
auth_status: { status: "logged_out" },
login_hint: "Run the Claude CLI to complete authentication.",
},
CODEX_NOT_INSTALLED,
BUZZ_AGENT_AVAILABLE,
],
acpAuthMethods: {
claude: { methods: [] },
},
});
await page.goto("/", { waitUntil: "domcontentloaded" });
await openSettings(page, "agents");
const row = page.getByTestId("doctor-runtime-claude");
await expect(row).toBeVisible({ timeout: 10_000 });
await expect(row).not.toContainText("Not authenticated");
await expect(row).toHaveCSS(
"height",
await page
.getByTestId("doctor-runtime-goose")
.evaluate((element) => getComputedStyle(element).height),
);
await page.getByTestId("doctor-runtime-menu-claude").click();
await expect(
page.getByRole("menuitem", { name: "CLI setup guide" }),
).toBeVisible();
await expect(
page.getByRole("menuitem", { name: "Sign in with ChatGPT" }),
).toHaveCount(0);
});
test("08-auth-method-discovery-error", async ({ page }) => {
await installMockBridge(page, {
acpRuntimesCatalog: [
GOOSE_AVAILABLE,
CLAUDE_AVAILABLE_LOGGED_IN,
{
...CODEX_NOT_INSTALLED,
availability: "available",
auth_status: { status: "logged_out" },
},
BUZZ_AGENT_AVAILABLE,
],
acpAuthMethodsErrors: {
codex: "Could not inspect the Codex adapter.",
},
});
await page.goto("/", { waitUntil: "domcontentloaded" });
await openSettings(page, "agents");
await expect(page.getByTestId("doctor-runtime-error-codex")).toContainText(
"Couldn't load sign-in options: Could not inspect the Codex adapter.",
);
});
test("09-connect-account-error", async ({ page }) => {
await installMockBridge(page, {
acpRuntimesCatalog: [
GOOSE_AVAILABLE,
CLAUDE_AVAILABLE_LOGGED_IN,
{
...CODEX_NOT_INSTALLED,
availability: "available",
auth_status: { status: "logged_out" },
},
BUZZ_AGENT_AVAILABLE,
],
acpAuthMethods: {
codex: {
methods: [
{
id: "chat-gpt",
name: "Sign in with ChatGPT",
type: "browser",
},
],
},
},
connectAcpRuntimeError: "The browser could not be opened.",
});
await page.goto("/", { waitUntil: "domcontentloaded" });
await openSettings(page, "agents");
await page.getByTestId("doctor-runtime-menu-codex").click();
await page.getByRole("menuitem", { name: "Sign in with ChatGPT" }).click();
await expect(page.getByTestId("doctor-runtime-error-codex")).toContainText(
"Couldn't connect Codex: The browser could not be opened.",
);
});
test("10-terminal-auth-completion-guidance", async ({ page }) => {
await installMockBridge(page, {
acpRuntimesCatalog: [
GOOSE_AVAILABLE,
CLAUDE_AVAILABLE_LOGGED_IN,
{
...CODEX_NOT_INSTALLED,
availability: "available",
auth_status: { status: "logged_out" },
},
BUZZ_AGENT_AVAILABLE,
],
acpAuthMethods: {
codex: {
methods: [
{
id: "terminal-login",
name: "Sign in from Terminal",
type: "terminal",
},
],
},
},
connectAcpRuntimeResult: { launched: true },
});
await page.goto("/", { waitUntil: "domcontentloaded" });
await openSettings(page, "agents");
await page.getByTestId("doctor-runtime-menu-codex").click();
await page.getByRole("menuitem", { name: "Sign in from Terminal" }).click();
await expect(
page.getByTestId("doctor-runtime-terminal-guidance-codex"),
).toContainText(
"Finish signing in from the Terminal window, then click Check again to re-check Codex.",
);
});
test("11-outdated-adapter-warning", async ({ page }) => {
await installMockBridge(page, {
acpRuntimesCatalog: [
GOOSE_AVAILABLE,
CLAUDE_AVAILABLE_LOGGED_IN,
{
...CODEX_NOT_INSTALLED,
availability: "adapter_outdated",
binary_path: "/usr/local/bin/codex-acp",
underlying_cli_path: "/usr/local/bin/codex",
can_auto_install: true,
},
BUZZ_AGENT_AVAILABLE,
],
installAcpRuntimeDelayMs: 250,
});
await page.goto("/", { waitUntil: "domcontentloaded" });
await openSettings(page, "agents");
await expect(page.getByTestId("doctor-runtime-status-codex")).toHaveText(
"Update needed",
);
const updateButton = page.getByTestId("doctor-runtime-install-codex");
await expect(updateButton).toHaveText("Update");
await updateButton.click();
const dialog = page.getByRole("alertdialog");
await expect(dialog).toContainText("Update Codex adapter?");
await expect(dialog).toContainText(
"Older Buzz releases using the legacy adapter may lose community access",
);
await expect(page.getByTestId("doctor-runtime-loading-codex")).toHaveCount(
0,
);
await page.getByTestId("doctor-runtime-confirm-update-codex").click();
const loading = page.getByTestId("doctor-runtime-loading-codex");
await expect(loading).toBeVisible();
await expect(loading).toContainText("Codex installing");
});
/**
* 08 — concurrent installs each keep their own spinner/result state;
* stale install failure is cleared when Check again fires (F1 fix).
*
* Flow:
* - Claude (400ms delay) → failure
* - Codex (100ms delay) → success
* Both started before either settles.
* After both settle: claude shows failure, codex shows success banner.
* Click Check again → both rows lose stale state (claude error gone).
*/
test("08-concurrent-installs-and-stale-clear", async ({ page }) => {
await installMockBridge(page, {
acpRuntimesCatalog: [
{
...CLAUDE_AVAILABLE_LOGGED_IN,
availability: "adapter_missing",
command: null,
binary_path: null,
can_auto_install: true,
auth_status: { status: "unknown" },
},
{
...CODEX_NOT_INSTALLED,
can_auto_install: true,
node_required: false,
},
GOOSE_AVAILABLE,
BUZZ_AGENT_AVAILABLE,
],
installAcpRuntimeByRuntime: {
claude: {
delayMs: 400,
result: {
success: false,
steps: [
{
step: "adapter",
command: "npm install -g @agentclientprotocol/claude-agent-acp",
success: false,
stdout: "",
stderr:
"npm ERR! code EACCES\nnpm ERR! syscall mkdir\nnpm ERR! path /usr/local\n\nHint: Check prefix permissions.",
exit_code: 1,
},
],
},
},
codex: {
delayMs: 100,
result: {
success: true,
steps: [
{
step: "adapter",
command: "npm install -g @zed-industries/codex-acp",
success: true,
stdout: "added 1 package",
stderr: "",
exit_code: 0,
},
],
},
},
},
// After the catalog refresh (triggered by a successful install or Check
// again), all runtimes report healthy so stale errors must clear.
acpRuntimesCatalogAfterInstall: [
{
...CLAUDE_AVAILABLE_LOGGED_IN,
availability: "available",
},
{
...CODEX_NOT_INSTALLED,
availability: "available",
command: "codex-acp",
binary_path: "/usr/local/bin/codex-acp",
auth_status: { status: "logged_in" },
},
GOOSE_AVAILABLE,
BUZZ_AGENT_AVAILABLE,
],
});
await page.goto("/", { waitUntil: "domcontentloaded" });
await openSettings(page, "agents");
const claudeRow = page.getByTestId("doctor-runtime-claude");
const codexRow = page.getByTestId("doctor-runtime-codex");
await expect(claudeRow).toBeVisible({ timeout: 10_000 });
await expect(codexRow).toBeVisible();
const claudeInstallButton = page.getByTestId(
"doctor-runtime-install-claude",
);
const codexInstallButton = page.getByTestId("doctor-runtime-install-codex");
const codexReadyChip = page.getByTestId("doctor-runtime-ready-codex");
// Start both installs before either settles.
await claudeInstallButton.click();
await codexInstallButton.click();
// Codex settles first (shorter delay): Ready chip appears, no error on
// codex. The catalog refresh triggered by codex's success immediately
// returns availability === "available", so the transient "installed.
// Checking..." banner is replaced by the stable ready state — assert the
// Ready chip instead.
await expect(codexReadyChip).toBeVisible({ timeout: 3_000 });
await expect(
page.getByTestId("doctor-runtime-install-error-codex"),
).toHaveCount(0);
// Claude settles (after its longer delay): failure error visible with
// multiline stderr. Codex must still be ready — unaffected by claude.
const claudeError = page.getByTestId("doctor-runtime-install-error-claude");
await expect(claudeError).toBeVisible({ timeout: 3_000 });
await expect(claudeError).toContainText("npm ERR!");
await expect(codexReadyChip).toBeVisible();
// Click Check again — epoch increments, RuntimeRow useEffect clears
// local installResult state, so the stale claude error disappears.
await page.getByRole("button", { name: "Check again" }).click();
await expect(claudeError).toHaveCount(0, { timeout: 5_000 });
// Codex stays ready (catalog still reports available after refresh).
await expect(codexReadyChip).toBeVisible({ timeout: 5_000 });
await claudeRow.scrollIntoViewIfNeeded();
await waitForAnimations(page);
await claudeRow.screenshot({
path: `${SHOTS}/08-concurrent-installs-and-stale-clear.png`,
});
});
/**
* 09 — install observability: the live output line appears while the install
* runs and disappears when it settles, and the failure message points at the
* install log rather than only the truncated last step.
*/
test("09-install-output-line-and-log-pointer", async ({ page }) => {
await installMockBridge(page, {
acpRuntimesCatalog: [
GOOSE_AVAILABLE,
CLAUDE_AVAILABLE_LOGGED_IN,
{
...CODEX_NOT_INSTALLED,
can_auto_install: true,
node_required: false,
},
BUZZ_AGENT_AVAILABLE,
],
installAcpRuntimeDelayMs: 500,
installAcpRuntimeOutputLines: [
"npm http fetch GET 200 @zed-industries/codex-acp",
"npm warn deprecated a transitive dependency",
],
installAcpRuntimeResult: {
success: false,
steps: [
{
step: "adapter",
command: "npm install -g @zed-industries/codex-acp",
success: false,
stdout: "",
stderr: "npm ERR! code E404",
exit_code: 1,
},
],
log_path: "/tmp/buzz-install-codex.log",
},
});
await page.goto("/", { waitUntil: "domcontentloaded" });
await openSettings(page, "agents");
const row = page.getByTestId("doctor-runtime-codex");
await expect(row).toBeVisible({ timeout: 10_000 });
const installButton = page.getByTestId("doctor-runtime-install-codex");
await expect(installButton).toBeEnabled();
await installButton.click();
// The bridge emits the attempt-start clear and the first line synchronously
// with the install invocation — before React commits the pending state — so
// observing this line proves the listener was already mounted at the click.
// A subscription that waited for the install state would have missed both.
const outputLine = page.getByTestId("doctor-runtime-install-output-codex");
await expect(outputLine).toContainText("npm http fetch", {
timeout: 5_000,
});
// Each new line replaces the previous one rather than accumulating.
await expect(outputLine).toContainText("npm warn deprecated", {
timeout: 5_000,
});
await expect(outputLine).not.toContainText("npm http fetch");
// Settled: the line clears, so a finished install leaves no stale output
// under a fresh Install button.
const installError = page.getByTestId("doctor-runtime-install-error-codex");
await expect(installError).toBeVisible({ timeout: 5_000 });
await expect(outputLine).toHaveCount(0);
// The failure points at the log holding bounded output for every attempt.
await expect(installError).toContainText("npm ERR! code E404");
await expect(installError).toContainText("/tmp/buzz-install-codex.log");
await row.scrollIntoViewIfNeeded();
await waitForAnimations(page);
await row.screenshot({
path: `${SHOTS}/09-install-output-line-and-log-pointer.png`,
});
// A second install shows its own output. The backend sequence restarts per
// run, so a display that kept the previous run's sequence number would
// reject every event of this one and show nothing at all.
await installButton.click();
await expect(outputLine).toContainText("npm http fetch", {
timeout: 5_000,
});
});
});