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
Signed-off-by: cls_宁波本机 <908705107@qq.com>
1074 lines
37 KiB
TypeScript
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,
|
|
});
|
|
});
|
|
});
|