mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
fix(desktop): honor selected onboarding runtime config (#2047)
Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Pinky <44b8e82baa6e0e254e0208d68f335c283c94e7b78dd1fa10d5a49d3f13dd0435@sprout-oss.stage.blox.sqprod.co>
This commit is contained in:
@@ -180,7 +180,7 @@ export function GlobalAgentConfigEditor({
|
||||
) : (
|
||||
<GlobalAgentConfigFields
|
||||
bakedEnv={bakedEnv}
|
||||
buzzAgentRuntime={buzzAgentRuntime}
|
||||
selectedRuntime={buzzAgentRuntime}
|
||||
config={config}
|
||||
isCustomModelEditing={isCustomModelEditing}
|
||||
isCustomProvider={isCustomProvider}
|
||||
|
||||
@@ -57,7 +57,7 @@ const BAKED_STRUCTURED_KEYS = new Set([
|
||||
|
||||
export type GlobalAgentConfigFieldsProps = {
|
||||
bakedEnv: BakedEnvEntry[];
|
||||
buzzAgentRuntime: AcpRuntimeCatalogEntry | undefined;
|
||||
selectedRuntime: AcpRuntimeCatalogEntry | undefined;
|
||||
config: GlobalAgentConfig;
|
||||
isCustomModelEditing: boolean;
|
||||
isCustomProvider: boolean;
|
||||
@@ -69,7 +69,7 @@ export type GlobalAgentConfigFieldsProps = {
|
||||
|
||||
export function GlobalAgentConfigFields({
|
||||
bakedEnv,
|
||||
buzzAgentRuntime,
|
||||
selectedRuntime,
|
||||
config,
|
||||
isCustomModelEditing,
|
||||
isCustomProvider,
|
||||
@@ -134,7 +134,7 @@ export function GlobalAgentConfigFields({
|
||||
modelFieldVisible: true,
|
||||
open: true,
|
||||
provider: providerForDiscovery,
|
||||
selectedRuntime: buzzAgentRuntime,
|
||||
selectedRuntime,
|
||||
});
|
||||
|
||||
const currentEffortForAutoClear =
|
||||
|
||||
@@ -80,12 +80,19 @@ function AgentDefaultsSection() {
|
||||
};
|
||||
}, []);
|
||||
|
||||
const buzzAgentRuntime = React.useMemo(
|
||||
() => (runtimesQuery.data ?? []).find((r) => r.id === "buzz-agent"),
|
||||
[runtimesQuery.data],
|
||||
const selectedRuntime = React.useMemo(
|
||||
() =>
|
||||
(runtimesQuery.data ?? []).find(
|
||||
(runtime) => runtime.id === config.preferred_runtime,
|
||||
),
|
||||
[config.preferred_runtime, runtimesQuery.data],
|
||||
);
|
||||
|
||||
const readiness = resolveAgentReadiness(runtimesQuery.data ?? [], config);
|
||||
const readiness = resolveAgentReadiness(
|
||||
runtimesQuery.data ?? [],
|
||||
config,
|
||||
"preferred",
|
||||
);
|
||||
|
||||
return (
|
||||
<section className="w-full space-y-4 text-left">
|
||||
@@ -98,7 +105,7 @@ function AgentDefaultsSection() {
|
||||
<div className="rounded-2xl bg-white/85 p-2 shadow-[0_0_55px_25px_rgba(255,255,255,0.6)]">
|
||||
<GlobalAgentConfigFields
|
||||
bakedEnv={bakedEnv}
|
||||
buzzAgentRuntime={buzzAgentRuntime}
|
||||
selectedRuntime={selectedRuntime}
|
||||
config={config}
|
||||
isCustomModelEditing={isCustomModelEditing}
|
||||
isCustomProvider={isCustomProvider}
|
||||
|
||||
@@ -30,6 +30,7 @@ function makeConfig(overrides = {}) {
|
||||
env_vars: {},
|
||||
provider: null,
|
||||
model: null,
|
||||
preferred_runtime: "goose",
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
@@ -38,27 +39,26 @@ function makeConfig(overrides = {}) {
|
||||
// CLI path
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test("resolveAgentReadiness_cli_returns_ready_when_cli_runtime_available_and_logged_in", () => {
|
||||
const runtimes = [makeRuntime({ id: "goose", label: "Goose" })];
|
||||
const result = resolveAgentReadiness(runtimes, makeConfig());
|
||||
test("resolveAgentReadiness_cli_returns_ready_when_preferred_cli_runtime_is_logged_in", () => {
|
||||
const runtimes = [makeRuntime({ id: "claude", label: "Claude" })];
|
||||
const result = resolveAgentReadiness(
|
||||
runtimes,
|
||||
makeConfig({ preferred_runtime: "claude" }),
|
||||
);
|
||||
assert.deepEqual(result, {
|
||||
ready: true,
|
||||
reason: "cli",
|
||||
runtimeLabel: "Goose",
|
||||
runtimeLabel: "Claude",
|
||||
});
|
||||
});
|
||||
|
||||
test("resolveAgentReadiness_cli_uses_first_matching_runtime", () => {
|
||||
test("resolveAgentReadiness_uses_only_the_preferred_runtime", () => {
|
||||
const runtimes = [
|
||||
makeRuntime({ id: "claude", label: "Claude" }),
|
||||
makeRuntime({ id: "goose", label: "Goose" }),
|
||||
];
|
||||
const result = resolveAgentReadiness(runtimes, makeConfig());
|
||||
assert.equal(result.ready, true);
|
||||
if (result.ready) {
|
||||
assert.equal(result.reason, "cli");
|
||||
assert.equal(result.runtimeLabel, "Claude");
|
||||
}
|
||||
const result = resolveAgentReadiness(runtimes, makeConfig(), "preferred");
|
||||
assert.equal(result.ready, false);
|
||||
});
|
||||
|
||||
test("resolveAgentReadiness_cli_skips_logged_out_runtimes", () => {
|
||||
@@ -69,13 +69,11 @@ test("resolveAgentReadiness_cli_skips_logged_out_runtimes", () => {
|
||||
authStatus: { status: "logged_out" },
|
||||
}),
|
||||
];
|
||||
const result = resolveAgentReadiness(runtimes, makeConfig());
|
||||
const result = resolveAgentReadiness(runtimes, makeConfig(), "preferred");
|
||||
assert.equal(result.ready, false);
|
||||
});
|
||||
|
||||
test("resolveAgentReadiness_cli_ready_for_login_free_harness_with_not_applicable_auth", () => {
|
||||
// Goose uses not_applicable because it has no login concept; it should
|
||||
// still show green if the runtime is available.
|
||||
test("resolveAgentReadiness_goose_requires_provider_and_model", () => {
|
||||
const runtimes = [
|
||||
makeRuntime({
|
||||
id: "goose",
|
||||
@@ -84,12 +82,8 @@ test("resolveAgentReadiness_cli_ready_for_login_free_harness_with_not_applicable
|
||||
authStatus: { status: "not_applicable" },
|
||||
}),
|
||||
];
|
||||
const result = resolveAgentReadiness(runtimes, makeConfig());
|
||||
assert.deepEqual(result, {
|
||||
ready: true,
|
||||
reason: "cli",
|
||||
runtimeLabel: "Goose",
|
||||
});
|
||||
const result = resolveAgentReadiness(runtimes, makeConfig(), "preferred");
|
||||
assert.equal(result.ready, false);
|
||||
});
|
||||
|
||||
test("resolveAgentReadiness_cli_not_ready_for_unknown_auth_status", () => {
|
||||
@@ -102,7 +96,7 @@ test("resolveAgentReadiness_cli_not_ready_for_unknown_auth_status", () => {
|
||||
authStatus: { status: "unknown" },
|
||||
}),
|
||||
];
|
||||
const result = resolveAgentReadiness(runtimes, makeConfig());
|
||||
const result = resolveAgentReadiness(runtimes, makeConfig(), "preferred");
|
||||
assert.equal(result.ready, false);
|
||||
});
|
||||
|
||||
@@ -115,7 +109,7 @@ test("resolveAgentReadiness_cli_not_ready_for_config_invalid_auth_status", () =>
|
||||
authStatus: { status: "config_invalid" },
|
||||
}),
|
||||
];
|
||||
const result = resolveAgentReadiness(runtimes, makeConfig());
|
||||
const result = resolveAgentReadiness(runtimes, makeConfig(), "preferred");
|
||||
assert.equal(result.ready, false);
|
||||
});
|
||||
|
||||
@@ -128,7 +122,7 @@ test("resolveAgentReadiness_cli_skips_unavailable_runtimes", () => {
|
||||
authStatus: { status: "logged_in" },
|
||||
}),
|
||||
];
|
||||
const result = resolveAgentReadiness(runtimes, makeConfig());
|
||||
const result = resolveAgentReadiness(runtimes, makeConfig(), "preferred");
|
||||
assert.equal(result.ready, false);
|
||||
});
|
||||
|
||||
@@ -141,7 +135,7 @@ test("resolveAgentReadiness_cli_ignores_buzz_agent_runtime", () => {
|
||||
authStatus: { status: "not_applicable" },
|
||||
}),
|
||||
];
|
||||
const result = resolveAgentReadiness(runtimes, makeConfig());
|
||||
const result = resolveAgentReadiness(runtimes, makeConfig(), "preferred");
|
||||
assert.equal(result.ready, false);
|
||||
});
|
||||
|
||||
@@ -151,12 +145,15 @@ test("resolveAgentReadiness_cli_ignores_buzz_agent_runtime", () => {
|
||||
|
||||
test("resolveAgentReadiness_buzz_agent_ready_when_provider_model_and_key_set", () => {
|
||||
// anthropic requires ANTHROPIC_API_KEY
|
||||
const config = makeConfig({
|
||||
provider: "anthropic",
|
||||
model: "claude-3-5-sonnet-latest",
|
||||
env_vars: { ANTHROPIC_API_KEY: "sk-ant-test" },
|
||||
});
|
||||
const result = resolveAgentReadiness([], config);
|
||||
const result = resolveAgentReadiness(
|
||||
[makeRuntime({ id: "buzz-agent", label: "Buzz Agent" })],
|
||||
makeConfig({
|
||||
preferred_runtime: "buzz-agent",
|
||||
provider: "anthropic",
|
||||
model: "claude-3-5-sonnet-latest",
|
||||
env_vars: { ANTHROPIC_API_KEY: "sk-ant-test" },
|
||||
}),
|
||||
);
|
||||
assert.deepEqual(result, { ready: true, reason: "buzz-agent" });
|
||||
});
|
||||
|
||||
@@ -199,20 +196,59 @@ test("resolveAgentReadiness_neither_returns_not_ready", () => {
|
||||
assert.deepEqual(result, { ready: false });
|
||||
});
|
||||
|
||||
test("resolveAgentReadiness_welcome_readiness_uses_ready_cli_without_preference", () => {
|
||||
const runtimes = [makeRuntime({ id: "claude", label: "Claude" })];
|
||||
const result = resolveAgentReadiness(
|
||||
runtimes,
|
||||
makeConfig({ preferred_runtime: null }),
|
||||
);
|
||||
assert.deepEqual(result, {
|
||||
ready: true,
|
||||
reason: "cli",
|
||||
runtimeLabel: "Claude",
|
||||
});
|
||||
});
|
||||
|
||||
test("resolveAgentReadiness_legacy_config_without_preference_uses_buzz_agent_fields", () => {
|
||||
const runtimes = [makeRuntime({ id: "buzz-agent", label: "Buzz Agent" })];
|
||||
const result = resolveAgentReadiness(
|
||||
runtimes,
|
||||
makeConfig({
|
||||
preferred_runtime: null,
|
||||
provider: "anthropic",
|
||||
model: "claude-3-5-sonnet-latest",
|
||||
env_vars: { ANTHROPIC_API_KEY: "sk-ant-test" },
|
||||
}),
|
||||
);
|
||||
assert.deepEqual(result, { ready: true, reason: "buzz-agent" });
|
||||
});
|
||||
|
||||
test("resolveAgentReadiness_legacy_config_does_not_treat_goose_binary_as_ready", () => {
|
||||
const result = resolveAgentReadiness(
|
||||
[makeRuntime({ id: "goose", label: "Goose" })],
|
||||
makeConfig({ preferred_runtime: null }),
|
||||
"preferred",
|
||||
);
|
||||
assert.deepEqual(result, { ready: false });
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// CLI path takes priority over buzz-agent path
|
||||
// Preferred runtime isolation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test("resolveAgentReadiness_cli_wins_over_buzz_agent_when_both_ready", () => {
|
||||
const runtimes = [makeRuntime({ id: "goose", label: "Goose" })];
|
||||
const config = makeConfig({
|
||||
provider: "anthropic",
|
||||
model: "claude-3-5-sonnet-latest",
|
||||
env_vars: { ANTHROPIC_API_KEY: "sk-ant-test" },
|
||||
});
|
||||
const result = resolveAgentReadiness(runtimes, config);
|
||||
assert.equal(result.ready, true);
|
||||
if (result.ready) {
|
||||
assert.equal(result.reason, "cli");
|
||||
}
|
||||
test("resolveAgentReadiness_preferred_goose_does_not_borrow_ready_buzz_agent_config", () => {
|
||||
const runtimes = [
|
||||
makeRuntime({ id: "goose", label: "Goose" }),
|
||||
makeRuntime({ id: "buzz-agent", label: "Buzz Agent" }),
|
||||
];
|
||||
const result = resolveAgentReadiness(
|
||||
runtimes,
|
||||
makeConfig({
|
||||
provider: "anthropic",
|
||||
model: null,
|
||||
env_vars: { ANTHROPIC_API_KEY: "sk-ant-test" },
|
||||
}),
|
||||
"preferred",
|
||||
);
|
||||
assert.equal(result.ready, false);
|
||||
});
|
||||
|
||||
@@ -12,35 +12,60 @@ export type AgentReadinessResult =
|
||||
/**
|
||||
* Determine whether the user has a working agent path configured.
|
||||
*
|
||||
* CLI path: at least one non-buzz-agent runtime is available and logged in.
|
||||
* buzz-agent path: provider and model are set, and all required credential
|
||||
* env vars for that provider are present.
|
||||
* CLI path: the preferred Claude or Codex runtime is available and logged in.
|
||||
* Provider path: the preferred Buzz Agent or Goose runtime has provider and
|
||||
* model set, plus all required credential env vars for that provider.
|
||||
*
|
||||
* Returns enough info for the UI to say which path matched, or that neither did.
|
||||
*/
|
||||
export function resolveAgentReadiness(
|
||||
runtimes: readonly AcpRuntimeCatalogEntry[],
|
||||
globalConfig: GlobalAgentConfig,
|
||||
scope: "any" | "preferred" = "any",
|
||||
): AgentReadinessResult {
|
||||
// CLI path — any non-buzz-agent runtime that is available and has an
|
||||
// auth status indicating it can run: logged_in for runtimes that require
|
||||
// auth (Claude Code), or not_applicable for login-free harnesses (Goose).
|
||||
for (const runtime of runtimes) {
|
||||
if (runtime.id === "buzz-agent") continue;
|
||||
if (
|
||||
runtime.availability === "available" &&
|
||||
(runtime.authStatus.status === "logged_in" ||
|
||||
runtime.authStatus.status === "not_applicable")
|
||||
) {
|
||||
return { ready: true, reason: "cli", runtimeLabel: runtime.label };
|
||||
if (scope === "any") {
|
||||
for (const runtime of runtimes) {
|
||||
if (runtime.id === "buzz-agent") continue;
|
||||
if (
|
||||
runtime.availability === "available" &&
|
||||
(runtime.authStatus.status === "logged_in" ||
|
||||
runtime.authStatus.status === "not_applicable")
|
||||
) {
|
||||
return { ready: true, reason: "cli", runtimeLabel: runtime.label };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// buzz-agent path — provider + model + required credential keys all present.
|
||||
const preferredRuntime =
|
||||
scope === "preferred"
|
||||
? runtimes.find(
|
||||
(runtime) => runtime.id === globalConfig.preferred_runtime,
|
||||
)
|
||||
: runtimes.find((runtime) => runtime.id === "buzz-agent");
|
||||
if (preferredRuntime?.availability !== "available") {
|
||||
return { ready: false };
|
||||
}
|
||||
|
||||
if (
|
||||
(preferredRuntime.id === "claude" || preferredRuntime.id === "codex") &&
|
||||
(preferredRuntime.authStatus.status === "logged_in" ||
|
||||
preferredRuntime.authStatus.status === "not_applicable")
|
||||
) {
|
||||
return {
|
||||
ready: true,
|
||||
reason: "cli",
|
||||
runtimeLabel: preferredRuntime.label,
|
||||
};
|
||||
}
|
||||
|
||||
if (preferredRuntime.id !== "buzz-agent" && preferredRuntime.id !== "goose") {
|
||||
return { ready: false };
|
||||
}
|
||||
|
||||
const provider = globalConfig.provider?.trim() ?? "";
|
||||
const model = globalConfig.model?.trim() ?? "";
|
||||
if (provider.length > 0 && model.length > 0) {
|
||||
const required = requiredCredentialEnvKeys("buzz-agent", provider);
|
||||
const required = requiredCredentialEnvKeys(preferredRuntime.id, provider);
|
||||
const allKeysPresent = required.every(
|
||||
(key) => (globalConfig.env_vars[key] ?? "").trim().length > 0,
|
||||
);
|
||||
|
||||
@@ -242,7 +242,7 @@ test("readiness wait returns the subset that became online by the deadline", asy
|
||||
[bumble.pubkey]: "offline",
|
||||
}),
|
||||
pollMs: 0,
|
||||
waitMs: 1,
|
||||
waitMs: 0,
|
||||
});
|
||||
|
||||
assert.deepEqual(online, [honey]);
|
||||
|
||||
@@ -186,7 +186,7 @@ export async function waitForWelcomeTeammatesOnline(
|
||||
const pubkeys = teammates.map((agent) => agent.pubkey);
|
||||
let latestOnline: ManagedAgent[] = [];
|
||||
|
||||
while (!options.isCancelled() && Date.now() < deadline) {
|
||||
while (!options.isCancelled()) {
|
||||
try {
|
||||
latestOnline = onlineWelcomeTeammates(
|
||||
teammates,
|
||||
@@ -198,6 +198,7 @@ export async function waitForWelcomeTeammatesOnline(
|
||||
} catch (error) {
|
||||
console.warn("Welcome teammate presence check failed; retrying.", error);
|
||||
}
|
||||
if (Date.now() >= deadline) break;
|
||||
await new Promise((resolve) => globalThis.setTimeout(resolve, pollMs));
|
||||
}
|
||||
return options.isCancelled() ? [] : latestOnline;
|
||||
|
||||
@@ -375,6 +375,32 @@ test("Finish button is always enabled on config page regardless of readiness", a
|
||||
// B1 regression: rapid consecutive edits must not lose the later change
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test("Goose config page discovers models through the selected Goose runtime", async ({
|
||||
page,
|
||||
}) => {
|
||||
await installMockBridge(
|
||||
page,
|
||||
{
|
||||
acpRuntimesCatalog: [
|
||||
availableRuntime("goose", { status: "not_applicable" }),
|
||||
],
|
||||
},
|
||||
{ skipCommunitySeed: true, skipOnboardingSeed: true },
|
||||
);
|
||||
await page.goto("/");
|
||||
await navigateToSetupPage(page);
|
||||
await page.getByTestId("onboarding-runtime-goose").click();
|
||||
await page.getByTestId("onboarding-setup-next").click();
|
||||
await expect(page.getByTestId("onboarding-page-config")).toBeVisible();
|
||||
|
||||
await page.locator("#global-agent-provider").selectOption("openai");
|
||||
await expect(
|
||||
page
|
||||
.locator("#global-agent-model")
|
||||
.getByRole("option", { name: "GPT-5.5" }),
|
||||
).toBeAttached();
|
||||
});
|
||||
|
||||
test("provider credentials are first-class and drive model discovery", async ({
|
||||
page,
|
||||
}) => {
|
||||
|
||||
Reference in New Issue
Block a user