repositories / pi-ext
pi-ext
bugabingas pi extensions
owned by admin
extensions/strata/__tests__/contract.test.ts
Rawimport {
appendFile,
mkdtemp,
readdir,
readFile,
rm,
writeFile,
} from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import {
type AssistantMessage,
type Context,
fauxAssistantMessage,
fauxProvider,
InMemoryCredentialStore,
} from "@earendil-works/pi-ai";
import type { ExtensionUIContext } from "@earendil-works/pi-coding-agent";
import { stripTerminalSequences } from "@earendil-works/pi-tui";
import { aroundEach, describe, expect, it, vi } from "vitest";
import {
createPiSession,
type PiSession,
} from "../../../test/fixture/create-pi-session.ts";
import {
cleanupNative,
fixturePlan,
git,
nativeOperation,
nativeTest,
pushCancelling,
testSignal,
withFakeGh,
} from "./fixture.js";
// Builds a real Pi session through `createPiSession` (public APIs only), then
// drives the extension's `/strata` command with a captured UI context and a
// deterministic scripted provider.
async function createFixture(mode: "tui" | "rpc" | "json" = "tui") {
const signal = testSignal();
signal.throwIfAborted();
const root = await mkdtemp(path.join(tmpdir(), "strata-pi-contract-"));
let session: PiSession | undefined;
let closing: Promise<void> | undefined;
const close = () =>
(closing ??= (async () => {
try {
await session?.dispose();
} finally {
await rm(root, {
recursive: true,
force: true,
maxRetries: 3,
retryDelay: 100,
});
}
})());
cleanupNative(close);
try {
await git(root, "init", "--quiet");
await appendFile(
path.join(root, ".git", "config"),
"[user]\n\tname = Strata Test\n\temail = strata@example.invalid\n",
);
await writeFile(
path.join(root, "contract.ts"),
"export const attempts = 1;\n",
);
await writeFile(
path.join(root, "consumer.ts"),
"export const retry = false;\n",
);
await git(root, "add", ".");
await git(root, "commit", "--quiet", "-m", "base");
await writeFile(
path.join(root, "contract.ts"),
"export const attempts = 3;\n",
);
await writeFile(
path.join(root, "consumer.ts"),
"export const retry = true;\n",
);
session = await createPiSession({
credentials: new InMemoryCredentialStore(),
cwd: root,
env: { STRATA_NO_OPEN: "1" },
extensionPaths: [path.resolve(import.meta.dirname, "../index.ts")],
});
const active = session;
signal.throwIfAborted();
const runner = active.session.extensionRunner;
const emit = runner.emit.bind(runner);
runner.emit = ((...args: Parameters<typeof emit>) =>
nativeOperation(() => emit(...args))) as typeof runner.emit;
const faux = fauxProvider({
provider: "strata-integration",
models: [{ id: "review", maxTokens: 16_384 }],
});
let calls = 0;
const prompts: string[] = [];
const respond = (context: Context) => {
calls++;
const message = context.messages.findLast((item) => item.role === "user");
const text =
typeof message?.content === "string"
? message.content
: (message?.content ?? [])
.filter((part) => part.type === "text")
.map((part) => part.text)
.join("");
prompts.push(text);
if (text.startsWith("Organize")) {
const hunks = [
...text.matchAll(
/<file path="(contract\.ts|consumer\.ts)">\s*<hunk id="([^"]+)"/g,
),
].map(([, filePath, id]) => ({ id: id!, path: filePath! }));
return fauxAssistantMessage(JSON.stringify(fixturePlan(hunks)));
}
return fauxAssistantMessage("Three attempts include the first call.");
};
faux.setResponses(Array.from({ length: 20 }, () => respond));
const base = runner.createContext();
base.modelRegistry.registerProvider(faux.provider);
const model = base.modelRegistry.find("strata-integration", "review");
if (!model) throw new Error("Fixture model missing");
await active.session.setModel(model);
const notifications: Array<{ text: string; severity: string }> = [];
const widgets: Array<{ key: string; content: unknown; options: unknown }> =
[];
const uiCalls: Array<{ method: string; args: unknown[] }> = [];
const statusMessages: Array<{
role: string;
customType?: unknown;
content?: unknown;
display?: unknown;
details?: unknown;
}> = [];
// pi.sendMessage maps to AgentSession.sendCustomMessage. Capture these
// without writing to stdout so no-UI modes assert the emitted custom
// messages.
vi.spyOn(active.session, "sendCustomMessage").mockImplementation(
async (message) => {
statusMessages.push({ role: "custom", ...message });
},
);
const capturingUi = {
notify: (text: string, severity = "info") => {
uiCalls.push({ method: "notify", args: [text, severity] });
notifications.push({ text, severity });
},
setWidget: (key: string, content: unknown, options?: unknown) => {
uiCalls.push({ method: "setWidget", args: [key, content, options] });
widgets.push({ key, content, options });
},
setStatus: (key: string, text?: string) => {
uiCalls.push({ method: "setStatus", args: [key, text] });
},
setFooter: () => {},
confirm: async () => true,
};
if (mode === "json") runner.setUIContext(undefined, "json");
else
runner.setUIContext(capturingUi as unknown as ExtensionUIContext, mode);
const ctx = Object.create(base);
Object.defineProperty(ctx, "waitForIdle", { value: async () => {} });
const command = active.extensions[0]!.commands.get("strata");
if (!command) throw new Error("strata command missing");
const cancel = () => {
const stopped = nativeOperation(() => command.handler("close", ctx));
pushCancelling(stopped);
};
signal.throwIfAborted();
signal.addEventListener("abort", cancel, { once: true });
cleanupNative(async () => {
signal.removeEventListener("abort", cancel);
});
const run = (args = "") =>
nativeOperation(async () => {
signal.throwIfAborted();
await command.handler(args, ctx);
signal.throwIfAborted();
});
return {
t: {
env: process.env,
session: active.session,
events: { ui: uiCalls, messages: statusMessages },
},
root,
ctx,
runner,
extensions: active.extensions,
command,
faux,
notifications,
widgets,
prompts,
run,
widgetLines(width = 120): string[] {
const content = widgets.findLast(
(item) => item.key === "strata",
)?.content;
if (content === undefined) return [];
if (Array.isArray(content)) return content as string[];
if (typeof content !== "function") return [];
const theme = {
bold: (text: string) => text,
fg: (_color: string, text: string) => text,
};
return content({}, theme).render(width);
},
widgetText(width = 120): string {
return this.widgetLines(width).map(stripTerminalSequences).join("\n");
},
get calls() {
return calls;
},
get url(): string {
const line = this.widgetLines(160)[1] ?? "";
const match = /\x1b]8;;([^\x1b]+)\x1b\\/.exec(line);
if (match?.[1]) return match[1];
const notice = notifications.findLast((item) =>
item.text.startsWith("Strata review: "),
);
if (notice) return notice.text.slice("Strata review: ".length);
const message = statusMessages.findLast(
(item) =>
item.role === "custom" &&
item.customType === "strata-status" &&
typeof item.content === "string" &&
item.content.startsWith("Strata review: "),
);
if (message && typeof message.content === "string")
return message.content.slice("Strata review: ".length);
throw new Error(`No review URL: ${line}`);
},
close,
};
} catch (error) {
await close();
throw error;
}
}
let fixture: Awaited<ReturnType<typeof createFixture>> | undefined;
aroundEach(async (run, context) => {
try {
await nativeTest(run, context);
} finally {
fixture = undefined;
vi.restoreAllMocks();
}
});
async function connect(url: string) {
const response = await fetch(url, {
redirect: "manual",
signal: testSignal(),
});
const cookie = response.headers.get("set-cookie")?.split(";")[0] ?? "";
const origin = new URL(url).origin;
let version = await (
await fetch(`${origin}/api/state`, {
headers: { Cookie: cookie },
signal: testSignal(),
})
).json();
return (route: string, body?: unknown) =>
nativeOperation(async () => {
const response = await fetch(`${origin}/api/${route}`, {
signal: testSignal(),
method: body === undefined ? "GET" : "POST",
headers: {
Cookie: cookie,
Origin: origin,
"Content-Type": "application/json",
"If-Match": String(version.revision),
"X-Strata-Snapshot": version.snapshotId,
},
...(body === undefined ? {} : { body: JSON.stringify(body) }),
});
const payload = await response.clone().json();
if (payload.revision !== undefined) version = payload;
return response;
});
}
function planResponse(context: unknown): AssistantMessage {
const messages = (
context as {
messages: Array<{
role: string;
content: string | Array<{ type: string; text?: string }>;
}>;
}
).messages;
const message = messages.findLast((item) => item.role === "user");
const text =
typeof message?.content === "string"
? message.content
: (message?.content ?? [])
.filter((part) => part.type === "text")
.map((part) => part.text ?? "")
.join("");
const hunks = [
...text.matchAll(
/<file path="(contract\.ts|consumer\.ts)">\s*<hunk id="([^"]+)"/g,
),
].map(([, filePath, id]) => ({ id: id!, path: filePath! }));
return fauxAssistantMessage(JSON.stringify(fixturePlan(hunks)));
}
function expectPhase(value: string): void {
expect(fixture?.widgetText()).toContain(`● ${value}`);
}
describe("Strata real Pi runtime", () => {
it("warns about missing Git using the real startup path", async () => {
fixture = await createFixture();
fixture.t.env["PATH"] = path.join(fixture.root, "missing-bin");
await fixture.runner.emit({ type: "session_start", reason: "reload" });
await vi.waitFor(() =>
expect(
fixture?.t.events.ui.some(
(call) =>
call.method === "notify" &&
call.args[0] === "Strata requires Git on PATH." &&
call.args[1] === "warning",
),
).toBe(true),
);
});
it("writes its opt-in lifecycle trace inside the fixture sandbox", async () => {
const root = await mkdtemp(path.join(tmpdir(), "pi-ext-strata-debug-"));
const session = await createPiSession({
credentials: new InMemoryCredentialStore(),
cwd: root,
extensionPaths: [path.resolve(import.meta.dirname, "../index.ts")],
env: { PI_STRATA_DEBUG: "1" },
});
try {
const runner = session.session.extensionRunner;
await runner.emit({ type: "session_start", reason: "startup" });
await session.extensions[0]!.commands.get("strata")?.handler(
"--invalid",
runner.createCommandContext(),
);
await runner.emit({ type: "session_shutdown", reason: "quit" });
const stateHome = session.env["XDG_STATE_HOME"];
if (!stateHome) throw new Error("missing sandbox state home");
const directory = path.join(stateHome, "pi-ext", "debug", "strata");
const [file] = await readdir(directory);
const records = (await readFile(path.join(directory, file!), "utf8"))
.trim()
.split("\n")
.map((line) => JSON.parse(line));
expect(records.map((record) => record.event)).toEqual(
expect.arrayContaining(["session.start", "session.shutdown"]),
);
expect(records).toContainEqual(
expect.objectContaining({
event: "review.open.finish",
outcome: "failed",
}),
);
} finally {
await session.dispose();
await rm(root, {
recursive: true,
force: true,
maxRetries: 3,
retryDelay: 100,
});
}
});
it("loads optional gh only for --pr and reports a missing CLI", async () => {
fixture = await createFixture();
const originalPath = fixture.t.env["PATH"];
fixture.t.env["PATH"] = path.join(fixture.root, "missing-bin");
await fixture.run("--pr 7");
expectPhase("Error");
expect(fixture.widgetText()).toContain("optional gh CLI on PATH");
await fixture.run("close");
fixture.t.env["PATH"] = originalPath;
});
it("reports gh authentication failure without raw CLI output", async () => {
fixture = await createFixture();
await withFakeGh(
`process.stderr.write("authentication failed: secret-token-value"); process.exitCode = 1;`,
async () => {
await fixture?.run("--pr 7");
},
);
expectPhase("Error");
expect(fixture.widgetText()).toContain("GitHub authentication failed");
expect(fixture.widgetText()).not.toContain("secret-token-value");
expect(fixture.calls).toBe(0);
});
it("refuses a dirty PR checkout through the real extension path", async () => {
fixture = await createFixture();
const commit = await git(fixture.root, "rev-parse", "HEAD");
await git(
fixture.root,
"remote",
"add",
"origin",
"https://github.com/acme/project.git",
);
const metadata = {
number: 7,
url: "https://github.com/acme/project/pull/7",
title: "Fixture PR",
body: "",
author: { login: "author" },
baseRefName: "main",
baseRefOid: commit,
headRefName: "feature",
headRefOid: commit,
headRepository: {
name: "project",
nameWithOwner: "acme/project",
},
headRepositoryOwner: { login: "acme" },
};
await withFakeGh(
`if (args[0] === "pr") process.stdout.write(${JSON.stringify(JSON.stringify(metadata))}); else process.stdout.write("[]");`,
async () => {
await fixture?.run("--pr 7");
},
);
expectPhase("Error");
expect(fixture.widgetText()).toContain("requires a clean worktree");
expect(fixture.calls).toBe(0);
});
it("places the link widget above the editor without footer or notification noise", async () => {
fixture = await createFixture();
expect(fixture.extensions[0]!.path).toBe(
path.resolve(import.meta.dirname, "../index.ts"),
);
expect(fixture.command.getArgumentCompletions?.("--")).toHaveLength(3);
expect(fixture.calls).toBe(0);
await fixture.run();
expect(fixture.calls).toBe(1);
expectPhase("Ready");
expect(fixture.widgetText()).toContain("0/2 reviewed");
expect(fixture.widgetText()).toContain("2 layers");
expect(fixture.widgets.at(-1)?.options).toEqual({
placement: "aboveEditor",
});
expect(
fixture.t.events.ui.filter((call) => call.method === "setStatus"),
).toEqual([]);
expect(
fixture.notifications.some((item) =>
item.text.startsWith("Strata review:"),
),
).toBe(false);
const url = fixture.url;
const widgetCalls = fixture.widgets.length;
await fixture.run();
expect(fixture.url).toBe(url);
expect(fixture.calls).toBe(1);
expect(fixture.widgets.length).toBe(widgetCalls + 1);
expect(fixture.notifications).toEqual([]);
});
it("persists draft progress, Ask history, close state, and snapshot-bound reopen", async () => {
fixture = await createFixture();
await fixture.run();
const firstUrl = fixture.url;
const api = await connect(firstUrl);
const { review } = await (await api("state")).json();
const draft = {
reviewed: [review.snapshot.hunks[0].id],
findings: [
{
id: "finding-1",
hunkId: review.snapshot.hunks[0].id,
side: "new",
line: 1,
severity: "minor",
text: "Check the bound",
},
],
notes: "Check retry bounds",
};
expect((await api("draft", draft)).status).toBe(200);
expect(fixture.widgetText()).toContain("1/2 reviewed");
expect(fixture.widgetText()).toContain("1 comment");
expectPhase("Ready");
const answer = await api("ask", {
layerId: "layer-1",
hunkId: review.snapshot.hunks[0].id,
question: "How many attempts?",
});
expect((await answer.json()).answer).toBe(
"Three attempts include the first call.",
);
expect(fixture.calls).toBe(2);
await fixture.run("close");
expect(fixture.widgetLines()).toEqual([]);
await fixture.run();
expect(fixture.calls).toBe(2);
expect(fixture.url).not.toBe(firstUrl);
const resumed = await connect(fixture.url);
const resumedState = await (await resumed("state")).json();
expect(resumedState.review.draft).toEqual(draft);
expect(resumedState.threads).toEqual([]);
expect(fixture.widgetText()).toContain("1/2 reviewed");
});
it("reflects pending Ask work and returns to Ready", async () => {
fixture = await createFixture();
await fixture.run();
const url = fixture.url;
const api = await connect(url);
const { review } = await (await api("state")).json();
let releaseAsk!: (message: AssistantMessage) => void;
let askStarted!: () => void;
const didAskStart = new Promise<void>((resolve) => {
askStarted = resolve;
});
vi.spyOn(fixture.ctx.modelRegistry, "complete").mockImplementationOnce(
async (_model, _context, options) => {
const signal = (options as { signal?: AbortSignal } | undefined)
?.signal;
return new Promise<AssistantMessage>((resolve, reject) => {
signal?.addEventListener("abort", () => reject(signal?.reason), {
once: true,
});
releaseAsk = resolve;
askStarted();
});
},
);
const asking = api("ask", {
layerId: "layer-1",
hunkId: review.snapshot.hunks[0].id,
question: "Wait for this answer",
});
await didAskStart;
expectPhase("Answering");
releaseAsk(fauxAssistantMessage("Done"));
expect((await asking).status).toBe(200);
expectPhase("Ready");
});
it("preserves the active link and counts after an Ask error", async () => {
fixture = await createFixture();
await fixture.run();
const url = fixture.url;
const api = await connect(url);
vi.spyOn(fixture.ctx.modelRegistry, "complete").mockRejectedValueOnce(
new Error("model\x1b]8;;https://evil.example\x07 unavailable"),
);
expect(
(
await api("ask", {
layerId: "layer-1",
hunkId: null,
question: "Fail",
})
).status,
).toBe(500);
expectPhase("Error");
expect(fixture.widgetText()).toContain(
"model ]8;;https://evil.example unavailable",
);
expect(fixture.url).toBe(url);
expect(fixture.widgetText()).toContain("0/2 reviewed");
});
it("avoids replanning when the snapshot and forge context are unchanged", async () => {
fixture = await createFixture();
await fixture.run();
const api = await connect(fixture.url);
const complete = vi.spyOn(fixture.ctx.modelRegistry, "complete");
expect((await api("refresh", { confirm: true })).status).toBe(200);
expect(complete).not.toHaveBeenCalled();
expectPhase("Ready");
});
it("keeps stale until refresh carries unchanged draft progress", async () => {
fixture = await createFixture();
await fixture.run();
const api = await connect(fixture.url);
const { review } = await (await api("state")).json();
const draft = {
reviewed: [review.snapshot.hunks[0].id],
findings: [],
notes: "Verify the retry contract.",
};
await writeFile(
path.join(fixture.root, "contract.ts"),
"export const attempts = 4;\n",
);
expect((await api("submit", { draft })).status).toBe(409);
expectPhase("Stale");
expect(fixture.widgetText()).toContain("1/2 reviewed");
expect((await api("draft", draft)).status).toBe(200);
expectPhase("Stale");
expect((await api("refresh", { confirm: true })).status).toBe(200);
expectPhase("Ready");
expect(fixture.widgetText()).toContain("1/2 reviewed");
});
it("keeps Feedback sent while preserving reviewed progress", async () => {
fixture = await createFixture();
await writeFile(
path.join(fixture.root, "contract.ts"),
"export const attempts = 4;\n",
);
await fixture.run();
const api = await connect(fixture.url);
const { review: refreshedReview } = await (await api("state")).json();
const send = vi
.spyOn(fixture.t.session, "prompt")
.mockResolvedValue(undefined);
const feedback = {
reviewed: [refreshedReview.snapshot.hunks[0].id],
findings: [],
notes: "Four attempts are intentional.",
};
expect((await api("submit", { draft: feedback })).status).toBe(200);
await vi.waitFor(() => expect(send).toHaveBeenCalled());
expect(send.mock.calls[0]![0]).toContain(refreshedReview.snapshot.id);
expectPhase("Feedback sent");
expect(fixture.widgetText()).toContain("1/2 reviewed");
expect((await (await api("state")).json()).review.draft).toMatchObject({
reviewed: [refreshedReview.snapshot.hunks[0].id],
findings: [],
notes: "",
});
});
it("persists local recovery across browser Close and a new workspace port", async () => {
fixture = await createFixture();
await fixture.run();
const originalUrl = fixture.url;
const api = await connect(originalUrl);
const original = await (await api("state")).json();
const text = JSON.stringify({
reviewed: [],
findings: [],
notes: "Unacknowledged browser work",
});
expect(
(
await api("close", {
recoveries: [{ snapshotId: original.snapshotId, text }],
})
).status,
).toBe(200);
await vi.waitFor(() => expect(fixture?.widgetText()).toBe(""));
await fixture.run();
expect(fixture.url).not.toBe(originalUrl);
const reopened = await connect(fixture.url);
const latest = await (await reopened("state")).json();
expect(latest.review.recoveries).toEqual([
expect.objectContaining({ snapshotId: original.snapshotId, text }),
]);
expect(latest.review.draft.notes).toBe("");
expect(fixture.calls).toBe(1);
});
it("retains URL and error discovery in RPC mode without component factories", async () => {
fixture = await createFixture("rpc");
await fixture.run();
const url = fixture.url;
expect(fixture.widgets).toEqual([]);
expect(fixture.notifications).toEqual([
{ text: `Strata review: ${url}`, severity: "info" },
]);
await fixture.run();
expect(fixture.notifications).toHaveLength(1);
await fixture.run("close");
await writeFile(
path.join(fixture.root, "contract.ts"),
"export const attempts = 5;\n",
);
fixture.faux.setResponses([fauxAssistantMessage("not json")]);
await fixture.run();
expect(fixture.notifications.at(-1)).toMatchObject({ severity: "error" });
});
it("emits URL and error custom messages without terminal writes or LLM turns when no UI exists", async () => {
const stdout = vi.spyOn(process.stdout, "write").mockReturnValue(true);
const stderr = vi.spyOn(process.stderr, "write").mockReturnValue(true);
fixture = await createFixture("json");
await fixture.run();
const statusMessages = () =>
fixture?.t.events.messages.filter(
(message) =>
message.role === "custom" && message.customType === "strata-status",
) ?? [];
expect(statusMessages()).toHaveLength(1);
expect(statusMessages()[0]).toMatchObject({
content: expect.stringMatching(
/Strata review: http:\/\/127\.0\.0\.1:\d+\/\?token=\S+/,
),
display: true,
details: { severity: "info" },
});
expect(fixture.calls).toBe(1);
await fixture.run();
expect(statusMessages()).toHaveLength(1);
expect(fixture.calls).toBe(1);
await fixture.run("close");
await writeFile(
path.join(fixture.root, "contract.ts"),
"export const attempts = 5;\n",
);
fixture.faux.setResponses([fauxAssistantMessage("not json")]);
await fixture.run();
expect(statusMessages().at(-1)).toMatchObject({
content: expect.stringContaining("Strata failed:"),
display: true,
details: { severity: "error" },
});
expect(fixture.calls).toBe(1);
expect(stdout).not.toHaveBeenCalled();
expect(stderr).not.toHaveBeenCalled();
});
it("keeps initial failures visible and resets them on reopen", async () => {
fixture = await createFixture();
fixture.faux.setResponses([fauxAssistantMessage("not json")]);
await fixture.run();
expectPhase("Error");
expect(fixture.widgetText()).toContain("Strata failed:");
expect(fixture.widgetText(240)).toContain("Run /strata to retry");
expect(fixture.notifications).toEqual([]);
await fixture.run("--unknown");
expectPhase("Error");
expect(fixture.widgetText()).toContain("usage:");
expect(fixture.widgetText()).not.toContain("model response is not JSON");
});
it("guards cancelled and replaced generations against late callbacks", async () => {
fixture = await createFixture();
const pendingCalls: Array<{
context: unknown;
resolve(message: AssistantMessage): void;
}> = [];
const started = Array.from({ length: 2 }, () =>
Promise.withResolvers<void>(),
);
const complete = vi
.spyOn(fixture.ctx.modelRegistry, "complete")
.mockImplementation(async (_model, context) => {
return new Promise<AssistantMessage>((resolve) => {
pendingCalls.push({ context, resolve });
started[pendingCalls.length - 1]!.resolve();
});
});
testSignal().addEventListener(
"abort",
() => {
for (const call of pendingCalls)
call.resolve(planResponse(call.context));
},
{ once: true },
);
const oldOpen = fixture.run();
await started[0]!.promise;
expect(pendingCalls).toHaveLength(1);
await fixture.run("close");
expect(fixture.widgetLines()).toEqual([]);
const replacement = fixture.run();
await started[1]!.promise;
expect(pendingCalls).toHaveLength(2);
pendingCalls[1]!.resolve(planResponse(pendingCalls[1]!.context));
await replacement;
expectPhase("Ready");
const replacementUrl = fixture.url;
pendingCalls[0]!.resolve(planResponse(pendingCalls[0]!.context));
await oldOpen;
expectPhase("Ready");
expect(fixture.url).toBe(replacementUrl);
expect(complete).toHaveBeenCalledTimes(2);
});
it("drains pending generation on shutdown and ignores its late callback", async () => {
fixture = await createFixture();
await writeFile(
path.join(fixture.root, "contract.ts"),
"export const attempts = 6;\n",
);
const started = Promise.withResolvers<void>();
const pending = Promise.withResolvers<AssistantMessage>();
let context: unknown;
const complete = vi
.spyOn(fixture.ctx.modelRegistry, "complete")
.mockImplementation(async (_model, value) => {
context = value;
started.resolve();
return pending.promise;
});
testSignal().addEventListener(
"abort",
() =>
pending.resolve(
context ? planResponse(context) : fauxAssistantMessage("cancelled"),
),
{ once: true },
);
const shutdownOpen = fixture.run();
await started.promise;
expect(complete).toHaveBeenCalledOnce();
let settled = false;
const shutdown = fixture.runner
.emit({ type: "session_shutdown", reason: "reload" })
.then(() => {
settled = true;
});
expect(fixture.widgetLines()).toEqual([]);
await Promise.resolve();
expect(settled).toBe(false);
pending.resolve(planResponse(context));
await shutdown;
await shutdownOpen;
expect(settled).toBe(true);
expect(fixture.widgetLines()).toEqual([]);
expect(fixture.notifications).toEqual([]);
});
});