import { afterEach, beforeAll, expect, mock, test } from "bun:test";
import { GlobalRegistrator } from "@happy-dom/global-registrator";
import { act, useMemo, useState, type ReactNode } from "react";
import { createRoot, type Root } from "react-dom/client";
import { FailedSessionActions } from "./failed-session-actions";
import { FailureRecoveryBoundary } from "./failure-recovery-boundary";
import { OpenGeniApiError } from "@opengeni/sdk/browser";
import {
createFailedSessionRetry,
type FailedSessionRetryInput,
} from "../../lib/failed-session-retry";
mock.module("../../lib/navigation", () => ({
Link: ({ children }: { children: ReactNode }) => {children},
}));
const { FailedSessionBanner } = await import("./failed-session-banner");
beforeAll(() => {
try {
GlobalRegistrator.register();
} catch {
/* shared DOM */
}
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean | undefined }).IS_REACT_ACT_ENVIRONMENT =
true;
});
let root: Root | undefined;
afterEach(async () => {
await act(async () => root?.unmount());
document.body.replaceChildren();
});
async function render(children: ReactNode) {
const container = document.createElement("div");
document.body.append(container);
root = createRoot(container);
await act(async () => root!.render(children));
return container;
}
const failure = {
reason: "Connection interrupted.",
failedAt: null,
consecutiveRecoveryCount: null,
};
const actions = { onRetry: async () => true, retryBlocker: null };
test("an incompatible accepted Knowledge scope offers a new message, never Retry", async () => {
const reason =
"This turn's Knowledge scope no longer matches this chat. Send a new message to continue with the current settings.";
const scopedFailure = { ...failure, failureCode: "knowledge_learning_scope_changed", reason };
const container = await render();
expect(container.textContent).toContain(reason);
expect(container.querySelector("button")).toBeNull();
await act(async () =>
root!.render(
,
),
);
expect(container.textContent).toContain(reason);
expect(container.querySelector("button")).toBeNull();
});
test("compact row offers one ghost Retry without duplicate controls or guidance", async () => {
const container = await render();
expect(container.querySelectorAll("button")).toHaveLength(1);
expect(container.querySelector("button")!.textContent).toBe("Retry");
expect(container.querySelector("button")!.dataset.variant).toBe("ghost");
expect(container.querySelector("svg")).not.toBeNull();
const details = container.querySelector("details")!;
expect(details.open).toBe(false);
expect(details.querySelector("p")!.textContent).toBe("Connection interrupted.");
expect(container.querySelector('[data-testid="failed-session-banner"] span')!.textContent).toBe(
"The session stopped unexpectedly.",
);
expect(container.textContent).not.toMatch(/Choose another model|composer|preserved|Try again/);
expect(container.querySelector('[data-testid="failed-session-banner"]')!.className).not.toMatch(
/bg-|border|rounded/,
);
});
test("uncategorized preclaim details never grant Retry or bypass its blocker", async () => {
const detail = "getaddrinfo ENOTFOUND database.example.test\nsynthetic diagnostic";
const preclaim = {
...failure,
reason: detail,
recordedDetail: detail,
failureCode: "pre_claim_failure",
};
let retries = 0;
const container = await render();
expect(container.querySelector("button")).toBeNull();
const details = container.querySelector("details")!;
expect(details.open).toBe(false);
expect(details.querySelector("p")!.textContent).toBe(detail);
expect(container.querySelector('[data-testid="failed-session-banner"] span')!.textContent).toBe(
"The session stopped unexpectedly.",
);
await act(async () =>
root!.render(
{
retries += 1;
return true;
},
retryBlocker: "permission",
}}
/>,
),
);
expect(container.querySelector("button")).toBeNull();
await act(async () => {
details.open = true;
details.dispatchEvent(new Event("toggle"));
});
expect(retries).toBe(0);
});
test("credential failures hide Retry until another model is chosen and keep raw detail folded", async () => {
const raw =
"401 Incorrect API key provided: sk-proj-****abcd. You can find your API key at https://platform.openai.com/account/api-keys.";
const credentialFailure = {
...failure,
reason:
"The model provider rejected this deployment's engine credentials. Sending messages won't help until the deployment's engine configuration is fixed.",
recordedDetail: raw,
};
const container = await render(
,
);
const banner = container.querySelector('[data-testid="failed-session-banner"]')!;
expect(banner.querySelector("button")).toBeNull();
const details = banner.querySelector("details")!;
expect(details.open).toBe(false);
expect(details.querySelector("summary")!.textContent).toBe("Details");
expect(details.querySelector("p")!.textContent).toBe(raw);
expect(banner.textContent?.replace(details.textContent ?? "", "")).toBe(
"The model provider rejected the credentials for this model. Choose another model below.",
);
await act(async () =>
root!.render(
,
),
);
expect(container.querySelector("button")!.textContent).toBe("Retry");
});
test("billing and daily-limit failures keep Retry on the same model", async () => {
const banner = (raw: string) => (
);
const container = await render(banner("402 Payment Required"));
for (const raw of [
"402 This request requires more credits. To increase, visit https://openrouter.ai/settings/credits and upgrade to a paid account",
"429 Rate limit exceeded: free-models-per-day. Add 10 credits to unlock more.",
]) {
await act(async () => root!.render(banner(raw)));
const row = container.querySelector('[data-testid="failed-session-banner"]')!;
expect(row.textContent).toContain("Choose another model below.");
expect(row.querySelectorAll("button")).toHaveLength(1);
expect(row.querySelector("button")!.textContent).toBe("Retry");
}
});
test("the free model's daily limit names it and offers credits, a subscription and Retry", async () => {
const detail =
"This model's daily limit at the model provider has been reached, so automatic retries stopped. Choose another model, or try again after the limit resets.\n429 Rate limit exceeded: free-models-per-day.";
const daily = {
...failure,
reason: detail.replace("\n", " "),
recordedDetail: detail,
failureCode: "provider_quota_exhausted",
quotaScope: "daily",
};
const banner = (props: {
freeModel?: boolean | undefined;
modelChanged?: boolean | undefined;
subscriptions?: { codex: boolean; supergrok: boolean } | undefined;
canChooseModel?: boolean | undefined;
hasModelPicker?: boolean | undefined;
}) => (
);
const container = await render(banner({ freeModel: true }));
const row = () => container.querySelector('[data-testid="failed-session-banner"]')!;
const headline = () => row().querySelector("span")!.textContent;
const labels = () => [...row().querySelectorAll("a, button")].map((node) => node.textContent);
expect(headline()).toBe(
"The free model has reached its daily limit. Buy Opengeni credits, connect ChatGPT or SuperGrok, or pick another model to keep going.",
);
expect(labels()).toEqual(["Buy credits", "Connect a subscription", "Retry"]);
expect(row().querySelector("details p")!.textContent).toBe(detail);
// A deployment without subscriptions never names them or links to them.
await act(async () =>
root!.render(banner({ freeModel: true, subscriptions: { codex: false, supergrok: false } })),
);
expect(headline()).toBe(
"The free model has reached its daily limit. Buy Opengeni credits, connect a model provider, or pick another model to keep going.",
);
expect(labels()).toEqual(["Buy credits", "Connect a model", "Retry"]);
await act(async () =>
root!.render(banner({ freeModel: true, subscriptions: { codex: true, supergrok: false } })),
);
expect(headline()).toContain("connect ChatGPT, or pick another model");
expect(labels()).toEqual(["Buy credits", "Connect ChatGPT", "Retry"]);
// Sending or a pending Retry locks the picker briefly; the remedies hold steady.
await act(async () =>
root!.render(banner({ freeModel: true, canChooseModel: false, hasModelPicker: true })),
);
expect(headline()).toContain("or pick another model to keep going.");
// Once another model is picked, Retry runs on it and the remedies step aside.
await act(async () => root!.render(banner({ freeModel: true, modelChanged: true })));
expect(headline()).toBe("The free model has reached its daily limit.");
expect(labels()).toEqual(["Retry"]);
// Any other model keeps the generic daily-limit wording and actions.
await act(async () => root!.render(banner({ freeModel: false })));
expect(headline()).toBe("This model's daily limit has been reached. Choose another model below.");
expect(labels()).toEqual(["Retry"]);
});
test("the free model keeps ordinary wording for failures other than its daily limit", async () => {
const container = await render(
,
);
const row = container.querySelector('[data-testid="failed-session-banner"]')!;
expect(row.querySelector("span")!.textContent).toBe(
"This model is throttled due to high demand. Select a different model, or try again in a few minutes.",
);
expect([...row.querySelectorAll("a, button")].map((node) => node.textContent)).toEqual(["Retry"]);
});
test("throttling points at the chat bar's model picker and keeps only Retry", async () => {
const banner = (modelChanged: boolean) => (
);
const container = await render(banner(false));
const row = container.querySelector('[data-testid="failed-session-banner"]')!;
expect(row.querySelector("span")!.textContent).toBe(
"This model is throttled due to high demand. Select a different model, or try again in a few minutes.",
);
expect([...row.querySelectorAll("button")].map((node) => node.textContent)).toEqual(["Retry"]);
await act(async () => root!.render(banner(true)));
const changedRow = container.querySelector('[data-testid="failed-session-banner"]')!;
expect(changedRow.querySelector("span")!.textContent).toBe(
"This model is throttled due to high demand. Try again in a few minutes.",
);
expect([...changedRow.querySelectorAll("button")].map((node) => node.textContent)).toEqual([
"Retry",
]);
});
test("double clicks and accepted submissions never duplicate recovery", async () => {
let settle!: (value: boolean) => void;
let sends = 0;
const container = await render(
{
sends++;
return new Promise((resolve) => {
settle = resolve;
});
}}
/>,
);
const button = container.querySelector("button")!;
await act(async () => {
button.click();
button.click();
});
expect(sends).toBe(1);
expect(button.textContent).toBe("Retrying…");
await act(async () => settle(true));
expect(container.querySelector("button")).toBeNull();
expect(container.querySelector('[role="status"]')!.textContent).toBe("Retry requested.");
});
test("typed structural sandbox failure never exposes retry or model-switch remedies", async () => {
const container = document.createElement("div");
document.body.append(container);
root = createRoot(container);
await act(async () =>
root!.render(
true,
retryBlocker: null,
}}
/>,
),
);
expect(container.textContent).not.toContain("Retry");
expect(container.textContent).not.toContain("Choose another model");
expect(container.textContent).toBe("Checkpoint is older than workspace");
expect(container.querySelectorAll("button")).toHaveLength(0);
});
test("all draft, delivery, work, permission and deliberate pause guards hide Retry", async () => {
let sends = 0;
const container = await render(null);
for (const retryBlocker of [
"draft",
"unsent",
"delivery",
"queued",
"loading",
"permission",
"paused",
] as const) {
await act(async () =>
root!.render(
{
sends++;
return true;
}}
/>,
),
);
expect(container.querySelector("button")).toBeNull();
expect(container.textContent).toBe("");
}
for (const composerBlocker of [
"upload",
"repository",
"policy",
"personal_decision",
"personal_loading",
] as const) {
await act(async () =>
root!.render(),
);
expect(container.querySelector("button")).toBeNull();
}
expect(sends).toBe(0);
});
test("sandbox projection gates compact Retry and structural failures override billing", async () => {
const sandboxRecovery = {
workspaceId: "workspace-a",
sessionId: "session-a",
canControl: true,
client: {
getSandboxRecovery: async () => ({
version: 1 as const,
status: "unsupported" as const,
reason: null,
operationId: null,
checkpoint: null,
}),
recoverSandbox: async () => {
throw new Error("Rendering must not submit consent");
},
},
};
const container = await render(
,
);
expect(container.querySelectorAll("button")).toHaveLength(1);
expect(container.querySelector("button")!.textContent).toBe("Retry");
await act(async () =>
root!.render(
,
),
);
expect(container.querySelectorAll('[data-testid="failed-session-banner"]')).toHaveLength(1);
expect(container.querySelectorAll("a")).toHaveLength(0);
expect(container.textContent).toContain("Checkpoint is older than workspace");
expect(container.textContent).not.toMatch(/Retry|Choose another|credits/);
expect(container.querySelector("button")!.textContent).toBe("Check recovery status");
});
test("a viewer who cannot use checkpoint recovery keeps Retry and sees no failed check", async () => {
const sandboxRecovery = {
workspaceId: "workspace-a",
sessionId: "session-a",
canControl: true,
client: {
getSandboxRecovery: async () => {
throw new OpenGeniApiError(
403,
JSON.stringify({ error: { code: "forbidden", message: "Managed human required." } }),
);
},
recoverSandbox: async () => {
throw new Error("Rendering must not submit consent");
},
},
};
const container = await render(
,
);
expect(container.textContent).not.toContain("checkpoint recovery");
expect(container.querySelectorAll("button")).toHaveLength(1);
expect(container.querySelector("button")!.textContent).toBe("Retry");
await act(async () =>
root!.render(
,
),
);
expect(container.textContent).toBe("Connection interrupted.");
expect(container.querySelector("button")).toBeNull();
});
test.each(["restored", "connected_machine", "automatic", "fresh_workspace"] as const)(
"%s projection exposes only explicit compact Retry through the structural banner",
async (route) => {
let retries = 0;
const sandboxRecovery = {
workspaceId: "workspace-a",
sessionId: "session-a",
canControl: true,
client: {
getSandboxRecovery: async () => ({
version: 1 as const,
status:
route === "restored"
? ("restored" as const)
: route === "automatic" || route === "fresh_workspace"
? ("eligible" as const)
: ("unsupported" as const),
reason: route === "connected_machine" ? "connected_machine_selected" : null,
operationId: route === "restored" ? "durable-operation" : null,
automaticAvailable: route === "automatic" || route === "fresh_workspace",
...(route === "automatic"
? { automaticLane: "checkpoint" as const }
: route === "fresh_workspace"
? { automaticLane: "fresh_workspace" as const }
: {}),
checkpoint:
route === "automatic"
? {
version: 1 as const,
sessionId: "session-a",
sandboxGroupId: "group-a",
leaseId: "lease-a",
routeEpoch: 0,
authorityEpoch: 1,
leaseEpoch: 2,
workspaceGeneration: 12,
archiveGeneration: 7,
artifactId: "artifact-a",
revision: "revision-a",
capturedAt: "2026-09-20T08:00:00.000Z",
}
: null,
}),
recoverSandbox: async () => {
throw new Error("Current route must not submit consent");
},
},
};
const failureProps = { ...failure, structuralSandboxFailure: true };
const retryProps = {
...actions,
onRetry: async () => {
retries++;
return true;
},
};
const container = await render(
,
);
expect(retries).toBe(0);
expect(container.querySelectorAll("button")).toHaveLength(1);
expect(container.querySelector("button")!.textContent).toBe("Retry");
if (route === "automatic") {
expect(container.textContent).toContain("Retry will use the latest verified checkpoint from");
expect(container.textContent).toContain("Newer files are unavailable.");
}
if (route === "fresh_workspace")
expect(container.textContent).toContain(
"Retry will continue with an empty workspace. Opengeni cannot restore the previous sandbox files automatically.",
);
expect(container.querySelector("button")!.dataset.variant).toBe("ghost");
expect(container.textContent).not.toContain("Choose another model");
await act(async () =>
root!.render(
,
),
);
expect(container.querySelector("button")).toBeNull();
await act(async () =>
root!.render(
,
),
);
expect(container.querySelector("button")).toBeNull();
await act(async () =>
root!.render(
,
),
);
expect(container.querySelector("button")).toBeNull();
await act(async () =>
root!.render(
,
),
);
expect(retries).toBe(0);
await act(async () => container.querySelector("button")!.click());
expect(retries).toBe(1);
expect(container.querySelector("button")).toBeNull();
},
);
test("local non-submission remains retryable", async () => {
const container = await render( false} />);
await act(async () => container.querySelector("button")!.click());
expect(container.querySelector('[role="alert"]')!.textContent).toContain("Could not retry");
expect(container.querySelector("button")!.disabled).toBe(false);
});
test("history hydration retains guards; a distinct failure resets them", async () => {
let settle!: (value: boolean) => void;
const props = {
...actions,
onRetry: () =>
new Promise((resolve) => {
settle = resolve;
}),
};
const container = await render();
await act(async () => container.querySelector("button")!.click());
await act(async () => root!.render());
expect(container.querySelector("button")!.textContent).toBe("Retrying…");
await act(async () => root!.render());
await act(async () => settle(true));
expect(container.querySelector("button")!.textContent).toBe("Retry");
});
test("unknown outcome checks the identical frozen operation instead of a new retry", async () => {
const inputs: FailedSessionRetryInput[] = [];
function Recovery() {
const [retryInput, setRetryInput] = useState(null);
const retry = useMemo(
() =>
createFailedSessionRetry(async (input) => {
inputs.push(input);
if (inputs.length === 1) throw new Error("Response lost");
}, setRetryInput),
[],
);
return (
retry("failure-a", {
model: "selected-model",
reasoningEffort: "medium",
latencyMode: "standard",
})
}
/>
);
}
const container = await render();
await act(async () => container.querySelector("button")!.click());
expect(container.querySelectorAll("button")).toHaveLength(1);
expect(container.querySelector("button")!.textContent).toBe("Check retry");
expect(container.textContent).not.toContain("Choose");
await act(async () => container.querySelector("button")!.click());
expect(inputs).toHaveLength(2);
expect(inputs[1]).toBe(inputs[0]);
expect(container.querySelector("button")).toBeNull();
});
test("definitive unsafe retry rejection removes the action", async () => {
const container = await render(
{
throw new OpenGeniApiError(409, "Unsupported failure", {
code: "RETRY_UNSUPPORTED_FAILURE",
});
}}
/>,
);
await act(async () => container.querySelector("button")!.click());
expect(container.querySelector("button")).toBeNull();
expect(container.querySelector('[role="alert"]')!.textContent).toContain(
"cannot be retried safely",
);
});
test("a permanently unknown tool outcome says what to do instead of still settling", async () => {
let sends = 0;
const container = await render(
{
sends += 1;
throw new OpenGeniApiError(409, "Unknown tool outcome", {
code: "RETRY_TOOL_OUTCOME_UNKNOWN",
});
}}
/>,
);
await act(async () => container.querySelector("button")!.click());
expect(sends).toBe(1);
// Permanent: the action is removed rather than inviting a retry that can
// never succeed, and the copy names the person's next step.
expect(container.querySelector("button")).toBeNull();
const alert = container.querySelector('[role="alert"]')!.textContent!;
expect(alert).not.toContain("settling");
expect(alert).toBe(
"A tool may already have run, so Retry could repeat it. Check its result, then send a message to continue.",
);
});
for (const code of ["RETRY_PAUSED", "RETRY_EXECUTION_UNRESOLVED"] as const) {
test(`${code} allows explicit retry after the same failure's transient blocker clears`, async () => {
let sends = 0;
const onRetry = async () => {
if (++sends === 1) throw new OpenGeniApiError(409, "Blocked", { code });
return true;
};
const container = await render(
,
);
await act(async () => container.querySelector("button")!.click());
expect(container.querySelector('[role="alert"]')!.textContent).toBe(
code === "RETRY_PAUSED"
? "This session is paused."
: "Earlier work is still settling. Try again in a moment.",
);
await act(async () =>
root!.render(
,
),
);
expect(container.querySelector("button")).toBeNull();
await act(async () =>
root!.render(
,
),
);
expect(sends).toBe(1);
await act(async () => container.querySelector("button")!.click());
expect(sends).toBe(2);
expect(container.querySelector("button")).toBeNull();
expect(container.querySelector('[role="alert"]')).toBeNull();
});
}
test("unsupported model directs to existing picker; a changed model permits recovery", async () => {
const unavailable = {
...failure,
reason: "The model `example` is not supported with this account.",
};
const container = await render(
,
);
expect(container.textContent).toBe("This model isn’t available. Choose another below.");
expect(container.querySelector("button")).toBeNull();
await act(async () =>
root!.render(
,
),
);
expect(container.textContent).toBe("This model isn’t available.");
expect(container.querySelector("button")).toBeNull();
await act(async () =>
root!.render(),
);
expect(container.querySelector("button")!.textContent).toBe("Retry");
});
test("safety refusal preserves draft and has no recovery action", async () => {
const container = await render(
<>
>,
);
expect(container.querySelector("button")).toBeNull();
expect(container.querySelector("textarea")!.value).toBe("My draft");
});
test("billing offers at most one authorized action and never Retry", async () => {
const container = await render(null);
for (const [canBuyCredits, canConnectModel, expected] of [
[true, true, "Buy credits"],
[false, true, "Connect a model"],
[false, false, null],
] as const) {
await act(async () =>
root!.render(
,
),
);
expect(container.querySelectorAll("a")).toHaveLength(expected ? 1 : 0);
expect(container.querySelector("a")?.textContent ?? null).toBe(expected);
expect(container.querySelector("button")).toBeNull();
}
});
test("free credit for other models offers a covered model before buying credits", async () => {
const chosen: string[] = [];
const gap = {
modelLabel: "Claude Opus 5.5",
covered: [
{ id: "azure-sol/gpt-6.1-sol", label: "GPT-6.1 Sol" },
{ id: "gpt-6-luna", label: "GPT-6 Luna" },
],
selectionUsable: false,
};
const banner = (props: {
modelChanged?: boolean | undefined;
selectionUsable?: boolean | undefined;
canBuyCredits?: boolean | undefined;
}) => (
chosen.push(model)}
workspaceId="workspace-a"
canBuyCredits={props.canBuyCredits ?? true}
canChooseModel
modelChanged={props.modelChanged}
actions={actions}
/>
);
const container = await render(banner({}));
const headline = container.querySelector('[data-testid="failed-session-banner"] span')!;
expect(headline.textContent).toBe(
"Your free credits don't cover Claude Opus 5.5. Switch to a model they cover, such as GPT-6.1 Sol or GPT-6 Luna, or add credits.",
);
expect(container.textContent).not.toMatch(/out of Opengeni credits/i);
const switchButton = container.querySelector("button")!;
expect(switchButton.textContent).toBe("Use GPT-6.1 Sol");
expect(container.querySelector("a")!.textContent).toBe("Buy credits");
await act(async () => switchButton.click());
expect(chosen).toEqual(["azure-sol/gpt-6.1-sol"]);
await act(async () => root!.render(banner({ canBuyCredits: false })));
expect(container.querySelector("button")!.textContent).toBe("Use GPT-6.1 Sol");
expect(container.querySelector("a")).toBeNull();
// Another uncovered model selected: the switch stays on offer.
await act(async () => root!.render(banner({ modelChanged: true })));
expect(container.querySelector("button")!.textContent).toBe("Use GPT-6.1 Sol");
expect(container.textContent).toContain("Switch to a model they cover");
// A usable model selected: the copy says how to continue and no switch is offered.
await act(async () => root!.render(banner({ modelChanged: true, selectionUsable: true })));
expect(container.textContent).toContain(
"Your free credits don't cover Claude Opus 5.5. Send a message to continue with the selected model.",
);
expect(container.querySelector("button")).toBeNull();
});
test("unknown provider evidence never invents expiry, reset or connection classification", async () => {
const container = await render(
,
);
const row = container.querySelector('[data-testid="failed-session-banner"]')!;
expect(row.querySelector("span")!.textContent).toBe("The session stopped unexpectedly.");
expect(row.querySelector("details p")!.textContent).toBe("Connection failed.");
expect(row.querySelector("details")!.open).toBe(false);
expect(row.querySelector("button")!.textContent).toBe("Retry");
expect(container.textContent).not.toMatch(/Codex|expired|reset|reconnect/i);
});
test("unavailable recovery chunk preserves error and composer draft", async () => {
function Unavailable(): never {
throw new Error("fixture unavailable");
}
const container = await render(
<>
Connection failed.
}>
>,
);
expect(container.querySelector('[role="alert"]')!.textContent).toBe("Connection failed.");
expect(container.querySelector("textarea")!.value).toBe("My draft");
});
test("a compaction failure the timeline already shows keeps only its recovery action", async () => {
const recorded =
"compaction summarization failed: 400 Missing required parameter: 'input[116].status'.";
const compaction = {
...failure,
reason: recorded,
recordedDetail: recorded,
failureCode: "context_compaction_failed",
};
const shown = await render(
,
);
// No second headline or Details: the timeline landmark owns both.
expect(shown.querySelector('[data-testid="failed-session-banner"]')).toBeNull();
expect(shown.querySelector("details")).toBeNull();
expect(shown.textContent).not.toContain("Couldn’t compact");
expect(shown.querySelector("button")!.textContent).toBe("Retry");
await act(async () => root?.unmount());
document.body.replaceChildren();
// Nothing to offer: nothing renders.
const empty = await render();
expect(empty.innerHTML).toBe("");
await act(async () => root?.unmount());
document.body.replaceChildren();
// Absent from the visible timeline (e.g. a cleared view), the banner keeps it.
const absent = await render();
expect(absent.querySelector('[data-testid="failed-session-banner"] span')!.textContent).toBe(
"Couldn’t compact conversation history. If the next message fails too, start a new session.",
);
expect(absent.querySelector("details p")!.textContent).toBe(recorded);
});
test("the timeline flag never hides a failure the timeline cannot own", async () => {
// Another failure kind keeps its banner even if the caller claims ownership.
const container = await render(
,
);
expect(container.querySelector('[data-testid="failed-session-banner"] span')!.textContent).toBe(
"The session stopped unexpectedly.",
);
await act(async () => root?.unmount());
document.body.replaceChildren();
// A compaction failure on an exhausted quota keeps its limit headline.
const quota = await render(
,
);
expect(quota.querySelector('[data-testid="failed-session-banner"]')).not.toBeNull();
expect(quota.textContent).toContain("daily limit");
});