mirror of
https://github.com/block/buzz.git
synced 2026-10-31 14:31:02 +00:00
fix: fail closed on onboarding config load
Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com>
This commit is contained in:
@@ -77,6 +77,11 @@ function AgentDefaultsSection({
|
||||
initialDraftRef.current?.isCustomModelEditing ?? false,
|
||||
);
|
||||
const [bakedEnv, setBakedEnv] = React.useState<BakedEnvEntry[]>([]);
|
||||
const [configLoadError, setConfigLoadError] = React.useState(false);
|
||||
const [configLoadNonce, retryConfigLoad] = React.useReducer(
|
||||
(nonce: number) => nonce + 1,
|
||||
0,
|
||||
);
|
||||
const configRef = React.useRef<GlobalAgentConfig>(
|
||||
initialDraftRef.current?.config ?? EMPTY_GLOBAL_CONFIG,
|
||||
);
|
||||
@@ -86,7 +91,9 @@ function AgentDefaultsSection({
|
||||
React.useEffect(() => {
|
||||
let unmounted = false;
|
||||
|
||||
async function loadDefaults() {
|
||||
async function loadDefaults(_retryAttempt: number) {
|
||||
setIsLoading(true);
|
||||
setConfigLoadError(false);
|
||||
const [configResult, bakedEnvResult] = await Promise.allSettled([
|
||||
getGlobalAgentConfig(),
|
||||
getBakedBuildEnv(),
|
||||
@@ -94,25 +101,27 @@ function AgentDefaultsSection({
|
||||
|
||||
if (unmounted) return;
|
||||
|
||||
if (
|
||||
initialDraftRef.current === null &&
|
||||
configResult.status === "fulfilled"
|
||||
) {
|
||||
configRef.current = configResult.value;
|
||||
setConfig(configResult.value);
|
||||
}
|
||||
if (bakedEnvResult.status === "fulfilled") {
|
||||
setBakedEnv(bakedEnvResult.value);
|
||||
}
|
||||
if (configResult.status === "rejected") {
|
||||
setConfigLoadError(true);
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
if (initialDraftRef.current === null) {
|
||||
configRef.current = configResult.value;
|
||||
setConfig(configResult.value);
|
||||
}
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
void loadDefaults();
|
||||
void loadDefaults(configLoadNonce);
|
||||
|
||||
return () => {
|
||||
unmounted = true;
|
||||
};
|
||||
}, []);
|
||||
}, [configLoadNonce]);
|
||||
|
||||
const effectiveReadyRuntimeIds = React.useMemo(
|
||||
() =>
|
||||
@@ -147,6 +156,7 @@ function AgentDefaultsSection({
|
||||
const configSurfaceLoading = isLoading || runtimesQuery.isLoading;
|
||||
|
||||
const configSurfaceError =
|
||||
configLoadError ||
|
||||
runtimesQuery.isError ||
|
||||
(!configSurfaceLoading &&
|
||||
effectiveReadyRuntimeIds.length > 0 &&
|
||||
@@ -212,12 +222,14 @@ function AgentDefaultsSection({
|
||||
// configIsValid comes from AgentConfigFields' onValidityChange and
|
||||
// covers model + provider credentials — a harness selection alone is
|
||||
// not a working default (e.g. buzz-agent with no provider configured).
|
||||
canComplete: selectedRuntimeId.length > 0 && configIsValid,
|
||||
canComplete:
|
||||
!configLoadError && selectedRuntimeId.length > 0 && configIsValid,
|
||||
commit: commitPersistence,
|
||||
});
|
||||
}, [
|
||||
commitPersistence,
|
||||
configIsValid,
|
||||
configLoadError,
|
||||
onPersistenceStateChange,
|
||||
selectedRuntimeId,
|
||||
]);
|
||||
@@ -233,6 +245,20 @@ function AgentDefaultsSection({
|
||||
<Spinner className="h-4 w-4 border-2" />
|
||||
Loading…
|
||||
</div>
|
||||
) : configLoadError ? (
|
||||
<div className="flex flex-col items-center gap-3 py-4 text-center text-sm">
|
||||
<p className="text-destructive">
|
||||
Couldn't load your existing default harness. Try again.
|
||||
</p>
|
||||
<Button
|
||||
className={`${ONBOARDING_PRIMARY_CTA_CLASS} text-sm`}
|
||||
disabled={isLoading}
|
||||
onClick={retryConfigLoad}
|
||||
type="button"
|
||||
>
|
||||
{isLoading ? "Checking…" : "Try again"}
|
||||
</Button>
|
||||
</div>
|
||||
) : configSurfaceError ? (
|
||||
<p className="py-4 text-center text-sm text-destructive">
|
||||
Couldn't load harness settings. Go back and try again.
|
||||
|
||||
@@ -62,8 +62,7 @@ export type ProfileStepActions = {
|
||||
|
||||
export type SetupStepActions = {
|
||||
back: () => void;
|
||||
next: (runtimeId: string) => void;
|
||||
skip: () => void;
|
||||
next: (readyRuntimeIds: readonly string[]) => void;
|
||||
navigateToAgentSettings?: () => void;
|
||||
};
|
||||
|
||||
|
||||
@@ -537,6 +537,8 @@ type E2eConfig = {
|
||||
model: string | null;
|
||||
preferred_runtime?: string | null;
|
||||
};
|
||||
/** Sequenced `get_global_agent_config` failures. Null succeeds; a string throws. */
|
||||
globalAgentConfigErrors?: (string | null)[];
|
||||
/** Explicit owner-only agent-access capability; independent of baked defaults. */
|
||||
ownerOnlyAccessBuild?: boolean;
|
||||
/** File-layer config returned by runtime id. */
|
||||
@@ -13153,6 +13155,8 @@ export function maybeInstallE2eTauriMocks() {
|
||||
return config.mock?.runtimeFileConfigs?.[runtimeId] ?? null;
|
||||
}
|
||||
case "get_global_agent_config": {
|
||||
const readError = activeConfig?.mock?.globalAgentConfigErrors?.shift();
|
||||
if (readError) throw new Error(readError);
|
||||
// Return the mutable persisted mock value, seeded from the test config.
|
||||
return (
|
||||
mockGlobalAgentConfig ?? {
|
||||
|
||||
@@ -491,6 +491,102 @@ test("defaults renders only fields supported by the selected harness", async ({
|
||||
).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("failed config read blocks Next, preserves existing config, and retry succeeds", async ({
|
||||
page,
|
||||
}) => {
|
||||
await installMockBridge(
|
||||
page,
|
||||
{
|
||||
acpRuntimesCatalog: [
|
||||
runtime("claude", "available", { status: "logged_in" }),
|
||||
runtime("codex", "available", { status: "logged_in" }),
|
||||
],
|
||||
globalAgentConfig: {
|
||||
env_vars: {
|
||||
ANTHROPIC_API_KEY: "sk-existing",
|
||||
BUZZ_AGENT_THINKING_EFFORT: "high",
|
||||
},
|
||||
provider: "anthropic",
|
||||
model: "claude-sonnet-4",
|
||||
preferred_runtime: "claude",
|
||||
},
|
||||
globalAgentConfigErrors: ["temporary config read failure", null],
|
||||
},
|
||||
{ skipCommunitySeed: true, skipOnboardingSeed: true },
|
||||
);
|
||||
await page.goto("/");
|
||||
await navigateToSetupPage(page);
|
||||
await page.getByTestId("onboarding-setup-next").click();
|
||||
|
||||
await expect(
|
||||
page.getByText("Couldn't load your existing default harness. Try again."),
|
||||
).toBeVisible();
|
||||
await expect(page.getByTestId("onboarding-finish")).toBeDisabled();
|
||||
await page.getByTestId("onboarding-finish").click({ force: true });
|
||||
await expect(page.getByTestId("onboarding-page-config")).toBeVisible();
|
||||
|
||||
let saved = await page.evaluate(async () => {
|
||||
return await (
|
||||
window as Window & {
|
||||
__BUZZ_E2E_INVOKE_MOCK_COMMAND__?: (
|
||||
command: string,
|
||||
payload: unknown,
|
||||
) => Promise<{
|
||||
env_vars: Record<string, string>;
|
||||
model: string | null;
|
||||
preferred_runtime: string | null;
|
||||
provider: string | null;
|
||||
}>;
|
||||
}
|
||||
).__BUZZ_E2E_INVOKE_MOCK_COMMAND__?.("get_global_agent_config", null);
|
||||
});
|
||||
expect(saved).toMatchObject({
|
||||
env_vars: {
|
||||
ANTHROPIC_API_KEY: "sk-existing",
|
||||
BUZZ_AGENT_THINKING_EFFORT: "high",
|
||||
},
|
||||
model: "claude-sonnet-4",
|
||||
preferred_runtime: "claude",
|
||||
provider: "anthropic",
|
||||
});
|
||||
|
||||
await page.getByRole("button", { name: "Try again" }).click();
|
||||
await expect(page.getByTestId("global-agent-default-harness")).toHaveText(
|
||||
"Claude Code",
|
||||
);
|
||||
const harness = page.getByTestId("global-agent-default-harness");
|
||||
await harness.click();
|
||||
await page.getByTestId("global-agent-default-harness-option-codex").click();
|
||||
await expect(page.getByTestId("onboarding-finish")).toBeEnabled();
|
||||
await page.getByTestId("onboarding-finish").click();
|
||||
await expect(page.getByText("Join or create a community")).toBeVisible();
|
||||
|
||||
saved = await page.evaluate(async () => {
|
||||
return await (
|
||||
window as Window & {
|
||||
__BUZZ_E2E_INVOKE_MOCK_COMMAND__?: (
|
||||
command: string,
|
||||
payload: unknown,
|
||||
) => Promise<{
|
||||
env_vars: Record<string, string>;
|
||||
model: string | null;
|
||||
preferred_runtime: string | null;
|
||||
provider: string | null;
|
||||
}>;
|
||||
}
|
||||
).__BUZZ_E2E_INVOKE_MOCK_COMMAND__?.("get_global_agent_config", null);
|
||||
});
|
||||
expect(saved).toMatchObject({
|
||||
env_vars: {
|
||||
ANTHROPIC_API_KEY: "sk-existing",
|
||||
},
|
||||
model: null,
|
||||
preferred_runtime: "codex",
|
||||
provider: null,
|
||||
});
|
||||
expect(saved?.env_vars).not.toHaveProperty("BUZZ_AGENT_THINKING_EFFORT");
|
||||
});
|
||||
|
||||
test("defaults hides model when optional harness has empty discovery", async ({
|
||||
page,
|
||||
}) => {
|
||||
|
||||
@@ -497,6 +497,8 @@ type MockBridgeOptions = {
|
||||
model: string | null;
|
||||
preferred_runtime?: string | null;
|
||||
};
|
||||
/** Sequenced `get_global_agent_config` failures. Null succeeds; a string throws. */
|
||||
globalAgentConfigErrors?: (string | null)[];
|
||||
ownerOnlyAccessBuild?: boolean;
|
||||
/** File-layer config returned by runtime id. */
|
||||
runtimeFileConfigs?: Record<
|
||||
|
||||
Reference in New Issue
Block a user