From 9ad58ddc7e1afed2d17737d9274b5f30e2fbbccf Mon Sep 17 00:00:00 2001 From: Ayaan Zaidi Date: Wed, 22 Apr 2026 16:30:24 +0530 Subject: [PATCH] test(cli): cover oauth auth epoch continuity --- src/agents/cli-auth-epoch.test.ts | 85 +++++++++++++++++++++-- src/agents/cli-runner.reliability.test.ts | 1 + src/agents/cli-runner.spawn.test.ts | 2 + src/agents/cli-session.test.ts | 40 ++++++++++- 4 files changed, 119 insertions(+), 9 deletions(-) diff --git a/src/agents/cli-auth-epoch.test.ts b/src/agents/cli-auth-epoch.test.ts index fc2ba01e00d..2bebf2598d3 100644 --- a/src/agents/cli-auth-epoch.test.ts +++ b/src/agents/cli-auth-epoch.test.ts @@ -30,20 +30,42 @@ describe("resolveCliAuthEpoch", () => { ).resolves.toBeUndefined(); }); - it("changes when claude cli credentials change", async () => { + it("keeps claude cli oauth epochs stable across access-token refreshes", async () => { let access = "access-a"; + let expires = 1; setCliAuthEpochTestDeps({ readClaudeCliCredentialsCached: () => ({ type: "oauth", provider: "anthropic", access, refresh: "refresh", - expires: 1, + expires, }), }); const first = await resolveCliAuthEpoch({ provider: "claude-cli" }); access = "access-b"; + expires = 2; + const second = await resolveCliAuthEpoch({ provider: "claude-cli" }); + + expect(first).toBeDefined(); + expect(second).toBe(first); + }); + + it("changes claude cli oauth epochs when the refresh token changes", async () => { + let refresh = "refresh-a"; + setCliAuthEpochTestDeps({ + readClaudeCliCredentialsCached: () => ({ + type: "oauth", + provider: "anthropic", + access: "access", + refresh, + expires: 1, + }), + }); + + const first = await resolveCliAuthEpoch({ provider: "claude-cli" }); + refresh = "refresh-b"; const second = await resolveCliAuthEpoch({ provider: "claude-cli" }); expect(first).toBeDefined(); @@ -51,7 +73,7 @@ describe("resolveCliAuthEpoch", () => { expect(second).not.toBe(first); }); - it("changes when auth profile credentials change", async () => { + it("keeps oauth auth-profile epochs stable across access-token refreshes", async () => { let store: AuthProfileStore = { version: 1, profiles: { @@ -80,6 +102,48 @@ describe("resolveCliAuthEpoch", () => { provider: "anthropic", access: "access-b", refresh: "refresh", + expires: 2, + }, + }, + }; + const second = await resolveCliAuthEpoch({ + provider: "google-gemini-cli", + authProfileId: "anthropic:work", + }); + + expect(first).toBeDefined(); + expect(second).toBe(first); + }); + + it("changes oauth auth-profile epochs when the refresh token changes", async () => { + let store: AuthProfileStore = { + version: 1, + profiles: { + "anthropic:work": { + type: "oauth", + provider: "anthropic", + access: "access", + refresh: "refresh-a", + expires: 1, + }, + }, + }; + setCliAuthEpochTestDeps({ + loadAuthProfileStoreForRuntime: () => store, + }); + + const first = await resolveCliAuthEpoch({ + provider: "google-gemini-cli", + authProfileId: "anthropic:work", + }); + store = { + version: 1, + profiles: { + "anthropic:work": { + type: "oauth", + provider: "anthropic", + access: "access", + refresh: "refresh-b", expires: 1, }, }, @@ -96,13 +160,14 @@ describe("resolveCliAuthEpoch", () => { it("mixes local codex and auth-profile state", async () => { let access = "local-access-a"; + let localRefresh = "local-refresh-a"; let refresh = "profile-refresh-a"; setCliAuthEpochTestDeps({ readCodexCliCredentialsCached: () => ({ type: "oauth", provider: "openai-codex", access, - refresh: "local-refresh", + refresh: localRefresh, expires: 1, accountId: "acct-1", }), @@ -129,17 +194,23 @@ describe("resolveCliAuthEpoch", () => { provider: "codex-cli", authProfileId: "openai:work", }); - refresh = "profile-refresh-b"; + localRefresh = "local-refresh-b"; const third = await resolveCliAuthEpoch({ provider: "codex-cli", authProfileId: "openai:work", }); + refresh = "profile-refresh-b"; + const fourth = await resolveCliAuthEpoch({ + provider: "codex-cli", + authProfileId: "openai:work", + }); expect(first).toBeDefined(); - expect(second).toBeDefined(); expect(third).toBeDefined(); - expect(second).not.toBe(first); + expect(fourth).toBeDefined(); + expect(second).toBe(first); expect(third).not.toBe(second); + expect(fourth).not.toBe(third); }); it("can ignore local codex state when the backend is profile-owned", async () => { diff --git a/src/agents/cli-runner.reliability.test.ts b/src/agents/cli-runner.reliability.test.ts index 6c021abf8f3..613e2f860ba 100644 --- a/src/agents/cli-runner.reliability.test.ts +++ b/src/agents/cli-runner.reliability.test.ts @@ -55,6 +55,7 @@ function buildPreparedContext(params?: { systemPrompt: "You are a helpful assistant.", systemPromptReport: {} as PreparedCliRunContext["systemPromptReport"], bootstrapPromptWarningLines: [], + authEpochVersion: 2, }; } diff --git a/src/agents/cli-runner.spawn.test.ts b/src/agents/cli-runner.spawn.test.ts index b4a4cde2f5f..7d7cb5ea01e 100644 --- a/src/agents/cli-runner.spawn.test.ts +++ b/src/agents/cli-runner.spawn.test.ts @@ -111,6 +111,7 @@ function buildPreparedCliRunContext(params: { systemPrompt: "You are a helpful assistant.", systemPromptReport: {} as PreparedCliRunContext["systemPromptReport"], bootstrapPromptWarningLines: [], + authEpochVersion: 2, }; } @@ -170,6 +171,7 @@ describe("runCliAgent spawn path", () => { systemPrompt: "You are a helpful assistant.", systemPromptReport: {} as PreparedCliRunContext["systemPromptReport"], bootstrapPromptWarningLines: [], + authEpochVersion: 2, }; await executePreparedCliRun(context); diff --git a/src/agents/cli-session.test.ts b/src/agents/cli-session.test.ts index 6fc2d94c155..4827d0c8046 100644 --- a/src/agents/cli-session.test.ts +++ b/src/agents/cli-session.test.ts @@ -20,6 +20,7 @@ describe("cli-session helpers", () => { sessionId: "cli-session-1", authProfileId: "anthropic:work", authEpoch: "auth-epoch", + authEpochVersion: 2, extraSystemPromptHash: "prompt-hash", mcpConfigHash: "mcp-hash", mcpResumeHash: "mcp-resume-hash", @@ -31,6 +32,7 @@ describe("cli-session helpers", () => { sessionId: "cli-session-1", authProfileId: "anthropic:work", authEpoch: "auth-epoch", + authEpochVersion: 2, extraSystemPromptHash: "prompt-hash", mcpConfigHash: "mcp-hash", mcpResumeHash: "mcp-resume-hash", @@ -84,6 +86,7 @@ describe("cli-session helpers", () => { sessionId: "cli-session-1", authProfileId: "anthropic:work", authEpoch: "auth-epoch-a", + authEpochVersion: 2, extraSystemPromptHash: "prompt-a", mcpConfigHash: "mcp-a", }; @@ -93,6 +96,7 @@ describe("cli-session helpers", () => { binding, authProfileId: "anthropic:personal", authEpoch: "auth-epoch-a", + authEpochVersion: 2, extraSystemPromptHash: "prompt-a", mcpConfigHash: "mcp-a", }), @@ -102,6 +106,7 @@ describe("cli-session helpers", () => { binding, authProfileId: "anthropic:work", authEpoch: "auth-epoch-b", + authEpochVersion: 2, extraSystemPromptHash: "prompt-a", mcpConfigHash: "mcp-a", }), @@ -111,6 +116,7 @@ describe("cli-session helpers", () => { binding, authProfileId: "anthropic:work", authEpoch: "auth-epoch-a", + authEpochVersion: 2, extraSystemPromptHash: "prompt-b", mcpConfigHash: "mcp-a", }), @@ -120,17 +126,18 @@ describe("cli-session helpers", () => { binding, authProfileId: "anthropic:work", authEpoch: "auth-epoch-a", + authEpochVersion: 2, extraSystemPromptHash: "prompt-a", mcpConfigHash: "mcp-b", }), ).toEqual({ invalidatedReason: "mcp" }); }); - it("does not treat model changes as a session mismatch", () => { + it("accepts unversioned auth epochs for binding upgrades", () => { const binding = { sessionId: "cli-session-1", authProfileId: "anthropic:work", - authEpoch: "auth-epoch-a", + authEpoch: "previous-auth-epoch", extraSystemPromptHash: "prompt-a", mcpConfigHash: "mcp-a", }; @@ -140,6 +147,29 @@ describe("cli-session helpers", () => { binding, authProfileId: "anthropic:work", authEpoch: "auth-epoch-a", + authEpochVersion: 2, + extraSystemPromptHash: "prompt-a", + mcpConfigHash: "mcp-a", + }), + ).toEqual({ sessionId: "cli-session-1" }); + }); + + it("does not treat model changes as a session mismatch", () => { + const binding = { + sessionId: "cli-session-1", + authProfileId: "anthropic:work", + authEpoch: "auth-epoch-a", + authEpochVersion: 2, + extraSystemPromptHash: "prompt-a", + mcpConfigHash: "mcp-a", + }; + + expect( + resolveCliSessionReuse({ + binding, + authProfileId: "anthropic:work", + authEpoch: "auth-epoch-a", + authEpochVersion: 2, extraSystemPromptHash: "prompt-a", mcpConfigHash: "mcp-a", }), @@ -151,6 +181,7 @@ describe("cli-session helpers", () => { sessionId: "cli-session-1", authProfileId: "anthropic:work", authEpoch: "auth-epoch-a", + authEpochVersion: 2, extraSystemPromptHash: "prompt-a", mcpConfigHash: "mcp-config-a", mcpResumeHash: "mcp-resume-a", @@ -161,6 +192,7 @@ describe("cli-session helpers", () => { binding, authProfileId: "anthropic:work", authEpoch: "auth-epoch-a", + authEpochVersion: 2, extraSystemPromptHash: "prompt-a", mcpConfigHash: "mcp-config-b", mcpResumeHash: "mcp-resume-a", @@ -171,6 +203,7 @@ describe("cli-session helpers", () => { binding, authProfileId: "anthropic:work", authEpoch: "auth-epoch-a", + authEpochVersion: 2, extraSystemPromptHash: "prompt-a", mcpConfigHash: "mcp-config-a", mcpResumeHash: "mcp-resume-b", @@ -183,6 +216,7 @@ describe("cli-session helpers", () => { sessionId: "cli-session-1", authProfileId: "anthropic:work", authEpoch: "auth-epoch-a", + authEpochVersion: 2, extraSystemPromptHash: "prompt-a", mcpConfigHash: "mcp-config-a", }; @@ -192,6 +226,7 @@ describe("cli-session helpers", () => { binding, authProfileId: "anthropic:work", authEpoch: "auth-epoch-a", + authEpochVersion: 2, extraSystemPromptHash: "prompt-a", mcpConfigHash: "mcp-config-a", mcpResumeHash: "mcp-resume-a", @@ -202,6 +237,7 @@ describe("cli-session helpers", () => { binding, authProfileId: "anthropic:work", authEpoch: "auth-epoch-a", + authEpochVersion: 2, extraSystemPromptHash: "prompt-a", mcpConfigHash: "mcp-config-b", mcpResumeHash: "mcp-resume-a",