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>
1023 lines
35 KiB
TypeScript
1023 lines
35 KiB
TypeScript
import { expect, test } from "@playwright/test";
|
|
|
|
import { waitForAnimations } from "../helpers/animations";
|
|
import { installMockBridge, TEST_IDENTITIES } from "../helpers/bridge";
|
|
|
|
const SHOTS = "test-results/global-agent-config";
|
|
|
|
/**
|
|
* Open Settings → Agents through the app UI and wait for the defaults card to
|
|
* load. CI serves the built SPA with a static file server, so navigating to
|
|
* `/settings` directly returns a 404 before the client router can start.
|
|
*/
|
|
async function openAiDefaultsSettings(page: import("@playwright/test").Page) {
|
|
await page.goto("/", { waitUntil: "domcontentloaded" });
|
|
await page.getByTestId("open-settings").click();
|
|
await page.getByTestId("profile-popover-settings").click();
|
|
await expect(page.getByTestId("settings-view")).toBeVisible();
|
|
await page.getByTestId("settings-nav-agents").click();
|
|
await expect(page.getByTestId("settings-global-agent-config")).toBeVisible({
|
|
timeout: 10_000,
|
|
});
|
|
await expect(page.locator(".animate-spin").first()).not.toBeVisible({
|
|
timeout: 5_000,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Navigate to the Agents view and open the Create Agent dialog via the
|
|
* "New agent" menu item, then fill a placeholder name.
|
|
*/
|
|
async function openCreateDialog(page: import("@playwright/test").Page) {
|
|
await page.goto("/");
|
|
await page.getByTestId("open-agents-view").click();
|
|
await page.getByTestId("new-agent-card").click();
|
|
await page.locator("#persona-display-name").fill("Test Agent");
|
|
}
|
|
|
|
async function customizeAgentAi(page: import("@playwright/test").Page) {
|
|
await page.getByRole("tab", { name: "Customize for this agent" }).click();
|
|
}
|
|
|
|
/**
|
|
* Pick an option from a PersonaDropdownField (menu-based, not a native
|
|
* <select>): focus the trigger, open it, then click the matching
|
|
* menuitemradio. Mirrors the helper in agent-readiness-screenshots.spec.ts.
|
|
*/
|
|
async function selectDropdownOption(
|
|
page: import("@playwright/test").Page,
|
|
trigger: import("@playwright/test").Locator,
|
|
optionName: string | RegExp,
|
|
) {
|
|
await expect(trigger).toBeVisible({ timeout: 10_000 });
|
|
await trigger.press("Enter");
|
|
await page
|
|
.getByRole("menuitemradio", { name: optionName })
|
|
.click({ timeout: 5_000 });
|
|
}
|
|
|
|
// A runtime catalog with both a provider-selection runtime (buzz-agent) and a
|
|
// CLI-login runtime (Claude Code) marked available, so Claude Code appears and
|
|
// is selectable in the harness dropdown. Same shape the readiness spec uses.
|
|
const CATALOG_WITH_CLAUDE = [
|
|
{
|
|
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: "Ships with the Buzz desktop app.",
|
|
install_instructions_url: "https://github.com/block/buzz",
|
|
can_auto_install: false,
|
|
underlying_cli_path: null,
|
|
},
|
|
{
|
|
id: "claude",
|
|
label: "Claude Code",
|
|
avatar_url: "",
|
|
availability: "available",
|
|
command: "/usr/local/bin/claude-agent",
|
|
binary_path: "/usr/local/bin/claude-agent",
|
|
default_args: ["acp"],
|
|
mcp_command: null,
|
|
install_hint: "Install the Claude Code ACP adapter via npm.",
|
|
install_instructions_url:
|
|
"https://www.npmjs.com/package/@anthropic-ai/claude-agent-acp",
|
|
can_auto_install: true,
|
|
underlying_cli_path: "/usr/local/bin/claude",
|
|
},
|
|
];
|
|
|
|
// A runtime catalog with Codex marked available (the default catalog ships it
|
|
// as `not_installed`). Codex is a CLI-login runtime — it drives its own
|
|
// provider, so the definition dialog hides the provider picker for it. Used by
|
|
// the Edit/Save-mode test to seed an editable Codex agent.
|
|
const CATALOG_WITH_CODEX = [
|
|
{
|
|
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: "Ships with the Buzz desktop app.",
|
|
install_instructions_url: "https://github.com/block/buzz",
|
|
can_auto_install: false,
|
|
underlying_cli_path: null,
|
|
},
|
|
{
|
|
id: "codex",
|
|
label: "Codex",
|
|
avatar_url: "",
|
|
availability: "available",
|
|
command: "/usr/local/bin/codex-agent",
|
|
binary_path: "/usr/local/bin/codex-agent",
|
|
default_args: ["acp"],
|
|
mcp_command: null,
|
|
install_hint: "The codex-acp adapter must be built from source.",
|
|
install_instructions_url: "https://github.com/openai/codex",
|
|
can_auto_install: false,
|
|
underlying_cli_path: "/usr/local/bin/codex",
|
|
},
|
|
];
|
|
|
|
// A catalog where every runtime is unavailable (not installed). With nothing
|
|
// available, getDefaultPersonaRuntime returns null, so the definition dialog's
|
|
// runtime auto-seed effect is a no-op and a runtime-less definition keeps its
|
|
// empty runtime — the precondition for blankRuntimeModelProviderEditable.
|
|
const CATALOG_NONE_AVAILABLE = [
|
|
{
|
|
id: "buzz-agent",
|
|
label: "Buzz Agent",
|
|
avatar_url: "",
|
|
availability: "not_installed",
|
|
command: "buzz-agent",
|
|
binary_path: null,
|
|
default_args: [],
|
|
mcp_command: "buzz-dev-mcp",
|
|
install_hint: "Ships with the Buzz desktop app.",
|
|
install_instructions_url: "https://github.com/block/buzz",
|
|
can_auto_install: false,
|
|
underlying_cli_path: null,
|
|
},
|
|
{
|
|
id: "goose",
|
|
label: "Goose",
|
|
avatar_url: "",
|
|
availability: "not_installed",
|
|
command: "goose",
|
|
binary_path: null,
|
|
default_args: [],
|
|
mcp_command: null,
|
|
install_hint: "Install Goose to use this runtime.",
|
|
install_instructions_url: "https://github.com/block/goose",
|
|
can_auto_install: false,
|
|
underlying_cli_path: null,
|
|
},
|
|
];
|
|
|
|
test.describe("global agent config screenshots", () => {
|
|
test.use({ viewport: { width: 1280, height: 900 } });
|
|
|
|
test.beforeEach(async ({ page }) => {
|
|
page.on("pageerror", (err) => {
|
|
console.error(
|
|
"PAGE ERROR:",
|
|
err.message,
|
|
err.stack?.split("\n").slice(0, 5).join("\n"),
|
|
);
|
|
});
|
|
});
|
|
|
|
// Shot 01: AgentDefaultsSettingsCard populated with provider + model +
|
|
// env var — shows the "Agent defaults" card in the Agents view as it looks
|
|
// when a user has set global defaults.
|
|
test("01-global-agent-config-card-populated", async ({ page }) => {
|
|
await installMockBridge(page, {
|
|
globalAgentConfig: {
|
|
provider: "anthropic",
|
|
model: "claude-opus-4-5",
|
|
env_vars: { ANTHROPIC_API_KEY: "sk-ant-placeholder" },
|
|
},
|
|
});
|
|
|
|
await openAiDefaultsSettings(page);
|
|
|
|
const card = page.getByTestId("settings-global-agent-config");
|
|
await card.scrollIntoViewIfNeeded();
|
|
await waitForAnimations(page);
|
|
|
|
await card.screenshot({
|
|
path: `${SHOTS}/01-global-agent-config-card-populated.png`,
|
|
});
|
|
});
|
|
|
|
test("settings renders and saves the persisted preferred harness", async ({
|
|
page,
|
|
}) => {
|
|
await installMockBridge(page, {
|
|
globalAgentConfig: {
|
|
preferred_runtime: "claude",
|
|
provider: null,
|
|
model: null,
|
|
env_vars: {},
|
|
},
|
|
acpRuntimesCatalog: [...CATALOG_WITH_CLAUDE, CATALOG_WITH_CODEX[1]],
|
|
});
|
|
|
|
await openAiDefaultsSettings(page);
|
|
|
|
const harness = page.getByTestId("global-agent-default-harness");
|
|
await expect(harness).toHaveText("Claude Code");
|
|
await expect(page.getByText("Provider", { exact: true })).toHaveCount(0);
|
|
await expect(page.locator("#global-agent-model")).toBeVisible();
|
|
|
|
// Make the form dirty, then return to Claude with no model override. The
|
|
// harness-native default keeps the now-actionable Save button enabled.
|
|
await harness.press("Enter");
|
|
await page.getByTestId("global-agent-default-harness-option-codex").click();
|
|
await harness.press("Enter");
|
|
await page
|
|
.getByTestId("global-agent-default-harness-option-claude")
|
|
.click();
|
|
await waitForAnimations(page);
|
|
await expect(page.getByTestId("global-agent-model")).toHaveText(
|
|
/Default model/,
|
|
);
|
|
await expect(
|
|
page.getByRole("button", { name: "Save defaults" }),
|
|
).toBeEnabled();
|
|
|
|
await harness.press("Enter");
|
|
await page.getByTestId("global-agent-default-harness-option-codex").click();
|
|
const model = page.getByTestId("global-agent-model");
|
|
await model.click();
|
|
await page.getByTestId("global-agent-model-option-gpt-5.5[high]").click();
|
|
await page.getByRole("button", { name: "Save defaults" }).click();
|
|
|
|
const saved = await page.evaluate(async () =>
|
|
(
|
|
window as typeof window & {
|
|
__BUZZ_E2E_INVOKE_MOCK_COMMAND__?: (
|
|
command: string,
|
|
payload: unknown,
|
|
) => Promise<unknown>;
|
|
}
|
|
).__BUZZ_E2E_INVOKE_MOCK_COMMAND__?.("get_global_agent_config", null),
|
|
);
|
|
expect(saved).toMatchObject({ preferred_runtime: "codex" });
|
|
});
|
|
|
|
test("defaults honor credentials set in the harness config file", async ({
|
|
page,
|
|
}) => {
|
|
await installMockBridge(page, {
|
|
globalAgentConfig: {
|
|
preferred_runtime: "goose",
|
|
provider: "databricks_v2",
|
|
model: "goose-claude-4-6-opus",
|
|
env_vars: {},
|
|
},
|
|
runtimeFileConfigs: {
|
|
goose: {
|
|
provider: "databricks_v2",
|
|
model: "goose-claude-4-6-opus",
|
|
satisfiedEnvKeys: ["DATABRICKS_HOST"],
|
|
},
|
|
},
|
|
});
|
|
|
|
await openAiDefaultsSettings(page);
|
|
|
|
const advanced = page.getByTestId("global-agent-advanced-toggle");
|
|
await expect(advanced).toHaveAttribute("aria-expanded", "false");
|
|
await expect(
|
|
page.getByTestId("global-agent-advanced-required-badge"),
|
|
).toHaveCount(0);
|
|
|
|
await page.getByTestId("global-agent-model").click();
|
|
await page.getByTestId("global-agent-model-option-gpt-5.5").click();
|
|
await expect(
|
|
page.getByRole("button", { name: "Save defaults" }),
|
|
).toBeEnabled();
|
|
|
|
await advanced.click();
|
|
await expect(page.getByTestId("env-vars-file-satisfied-key")).toHaveText(
|
|
"DATABRICKS_HOST",
|
|
);
|
|
});
|
|
|
|
test("02-create-global-provider-shows-top-level-api-key", async ({
|
|
page,
|
|
}) => {
|
|
await installMockBridge(page, {
|
|
globalAgentConfig: {
|
|
provider: "anthropic",
|
|
model: null,
|
|
env_vars: {},
|
|
},
|
|
});
|
|
|
|
await openCreateDialog(page);
|
|
await customizeAgentAi(page);
|
|
|
|
await expect(
|
|
page
|
|
.getByTestId("agent-custom-configuration-section")
|
|
.locator("#persona-runtime"),
|
|
).toBeVisible();
|
|
await expect(page.getByLabel("Anthropic API Key")).toBeVisible({
|
|
timeout: 10_000,
|
|
});
|
|
await expect(
|
|
page.getByRole("button", { name: "Advanced", exact: true }),
|
|
).toHaveAttribute("aria-expanded", "false");
|
|
await expect(page.getByTestId("env-vars-required-key")).not.toBeVisible();
|
|
|
|
await waitForAnimations(page);
|
|
await page.getByRole("dialog").screenshot({
|
|
path: `${SHOTS}/02-create-custom-agent-configuration.png`,
|
|
});
|
|
|
|
const advanced = page.getByRole("button", {
|
|
name: "Advanced",
|
|
exact: true,
|
|
});
|
|
await selectDropdownOption(
|
|
page,
|
|
page.locator("#persona-llm-provider"),
|
|
"Databricks v2",
|
|
);
|
|
await expect(advanced).toHaveAttribute("aria-expanded", "false");
|
|
await expect(
|
|
page.getByTestId("persona-advanced-required-badge"),
|
|
).toHaveText("Required");
|
|
await advanced.click();
|
|
await expect(advanced).toHaveAttribute("aria-expanded", "true");
|
|
});
|
|
|
|
test("03-global-env-satisfies-required-key", async ({ page }) => {
|
|
await installMockBridge(page, {
|
|
globalAgentConfig: {
|
|
provider: "anthropic",
|
|
model: "claude-opus-4-5",
|
|
env_vars: { ANTHROPIC_API_KEY: "sk-ant-global-value" },
|
|
},
|
|
});
|
|
|
|
await openCreateDialog(page);
|
|
await customizeAgentAi(page);
|
|
|
|
await expect(page.getByLabel("Anthropic API Key")).toHaveAttribute(
|
|
"placeholder",
|
|
"Inherited from global config",
|
|
);
|
|
await expect(page.getByTestId("env-vars-required-key")).not.toBeVisible({
|
|
timeout: 5_000,
|
|
});
|
|
await expect(page.getByTestId("persona-dialog-submit")).toBeEnabled({
|
|
timeout: 5_000,
|
|
});
|
|
});
|
|
|
|
test("06-baked-defaults-labels-appear-in-create-dialog", async ({ page }) => {
|
|
await installMockBridge(page, {
|
|
bakedBuildEnv: [
|
|
{
|
|
key: "BUZZ_AGENT_PROVIDER",
|
|
value: "anthropic",
|
|
masked: false,
|
|
},
|
|
{
|
|
key: "BUZZ_AGENT_MODEL",
|
|
value: "claude-opus-4-8",
|
|
masked: false,
|
|
},
|
|
{
|
|
key: "BUZZ_AGENT_THINKING_EFFORT",
|
|
value: "high",
|
|
masked: false,
|
|
},
|
|
{
|
|
key: "ANTHROPIC_API_KEY",
|
|
value: "sk-ant-baked-test",
|
|
masked: true,
|
|
},
|
|
],
|
|
});
|
|
|
|
await openCreateDialog(page);
|
|
|
|
const defaults = page.getByTestId("agent-ai-defaults-notice");
|
|
await expect(
|
|
defaults.getByText("Anthropic", { exact: true }),
|
|
).toBeVisible();
|
|
await expect(
|
|
defaults.getByText("claude-opus-4-8", { exact: true }),
|
|
).toBeVisible();
|
|
await expect(page.locator("#persona-llm-provider")).not.toBeVisible();
|
|
await expect(page.locator("#persona-model")).not.toBeVisible();
|
|
});
|
|
|
|
test("07-explicit-global-defaults-override-baked-labels", async ({
|
|
page,
|
|
}) => {
|
|
await installMockBridge(page, {
|
|
globalAgentConfig: {
|
|
provider: "anthropic",
|
|
model: "claude-opus-4-5",
|
|
env_vars: { BUZZ_AGENT_THINKING_EFFORT: "low" },
|
|
},
|
|
bakedBuildEnv: [
|
|
{
|
|
key: "BUZZ_AGENT_PROVIDER",
|
|
value: "databricks_v2",
|
|
masked: false,
|
|
},
|
|
{ key: "BUZZ_AGENT_MODEL", value: "build-model", masked: false },
|
|
{
|
|
key: "BUZZ_AGENT_THINKING_EFFORT",
|
|
value: "high",
|
|
masked: false,
|
|
},
|
|
{
|
|
key: "ANTHROPIC_API_KEY",
|
|
value: "sk-ant-baked-test",
|
|
masked: true,
|
|
},
|
|
],
|
|
});
|
|
|
|
await openCreateDialog(page);
|
|
|
|
const defaults = page.getByTestId("agent-ai-defaults-notice");
|
|
await expect(
|
|
defaults.getByText("Anthropic", { exact: true }),
|
|
).toBeVisible();
|
|
await expect(
|
|
defaults.getByText("claude-opus-4-5", { exact: true }),
|
|
).toBeVisible();
|
|
await expect(page.locator("#persona-llm-provider")).not.toBeVisible();
|
|
await expect(page.locator("#persona-model")).not.toBeVisible();
|
|
});
|
|
|
|
// Shot 04: Create gate BLOCKED — no per-agent provider, no global provider
|
|
// set → submit button disabled (provider-default rule, Test 5 / shots 01/08
|
|
// from agent-readiness-screenshots.spec.ts).
|
|
test("04-create-blocked-no-provider-no-global", async ({ page }) => {
|
|
// Default mock bridge has no global provider.
|
|
await installMockBridge(page);
|
|
|
|
await openCreateDialog(page);
|
|
|
|
// Provider empty + no global provider → submit BLOCKED.
|
|
await expect(page.getByTestId("persona-dialog-submit")).toBeDisabled({
|
|
timeout: 10_000,
|
|
});
|
|
|
|
const defaults = page.getByTestId("agent-ai-defaults-notice");
|
|
await expect(defaults).toContainText("Global defaults not set");
|
|
await expect(defaults.getByText("Harness", { exact: true })).toHaveCount(0);
|
|
await expect(defaults.getByText("Provider", { exact: true })).toHaveCount(
|
|
0,
|
|
);
|
|
await expect(defaults.getByText("Model", { exact: true })).toHaveCount(0);
|
|
|
|
const setDefaults = defaults.getByRole("button", {
|
|
name: "Set",
|
|
});
|
|
await expect(setDefaults).toBeVisible();
|
|
await expect(page.getByTestId("persona-dialog-submit-reason")).toHaveCount(
|
|
0,
|
|
);
|
|
|
|
await setDefaults.click();
|
|
const defaultsDialog = page.getByTestId("agent-ai-defaults-dialog");
|
|
await expect(defaultsDialog).toBeVisible();
|
|
await expect(
|
|
defaultsDialog.getByTestId("global-agent-config-fields"),
|
|
).toBeVisible();
|
|
await expect(defaultsDialog.getByTestId("global-agent-model")).toHaveCount(
|
|
0,
|
|
);
|
|
|
|
const harness = defaultsDialog.getByTestId("global-agent-default-harness");
|
|
await expect(harness).toHaveText("Buzz Agent");
|
|
const provider = defaultsDialog.getByTestId("global-agent-provider");
|
|
await expect(provider).toBeVisible();
|
|
await waitForAnimations(page);
|
|
const providerHeight = (await defaultsDialog.boundingBox())?.height ?? 0;
|
|
|
|
await provider.click();
|
|
await page.getByTestId("global-agent-provider-option-anthropic").click();
|
|
await expect(
|
|
defaultsDialog.getByTestId("global-agent-model"),
|
|
).toBeVisible();
|
|
for (const field of [
|
|
harness,
|
|
provider,
|
|
defaultsDialog.getByTestId("global-agent-model"),
|
|
defaultsDialog.getByTestId("global-agent-thinking-effort-select"),
|
|
]) {
|
|
await expect(field).toHaveClass(/h-11/);
|
|
await expect(field).toHaveClass(/rounded-xl/);
|
|
await expect(field).toHaveClass(/bg-muted\/40/);
|
|
await expect(field).toHaveClass(/shadow-none/);
|
|
}
|
|
await waitForAnimations(page);
|
|
const configuredHeight = (await defaultsDialog.boundingBox())?.height ?? 0;
|
|
expect(configuredHeight).toBeGreaterThan(providerHeight);
|
|
await expect(
|
|
defaultsDialog.getByTestId("global-agent-config-fields"),
|
|
).not.toHaveClass(/bg-muted\/20/);
|
|
const harnessBox = await defaultsDialog
|
|
.getByTestId("global-agent-default-harness")
|
|
.boundingBox();
|
|
const providerBox = await defaultsDialog
|
|
.getByTestId("global-agent-provider")
|
|
.boundingBox();
|
|
expect(harnessBox?.x).toBe(providerBox?.x);
|
|
expect(harnessBox?.width).toBe(providerBox?.width);
|
|
await waitForAnimations(page);
|
|
await defaultsDialog.screenshot({
|
|
path: `${SHOTS}/04-global-defaults-dialog-flat.png`,
|
|
});
|
|
const advanced = defaultsDialog.getByTestId("global-agent-advanced-toggle");
|
|
await provider.click();
|
|
await page
|
|
.getByTestId("global-agent-provider-option-databricks_v2")
|
|
.click();
|
|
await expect(advanced).toHaveAttribute("aria-expanded", "false");
|
|
const saveDefaults = defaultsDialog.getByRole("button", {
|
|
name: "Save defaults",
|
|
});
|
|
await expect(
|
|
defaultsDialog.getByTestId("global-agent-advanced-required-badge"),
|
|
).toHaveText("Required");
|
|
await expect(saveDefaults).toBeDisabled();
|
|
await advanced.click();
|
|
await expect(advanced).toHaveAttribute("aria-expanded", "true");
|
|
await defaultsDialog
|
|
.getByLabel("Value for DATABRICKS_HOST")
|
|
.fill("https://databricks.example.test");
|
|
await expect(saveDefaults).toBeEnabled();
|
|
await defaultsDialog
|
|
.getByRole("button", {
|
|
name: "Close",
|
|
})
|
|
.click();
|
|
await page.getByRole("button", { name: "Discard changes" }).click();
|
|
await expect(defaultsDialog).not.toBeVisible();
|
|
|
|
await waitForAnimations(page);
|
|
|
|
const dialog = page.getByRole("dialog");
|
|
await dialog.screenshot({
|
|
path: `${SHOTS}/04-create-blocked-no-provider-no-global.png`,
|
|
});
|
|
});
|
|
|
|
test("unset defaults persist the visible Buzz Agent fallback", async ({
|
|
page,
|
|
}) => {
|
|
await installMockBridge(page);
|
|
await openCreateDialog(page);
|
|
await page
|
|
.getByTestId("agent-ai-defaults-notice")
|
|
.getByRole("button", { name: "Set" })
|
|
.click();
|
|
|
|
const defaultsDialog = page.getByTestId("agent-ai-defaults-dialog");
|
|
await expect(
|
|
defaultsDialog.getByTestId("global-agent-default-harness"),
|
|
).toHaveText("Buzz Agent");
|
|
|
|
await defaultsDialog.getByTestId("global-agent-provider").click();
|
|
await page.getByTestId("global-agent-provider-option-anthropic").click();
|
|
await defaultsDialog.getByLabel("Anthropic API Key").fill("sk-ant-test");
|
|
await expect(
|
|
defaultsDialog.getByRole("button", { name: "Save defaults" }),
|
|
).toBeEnabled();
|
|
await defaultsDialog.getByRole("button", { name: "Save defaults" }).click();
|
|
await expect(defaultsDialog).not.toBeVisible();
|
|
|
|
const saved = await page.evaluate(async () =>
|
|
(
|
|
window as typeof window & {
|
|
__BUZZ_E2E_INVOKE_MOCK_COMMAND__?: (
|
|
command: string,
|
|
payload: unknown,
|
|
) => Promise<unknown>;
|
|
}
|
|
).__BUZZ_E2E_INVOKE_MOCK_COMMAND__?.("get_global_agent_config", null),
|
|
);
|
|
expect(saved).toMatchObject({
|
|
preferred_runtime: "buzz-agent",
|
|
provider: "anthropic",
|
|
});
|
|
});
|
|
|
|
test("create defaults follow a preferred harness saved while open", async ({
|
|
page,
|
|
}) => {
|
|
await installMockBridge(page, {
|
|
acpRuntimesCatalog: CATALOG_WITH_CLAUDE,
|
|
globalAgentConfig: {
|
|
preferred_runtime: "buzz-agent",
|
|
provider: "anthropic",
|
|
model: "claude-opus-4-5",
|
|
env_vars: { ANTHROPIC_API_KEY: "sk-ant-global-value" },
|
|
},
|
|
});
|
|
await openCreateDialog(page);
|
|
|
|
const defaults = page.getByTestId("agent-ai-defaults-notice");
|
|
await expect(defaults).toContainText("Buzz Agent");
|
|
await defaults
|
|
.getByRole("button", { name: "Edit global defaults" })
|
|
.click();
|
|
|
|
const defaultsDialog = page.getByTestId("agent-ai-defaults-dialog");
|
|
const harness = defaultsDialog.getByTestId("global-agent-default-harness");
|
|
await harness.press("Enter");
|
|
await page
|
|
.getByTestId("global-agent-default-harness-option-claude")
|
|
.click();
|
|
await defaultsDialog.getByRole("button", { name: "Save defaults" }).click();
|
|
await expect(defaultsDialog).not.toBeVisible();
|
|
|
|
const harnessDefaults = page.getByTestId("agent-harness-defaults-notice");
|
|
await expect(harnessDefaults).toContainText("Claude Code");
|
|
await expect(page.getByTestId("persona-dialog-submit")).toBeEnabled();
|
|
await page.getByTestId("persona-dialog-submit").click();
|
|
|
|
await expect
|
|
.poll(() =>
|
|
page.evaluate(() => {
|
|
const log = (
|
|
window as Window & {
|
|
__BUZZ_E2E_COMMAND_LOG__?: Array<{
|
|
command: string;
|
|
payload: { input?: Record<string, unknown> };
|
|
}>;
|
|
}
|
|
).__BUZZ_E2E_COMMAND_LOG__;
|
|
const createPayload = log?.find(
|
|
(entry) => entry.command === "create_persona",
|
|
)?.payload.input;
|
|
return createPayload?.runtime;
|
|
}),
|
|
)
|
|
.toBe("claude");
|
|
});
|
|
|
|
test("missing global credentials show the unset defaults notice", async ({
|
|
page,
|
|
}) => {
|
|
await installMockBridge(page, {
|
|
globalAgentConfig: {
|
|
preferred_runtime: "buzz-agent",
|
|
provider: "anthropic",
|
|
model: "claude-opus-4-5",
|
|
env_vars: {},
|
|
},
|
|
});
|
|
await openCreateDialog(page);
|
|
|
|
const defaults = page.getByTestId("agent-ai-defaults-notice");
|
|
await expect(defaults).toContainText("Global defaults not set");
|
|
await expect(defaults.getByRole("button", { name: "Set" })).toBeVisible();
|
|
await expect(defaults.getByText("Harness", { exact: true })).toHaveCount(0);
|
|
await expect(defaults.getByText("Provider", { exact: true })).toHaveCount(
|
|
0,
|
|
);
|
|
await expect(defaults.getByText("Model", { exact: true })).toHaveCount(0);
|
|
await expect(page.getByTestId("persona-dialog-submit")).toBeDisabled();
|
|
});
|
|
|
|
test("create exposes setup guidance when no harness is available", async ({
|
|
page,
|
|
}) => {
|
|
await installMockBridge(page, {
|
|
acpRuntimesCatalog: CATALOG_NONE_AVAILABLE,
|
|
});
|
|
await openCreateDialog(page);
|
|
|
|
const customSection = page.getByTestId(
|
|
"agent-custom-configuration-section",
|
|
);
|
|
const harness = customSection.locator("#persona-runtime");
|
|
await expect(harness).toBeVisible();
|
|
await expect(harness).toContainText("Choose a harness");
|
|
|
|
await selectDropdownOption(page, harness, "Buzz Agent (not installed)");
|
|
await expect(
|
|
customSection
|
|
.locator("p")
|
|
.filter({ hasText: "Buzz Agent is not installed." }),
|
|
).toContainText(
|
|
"Buzz Agent is not installed. Ships with the Buzz desktop app. Visit Settings > Agents to set it up.",
|
|
);
|
|
await expect(page.getByTestId("persona-dialog-submit")).toBeDisabled();
|
|
});
|
|
|
|
test("create with a missing name has no footer message", async ({ page }) => {
|
|
await installMockBridge(page);
|
|
|
|
await page.goto("/");
|
|
await page.getByTestId("open-agents-view").click();
|
|
await page.getByTestId("new-agent-card").click();
|
|
|
|
await expect(page.getByTestId("persona-dialog-submit")).toBeDisabled({
|
|
timeout: 10_000,
|
|
});
|
|
await expect(page.getByTestId("persona-dialog-submit-reason")).toHaveCount(
|
|
0,
|
|
);
|
|
});
|
|
|
|
// Shot 05: Create gate ENABLED — global provider = anthropic provides a
|
|
// default, so the empty per-agent provider is resolved → submit enabled.
|
|
test("05-create-enabled-with-global-provider", async ({ page }) => {
|
|
await installMockBridge(page, {
|
|
globalAgentConfig: {
|
|
provider: "anthropic",
|
|
model: "claude-opus-4-5",
|
|
env_vars: { ANTHROPIC_API_KEY: "sk-ant-global-value" },
|
|
},
|
|
});
|
|
|
|
await openCreateDialog(page);
|
|
|
|
const defaultsSection = page.getByTestId(
|
|
"agent-defaults-configuration-section",
|
|
);
|
|
await expect(defaultsSection.locator("#persona-runtime")).toHaveCount(0);
|
|
await expect(
|
|
defaultsSection.getByTestId("agent-ai-defaults-notice"),
|
|
).toBeVisible();
|
|
await expect(
|
|
defaultsSection.getByText("Harness", { exact: true }),
|
|
).toBeVisible();
|
|
await expect(
|
|
defaultsSection.getByText("Buzz Agent", { exact: true }),
|
|
).toBeVisible();
|
|
|
|
// Global provider satisfies the provider-default rule → submit enabled.
|
|
await expect(page.getByTestId("persona-dialog-submit")).toBeEnabled({
|
|
timeout: 10_000,
|
|
});
|
|
// The reason is null exactly when the form can submit — no footer reason.
|
|
await expect(page.getByTestId("persona-dialog-submit-reason")).toHaveCount(
|
|
0,
|
|
);
|
|
|
|
await waitForAnimations(page);
|
|
|
|
const dialog = page.getByRole("dialog");
|
|
await dialog.screenshot({
|
|
path: `${SHOTS}/05-create-enabled-with-global-provider.png`,
|
|
});
|
|
});
|
|
|
|
// Shot 09: CLI-login runtime (Claude Code / Codex) drives its own provider,
|
|
// so the provider picker is intentionally hidden. This is Ian's regression:
|
|
// before the provider-aware gate, the hidden provider left the button
|
|
// permanently disabled with no explanation. Now the provider is not required,
|
|
// the button is enabled, and — critically — no spurious provider reason is
|
|
// shown in the footer. Create and Save share this rendering path.
|
|
test("09-cli-login-runtime-enabled-no-reason", async ({ page }) => {
|
|
await installMockBridge(page, {
|
|
acpRuntimesCatalog: CATALOG_WITH_CLAUDE,
|
|
});
|
|
|
|
await openCreateDialog(page);
|
|
|
|
// Harness selection belongs to the per-agent customization flow.
|
|
await customizeAgentAi(page);
|
|
await selectDropdownOption(
|
|
page,
|
|
page.locator("#persona-runtime"),
|
|
"Claude Code",
|
|
);
|
|
await page.getByRole("tab", { name: "Use harness defaults" }).click();
|
|
|
|
// Provider picker hidden — the runtime drives its own provider.
|
|
await expect(page.locator("#persona-llm-provider")).not.toBeVisible();
|
|
// The hidden provider must not block submit, and must not surface a reason.
|
|
await expect(page.getByTestId("persona-dialog-submit")).toBeEnabled({
|
|
timeout: 10_000,
|
|
});
|
|
await expect(page.getByTestId("persona-dialog-submit-reason")).toHaveCount(
|
|
0,
|
|
);
|
|
|
|
await waitForAnimations(page);
|
|
|
|
const dialog = page.getByRole("dialog");
|
|
await dialog.screenshot({
|
|
path: `${SHOTS}/09-cli-login-runtime-enabled-no-reason.png`,
|
|
});
|
|
});
|
|
|
|
// Shot 10: the ORIGINAL defect — Ian's "Save button stays disabled after
|
|
// editing an agent." This drives the real EDIT/Save path (not create): a
|
|
// persona-linked Codex agent with an explicit custom model and no provider is
|
|
// opened via the Agents view → profile → Edit affordance, which mounts
|
|
// AgentDefinitionDialog in edit mode (id present in initialValues, "Save
|
|
// changes" label). Before the provider-aware gate, the hidden Codex provider
|
|
// left Save permanently disabled on a value the user could never set. Now:
|
|
// provider picker hidden, Save enabled, and no submit-block reason. Create
|
|
// and Save share this rendering path, but the defect was Save-specific, so
|
|
// this exercises Save directly.
|
|
test("10-edit-codex-custom-model-save-enabled-no-reason", async ({
|
|
page,
|
|
}) => {
|
|
const PERSONA_ID = "persona-codex-edit-e2e";
|
|
await installMockBridge(page, {
|
|
acpRuntimesCatalog: CATALOG_WITH_CODEX,
|
|
managedAgents: [
|
|
{
|
|
pubkey: TEST_IDENTITIES.tyler.pubkey,
|
|
name: "Codex Editor",
|
|
personaId: PERSONA_ID,
|
|
status: "stopped",
|
|
channelNames: ["agents"],
|
|
},
|
|
],
|
|
personas: [
|
|
{
|
|
id: PERSONA_ID,
|
|
displayName: "Codex Editor",
|
|
systemPrompt: "You are the Codex edit-mode e2e persona.",
|
|
// CLI-login runtime with an explicit custom model and NO provider —
|
|
// the exact shape that used to pin Save disabled.
|
|
runtime: "codex",
|
|
model: "gpt-5-codex",
|
|
provider: null,
|
|
},
|
|
],
|
|
});
|
|
|
|
// Agents view → persona-grouped agent card → Edit quick action.
|
|
await page.goto("/");
|
|
await page.getByTestId("open-agents-view").click();
|
|
const agentButton = page.getByRole("button", {
|
|
name: "Codex Editor agent profile",
|
|
});
|
|
await expect(agentButton).toBeVisible({ timeout: 10_000 });
|
|
await agentButton.click();
|
|
await expect(page.getByTestId("user-profile-panel")).toBeVisible({
|
|
timeout: 10_000,
|
|
});
|
|
await page.getByTestId("user-profile-edit-agent").click();
|
|
|
|
// The definition dialog opens in EDIT mode ("Save changes"), seeded from
|
|
// the persona — confirm it's the edit path, not create.
|
|
await expect(page.getByTestId("persona-dialog")).toBeVisible({
|
|
timeout: 10_000,
|
|
});
|
|
await expect(page.locator("#persona-display-name")).toHaveValue(
|
|
"Codex Editor",
|
|
);
|
|
await expect(page.getByTestId("persona-dialog-submit")).toHaveText(
|
|
/Save changes/,
|
|
);
|
|
|
|
// The core assertions: Codex hides the provider picker, so the hidden
|
|
// provider must NOT block Save and must NOT surface a reason.
|
|
await expect(page.locator("#persona-llm-provider")).not.toBeVisible();
|
|
await expect(page.getByTestId("persona-dialog-submit")).toBeEnabled({
|
|
timeout: 10_000,
|
|
});
|
|
await expect(page.getByTestId("persona-dialog-submit-reason")).toHaveCount(
|
|
0,
|
|
);
|
|
|
|
await waitForAnimations(page);
|
|
|
|
const dialog = page.getByRole("dialog");
|
|
await dialog.screenshot({
|
|
path: `${SHOTS}/10-edit-codex-custom-model-save-enabled-no-reason.png`,
|
|
});
|
|
});
|
|
|
|
// Shot 11: the inverse of Ian's fix, and wesbillman's blocking review point.
|
|
// A runtime-LESS legacy/builtin definition (no runtime, but a saved model)
|
|
// still EXPOSES the provider picker via blankRuntimeModelProviderEditable, so
|
|
// an empty provider must keep Save DISABLED. The gate must key off the field's
|
|
// visibility (runtimeCanChooseLlmProvider), not the raw runtime capability —
|
|
// otherwise Save persists `provider: undefined` despite the visible picker.
|
|
// A global provider/model default keeps localMode satisfied, so the ONLY thing
|
|
// that can block Save here is the Customize-pair provider gate (step 7), which
|
|
// is exactly what this regression pins.
|
|
test("11-edit-runtime-less-provider-required-save-blocked", async ({
|
|
page,
|
|
}) => {
|
|
const PERSONA_ID = "persona-runtime-less-edit-e2e";
|
|
await installMockBridge(page, {
|
|
// No runtime is available, so getDefaultPersonaRuntime returns null and
|
|
// the dialog does NOT auto-seed a runtime on open — the runtime-less
|
|
// definition stays runtime-less, which is the only state where
|
|
// blankRuntimeModelProviderEditable exposes the provider picker.
|
|
acpRuntimesCatalog: CATALOG_NONE_AVAILABLE,
|
|
// Global defaults satisfy localMode, so any block is the pair gate alone.
|
|
globalAgentConfig: {
|
|
provider: "anthropic",
|
|
model: "claude-opus-4-5",
|
|
env_vars: { ANTHROPIC_API_KEY: "sk-ant-global-value" },
|
|
},
|
|
managedAgents: [
|
|
{
|
|
pubkey: TEST_IDENTITIES.tyler.pubkey,
|
|
name: "Legacy Editor",
|
|
personaId: PERSONA_ID,
|
|
status: "stopped",
|
|
channelNames: ["agents"],
|
|
},
|
|
],
|
|
personas: [
|
|
{
|
|
id: PERSONA_ID,
|
|
displayName: "Legacy Editor",
|
|
systemPrompt: "You are the runtime-less edit-mode e2e persona.",
|
|
// Runtime-less definition with a saved model and NO provider — the
|
|
// picker is editable-without-runtime, so the provider stays required.
|
|
runtime: null,
|
|
model: "claude-opus-4-5",
|
|
provider: null,
|
|
},
|
|
],
|
|
});
|
|
|
|
// Agents view → persona-grouped agent card → Edit quick action.
|
|
await page.goto("/");
|
|
await page.getByTestId("open-agents-view").click();
|
|
const agentButton = page.getByRole("button", {
|
|
name: "Legacy Editor agent profile",
|
|
});
|
|
await expect(agentButton).toBeVisible({ timeout: 10_000 });
|
|
await agentButton.click();
|
|
await expect(page.getByTestId("user-profile-panel")).toBeVisible({
|
|
timeout: 10_000,
|
|
});
|
|
await page.getByTestId("user-profile-edit-agent").click();
|
|
|
|
// Confirm the real EDIT dialog, seeded from the persona.
|
|
await expect(page.getByTestId("persona-dialog")).toBeVisible({
|
|
timeout: 10_000,
|
|
});
|
|
await expect(page.locator("#persona-display-name")).toHaveValue(
|
|
"Legacy Editor",
|
|
);
|
|
await expect(page.getByTestId("persona-dialog-submit")).toHaveText(
|
|
/Save changes/,
|
|
);
|
|
|
|
// The provider picker IS visible (runtime-less editable definition) …
|
|
await expect(page.locator("#persona-llm-provider")).toBeVisible({
|
|
timeout: 10_000,
|
|
});
|
|
// … so the empty provider must block Save …
|
|
await expect(page.getByTestId("persona-dialog-submit")).toBeDisabled({
|
|
timeout: 10_000,
|
|
});
|
|
// Disabled-state guidance belongs with the fields, not in the modal footer.
|
|
await expect(page.getByTestId("persona-dialog-submit-reason")).toHaveCount(
|
|
0,
|
|
);
|
|
|
|
await waitForAnimations(page);
|
|
|
|
const dialog = page.getByRole("dialog");
|
|
await dialog.screenshot({
|
|
path: `${SHOTS}/11-edit-runtime-less-provider-required-save-blocked.png`,
|
|
});
|
|
});
|
|
|
|
// Will's exact stuck path: databricks_v2 global provider + saved global
|
|
// OPENAI_API_KEY. The cue must be visible without opening Advanced; once
|
|
// Advanced is opened the annotation must appear on the matching row.
|
|
test("card-mint-key-cue-visible-and-annotation-in-advanced", async ({
|
|
page,
|
|
}) => {
|
|
await installMockBridge(page, {
|
|
globalAgentConfig: {
|
|
provider: "databricks_v2",
|
|
model: null,
|
|
preferred_runtime: "buzz-agent",
|
|
env_vars: { OPENAI_API_KEY: "sk-placeholder" },
|
|
},
|
|
});
|
|
|
|
await openAiDefaultsSettings(page);
|
|
|
|
const card = page.getByTestId("settings-global-agent-config");
|
|
|
|
// The cue must be visible without the user opening Advanced.
|
|
await expect(card.getByTestId("card-mint-key-cue")).toBeVisible();
|
|
await expect(card.getByTestId("card-mint-key-cue")).toContainText(
|
|
"OPENAI_API_KEY",
|
|
);
|
|
await expect(card.getByTestId("card-mint-key-cue")).toContainText(
|
|
"Advanced → Environment variables",
|
|
);
|
|
|
|
// Advanced is collapsed at this point.
|
|
const advancedToggle = card.getByTestId("global-agent-advanced-toggle");
|
|
await expect(advancedToggle).toHaveAttribute("aria-expanded", "false");
|
|
|
|
// Open Advanced — the OPENAI_API_KEY row's annotation must be visible.
|
|
await advancedToggle.click();
|
|
await expect(advancedToggle).toHaveAttribute("aria-expanded", "true");
|
|
await expect(
|
|
card.getByText("Used for minting agent trading cards"),
|
|
).toBeVisible();
|
|
});
|
|
});
|