diff --git a/test/codex-prompts.test.ts b/test/codex-prompts.test.ts index 17131b8f5..8e72bac9a 100644 --- a/test/codex-prompts.test.ts +++ b/test/codex-prompts.test.ts @@ -213,6 +213,65 @@ describe("Codex Prompts Module", () => { const second = await getCodexInstructions("gpt-5.1-codex"); expect(second).toBe("new version content"); }); + + it("deduplicates background refresh for concurrent stale calls", async () => { + const oldTimestamp = Date.now() - 20 * 60 * 1000; + let resolvePromptText: ((value: string) => void) | null = null; + const promptText = new Promise((resolve) => { + resolvePromptText = resolve; + }); + + mockedReadFile.mockImplementation((filePath) => { + if (typeof filePath === "string" && filePath.includes("-meta.json")) { + return Promise.resolve( + JSON.stringify({ + etag: "old-etag", + tag: "rust-v0.40.0", + lastChecked: oldTimestamp, + url: "https://example.com", + }), + ); + } + return Promise.resolve("stale disk content"); + }); + mockFetch.mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve({ tag_name: "rust-v0.50.0" }), + }); + mockFetch.mockImplementationOnce(() => + Promise.resolve({ + ok: true, + text: () => promptText, + headers: { get: () => "new-etag" }, + }), + ); + mockedMkdir.mockResolvedValue(undefined); + mockedWriteFile.mockResolvedValue(undefined); + + const [first, second] = await Promise.all([ + getCodexInstructions("gpt-5.1-codex"), + getCodexInstructions("gpt-5.1-codex"), + ]); + + expect(first).toBe("stale disk content"); + expect(second).toBe("stale disk content"); + await vi.waitFor(() => { + expect(mockFetch).toHaveBeenCalledTimes(2); + }); + + resolvePromptText?.("fresh deduped content"); + await vi.waitFor(() => { + expect(mockedWriteFile).toHaveBeenCalled(); + }); + const settledFetchCalls = mockFetch.mock.calls.length; + expect(settledFetchCalls).toBe(2); + + const refreshed = await getCodexInstructions("gpt-5.1-codex"); + expect(refreshed).toBe("fresh deduped content"); + await vi.waitFor(() => { + expect(mockFetch).toHaveBeenCalledTimes(settledFetchCalls); + }); + }); }); describe("GitHub HTML fallback", () => { diff --git a/test/paths.test.ts b/test/paths.test.ts index d289bf2cd..36ed7ad5d 100644 --- a/test/paths.test.ts +++ b/test/paths.test.ts @@ -654,6 +654,22 @@ describe("Storage Paths Module", () => { expect(resolved).toBe(projectRoot); }); + it("falls back when .git entry exists but is neither file nor directory", () => { + const projectRoot = "/repo/weird"; + const gitEntry = path.join(projectRoot, ".git"); + + mockedExistsSync.mockImplementation((candidate) => candidate === gitEntry); + mockedStatSync.mockImplementation((candidate) => { + expect(candidate).toBe(gitEntry); + return buildMockStat({ isDirectory: false, isFile: false }); + }); + + const resolved = resolveProjectStorageIdentityRoot(projectRoot); + + expect(resolved).toBe(projectRoot); + expect(mockedReadFileSync).not.toHaveBeenCalled(); + }); + it("keeps project root when .git file does not point to worktrees", () => { const projectRoot = "/repo/submodule"; const gitEntry = path.join(projectRoot, ".git"); diff --git a/test/server.unit.test.ts b/test/server.unit.test.ts index 560f0a35e..28aeb89e8 100644 --- a/test/server.unit.test.ts +++ b/test/server.unit.test.ts @@ -300,6 +300,31 @@ describe('OAuth Server Unit Tests', () => { expect(code).toEqual({ code: 'the-code' }); }); + it('waitForCode returns null when close aborts active poll loop', async () => { + vi.useFakeTimers(); + try { + (mockServer.listen as ReturnType).mockImplementation( + (_port: number, _host: string, callback: () => void) => { + callback(); + return mockServer; + } + ); + (mockServer.on as ReturnType).mockReturnValue(mockServer); + (mockServer.close as ReturnType).mockImplementation(() => undefined); + + const result = await startLocalOAuthServer({ state: 'test-state' }); + const codePromise = result.waitForCode('test-state'); + result.close(); + + await vi.advanceTimersByTimeAsync(200); + + await expect(codePromise).resolves.toBeNull(); + expect(logWarn).not.toHaveBeenCalledWith('OAuth poll timeout after 5 minutes'); + } finally { + vi.useRealTimers(); + } + }); + it('should return null after 5 minute timeout', async () => { vi.useFakeTimers();