diff --git a/benchmarks/aft-search/slice-descriptors/land-test-subprocess-waits.json b/benchmarks/aft-search/slice-descriptors/land-test-subprocess-waits.json new file mode 100644 index 000000000..ebd574711 --- /dev/null +++ b/benchmarks/aft-search/slice-descriptors/land-test-subprocess-waits.json @@ -0,0 +1,8 @@ +{ + "targeted_mechanism": "none", + "kind": "harness", + "slice_class": "non_ranking", + "fixtures": [ + "harness-goldens" + ] +} diff --git a/packages/opencode-plugin/src/__tests__/e2e/outside-permissions.test.ts b/packages/opencode-plugin/src/__tests__/e2e/outside-permissions.test.ts index e98b88c6e..213b6b199 100644 --- a/packages/opencode-plugin/src/__tests__/e2e/outside-permissions.test.ts +++ b/packages/opencode-plugin/src/__tests__/e2e/outside-permissions.test.ts @@ -20,7 +20,16 @@ async function stdoutPath(storage: string, id: string): Promise { while (pending.length) { const directory = pending.pop(); if (!directory) break; - for (const entry of await readdir(directory, { withFileTypes: true })) { + // The engine's index builders create and remove temporary folders under + // the same storage root while this walk runs, so a listed folder can be + // gone by the time it is read; skip it. + const entries = await readdir(directory, { withFileTypes: true }).catch( + (error: NodeJS.ErrnoException) => { + if (error.code === "ENOENT") return []; + throw error; + }, + ); + for (const entry of entries) { const path = join(directory, entry.name); if (entry.isDirectory()) pending.push(path); else if (entry.name === `${id}.stdout` || (entry.name === "stdout" && path.includes(id))) diff --git a/packages/pi-plugin/src/__tests__/headless-exit.test.ts b/packages/pi-plugin/src/__tests__/headless-exit.test.ts index 9c43754e8..0da22d8e6 100644 --- a/packages/pi-plugin/src/__tests__/headless-exit.test.ts +++ b/packages/pi-plugin/src/__tests__/headless-exit.test.ts @@ -26,6 +26,9 @@ import { linkCachedExecutable } from "../../../aft-bridge/src/__tests__/test-uti const EXIT_BOUND_MS = 5_000; /** When to stop waiting and kill a child that is evidently not going to exit. */ const HANG_KILL_MS = 15_000; +// Each notification test spawns a harness process; on a loaded runner it can take +// longer than bun's 5 s default to finish, so allow more than the 15 s rescue kill. +const SUBPROCESS_TEST_MS = 30_000; interface HarnessRun { events: Array<{ name: string; detail: string; at: number }>; @@ -585,111 +588,135 @@ describe.skipIf(process.platform === "win32")("approved session UI binding", () return { code, stdout, stderr }; } - test("factory without UI delivers the actual plugin version through session context", async () => { - const run = await notifications("notification-upgrade"); - expect(run.code).toBe(0); - const toasts = run.stdout.split("\n").filter((line) => line.startsWith("EVENT ui-toast ")); - expect(toasts).toHaveLength(1); - const toast = JSON.parse(toasts[0]?.slice("EVENT ui-toast ".length) ?? "null") as { - sessionId: string; - message: string; - type: string; - }; - const version = ( - JSON.parse(readFileSync(join(import.meta.dir, "../../package.json"), "utf8")) as { - version: string; - } - ).version; - expect(toast.sessionId).toBe("harness-session"); - expect(toast.type).toBe("info"); - expect(toast.message).toContain(`v${version}:`); - expect(run.stdout).toContain(`EVENT announcement-marker ${version}`); - }); + test( + "factory without UI delivers the actual plugin version through session context", + async () => { + const run = await notifications("notification-upgrade"); + expect(run.code).toBe(0); + const toasts = run.stdout.split("\n").filter((line) => line.startsWith("EVENT ui-toast ")); + expect(toasts).toHaveLength(1); + const toast = JSON.parse(toasts[0]?.slice("EVENT ui-toast ".length) ?? "null") as { + sessionId: string; + message: string; + type: string; + }; + const version = ( + JSON.parse(readFileSync(join(import.meta.dir, "../../package.json"), "utf8")) as { + version: string; + } + ).version; + expect(toast.sessionId).toBe("harness-session"); + expect(toast.type).toBe("info"); + expect(toast.message).toContain(`v${version}:`); + expect(run.stdout).toContain(`EVENT announcement-marker ${version}`); + }, + SUBPROCESS_TEST_MS, + ); - test("real headless context with callable UI preserves pending upgrade", async () => { - const run = await notifications("notification-headless"); - expect(run.code).toBe(0); - expect(run.stdout).not.toContain("EVENT ui-toast "); - expect(run.stdout).toContain("EVENT announcement-marker 0.1.0"); - }); + test( + "real headless context with callable UI preserves pending upgrade", + async () => { + const run = await notifications("notification-headless"); + expect(run.code).toBe(0); + expect(run.stdout).not.toContain("EVENT ui-toast "); + expect(run.stdout).toContain("EVENT announcement-marker 0.1.0"); + }, + SUBPROCESS_TEST_MS, + ); - test("pending announcements and install summaries retry once on a later UI context", async () => { - const run = await notifications("notification-retry"); - expect(run.code).toBe(0); - expect(run.stdout).toContain("EVENT pending-marker 0.1.0"); - const toasts = run.stdout - .split("\n") - .filter((line) => line.startsWith("EVENT ui-toast ")) - .map((line) => JSON.parse(line.slice("EVENT ui-toast ".length))); - expect(toasts.map((toast) => [toast.sessionId, toast.type])).toEqual([ - ["retry-session", "info"], - ["retry-session", "warning"], - ]); - expect(toasts[1].message).toContain("fixture install failed"); - expect(toasts[1].message).not.toContain("routine-server"); - }); + test( + "pending announcements and install summaries retry once on a later UI context", + async () => { + const run = await notifications("notification-retry"); + expect(run.code).toBe(0); + expect(run.stdout).toContain("EVENT pending-marker 0.1.0"); + const toasts = run.stdout + .split("\n") + .filter((line) => line.startsWith("EVENT ui-toast ")) + .map((line) => JSON.parse(line.slice("EVENT ui-toast ".length))); + expect(toasts.map((toast) => [toast.sessionId, toast.type])).toEqual([ + ["retry-session", "info"], + ["retry-session", "warning"], + ]); + expect(toasts[1].message).toContain("fixture install failed"); + expect(toasts[1].message).not.toContain("routine-server"); + }, + SUBPROCESS_TEST_MS, + ); - test("install completion notifies the active session UI rather than the factory API", async () => { - const run = await notifications("notification-summary"); - expect(run.code).toBe(0); - const toasts = run.stdout - .split("\n") - .filter((line) => line.startsWith("EVENT ui-toast ")) - .map( - (line) => - JSON.parse(line.slice("EVENT ui-toast ".length)) as { - sessionId: string; - message: string; - type: string; - }, - ); - const warnings = toasts.filter((toast) => toast.type === "warning"); - expect(warnings).toHaveLength(1); - expect(warnings[0]?.sessionId).toBe("harness-session"); - expect(warnings[0]?.message).toContain("fixture install failed"); - expect(warnings[0]?.message).not.toContain("routine-server"); - }); + test( + "install completion notifies the active session UI rather than the factory API", + async () => { + const run = await notifications("notification-summary"); + expect(run.code).toBe(0); + const toasts = run.stdout + .split("\n") + .filter((line) => line.startsWith("EVENT ui-toast ")) + .map( + (line) => + JSON.parse(line.slice("EVENT ui-toast ".length)) as { + sessionId: string; + message: string; + type: string; + }, + ); + const warnings = toasts.filter((toast) => toast.type === "warning"); + expect(warnings).toHaveLength(1); + expect(warnings[0]?.sessionId).toBe("harness-session"); + expect(warnings[0]?.message).toContain("fixture install failed"); + expect(warnings[0]?.message).not.toContain("routine-server"); + }, + SUBPROCESS_TEST_MS, + ); - test("headless install-summary retries log once and deliver once when UI becomes available", async () => { - const run = await notifications("notification-log-retry"); - expect(run.code).toBe(0); - expect(run.stdout).toContain("EVENT headless-turns-done"); - expect(run.stdout).toContain("EVENT ui-turns-done"); - expect(run.stdout.split("EVENT headless-turns-done")[0]).not.toContain("EVENT ui-toast "); - const summaryLogs = run.stderr.match( - /\] WARN \[aft-pi\] \[lsp\] skipped or failed to install \d+ server\(s\):/g, - ); - expect(summaryLogs).toHaveLength(1); - const warnings = run.stdout - .split("\n") - .filter((line) => line.startsWith("EVENT ui-toast ")) - .map((line) => JSON.parse(line.slice("EVENT ui-toast ".length))) - .filter((toast) => toast.type === "warning"); - expect(warnings).toHaveLength(1); - expect(warnings[0]?.sessionId).toBe("retry-session"); - expect(warnings[0]?.message).toContain("fixture install failed"); - expect(warnings[0]?.message).not.toContain("routine-server"); - }); + test( + "headless install-summary retries log once and deliver once when UI becomes available", + async () => { + const run = await notifications("notification-log-retry"); + expect(run.code).toBe(0); + expect(run.stdout).toContain("EVENT headless-turns-done"); + expect(run.stdout).toContain("EVENT ui-turns-done"); + expect(run.stdout.split("EVENT headless-turns-done")[0]).not.toContain("EVENT ui-toast "); + const summaryLogs = run.stderr.match( + /\] WARN \[aft-pi\] \[lsp\] skipped or failed to install \d+ server\(s\):/g, + ); + expect(summaryLogs).toHaveLength(1); + const warnings = run.stdout + .split("\n") + .filter((line) => line.startsWith("EVENT ui-toast ")) + .map((line) => JSON.parse(line.slice("EVENT ui-toast ".length))) + .filter((toast) => toast.type === "warning"); + expect(warnings).toHaveLength(1); + expect(warnings[0]?.sessionId).toBe("retry-session"); + expect(warnings[0]?.message).toContain("fixture install failed"); + expect(warnings[0]?.message).not.toContain("routine-server"); + }, + SUBPROCESS_TEST_MS, + ); - test("headless announcement retries log once and deliver once when UI becomes available", async () => { - const run = await notifications("notification-log-retry"); - expect(run.code).toBe(0); - expect(run.stdout).toContain("EVENT headless-turns-done"); - expect(run.stdout).toContain("EVENT ui-turns-done"); - expect(run.stdout.split("EVENT headless-turns-done")[0]).not.toContain("EVENT ui-toast "); - const announcementLogs = run.stderr.match(/\] INFO \[aft-pi\] 🔧 AFT: ✨ v[^\r\n]+:/g); - expect(announcementLogs).toHaveLength(1); - const announcements = run.stdout - .split("\n") - .filter((line) => line.startsWith("EVENT ui-toast ")) - .map((line) => JSON.parse(line.slice("EVENT ui-toast ".length))) - .filter((toast) => toast.type === "info" && toast.message.startsWith("🔧 AFT: ✨")); - expect(announcements).toHaveLength(1); - expect(announcements[0]?.sessionId).toBe("retry-session"); - const version = JSON.parse( - readFileSync(join(import.meta.dir, "../../package.json"), "utf8"), - ).version; - expect(announcements[0]?.message).toContain(`v${version}:`); - expect(run.stdout).toContain(`EVENT announcement-marker ${version}`); - }); + test( + "headless announcement retries log once and deliver once when UI becomes available", + async () => { + const run = await notifications("notification-log-retry"); + expect(run.code).toBe(0); + expect(run.stdout).toContain("EVENT headless-turns-done"); + expect(run.stdout).toContain("EVENT ui-turns-done"); + expect(run.stdout.split("EVENT headless-turns-done")[0]).not.toContain("EVENT ui-toast "); + const announcementLogs = run.stderr.match(/\] INFO \[aft-pi\] 🔧 AFT: ✨ v[^\r\n]+:/g); + expect(announcementLogs).toHaveLength(1); + const announcements = run.stdout + .split("\n") + .filter((line) => line.startsWith("EVENT ui-toast ")) + .map((line) => JSON.parse(line.slice("EVENT ui-toast ".length))) + .filter((toast) => toast.type === "info" && toast.message.startsWith("🔧 AFT: ✨")); + expect(announcements).toHaveLength(1); + expect(announcements[0]?.sessionId).toBe("retry-session"); + const version = JSON.parse( + readFileSync(join(import.meta.dir, "../../package.json"), "utf8"), + ).version; + expect(announcements[0]?.message).toContain(`v${version}:`); + expect(run.stdout).toContain(`EVENT announcement-marker ${version}`); + }, + SUBPROCESS_TEST_MS, + ); });