diff --git a/tests/cli/sibling-home-client-sync.test.ts b/tests/cli/sibling-home-client-sync.test.ts index f8259141666..82289566c56 100644 --- a/tests/cli/sibling-home-client-sync.test.ts +++ b/tests/cli/sibling-home-client-sync.test.ts @@ -15,6 +15,7 @@ import { } from "../../src/lib/local-management-attestation"; import { removeTreeWithRetry } from "../helpers/remove-tree"; import { repoPath } from "../helpers/repo-root"; +import { captureStartupChildOutput, STARTUP_OUTPUT_TAIL_CHARS } from "../helpers/startup-child-output"; const originalEnv = { ...process.env }; const roots: string[] = []; @@ -78,6 +79,19 @@ function codexRouting(port: number | string) { return `model_provider = "opencodex"\n[model_providers.opencodex]\nbase_url = "http://127.0.0.1:${port}/v1"\n`; } +const capturedChildren = new Map, ReturnType>(); +function trackChild(child: ReturnType) { + children.push(child); + capturedChildren.set(child, captureStartupChildOutput(child.stdout, child.stderr)); +} +function childDiagnostics(child: ReturnType): string { + return JSON.stringify({ pid: child.pid, exitCode: child.exitCode, ...capturedChildren.get(child)!.snapshot() }); +} +async function finishChildOutput(child: ReturnType) { + await child.exited; + return capturedChildren.get(child)!.finish(); +} + async function waitForRuntime(path: string, child: ReturnType) { const deadline = Date.now() + 15_000; while (Date.now() < deadline) { @@ -87,36 +101,35 @@ async function waitForRuntime(path: string, child: ReturnType) if (record.pid === child.pid) return record; } catch { /* publication in progress */ } } - if (child.exitCode !== null) throw new Error(`secondary exited ${child.exitCode}: ${await new Response(child.stderr).text()}`); + if (child.exitCode !== null) { + await finishChildOutput(child); + throw new Error(`secondary exited before runtime publication: ${childDiagnostics(child)}`); + } await Bun.sleep(20); } - throw new Error("timed out waiting for secondary runtime record"); + throw new Error(`timed out waiting for secondary runtime record: ${childDiagnostics(child)}`); } async function waitForClientStartup(child: ReturnType): Promise { - const reader = child.stdout.getReader(); - const decoder = new TextDecoder(); - let output = ""; - let timer: ReturnType | undefined; - const timeout = new Promise((_, reject) => { - timer = setTimeout(() => reject(new Error("timed out waiting for client startup")), 15_000); - }); - try { - while (!output.includes("Client startup work complete.")) { - const chunk = await Promise.race([reader.read(), timeout]); - if (chunk.done) throw new Error(`secondary exited before client startup: ${output}`); - output += decoder.decode(chunk.value, { stream: true }); + const deadline = Date.now() + 15_000; + while (Date.now() < deadline) { + if (capturedChildren.get(child)!.snapshot().startupComplete) return; + if (child.exitCode !== null) { + const output = await finishChildOutput(child); + if (output.startupComplete) return; + throw new Error(`secondary exited before client startup: ${childDiagnostics(child)}`); } - } finally { - clearTimeout(timer); - reader.releaseLock(); + await Bun.sleep(20); } + throw new Error(`timed out waiting for client startup: ${childDiagnostics(child)}`); } afterEach(async () => { resetSiblingStartForTests(); for (const child of children) if (child.exitCode === null) child.kill("SIGTERM"); for (const child of children.splice(0)) await child.exited; + for (const capture of capturedChildren.values()) await capture.finish(); + capturedChildren.clear(); for (const pid of detachedPids.splice(0)) { try { process.kill(pid, "SIGTERM"); } catch { /* already exited */ } } @@ -328,7 +341,7 @@ test("a secondary start preserves shared client bytes and records the sibling ow const child = Bun.spawn([process.execPath, repoPath("src/cli/index.ts"), "start", "--port", String(secondaryPort)], { cwd: fx.root, env: { ...process.env, NO_PROXY: "127.0.0.1,localhost" }, stdout: "pipe", stderr: "pipe", }); - children.push(child); + trackChild(child); const runtime = await waitForRuntime(join(fx.ocx, "runtime-port.json"), child); expect(runtime.siblingOfPort).toBe(ownerPort); await waitForClientStartup(child); @@ -360,9 +373,10 @@ test("a secondary ensure parent preserves shared Grok, Codex and Claude agent by const ensure = Bun.spawn([process.execPath, repoPath("src/cli/index.ts"), "ensure"], { cwd: fx.root, env: { ...process.env, NO_PROXY: "127.0.0.1,localhost" }, stdout: "pipe", stderr: "pipe", }); - children.push(ensure); - const output = await new Response(ensure.stdout).text(); - const error = await new Response(ensure.stderr).text(); + trackChild(ensure); + const captured = await finishChildOutput(ensure); + const output = captured.stdout.tail; + const error = captured.stderr.tail; expect(await ensure.exited).toBe(0); expect(output + error).toContain(`Proxy running on port ${secondaryPort}`); const runtime = JSON.parse(readFileSync(join(fx.ocx, "runtime-port.json"), "utf8")) as { @@ -377,9 +391,8 @@ test("a secondary ensure parent preserves shared Grok, Codex and Claude agent by const again = Bun.spawn([process.execPath, repoPath("src/cli/index.ts"), "ensure"], { cwd: fx.root, env: { ...process.env, NO_PROXY: "127.0.0.1,localhost" }, stdout: "pipe", stderr: "pipe", }); - children.push(again); - await new Response(again.stdout).text(); - await new Response(again.stderr).text(); + trackChild(again); + await finishChildOutput(again); expect(await again.exited).toBe(0); expect([grokPath, codexPath, claudePath].map(path => readFileSync(path))).toEqual(before); }, 30_000); @@ -401,7 +414,7 @@ test("a lone custom-home start still syncs Grok and prunes its own Claude roster const child = Bun.spawn([process.execPath, repoPath("src/cli/index.ts"), "start", "--port", String(port)], { cwd: fx.root, env: { ...process.env, NO_PROXY: "127.0.0.1,localhost" }, stdout: "pipe", stderr: "pipe", }); - children.push(child); + trackChild(child); const runtime = await waitForRuntime(join(fx.ocx, "runtime-port.json"), child); expect(runtime.siblingOfPort).toBeUndefined(); const deadline = Date.now() + 10_000; @@ -412,9 +425,8 @@ test("a lone custom-home start still syncs Grok and prunes its own Claude roster const ensure = Bun.spawn([process.execPath, repoPath("src/cli/index.ts"), "ensure"], { cwd: fx.root, env: { ...process.env, NO_PROXY: "127.0.0.1,localhost" }, stdout: "pipe", stderr: "pipe", }); - children.push(ensure); - await new Response(ensure.stdout).text(); - await new Response(ensure.stderr).text(); + trackChild(ensure); + await finishChildOutput(ensure); expect(await ensure.exited).toBe(0); expect(existsSync(claudePath)).toBe(false); }, 30_000); @@ -642,7 +654,7 @@ async function secondaryStartPreservesBytes( const child = Bun.spawn([process.execPath, repoPath("src/cli/index.ts"), "start", "--port", String(secondaryPort)], { cwd: fx.root, env: { ...process.env, NO_PROXY: "127.0.0.1,localhost" }, stdout: "pipe", stderr: "pipe", }); - children.push(child); + trackChild(child); const runtime = await waitForRuntime(join(fx.ocx, "runtime-port.json"), child); expect(runtime.siblingOfPort).toBe(expectedSiblingPort); await waitForClientStartup(child); @@ -719,7 +731,7 @@ test("a registry-only discovered owner vetoes shared writes end to end", async ( const child = Bun.spawn([process.execPath, repoPath("src/cli/index.ts"), "start", "--port", String(secondaryPort)], { cwd: fx.root, env: { ...process.env, NO_PROXY: "127.0.0.1,localhost" }, stdout: "pipe", stderr: "pipe", }); - children.push(child); + trackChild(child); const runtime = await waitForRuntime(join(fx.ocx, "runtime-port.json"), child); expect(runtime.siblingOfPort).toBe(ownerPort); await waitForClientStartup(child); @@ -728,3 +740,91 @@ test("a registry-only discovered owner vetoes shared writes end to end", async ( await child.exited; expect([grokPath, codexPath, claudePath].map(path => readFileSync(path))).toEqual(before); }, 30_000); + +test("startup diagnostics drain both pipes before runtime publication", async () => { + const fx = fixture(); + const runtimePath = join(fx.ocx, "runtime-port.json"); + const script = ` + import { writeFileSync } from "node:fs"; + const write = (stream, text) => new Promise((resolve, reject) => stream.write(text, error => error ? reject(error) : resolve())); + await write(process.stdout, "Client startup work complete.\\n"); + await Promise.all([process.stdout, process.stderr].map(async stream => { + for (let i = 0; i < 128; i++) await write(stream, "x".repeat(65536)); + })); + await write(process.stdout, "stdout-tail\\n"); + await write(process.stderr, "stderr-tail\\n"); + writeFileSync(${JSON.stringify(runtimePath)}, JSON.stringify({ pid: process.pid, port: 1 })); + setInterval(() => {}, 1000); + `; + const child = Bun.spawn([process.execPath, "-e", script], { + cwd: fx.root, env: { ...process.env }, stdout: "pipe", stderr: "pipe", + }); + trackChild(child); + expect((await waitForRuntime(runtimePath, child)).pid).toBe(child.pid); + await waitForClientStartup(child); + child.kill("SIGTERM"); + const captured = await finishChildOutput(child); + expect(captured.stdout.tail.length).toBeLessThanOrEqual(STARTUP_OUTPUT_TAIL_CHARS); + expect(captured.stderr.tail.length).toBeLessThanOrEqual(STARTUP_OUTPUT_TAIL_CHARS); + expect(captured.stdout.tail).toContain("stdout-tail"); + expect(captured.stderr.tail).toContain("stderr-tail"); + expect(captured.stdout.tail).not.toContain("Client startup work complete."); + expect(captured.startupComplete).toBe(true); +}, 30_000); + +test("startup diagnostics include both child tails and exit state on publication failure", async () => { + const fx = fixture(); + const child = Bun.spawn([process.execPath, "-e", 'console.log("before-publication"); console.error("fixture-start-failure"); process.exitCode = 2;'], { + cwd: fx.root, env: { ...process.env }, stdout: "pipe", stderr: "pipe", + }); + trackChild(child); + await child.exited; + let failure = ""; + try { await waitForRuntime(join(fx.ocx, "runtime-port.json"), child); } + catch (error) { failure = String(error); } + expect(failure).toContain("before-publication"); + expect(failure).toContain("fixture-start-failure"); + expect(failure).toContain('"exitCode":2'); + expect(failure).toContain(`"pid":${child.pid}`); + expect(failure).toContain("lastOutputAgoMs"); +}); + +test("startup diagnostics preserve split UTF-8 and startup markers before tail eviction", async () => { + const bytes = new TextEncoder().encode("한글 Client startup work complete."); + const stdout = new ReadableStream({ + start(controller) { + for (const byte of bytes) controller.enqueue(Uint8Array.of(byte)); + controller.enqueue(new TextEncoder().encode("z".repeat(10000))); + controller.close(); + }, + }); + const stderr = new ReadableStream({ + start(controller) { + for (const byte of new TextEncoder().encode("오류")) controller.enqueue(Uint8Array.of(byte)); + controller.close(); + }, + }); + const output = await captureStartupChildOutput(stdout, stderr).finish(); + expect(output.startupComplete).toBe(true); + expect(output.stdout.tail).toBe("z".repeat(8192)); + expect(output.stderr.tail).toBe("오류"); + expect(output.stdout.complete && output.stderr.complete).toBe(true); +}); + +for (const cancellation of ["pending", "rejected"] as const) { + test(`startup diagnostics bound ${cancellation} pipe cancellation`, async () => { + let cancelled = 0; + const open = () => new ReadableStream({ + start(controller) { controller.enqueue(new TextEncoder().encode("partial")); }, + cancel() { + cancelled++; + return cancellation === "pending" ? new Promise(() => {}) : Promise.reject(new Error("fixture cancel failure")); + }, + }); + const output = await captureStartupChildOutput(open(), open()).finish(20); + expect(output.stdout.tail).toBe("partial"); + expect(output.stderr.tail).toBe("partial"); + expect(output.stdout.complete || output.stderr.complete).toBe(false); + expect(cancelled).toBe(2); + }, 2000); +} diff --git a/tests/helpers/startup-child-output.ts b/tests/helpers/startup-child-output.ts new file mode 100644 index 00000000000..b28a5e73e3d --- /dev/null +++ b/tests/helpers/startup-child-output.ts @@ -0,0 +1,69 @@ +export const STARTUP_OUTPUT_TAIL_CHARS = 8192; +const STARTUP_MARKER = "Client startup work complete."; + +/** Fixture-only live diagnostics: drain both pipes without retaining an unbounded transcript. */ +export function captureStartupChildOutput(stdout: ReadableStream, stderr: ReadableStream) { + const startedAt = Date.now(); + let lastOutputAt: number | null = null; + let startupComplete = false; + const collect = (stream: ReadableStream, isStdout: boolean) => { + const reader = stream.getReader(); + const decoder = new TextDecoder(); + let tail = ""; + let markerPrefix = ""; + let complete = false; + let stopped = false; + let readFailed = false; + const append = (text: string) => { + if (!text) return; + lastOutputAt = Date.now(); + if (isStdout && !startupComplete) { + const search = markerPrefix + text; + startupComplete = search.includes(STARTUP_MARKER); + markerPrefix = search.slice(-(STARTUP_MARKER.length - 1)); + } + tail = (tail + text).slice(-STARTUP_OUTPUT_TAIL_CHARS); + }; + const done = (async () => { + try { + while (!stopped) { + const next = await reader.read(); + if (stopped) break; + if (next.done) { append(decoder.decode()); complete = true; break; } + append(decoder.decode(next.value, { stream: true })); + } + } catch { readFailed = true; } + finally { reader.releaseLock(); } + })(); + return { + done, + snapshot: () => ({ tail, complete, readFailed }), + stop() { + if (stopped || complete || readFailed) return; + stopped = true; + void reader.cancel().catch(() => {}); + }, + }; + }; + const out = collect(stdout, true); + const err = collect(stderr, false); + const snapshot = () => ({ + stdout: out.snapshot(), stderr: err.snapshot(), startupComplete, + elapsedMs: Date.now() - startedAt, + lastOutputAgoMs: lastOutputAt === null ? null : Date.now() - lastOutputAt, + }); + const stop = () => { out.stop(); err.stop(); }; + return { + snapshot, stop, + async finish(timeoutMs = 1000) { + let timer: ReturnType | undefined; + try { + await Promise.race([ + Promise.all([out.done, err.done]), + new Promise(resolve => { timer = setTimeout(resolve, timeoutMs); }), + ]); + } finally { clearTimeout(timer); stop(); } + return snapshot(); + }, + }; +} diff --git a/tests/providers/nous-oauth.test.ts b/tests/providers/nous-oauth.test.ts index c1294cfc5fa..a19376d4b21 100644 --- a/tests/providers/nous-oauth.test.ts +++ b/tests/providers/nous-oauth.test.ts @@ -7,13 +7,67 @@ import { getCredential, listAccounts, saveCredential } from "../../src/oauth/sto import type { OAuthController } from "../../src/oauth/types"; import * as configModule from "../../src/config"; import { BOUNDED_BODY_MAX_BYTES } from "../../src/lib/bounded-body"; -import { removeTreeWithRetry } from "../helpers/remove-tree"; +import { createTempHome, type TempHome } from "../helpers/temp-home"; +import { flushConfigDirHardeningAndReaps, hardenConfigDir } from "../../src/config/paths"; +import { resetHardenedStateForTests, setAsyncIcaclsRunnerForTests, setPlatformForTests } from "../../src/lib/windows-secret-acl"; -const TEST_DIR = join(import.meta.dir, ".tmp-nous-oauth-test"); +let TEST_DIR: string; +let testHome: TempHome; const TEST_PORTAL = "https://portal.test"; -let previousOpencodexHome: string | undefined; let previousPortalBase: string | undefined; +beforeEach(() => { + testHome = createTempHome("ocx-nous-oauth-"); + TEST_DIR = testHome.root; +}); + +async function cleanupNousHome(home: TempHome): Promise { + // Optional ACL subprocesses must finish before Windows can remove their working tree. + await flushConfigDirHardeningAndReaps(home.root); + home.remove(); +} + +afterEach(async () => { + await cleanupNousHome(testHome); +}); + +test("Nous fixture cleanup waits for pending Windows directory hardening", async () => { + let release!: () => void; + let started!: () => void; + const blocked = new Promise(resolve => { release = resolve; }); + const entered = new Promise(resolve => { started = resolve; }); + resetHardenedStateForTests(); + setPlatformForTests("win32"); + setAsyncIcaclsRunnerForTests(async () => { + started(); + await blocked; + return { success: true, exitCode: 0, timedOut: false, stdout: "" }; + }); + let cleanup: Promise | undefined; + try { + hardenConfigDir(); + await entered; + cleanup = cleanupNousHome(testHome); + await Promise.resolve(); + expect(existsSync(TEST_DIR)).toBe(true); + expect(process.env.OPENCODEX_HOME).toBe(TEST_DIR); + release(); + await cleanup; + expect(existsSync(TEST_DIR)).toBe(false); + expect(process.env.OPENCODEX_HOME).not.toBe(TEST_DIR); + } finally { + release(); + try { + await flushConfigDirHardeningAndReaps(TEST_DIR); + await cleanup; + } finally { + setAsyncIcaclsRunnerForTests(null); + setPlatformForTests(null); + resetHardenedStateForTests(); + } + } +}); + function jwtWithClaims(claims: Record): string { // A real Nous inference JWT carries the inference:invoke scope; callers that // need to exercise the missing-scope path pass an explicit `scope` override. @@ -62,23 +116,13 @@ describe("Nous token-response wiring", () => { beforeEach(() => { previousPortalBase = process.env.NOUS_PORTAL_BASE_URL; - previousOpencodexHome = process.env.OPENCODEX_HOME; process.env.NOUS_PORTAL_BASE_URL = TEST_PORTAL; - // Isolate durable refresh-intent state so this block never leaves intent - // files in the developer/runner config tree (review: 1st wiring test must - // isolate OPENCODEX_HOME). - if (existsSync(TEST_DIR)) removeTreeWithRetry(TEST_DIR); - mkdirSync(TEST_DIR, { recursive: true }); - process.env.OPENCODEX_HOME = TEST_DIR; }); afterEach(() => { globalThis.fetch = realFetch; if (previousPortalBase === undefined) delete process.env.NOUS_PORTAL_BASE_URL; else process.env.NOUS_PORTAL_BASE_URL = previousPortalBase; - if (previousOpencodexHome === undefined) delete process.env.OPENCODEX_HOME; - else process.env.OPENCODEX_HOME = previousOpencodexHome; - if (existsSync(TEST_DIR)) removeTreeWithRetry(TEST_DIR); }); test("refreshNousToken posts the refresh token in the x-nous-refresh-token header and keeps the rotated token", async () => { @@ -451,17 +495,8 @@ describe("Nous device-flow error handling", () => { describe("Nous Portal base URL hardening", () => { const realFetch = globalThis.fetch; - beforeEach(() => { - previousOpencodexHome = process.env.OPENCODEX_HOME; - if (existsSync(TEST_DIR)) removeTreeWithRetry(TEST_DIR); - mkdirSync(TEST_DIR, { recursive: true }); - process.env.OPENCODEX_HOME = TEST_DIR; - }); afterEach(() => { globalThis.fetch = realFetch; - if (previousOpencodexHome === undefined) delete process.env.OPENCODEX_HOME; - else process.env.OPENCODEX_HOME = previousOpencodexHome; - if (existsSync(TEST_DIR)) removeTreeWithRetry(TEST_DIR); }); test("an HTTP override fails before fetch is invoked", async () => { @@ -579,20 +614,13 @@ describe("Nous refresh token safety", () => { beforeEach(() => { previousPortalBase = process.env.NOUS_PORTAL_BASE_URL; - previousOpencodexHome = process.env.OPENCODEX_HOME; - if (existsSync(TEST_DIR)) removeTreeWithRetry(TEST_DIR); - mkdirSync(TEST_DIR, { recursive: true }); process.env.NOUS_PORTAL_BASE_URL = TEST_PORTAL; - process.env.OPENCODEX_HOME = TEST_DIR; }); afterEach(() => { globalThis.fetch = realFetch; if (previousPortalBase === undefined) delete process.env.NOUS_PORTAL_BASE_URL; else process.env.NOUS_PORTAL_BASE_URL = previousPortalBase; - if (previousOpencodexHome === undefined) delete process.env.OPENCODEX_HOME; - else process.env.OPENCODEX_HOME = previousOpencodexHome; - if (existsSync(TEST_DIR)) removeTreeWithRetry(TEST_DIR); }); test("rejecting a missing replacement refresh token does not reuse the consumed one", async () => { @@ -647,19 +675,6 @@ describe("Nous refresh token safety", () => { }); describe("Nous multiauth via saveCredential", () => { - beforeEach(() => { - previousOpencodexHome = process.env.OPENCODEX_HOME; - if (existsSync(TEST_DIR)) removeTreeWithRetry(TEST_DIR); - mkdirSync(TEST_DIR, { recursive: true }); - process.env.OPENCODEX_HOME = TEST_DIR; - }); - - afterEach(() => { - if (previousOpencodexHome === undefined) delete process.env.OPENCODEX_HOME; - else process.env.OPENCODEX_HOME = previousOpencodexHome; - if (existsSync(TEST_DIR)) removeTreeWithRetry(TEST_DIR); - }); - test("two distinct subs append two nous accounts", async () => { const accessA = jwtWithClaims({ sub: "nous-a" }); const accessB = jwtWithClaims({ sub: "nous-b" }); @@ -703,25 +718,16 @@ describe("Nous multiauth via saveCredential", () => { describe("Nous refresh failure-atomicity + terminal errors", () => { const realFetch = globalThis.fetch; - let intentHome: string | undefined; beforeEach(() => { previousPortalBase = process.env.NOUS_PORTAL_BASE_URL; process.env.NOUS_PORTAL_BASE_URL = TEST_PORTAL; - // Isolate the refresh-intent dir under a temp OPENCODEX_HOME. - previousOpencodexHome = process.env.OPENCODEX_HOME; - if (existsSync(TEST_DIR)) removeTreeWithRetry(TEST_DIR); - mkdirSync(TEST_DIR, { recursive: true }); - process.env.OPENCODEX_HOME = TEST_DIR; }); afterEach(() => { globalThis.fetch = realFetch; if (previousPortalBase === undefined) delete process.env.NOUS_PORTAL_BASE_URL; else process.env.NOUS_PORTAL_BASE_URL = previousPortalBase; - if (previousOpencodexHome === undefined) delete process.env.OPENCODEX_HOME; - else process.env.OPENCODEX_HOME = previousOpencodexHome; - if (existsSync(TEST_DIR)) removeTreeWithRetry(TEST_DIR); }); test("a successful rotation leaves the intent submitted until the store persists", async () => { @@ -847,20 +853,9 @@ describe("Nous refresh failure-atomicity + terminal errors", () => { describe("Nous refresh-intent schema is validated fail-closed", () => { const realFetch = globalThis.fetch; - let previousHome: string | undefined; - - beforeEach(() => { - previousHome = process.env.OPENCODEX_HOME; - if (existsSync(TEST_DIR)) removeTreeWithRetry(TEST_DIR); - mkdirSync(TEST_DIR, { recursive: true }); - process.env.OPENCODEX_HOME = TEST_DIR; - }); afterEach(() => { globalThis.fetch = realFetch; - if (previousHome === undefined) delete process.env.OPENCODEX_HOME; - else process.env.OPENCODEX_HOME = previousHome; - if (existsSync(TEST_DIR)) removeTreeWithRetry(TEST_DIR); }); // Each corrupt/unknown shape must block replay (fail-closed): the intent file @@ -925,20 +920,9 @@ describe("Nous refresh-intent schema is validated fail-closed", () => { describe("Nous HTTP refresh failure-atomicity classification", () => { const realFetch = globalThis.fetch; - let previousHome: string | undefined; - - beforeEach(() => { - previousHome = process.env.OPENCODEX_HOME; - if (existsSync(TEST_DIR)) removeTreeWithRetry(TEST_DIR); - mkdirSync(TEST_DIR, { recursive: true }); - process.env.OPENCODEX_HOME = TEST_DIR; - }); afterEach(() => { globalThis.fetch = realFetch; - if (previousHome === undefined) delete process.env.OPENCODEX_HOME; - else process.env.OPENCODEX_HOME = previousHome; - if (existsSync(TEST_DIR)) removeTreeWithRetry(TEST_DIR); }); test("an ambiguous 5xx response leaves the old token blocked (never replayable)", async () => {