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>
1121 lines
36 KiB
TypeScript
1121 lines
36 KiB
TypeScript
import { expect, test } from "@playwright/test";
|
|
import { installMockBridge } from "../helpers/bridge";
|
|
import { passThroughBackupStep } from "../helpers/onboarding";
|
|
|
|
function runtime(
|
|
id: "buzz-agent" | "claude" | "codex" | "goose",
|
|
availability: string,
|
|
authStatus: Record<string, unknown>,
|
|
overrides: Record<string, unknown> = {},
|
|
) {
|
|
return {
|
|
id,
|
|
label:
|
|
id === "buzz-agent"
|
|
? "Buzz Agent"
|
|
: id === "claude"
|
|
? "Claude Code"
|
|
: id === "codex"
|
|
? "Codex"
|
|
: "Goose",
|
|
avatar_url: "",
|
|
availability,
|
|
command: availability === "available" ? id : null,
|
|
binary_path: availability === "available" ? `/usr/local/bin/${id}` : null,
|
|
default_args: [],
|
|
mcp_command: null,
|
|
install_hint: `Install ${id}`,
|
|
install_instructions_url: "https://example.com",
|
|
can_auto_install: true,
|
|
underlying_cli_path: null,
|
|
node_required: false,
|
|
auth_status: authStatus,
|
|
login_hint: `Sign in to ${id}`,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
async function navigateToSetupPage(
|
|
page: Parameters<typeof installMockBridge>[0],
|
|
) {
|
|
await page.getByRole("button", { name: "Create a new identity key" }).click();
|
|
await passThroughBackupStep(page);
|
|
await expect(page.getByTestId("onboarding-page-2")).toBeVisible();
|
|
}
|
|
|
|
async function readSavedRuntime(page: Parameters<typeof installMockBridge>[0]) {
|
|
return await page.evaluate(async () => {
|
|
const result = await (
|
|
window as Window & {
|
|
__BUZZ_E2E_INVOKE_MOCK_COMMAND__?: (
|
|
command: string,
|
|
payload: unknown,
|
|
) => Promise<{ preferred_runtime?: string | null }>;
|
|
}
|
|
).__BUZZ_E2E_INVOKE_MOCK_COMMAND__?.("get_global_agent_config", null);
|
|
return result?.preferred_runtime ?? null;
|
|
});
|
|
}
|
|
|
|
async function readGlobalConfigSetterCallCount(
|
|
page: Parameters<typeof installMockBridge>[0],
|
|
) {
|
|
return await page.evaluate(async () => {
|
|
return await (
|
|
window as Window & {
|
|
__BUZZ_E2E_INVOKE_MOCK_COMMAND__?: (
|
|
command: string,
|
|
payload: unknown,
|
|
) => Promise<number>;
|
|
}
|
|
).__BUZZ_E2E_INVOKE_MOCK_COMMAND__?.(
|
|
"get_global_agent_config_set_call_count",
|
|
null,
|
|
);
|
|
});
|
|
}
|
|
|
|
test("setup shows all bundled harnesses as detected", async ({ page }) => {
|
|
await installMockBridge(
|
|
page,
|
|
{
|
|
acpRuntimesCatalog: [
|
|
runtime("buzz-agent", "available", { status: "not_applicable" }),
|
|
runtime("goose", "available", { status: "not_applicable" }),
|
|
runtime("codex", "available", { status: "logged_in" }),
|
|
runtime("claude", "available", { status: "logged_in" }),
|
|
],
|
|
},
|
|
{ skipCommunitySeed: true, skipOnboardingSeed: true },
|
|
);
|
|
await page.goto("/");
|
|
await navigateToSetupPage(page);
|
|
|
|
await expect(page.getByTestId("onboarding-runtime-claude")).toBeVisible();
|
|
await expect(page.getByTestId("onboarding-runtime-codex")).toBeVisible();
|
|
await expect(page.getByTestId("onboarding-runtime-goose")).toBeVisible();
|
|
await expect(page.getByTestId("onboarding-runtime-buzz-agent")).toBeVisible();
|
|
await expect(page.getByRole("checkbox")).toHaveCount(0);
|
|
});
|
|
|
|
test("setup distinguishes a missing CLI from an installed desktop app", async ({
|
|
page,
|
|
}) => {
|
|
await installMockBridge(
|
|
page,
|
|
{
|
|
acpRuntimesCatalog: [
|
|
runtime(
|
|
"codex",
|
|
"not_installed",
|
|
{ status: "unknown" },
|
|
{
|
|
install_hint: "Buzz talks to Codex through the Codex CLI.",
|
|
install_instructions_url:
|
|
"https://developers.openai.com/codex/cli/",
|
|
},
|
|
),
|
|
],
|
|
},
|
|
{ skipCommunitySeed: true, skipOnboardingSeed: true },
|
|
);
|
|
await page.goto("/");
|
|
await navigateToSetupPage(page);
|
|
|
|
const card = page.getByTestId("onboarding-runtime-codex");
|
|
await expect(card).toContainText("CLI not detected.");
|
|
await expect(card.getByTestId("onboarding-runtime-install-codex")).toHaveText(
|
|
"INSTALL",
|
|
);
|
|
});
|
|
|
|
test("ready state is detected and enables Next without persisting a default", async ({
|
|
page,
|
|
}) => {
|
|
await installMockBridge(
|
|
page,
|
|
{
|
|
acpRuntimesCatalog: [
|
|
runtime("claude", "available", { status: "logged_in" }),
|
|
runtime("codex", "available", { status: "logged_out" }),
|
|
],
|
|
},
|
|
{ skipCommunitySeed: true, skipOnboardingSeed: true },
|
|
);
|
|
await page.goto("/");
|
|
await navigateToSetupPage(page);
|
|
|
|
await expect(page.getByTestId("onboarding-runtime-ready-claude")).toHaveText(
|
|
"READY",
|
|
);
|
|
await expect(
|
|
page.getByTestId("onboarding-runtime-checkmark-claude"),
|
|
).toHaveCount(0);
|
|
await expect(
|
|
page.getByTestId("onboarding-runtime-checkmark-codex"),
|
|
).toHaveCount(0);
|
|
await expect(page.getByTestId("onboarding-setup-next")).toBeEnabled();
|
|
expect(await readSavedRuntime(page)).toBeNull();
|
|
});
|
|
|
|
test("setup shows runtime discovery loading before rendering harnesses", async ({
|
|
page,
|
|
}) => {
|
|
await installMockBridge(
|
|
page,
|
|
{
|
|
acpRuntimesCatalog: [
|
|
runtime("claude", "available", { status: "logged_in" }),
|
|
],
|
|
acpRuntimesDelayMs: 500,
|
|
},
|
|
{ skipCommunitySeed: true, skipOnboardingSeed: true },
|
|
);
|
|
await page.goto("/");
|
|
await navigateToSetupPage(page);
|
|
|
|
await expect(page.getByTestId("onboarding-runtime-loading")).toBeVisible();
|
|
await expect(page.getByTestId("onboarding-runtime-claude")).toBeVisible();
|
|
await expect(page.getByTestId("onboarding-runtime-loading")).toHaveCount(0);
|
|
});
|
|
|
|
test("unknown authentication can be checked again", async ({ page }) => {
|
|
const unknown = runtime("claude", "available", { status: "unknown" });
|
|
const loggedIn = runtime("claude", "available", { status: "logged_in" });
|
|
await installMockBridge(
|
|
page,
|
|
{ acpRuntimesCatalogSequence: [[unknown], [loggedIn]] },
|
|
{ skipCommunitySeed: true, skipOnboardingSeed: true },
|
|
);
|
|
await page.goto("/");
|
|
await navigateToSetupPage(page);
|
|
|
|
const checkAgain = page.getByRole("button", {
|
|
name: "Check Claude Code again",
|
|
});
|
|
await expect(checkAgain).toHaveText("CHECK AGAIN");
|
|
await checkAgain.click();
|
|
await expect(page.getByTestId("onboarding-runtime-ready-claude")).toHaveText(
|
|
"READY",
|
|
);
|
|
});
|
|
|
|
test("auth discovery failure stays actionable without exposing internals", async ({
|
|
page,
|
|
}) => {
|
|
await installMockBridge(
|
|
page,
|
|
{
|
|
acpRuntimesCatalog: [
|
|
runtime("claude", "available", { status: "logged_out" }),
|
|
],
|
|
acpAuthMethodsError: "sensitive auth discovery details",
|
|
},
|
|
{ skipCommunitySeed: true, skipOnboardingSeed: true },
|
|
);
|
|
await page.goto("/");
|
|
await navigateToSetupPage(page);
|
|
|
|
const card = page.getByTestId("onboarding-runtime-claude");
|
|
await expect(
|
|
card.getByRole("status", { name: /Sign-in unavailable/ }),
|
|
).toBeVisible();
|
|
await expect(
|
|
card.getByTestId("onboarding-runtime-instructions-claude"),
|
|
).toHaveText("SIGN IN");
|
|
await expect(card).not.toContainText("sensitive auth discovery details");
|
|
});
|
|
|
|
test("terminal launch failure keeps Sign in available", async ({ page }) => {
|
|
await installMockBridge(
|
|
page,
|
|
{
|
|
acpRuntimesCatalog: [
|
|
runtime("claude", "available", { status: "logged_out" }),
|
|
],
|
|
acpAuthMethods: {
|
|
claude: {
|
|
methods: [
|
|
{
|
|
id: "subscription",
|
|
name: "Claude.ai subscription",
|
|
description: null,
|
|
type: "terminal",
|
|
},
|
|
],
|
|
},
|
|
},
|
|
connectAcpRuntimeError: "sensitive launch details",
|
|
},
|
|
{ skipCommunitySeed: true, skipOnboardingSeed: true },
|
|
);
|
|
await page.goto("/");
|
|
await navigateToSetupPage(page);
|
|
|
|
const card = page.getByTestId("onboarding-runtime-claude");
|
|
const signIn = card.getByRole("button", { name: "Sign in to Claude Code" });
|
|
await signIn.click();
|
|
await expect(
|
|
card.getByRole("status", { name: /Sign-in failed/ }),
|
|
).toBeVisible();
|
|
await expect(signIn).toHaveText("SIGN IN");
|
|
await expect(card).not.toContainText("sensitive launch details");
|
|
});
|
|
|
|
test("sign in stays pending until catalog detection confirms Ready", async ({
|
|
page,
|
|
}) => {
|
|
const loggedOut = runtime("claude", "available", { status: "logged_out" });
|
|
const loggedIn = runtime("claude", "available", { status: "logged_in" });
|
|
await installMockBridge(
|
|
page,
|
|
{
|
|
acpRuntimesCatalogSequence: [[loggedOut], [loggedOut], [loggedIn]],
|
|
acpAuthMethods: {
|
|
claude: {
|
|
methods: [
|
|
{
|
|
id: "subscription",
|
|
name: "Claude.ai subscription",
|
|
description: null,
|
|
type: "terminal",
|
|
},
|
|
],
|
|
},
|
|
},
|
|
},
|
|
{ skipCommunitySeed: true, skipOnboardingSeed: true },
|
|
);
|
|
await page.goto("/");
|
|
await navigateToSetupPage(page);
|
|
|
|
const signIn = page.getByRole("button", { name: "Sign in to Claude Code" });
|
|
await expect(signIn).toHaveText("SIGN IN");
|
|
await expect(page.getByTestId("onboarding-setup-next")).toBeDisabled();
|
|
await signIn.click();
|
|
await expect(signIn).toHaveText("CHECKING…");
|
|
await expect(page.getByTestId("onboarding-setup-next")).toBeDisabled();
|
|
await expect(page.getByTestId("onboarding-runtime-ready-claude")).toHaveText(
|
|
"READY",
|
|
{ timeout: 5_000 },
|
|
);
|
|
await expect(page.getByTestId("onboarding-setup-next")).toBeEnabled();
|
|
});
|
|
|
|
test("failed install can be retried without shifting card content", async ({
|
|
page,
|
|
}) => {
|
|
const notInstalled = runtime("claude", "adapter_missing", {
|
|
status: "unknown",
|
|
});
|
|
await installMockBridge(
|
|
page,
|
|
{
|
|
acpRuntimesCatalog: [notInstalled],
|
|
installAcpRuntimeResults: [
|
|
{
|
|
success: false,
|
|
steps: [
|
|
{
|
|
step: "adapter",
|
|
command: "mock install claude",
|
|
success: false,
|
|
stdout: "",
|
|
stderr: "sensitive install details",
|
|
exit_code: 1,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
success: true,
|
|
steps: [
|
|
{
|
|
step: "adapter",
|
|
command: "mock install claude",
|
|
success: true,
|
|
stdout: "installed",
|
|
stderr: "",
|
|
exit_code: 0,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
acpRuntimesCatalogAfterInstall: [
|
|
runtime("claude", "available", { status: "logged_in" }),
|
|
],
|
|
},
|
|
{ skipCommunitySeed: true, skipOnboardingSeed: true },
|
|
);
|
|
await page.goto("/");
|
|
await navigateToSetupPage(page);
|
|
|
|
const card = page.getByTestId("onboarding-runtime-claude");
|
|
const heading = card.getByRole("heading", { name: "Claude Code" });
|
|
const headingTop = await heading.evaluate(
|
|
(element) => element.getBoundingClientRect().top,
|
|
);
|
|
const install = page.getByTestId("onboarding-runtime-install-claude");
|
|
await install.click();
|
|
const error = page.getByTestId("onboarding-runtime-error-claude");
|
|
await expect(error).toBeVisible();
|
|
await expect(install).toHaveText("RETRY INSTALL");
|
|
await expect(error).not.toContainText("sensitive install details");
|
|
expect(
|
|
await heading.evaluate((element) => element.getBoundingClientRect().top),
|
|
).toBe(headingTop);
|
|
await install.click();
|
|
await expect(page.getByTestId("onboarding-runtime-ready-claude")).toHaveText(
|
|
"READY",
|
|
);
|
|
});
|
|
|
|
test("install transitions through Sign in to Ready", async ({ page }) => {
|
|
const notInstalled = runtime("claude", "adapter_missing", {
|
|
status: "unknown",
|
|
});
|
|
const loggedOut = runtime("claude", "available", { status: "logged_out" });
|
|
const loggedIn = runtime("claude", "available", { status: "logged_in" });
|
|
await installMockBridge(
|
|
page,
|
|
{
|
|
acpRuntimesCatalog: [notInstalled],
|
|
acpRuntimesCatalogAfterInstallSequence: [[loggedOut], [loggedIn]],
|
|
installAcpRuntimeDelayMs: 500,
|
|
acpAuthMethods: {
|
|
claude: {
|
|
methods: [
|
|
{
|
|
id: "subscription",
|
|
name: "Claude.ai subscription",
|
|
description: null,
|
|
type: "terminal",
|
|
},
|
|
],
|
|
},
|
|
},
|
|
},
|
|
{ skipCommunitySeed: true, skipOnboardingSeed: true },
|
|
);
|
|
await page.goto("/");
|
|
await navigateToSetupPage(page);
|
|
|
|
const install = page.getByTestId("onboarding-runtime-install-claude");
|
|
await expect(install).toHaveText("INSTALL");
|
|
await install.click();
|
|
|
|
const signIn = page.getByRole("button", { name: "Sign in to Claude Code" });
|
|
await expect(signIn).toHaveText("SIGN IN");
|
|
await expect(page.getByTestId("onboarding-setup-next")).toBeDisabled();
|
|
await signIn.click();
|
|
await expect(page.getByTestId("onboarding-runtime-ready-claude")).toHaveText(
|
|
"READY",
|
|
{ timeout: 5_000 },
|
|
);
|
|
await expect(
|
|
page.getByTestId("onboarding-runtime-checkmark-claude"),
|
|
).toHaveCount(0);
|
|
});
|
|
|
|
test("defaults waits for baked configuration before rendering fields", async ({
|
|
page,
|
|
}) => {
|
|
await installMockBridge(
|
|
page,
|
|
{
|
|
acpRuntimesCatalog: [
|
|
runtime("claude", "available", { status: "logged_in" }),
|
|
],
|
|
bakedBuildEnv: [
|
|
{ key: "ANTHROPIC_API_KEY", masked: true, value: "••••••" },
|
|
],
|
|
bakedBuildEnvDelayMs: 500,
|
|
},
|
|
{ skipCommunitySeed: true, skipOnboardingSeed: true },
|
|
);
|
|
await page.goto("/");
|
|
await navigateToSetupPage(page);
|
|
await page.getByTestId("onboarding-setup-next").click();
|
|
|
|
await expect(page.getByText("Loading…")).toBeVisible();
|
|
await expect(page.getByTestId("global-agent-default-harness")).toHaveText(
|
|
"Claude Code",
|
|
);
|
|
});
|
|
|
|
test("defaults renders only fields supported by the selected harness", async ({
|
|
page,
|
|
}) => {
|
|
await installMockBridge(
|
|
page,
|
|
{
|
|
acpRuntimesCatalog: [
|
|
runtime("claude", "available", { status: "logged_in" }),
|
|
],
|
|
globalAgentConfig: {
|
|
env_vars: { BUZZ_AGENT_THINKING_EFFORT: "high" },
|
|
provider: null,
|
|
model: "stale-model",
|
|
preferred_runtime: null,
|
|
},
|
|
},
|
|
{ skipCommunitySeed: true, skipOnboardingSeed: true },
|
|
);
|
|
await page.goto("/");
|
|
await navigateToSetupPage(page);
|
|
await page.getByTestId("onboarding-setup-next").click();
|
|
|
|
await expect(page.getByTestId("global-agent-default-harness")).toHaveText(
|
|
"Claude Code",
|
|
);
|
|
await expect(page.getByTestId("global-agent-provider")).toHaveCount(0);
|
|
await expect(page.getByTestId("global-agent-model")).toHaveText(
|
|
"Default model",
|
|
);
|
|
await expect(
|
|
page.getByTestId("global-agent-thinking-effort-select"),
|
|
).toHaveCount(0);
|
|
});
|
|
|
|
test("defaults hides model when optional harness has empty discovery", async ({
|
|
page,
|
|
}) => {
|
|
await installMockBridge(
|
|
page,
|
|
{
|
|
acpRuntimesCatalog: [
|
|
runtime("claude", "available", { status: "logged_in" }),
|
|
],
|
|
discoverAgentModels: {
|
|
models: [],
|
|
supportsSwitching: false,
|
|
},
|
|
globalAgentConfig: {
|
|
env_vars: {},
|
|
provider: null,
|
|
model: null,
|
|
preferred_runtime: null,
|
|
},
|
|
},
|
|
{ skipCommunitySeed: true, skipOnboardingSeed: true },
|
|
);
|
|
await page.goto("/");
|
|
await navigateToSetupPage(page);
|
|
await page.getByTestId("onboarding-setup-next").click();
|
|
|
|
await expect(page.getByTestId("onboarding-page-config")).toBeVisible();
|
|
await expect(page.getByTestId("global-agent-default-harness")).toHaveText(
|
|
"Claude Code",
|
|
);
|
|
// Confirmed successful empty catalog — omit the Model control; harness
|
|
// default applies and Finish stays available.
|
|
await expect(page.getByTestId("global-agent-model")).toHaveCount(0);
|
|
await expect(page.getByTestId("onboarding-finish")).toBeEnabled();
|
|
});
|
|
|
|
test("defaults keeps model control when optional harness discovery fails", async ({
|
|
page,
|
|
}) => {
|
|
await installMockBridge(
|
|
page,
|
|
{
|
|
acpRuntimesCatalog: [
|
|
runtime("claude", "available", { status: "logged_in" }),
|
|
],
|
|
discoverAgentModelsError: "CLI discovery timed out",
|
|
globalAgentConfig: {
|
|
env_vars: {},
|
|
provider: null,
|
|
model: null,
|
|
preferred_runtime: null,
|
|
},
|
|
},
|
|
{ skipCommunitySeed: true, skipOnboardingSeed: true },
|
|
);
|
|
await page.goto("/");
|
|
await navigateToSetupPage(page);
|
|
await page.getByTestId("onboarding-setup-next").click();
|
|
|
|
await expect(page.getByTestId("onboarding-page-config")).toBeVisible();
|
|
await expect(page.getByTestId("global-agent-default-harness")).toHaveText(
|
|
"Claude Code",
|
|
);
|
|
// Failed discovery must not look like successful empty: keep the control
|
|
// and surface #2246 failure UI (status line bypasses onboarding-essential).
|
|
await expect(page.getByTestId("global-agent-model")).toBeVisible();
|
|
await expect(page.getByText(/Could not load live models/i)).toBeVisible();
|
|
await expect(page.getByTestId("onboarding-finish")).toBeEnabled();
|
|
});
|
|
|
|
test("defaults can be skipped while loading without persisting configuration", async ({
|
|
page,
|
|
}) => {
|
|
await installMockBridge(
|
|
page,
|
|
{
|
|
acpRuntimesCatalog: [
|
|
runtime("claude", "available", { status: "logged_in" }),
|
|
],
|
|
bakedBuildEnvDelayMs: 500,
|
|
globalAgentConfig: {
|
|
env_vars: {},
|
|
provider: null,
|
|
model: null,
|
|
preferred_runtime: null,
|
|
},
|
|
},
|
|
{ skipCommunitySeed: true, skipOnboardingSeed: true },
|
|
);
|
|
await page.goto("/");
|
|
await navigateToSetupPage(page);
|
|
await page.getByTestId("onboarding-setup-next").click();
|
|
|
|
await expect(page.getByText("Loading…")).toBeVisible();
|
|
await page.getByTestId("onboarding-config-skip").click();
|
|
|
|
await expect(page.getByText("Join or create a community")).toBeVisible();
|
|
expect(await readSavedRuntime(page)).toBeNull();
|
|
});
|
|
|
|
test("defaults stages auto-selection and edits without writing when skipped", async ({
|
|
page,
|
|
}) => {
|
|
await installMockBridge(
|
|
page,
|
|
{
|
|
acpRuntimesCatalog: [
|
|
runtime("claude", "available", { status: "logged_in" }),
|
|
],
|
|
globalAgentConfig: {
|
|
env_vars: {},
|
|
provider: null,
|
|
model: null,
|
|
preferred_runtime: null,
|
|
},
|
|
},
|
|
{ skipCommunitySeed: true, skipOnboardingSeed: true },
|
|
);
|
|
await page.goto("/");
|
|
await navigateToSetupPage(page);
|
|
await page.getByTestId("onboarding-setup-next").click();
|
|
|
|
await expect(page.getByTestId("global-agent-default-harness")).toHaveText(
|
|
"Claude Code",
|
|
);
|
|
await page.getByTestId("global-agent-model").click();
|
|
await page
|
|
.getByTestId("global-agent-model-option-claude-opus-4-20250514")
|
|
.click();
|
|
expect(await readGlobalConfigSetterCallCount(page)).toBe(0);
|
|
await expect(
|
|
page.getByText(
|
|
"Configure default models in Settings → Agents after setup.",
|
|
),
|
|
).toBeVisible();
|
|
|
|
await page.getByTestId("onboarding-config-skip").click();
|
|
|
|
await expect(page.getByText("Join or create a community")).toBeVisible();
|
|
expect(await readSavedRuntime(page)).toBeNull();
|
|
expect(await readGlobalConfigSetterCallCount(page)).toBe(0);
|
|
});
|
|
|
|
test("Back preserves incomplete defaults draft without writing", async ({
|
|
page,
|
|
}) => {
|
|
await installMockBridge(
|
|
page,
|
|
{
|
|
acpRuntimesCatalog: [
|
|
runtime("buzz-agent", "available", { status: "not_applicable" }),
|
|
runtime("claude", "available", { status: "logged_in" }),
|
|
],
|
|
discoverAgentModels: {
|
|
models: [{ id: "claude-sonnet-4", name: "Claude Sonnet 4" }],
|
|
supportsSwitching: true,
|
|
},
|
|
globalAgentConfig: {
|
|
env_vars: {},
|
|
provider: null,
|
|
model: null,
|
|
preferred_runtime: null,
|
|
},
|
|
},
|
|
{ skipCommunitySeed: true, skipOnboardingSeed: true },
|
|
);
|
|
await page.goto("/");
|
|
await navigateToSetupPage(page);
|
|
await page.getByTestId("onboarding-setup-next").click();
|
|
|
|
const harness = page.getByTestId("global-agent-default-harness");
|
|
await harness.click();
|
|
await page
|
|
.getByTestId("global-agent-default-harness-option-buzz-agent")
|
|
.click();
|
|
await page.getByTestId("global-agent-provider").click();
|
|
await page.getByTestId("global-agent-provider-option-anthropic").click();
|
|
await expect(page.getByTestId("onboarding-finish")).toBeDisabled();
|
|
|
|
await page.getByTestId("onboarding-back").click();
|
|
await expect(page.getByTestId("onboarding-page-2")).toBeVisible();
|
|
expect(await readSavedRuntime(page)).toBeNull();
|
|
expect(await readGlobalConfigSetterCallCount(page)).toBe(0);
|
|
|
|
await page.getByTestId("onboarding-setup-next").click();
|
|
await expect(harness).toHaveText("Buzz");
|
|
await expect(page.getByTestId("global-agent-provider")).toHaveText(
|
|
"Anthropic",
|
|
);
|
|
await expect(page.getByTestId("onboarding-finish")).toBeDisabled();
|
|
expect(await readGlobalConfigSetterCallCount(page)).toBe(0);
|
|
});
|
|
|
|
test("defaults auto-selects the only ready visible harness", async ({
|
|
page,
|
|
}) => {
|
|
await installMockBridge(
|
|
page,
|
|
{
|
|
acpRuntimesCatalog: [
|
|
runtime("buzz-agent", "not_installed", { status: "not_applicable" }),
|
|
runtime("goose", "not_installed", { status: "not_applicable" }),
|
|
runtime("claude", "available", { status: "logged_in" }),
|
|
runtime("codex", "available", { status: "logged_out" }),
|
|
],
|
|
globalAgentConfig: {
|
|
env_vars: {},
|
|
provider: null,
|
|
model: null,
|
|
preferred_runtime: null,
|
|
},
|
|
},
|
|
{ skipCommunitySeed: true, skipOnboardingSeed: true },
|
|
);
|
|
await page.goto("/");
|
|
await navigateToSetupPage(page);
|
|
await page.getByTestId("onboarding-setup-next").click();
|
|
await expect(page.getByTestId("onboarding-page-config")).toBeVisible();
|
|
|
|
await expect(page.getByTestId("global-agent-default-harness")).toHaveText(
|
|
"Claude Code",
|
|
);
|
|
await expect(page.getByTestId("onboarding-finish")).toBeEnabled();
|
|
expect(await readSavedRuntime(page)).toBeNull();
|
|
});
|
|
|
|
test("Next persists the latest staged harness choice", async ({ page }) => {
|
|
await installMockBridge(
|
|
page,
|
|
{
|
|
acpRuntimesCatalog: [
|
|
runtime("claude", "available", { status: "logged_in" }),
|
|
runtime("codex", "available", { status: "logged_in" }),
|
|
],
|
|
globalAgentConfig: {
|
|
env_vars: {},
|
|
provider: null,
|
|
model: null,
|
|
preferred_runtime: null,
|
|
},
|
|
setGlobalAgentConfigDelayMs: 300,
|
|
},
|
|
{ skipCommunitySeed: true, skipOnboardingSeed: true },
|
|
);
|
|
await page.goto("/");
|
|
await navigateToSetupPage(page);
|
|
await page.getByTestId("onboarding-setup-next").click();
|
|
|
|
const harness = page.getByTestId("global-agent-default-harness");
|
|
await harness.click();
|
|
await page.getByTestId("global-agent-default-harness-option-claude").click();
|
|
await harness.click();
|
|
await page.getByTestId("global-agent-default-harness-option-codex").click();
|
|
const finish = page.getByTestId("onboarding-finish");
|
|
await expect(finish).toBeEnabled();
|
|
expect(await readGlobalConfigSetterCallCount(page)).toBe(0);
|
|
await finish.click();
|
|
await expect(page.getByText("Join or create a community")).toBeVisible();
|
|
await expect.poll(() => readSavedRuntime(page)).toBe("codex");
|
|
});
|
|
|
|
test("Next shows saving state and advances only after persistence", async ({
|
|
page,
|
|
}) => {
|
|
await installMockBridge(
|
|
page,
|
|
{
|
|
acpRuntimesCatalog: [
|
|
runtime("claude", "available", { status: "logged_in" }),
|
|
runtime("codex", "available", { status: "logged_in" }),
|
|
],
|
|
globalAgentConfig: {
|
|
env_vars: {},
|
|
provider: null,
|
|
model: null,
|
|
preferred_runtime: null,
|
|
},
|
|
setGlobalAgentConfigDelayMs: 500,
|
|
},
|
|
{ skipCommunitySeed: true, skipOnboardingSeed: true },
|
|
);
|
|
await page.goto("/");
|
|
await navigateToSetupPage(page);
|
|
await page.getByTestId("onboarding-setup-next").click();
|
|
|
|
const harness = page.getByTestId("global-agent-default-harness");
|
|
await harness.click();
|
|
await page.getByTestId("global-agent-default-harness-option-codex").click();
|
|
await page.getByTestId("onboarding-finish").click();
|
|
|
|
await expect(page.getByTestId("onboarding-finish")).toHaveText("Saving…");
|
|
await expect(page.getByTestId("onboarding-config-skip")).toBeDisabled();
|
|
await expect(page.getByTestId("onboarding-back")).toBeDisabled();
|
|
await expect(page.getByTestId("onboarding-page-config")).toBeVisible();
|
|
expect(await readSavedRuntime(page)).toBeNull();
|
|
|
|
await expect(page.getByText("Join or create a community")).toBeVisible();
|
|
expect(await readSavedRuntime(page)).toBe("codex");
|
|
});
|
|
|
|
test("Next keeps the draft and retries after a save failure", async ({
|
|
page,
|
|
}) => {
|
|
await installMockBridge(
|
|
page,
|
|
{
|
|
acpRuntimesCatalog: [
|
|
runtime("claude", "available", { status: "logged_in" }),
|
|
],
|
|
globalAgentConfig: {
|
|
env_vars: {},
|
|
provider: null,
|
|
model: null,
|
|
preferred_runtime: null,
|
|
},
|
|
setGlobalAgentConfigErrors: ["Disk is read-only", null],
|
|
},
|
|
{ skipCommunitySeed: true, skipOnboardingSeed: true },
|
|
);
|
|
await page.goto("/");
|
|
await navigateToSetupPage(page);
|
|
await page.getByTestId("onboarding-setup-next").click();
|
|
await expect(page.getByTestId("global-agent-default-harness")).toHaveText(
|
|
"Claude Code",
|
|
);
|
|
|
|
await page.getByTestId("onboarding-finish").click();
|
|
|
|
await expect(page.getByTestId("onboarding-page-config")).toBeVisible();
|
|
await expect(page.getByTestId("onboarding-config-save-error")).toContainText(
|
|
"Disk is read-only",
|
|
);
|
|
await expect(page.getByTestId("onboarding-finish")).toBeEnabled();
|
|
expect(await readSavedRuntime(page)).toBeNull();
|
|
expect(await readGlobalConfigSetterCallCount(page)).toBe(1);
|
|
|
|
await page.getByTestId("onboarding-finish").click();
|
|
await expect(page.getByText("Join or create a community")).toBeVisible();
|
|
expect(await readSavedRuntime(page)).toBe("claude");
|
|
expect(await readGlobalConfigSetterCallCount(page)).toBe(2);
|
|
});
|
|
|
|
test("defaults requires a choice when multiple visible harnesses are ready", async ({
|
|
page,
|
|
}) => {
|
|
await installMockBridge(
|
|
page,
|
|
{
|
|
acpRuntimesCatalog: [
|
|
runtime("buzz-agent", "available", { status: "not_applicable" }),
|
|
runtime("goose", "available", { status: "not_applicable" }),
|
|
runtime("claude", "available", { status: "logged_in" }),
|
|
runtime("codex", "available", { status: "logged_in" }),
|
|
],
|
|
globalAgentConfig: {
|
|
env_vars: {},
|
|
provider: null,
|
|
model: null,
|
|
preferred_runtime: null,
|
|
},
|
|
},
|
|
{ skipCommunitySeed: true, skipOnboardingSeed: true },
|
|
);
|
|
await page.goto("/");
|
|
await navigateToSetupPage(page);
|
|
await page.getByTestId("onboarding-setup-next").click();
|
|
await expect(page.getByTestId("onboarding-page-config")).toBeVisible();
|
|
|
|
const harness = page.getByTestId("global-agent-default-harness");
|
|
await expect(harness).toHaveText("Select a harness");
|
|
await expect(page.getByTestId("onboarding-finish")).toBeDisabled();
|
|
await harness.click();
|
|
await expect(
|
|
page.getByTestId("global-agent-default-harness-option-claude"),
|
|
).toBeVisible();
|
|
await expect(
|
|
page.getByTestId("global-agent-default-harness-option-codex"),
|
|
).toBeVisible();
|
|
await expect(
|
|
page.getByTestId("global-agent-default-harness-option-goose"),
|
|
).toBeVisible();
|
|
await expect(
|
|
page.getByTestId("global-agent-default-harness-option-buzz-agent"),
|
|
).toBeVisible();
|
|
await page.getByTestId("global-agent-default-harness-option-codex").click();
|
|
await expect(harness).toHaveText("Codex");
|
|
await expect(page.getByTestId("onboarding-finish")).toBeEnabled();
|
|
expect(await readSavedRuntime(page)).toBeNull();
|
|
});
|
|
|
|
/**
|
|
* Two installs started concurrently — claude fails with a multiline error
|
|
* (rich hint+stderr in the tooltip) while codex succeeds. Each card must
|
|
* keep its own independent spinner and its own terminal result; neither card
|
|
* may show the other's outcome.
|
|
*
|
|
* This is the behavioral regression test for the per-card mutation fix
|
|
* (Bug B) and the multiline tooltip fix (Bug A / F3 from Thufir pass 1).
|
|
*/
|
|
test("concurrent installs each keep their own state — one fails, one succeeds", async ({
|
|
page,
|
|
}) => {
|
|
// Realistic 512-head + 1024-tail shape: many short lines followed by one
|
|
// long unbroken Windows path. This exercises both overflow axes:
|
|
// • vertical: enough lines to exceed max-h-48 (192px at ~16px/line)
|
|
// • horizontal: the long path has no spaces, so only break-words prevents
|
|
// scrollWidth > clientWidth.
|
|
const longWindowsPath =
|
|
"C:\\Users\\willp\\AppData\\Roaming\\npm\\node_modules\\@agentclientprotocol\\claude-agent-acp\\dist\\bin\\claude-agent-acp.exe";
|
|
const multilineError = [
|
|
"npm ERR! code EACCES",
|
|
"npm ERR! syscall mkdir",
|
|
"npm ERR! path C:\\Users\\willp\\AppData\\Roaming\\npm",
|
|
"npm ERR! errno -4048",
|
|
"npm ERR! Error: EACCES: permission denied, mkdir 'C:\\Users\\willp\\AppData\\Roaming\\npm'",
|
|
"npm ERR! { [Error: EACCES: permission denied, mkdir 'C:\\Users\\willp\\AppData\\Roaming\\npm']",
|
|
"npm ERR! errno: -4048,",
|
|
"npm ERR! code: 'EACCES',",
|
|
"npm ERR! syscall: 'mkdir',",
|
|
"npm ERR! path: 'C:\\\\Users\\\\willp\\\\AppData\\\\Roaming\\\\npm' }",
|
|
"npm ERR!",
|
|
"npm ERR! The operation was rejected by your operating system.",
|
|
"npm ERR! It is likely you do not have the permissions to access this file as the current user",
|
|
"npm ERR!",
|
|
`npm ERR! If you believe this might be a permissions issue, please double-check the`,
|
|
`npm ERR! permissions of the file and its containing directories, or try running`,
|
|
`npm ERR! the command again as root/Administrator.`,
|
|
"",
|
|
`Hint: Run as Administrator or change npm prefix: npm config set prefix ${longWindowsPath}`,
|
|
].join("\n");
|
|
const claudeNotInstalled = runtime("claude", "adapter_missing", {
|
|
status: "unknown",
|
|
});
|
|
const codexNotInstalled = runtime("codex", "adapter_missing", {
|
|
status: "unknown",
|
|
});
|
|
await installMockBridge(
|
|
page,
|
|
{
|
|
acpRuntimesCatalog: [claudeNotInstalled, codexNotInstalled],
|
|
// Claude: long delay then failure with multiline stderr + hint.
|
|
// Codex: short delay then success.
|
|
// Per-runtime config lets both be in flight simultaneously.
|
|
installAcpRuntimeByRuntime: {
|
|
claude: {
|
|
delayMs: 600,
|
|
result: {
|
|
success: false,
|
|
steps: [
|
|
{
|
|
step: "adapter",
|
|
command: "npm install -g @agentclientprotocol/claude-agent-acp",
|
|
success: false,
|
|
stdout: "",
|
|
stderr: multilineError,
|
|
exit_code: 1,
|
|
},
|
|
],
|
|
},
|
|
},
|
|
codex: {
|
|
delayMs: 200,
|
|
result: {
|
|
success: true,
|
|
steps: [
|
|
{
|
|
step: "adapter",
|
|
command: "npm install -g @zed-industries/codex-acp",
|
|
success: true,
|
|
stdout: "added 1 package",
|
|
stderr: "",
|
|
exit_code: 0,
|
|
},
|
|
],
|
|
},
|
|
},
|
|
},
|
|
acpRuntimesCatalogAfterInstall: [
|
|
runtime("claude", "adapter_missing", { status: "unknown" }),
|
|
runtime("codex", "available", { status: "logged_in" }),
|
|
],
|
|
},
|
|
{ skipCommunitySeed: true, skipOnboardingSeed: true },
|
|
);
|
|
await page.goto("/");
|
|
await navigateToSetupPage(page);
|
|
|
|
const claudeInstall = page.getByTestId("onboarding-runtime-install-claude");
|
|
const codexInstall = page.getByTestId("onboarding-runtime-install-codex");
|
|
|
|
// Start both installs before either settles.
|
|
await claudeInstall.click();
|
|
await codexInstall.click();
|
|
|
|
// While in flight: both install buttons must be absent (no duplicate clicks).
|
|
await expect(claudeInstall).toHaveCount(0);
|
|
await expect(codexInstall).toHaveCount(0);
|
|
|
|
// Codex settles first (shorter delay): success indicator, no error.
|
|
await expect(page.getByTestId("onboarding-runtime-ready-codex")).toBeVisible({
|
|
timeout: 3_000,
|
|
});
|
|
await expect(page.getByTestId("onboarding-runtime-error-codex")).toHaveCount(
|
|
0,
|
|
);
|
|
|
|
// Claude still in flight: its install button must still be absent.
|
|
await expect(claudeInstall).toHaveCount(0);
|
|
|
|
// Claude settles: failure error visible; codex still shows ready (not reset).
|
|
const claudeError = page.getByTestId("onboarding-runtime-error-claude");
|
|
await expect(claudeError).toBeVisible({ timeout: 3_000 });
|
|
await expect(
|
|
page.getByTestId("onboarding-runtime-ready-codex"),
|
|
).toBeVisible();
|
|
await expect(page.getByTestId("onboarding-runtime-error-codex")).toHaveCount(
|
|
0,
|
|
);
|
|
|
|
// The error trigger has the full aria-label (label + detail).
|
|
await expect(claudeError).toHaveAttribute("aria-label", /npm ERR!/);
|
|
// Open the tooltip and verify the detail span handles overflow correctly:
|
|
// • vertical overflow exists and is scrollable (max-h-48 + overflow-y-auto)
|
|
// • no horizontal overflow (break-words forces the long unbroken path to wrap)
|
|
await claudeError.focus();
|
|
const tooltip = page.getByRole("tooltip");
|
|
await expect(tooltip).toBeVisible({ timeout: 2_000 });
|
|
await expect(tooltip).toContainText("npm ERR! code EACCES");
|
|
await expect(tooltip).toContainText("Hint: Run as Administrator");
|
|
|
|
// Locate the scroll container using page-level locator since Radix portals
|
|
// can place content outside the tooltip role element's subtree in the DOM.
|
|
// Use .first() because Radix keeps a hidden duplicate in the light DOM.
|
|
const detailSpan = page.locator("span.overflow-y-auto").first();
|
|
await expect(detailSpan).toBeVisible();
|
|
|
|
// Vertical: scrollHeight must exceed clientHeight (content taller than max-h-48).
|
|
// Scroll position must advance when set, proving scrollability.
|
|
const isVerticallyScrollable = await detailSpan.evaluate((el) => {
|
|
return el.scrollHeight > el.clientHeight;
|
|
});
|
|
expect(isVerticallyScrollable).toBe(true);
|
|
|
|
// Confirm scroll position can actually advance.
|
|
await detailSpan.evaluate((el) => {
|
|
el.scrollTop = 9999;
|
|
});
|
|
const scrolledDown = await detailSpan.evaluate((el) => el.scrollTop > 0);
|
|
expect(scrolledDown).toBe(true);
|
|
|
|
// Horizontal: break-words must prevent horizontal overflow.
|
|
const hasHorizontalOverflow = await detailSpan.evaluate((el) => {
|
|
return el.scrollWidth > el.clientWidth;
|
|
});
|
|
expect(hasHorizontalOverflow).toBe(false);
|
|
});
|
|
|
|
test("Finish stays disabled until a provider-required harness is fully configured", async ({
|
|
page,
|
|
}) => {
|
|
await installMockBridge(
|
|
page,
|
|
{
|
|
acpRuntimesCatalog: [
|
|
runtime("buzz-agent", "available", { status: "not_applicable" }),
|
|
],
|
|
discoverAgentModels: {
|
|
models: [{ id: "claude-sonnet-4", name: "Claude Sonnet 4" }],
|
|
supportsSwitching: true,
|
|
},
|
|
globalAgentConfig: {
|
|
env_vars: {},
|
|
provider: null,
|
|
model: null,
|
|
preferred_runtime: null,
|
|
},
|
|
},
|
|
{ skipCommunitySeed: true, skipOnboardingSeed: true },
|
|
);
|
|
await page.goto("/");
|
|
await navigateToSetupPage(page);
|
|
await page.getByTestId("onboarding-setup-next").click();
|
|
await expect(page.getByTestId("onboarding-page-config")).toBeVisible();
|
|
|
|
// buzz-agent auto-selects as the only ready harness, but with no provider
|
|
// configured the default is not launchable — Finish must be gated.
|
|
await expect(page.getByTestId("global-agent-default-harness")).toHaveText(
|
|
"Buzz",
|
|
);
|
|
const finish = page.getByTestId("onboarding-finish");
|
|
await expect(finish).toBeDisabled();
|
|
|
|
// Configure provider + credential; model resolves via discovery/fallback.
|
|
await page.getByTestId("global-agent-provider").click();
|
|
await page.getByTestId("global-agent-provider-option-anthropic").click();
|
|
await page.getByTestId("persona-provider-api-key").fill("sk-test-key");
|
|
|
|
await expect(finish).toBeEnabled();
|
|
await finish.click();
|
|
await expect(page.getByText("Join or create a community")).toBeVisible();
|
|
expect(await readSavedRuntime(page)).toBe("buzz-agent");
|
|
});
|
|
|
|
test("baked build config keeps Finish enabled without manual provider setup", async ({
|
|
page,
|
|
}) => {
|
|
await installMockBridge(
|
|
page,
|
|
{
|
|
acpRuntimesCatalog: [
|
|
runtime("buzz-agent", "available", { status: "not_applicable" }),
|
|
],
|
|
bakedBuildEnv: [
|
|
{ key: "BUZZ_AGENT_PROVIDER", masked: false, value: "databricks_v2" },
|
|
{
|
|
key: "DATABRICKS_HOST",
|
|
masked: false,
|
|
value: "https://example.cloud.databricks.com",
|
|
},
|
|
{ key: "DATABRICKS_MODEL", masked: false, value: "baked-model" },
|
|
],
|
|
globalAgentConfig: {
|
|
env_vars: {},
|
|
provider: null,
|
|
model: null,
|
|
preferred_runtime: null,
|
|
},
|
|
},
|
|
{ skipCommunitySeed: true, skipOnboardingSeed: true },
|
|
);
|
|
await page.goto("/");
|
|
await navigateToSetupPage(page);
|
|
await page.getByTestId("onboarding-setup-next").click();
|
|
await expect(page.getByTestId("onboarding-page-config")).toBeVisible();
|
|
|
|
// Internal builds bake provider/model/credentials — the gate must treat
|
|
// baked config as complete and never block Finish.
|
|
await expect(page.getByTestId("global-agent-default-harness")).toHaveText(
|
|
"Buzz",
|
|
);
|
|
await expect(page.getByTestId("onboarding-finish")).toBeEnabled();
|
|
});
|