fix: fail closed on onboarding config load

Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com>
This commit is contained in:
morgmart
2026-08-19 20:40:59 -07:00
parent 5a3d7a2be5
commit 8b85a2b315
5 changed files with 140 additions and 13 deletions

View File

@@ -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.

View File

@@ -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;
};

View File

@@ -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 ?? {

View File

@@ -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,
}) => {

View File

@@ -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<