|
1 | | -import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test"; |
| 1 | +import { afterAll, afterEach, beforeEach, describe, expect, mock, test } from "bun:test" |
2 | 2 |
|
3 | 3 | // Reused by both the TUI and exec boot paths (src/tui/runner.ts, |
4 | 4 | // src/exec/runner.ts) to refresh the pinned Codex instructions before first |
5 | 5 | // Codex inference. This exercises the shared refresh/fallback logic directly, |
6 | 6 | // with disk I/O faked so tests never touch the real ~/.corbits cache. |
| 7 | +// |
| 8 | +// Shallow-copy + afterAll restore is required: Bun mutates the live module |
| 9 | +// namespace when mock.module runs, and CI loads ./src before ./tests — a leaked |
| 10 | +// node:fs mock turns later suites into ENOENT / missing-settings failures. |
7 | 11 |
|
8 | | -let fakeDisk = new Map<string, string>(); |
| 12 | +const realFs = { ...(await import("node:fs")) } |
| 13 | + |
| 14 | +let fakeDisk = new Map<string, string>() |
9 | 15 |
|
10 | 16 | mock.module("node:fs", () => ({ |
| 17 | + ...realFs, |
11 | 18 | readFileSync: (path: string) => { |
12 | | - const contents = fakeDisk.get(path); |
| 19 | + const contents = fakeDisk.get(path) |
13 | 20 | if (contents === undefined) { |
14 | | - const err = new Error("ENOENT") as NodeJS.ErrnoException; |
15 | | - err.code = "ENOENT"; |
16 | | - throw err; |
| 21 | + const err = new Error("ENOENT") as NodeJS.ErrnoException |
| 22 | + err.code = "ENOENT" |
| 23 | + throw err |
17 | 24 | } |
18 | | - return contents; |
| 25 | + return contents |
19 | 26 | }, |
20 | 27 | writeFileSync: (path: string, contents: string) => { |
21 | | - fakeDisk.set(path, contents); |
| 28 | + fakeDisk.set(path, contents) |
22 | 29 | }, |
23 | 30 | mkdirSync: () => undefined, |
24 | | -})); |
| 31 | +})) |
| 32 | + |
| 33 | +afterAll(() => { |
| 34 | + mock.module("node:fs", () => realFs) |
| 35 | +}) |
25 | 36 |
|
26 | 37 | const { refreshCodexInstructions, codexInstructions, codexInstructionsHash } = await import( |
27 | | - "./instructions.js" |
28 | | -); |
29 | | -const { GPT_5_CODEX_PROMPT } = await import("./prompts/gpt-5-codex.js"); |
| 38 | + "./instructions.js", |
| 39 | +) |
| 40 | +const { GPT_5_CODEX_PROMPT } = await import("./prompts/gpt-5-codex.js") |
30 | 41 |
|
31 | | -const VALID_PROMPT = `You are Codex${"x".repeat(1200)}`; |
| 42 | +const VALID_PROMPT = `You are Codex${"x".repeat(1200)}` |
32 | 43 |
|
33 | 44 | function mockFetchSequence(tag: string, promptResponse: () => Response): typeof fetch { |
34 | 45 | return (async (input: RequestInfo | URL) => { |
35 | | - const url = String(input); |
| 46 | + const url = String(input) |
36 | 47 | if (url.includes("releases/latest")) { |
37 | | - return new Response(JSON.stringify({ tag_name: tag }), { status: 200 }); |
| 48 | + return new Response(JSON.stringify({ tag_name: tag }), { status: 200 }) |
38 | 49 | } |
39 | | - return promptResponse(); |
40 | | - }) as typeof fetch; |
| 50 | + return promptResponse() |
| 51 | + }) as typeof fetch |
41 | 52 | } |
42 | 53 |
|
43 | 54 | describe("refreshCodexInstructions", () => { |
44 | | - const originalFetch = global.fetch; |
| 55 | + const originalFetch = global.fetch |
45 | 56 |
|
46 | 57 | beforeEach(() => { |
47 | | - fakeDisk = new Map(); |
48 | | - }); |
| 58 | + fakeDisk = new Map() |
| 59 | + }) |
49 | 60 |
|
50 | 61 | afterEach(() => { |
51 | | - global.fetch = originalFetch; |
52 | | - }); |
| 62 | + global.fetch = originalFetch |
| 63 | + }) |
53 | 64 |
|
54 | 65 | test("bundled copy is used before any refresh", () => { |
55 | | - expect(codexInstructions()).toBe(GPT_5_CODEX_PROMPT); |
56 | | - }); |
| 66 | + expect(codexInstructions()).toBe(GPT_5_CODEX_PROMPT) |
| 67 | + }) |
57 | 68 |
|
58 | 69 | test("updates in-memory instructions on a successful fetch", async () => { |
59 | | - global.fetch = mockFetchSequence("rust-v1.2.3", () => new Response(VALID_PROMPT, { status: 200 })); |
| 70 | + global.fetch = mockFetchSequence("rust-v1.2.3", () => new Response(VALID_PROMPT, { status: 200 })) |
60 | 71 |
|
61 | | - await refreshCodexInstructions(); |
62 | | - expect(codexInstructions()).toBe(VALID_PROMPT); |
63 | | - }); |
| 72 | + await refreshCodexInstructions() |
| 73 | + expect(codexInstructions()).toBe(VALID_PROMPT) |
| 74 | + }) |
64 | 75 |
|
65 | 76 | test("falls back without throwing the run when the release lookup network call fails", async () => { |
66 | | - const before = codexInstructions(); |
67 | | - global.fetch = (() => Promise.reject(new Error("network down"))) as unknown as typeof fetch; |
| 77 | + const before = codexInstructions() |
| 78 | + global.fetch = (() => Promise.reject(new Error("network down"))) as unknown as typeof fetch |
68 | 79 |
|
69 | 80 | // The function itself rejects; callers (TUI/exec boot) catch this and |
70 | 81 | // keep running on cache/bundled instructions — see src/exec/runner.ts and |
71 | 82 | // src/tui/runner.ts refresh call sites. |
72 | | - await expect(refreshCodexInstructions()).rejects.toThrow("network down"); |
73 | | - expect(codexInstructions()).toBe(before); |
74 | | - }); |
| 83 | + await expect(refreshCodexInstructions()).rejects.toThrow("network down") |
| 84 | + expect(codexInstructions()).toBe(before) |
| 85 | + }) |
75 | 86 |
|
76 | 87 | test("falls back without throwing when the prompt fetch returns a non-200", async () => { |
77 | | - const before = codexInstructions(); |
78 | | - global.fetch = mockFetchSequence("rust-v1.2.3", () => new Response("not found", { status: 404 })); |
| 88 | + const before = codexInstructions() |
| 89 | + global.fetch = mockFetchSequence("rust-v1.2.3", () => new Response("not found", { status: 404 })) |
79 | 90 |
|
80 | | - await expect(refreshCodexInstructions()).rejects.toThrow(/HTTP 404/); |
81 | | - expect(codexInstructions()).toBe(before); |
82 | | - }); |
| 91 | + await expect(refreshCodexInstructions()).rejects.toThrow(/HTTP 404/) |
| 92 | + expect(codexInstructions()).toBe(before) |
| 93 | + }) |
83 | 94 |
|
84 | 95 | test("rejects a 200 response whose body is not a valid Codex prompt (CDN error page)", async () => { |
85 | | - const before = codexInstructions(); |
86 | | - global.fetch = mockFetchSequence("rust-v1.2.3", () => new Response("<html>oops</html>", { status: 200 })); |
| 96 | + const before = codexInstructions() |
| 97 | + global.fetch = mockFetchSequence( |
| 98 | + "rust-v1.2.3", |
| 99 | + () => new Response("<html>oops</html>", { status: 200 }), |
| 100 | + ) |
87 | 101 |
|
88 | | - await expect(refreshCodexInstructions()).rejects.toThrow(/unexpected body/); |
89 | | - expect(codexInstructions()).toBe(before); |
90 | | - }); |
| 102 | + await expect(refreshCodexInstructions()).rejects.toThrow(/unexpected body/) |
| 103 | + expect(codexInstructions()).toBe(before) |
| 104 | + }) |
91 | 105 |
|
92 | 106 | test("rejects within the timeout when a fetch never resolves (hung connection)", async () => { |
93 | | - const before = codexInstructions(); |
| 107 | + const before = codexInstructions() |
94 | 108 | global.fetch = ((_input: RequestInfo | URL, init?: RequestInit) => { |
95 | 109 | return new Promise((_resolve, reject) => { |
96 | | - const signal = init?.signal; |
| 110 | + const signal = init?.signal |
97 | 111 | if (signal) { |
98 | | - signal.addEventListener("abort", () => reject(signal.reason as Error)); |
| 112 | + signal.addEventListener("abort", () => reject(signal.reason as Error)) |
99 | 113 | } |
100 | | - }); |
101 | | - }) as unknown as typeof fetch; |
| 114 | + }) |
| 115 | + }) as unknown as typeof fetch |
102 | 116 |
|
103 | | - const started = Date.now(); |
104 | | - await expect(refreshCodexInstructions()).rejects.toBeTruthy(); |
105 | | - expect(Date.now() - started).toBeLessThan(15_000); |
106 | | - expect(codexInstructions()).toBe(before); |
107 | | - }, 20_000); |
| 117 | + const started = Date.now() |
| 118 | + await expect(refreshCodexInstructions()).rejects.toBeTruthy() |
| 119 | + expect(Date.now() - started).toBeLessThan(15_000) |
| 120 | + expect(codexInstructions()).toBe(before) |
| 121 | + }, 20_000) |
108 | 122 |
|
109 | 123 | test("codexInstructionsHash reflects the currently resolved instructions text", async () => { |
110 | | - const hashBefore = codexInstructionsHash(); |
111 | | - expect(hashBefore).toMatch(/^[0-9a-f]{12}$/); |
| 124 | + const hashBefore = codexInstructionsHash() |
| 125 | + expect(hashBefore).toMatch(/^[0-9a-f]{12}$/) |
112 | 126 |
|
113 | | - const otherPrompt = `You are Codex${"y".repeat(1200)}`; |
114 | | - global.fetch = mockFetchSequence("rust-v1.2.4", () => new Response(otherPrompt, { status: 200 })); |
115 | | - await refreshCodexInstructions(); |
| 127 | + const otherPrompt = `You are Codex${"y".repeat(1200)}` |
| 128 | + global.fetch = mockFetchSequence("rust-v1.2.4", () => new Response(otherPrompt, { status: 200 })) |
| 129 | + await refreshCodexInstructions() |
116 | 130 |
|
117 | | - expect(codexInstructionsHash()).not.toBe(hashBefore); |
118 | | - }); |
119 | | -}); |
| 131 | + expect(codexInstructionsHash()).not.toBe(hashBefore) |
| 132 | + }) |
| 133 | +}) |
0 commit comments