{"version":3,"file":"agent-session.test.d.ts","sourceRoot":"","sources":["../../src/core/agent-session.test.ts"],"names":[],"mappings":"","sourcesContent":["import { mkdtempSync, rmSync } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { Agent } from \"@apholdings/jensen-agent-core\";\nimport { describe, expect, it, vi } from \"vitest\";\nimport { AgentSession } from \"./agent-session.js\";\nimport { AuthStorage } from \"./auth-storage.js\";\nimport { ModelRegistry } from \"./model-registry.js\";\nimport { DefaultResourceLoader } from \"./resource-loader.js\";\nimport { SessionManager } from \"./session-manager.js\";\nimport { SettingsManager } from \"./settings-manager.js\";\n\nfunction createTempDir(): string {\n\treturn mkdtempSync(join(tmpdir(), \"jensen-fallback-\"));\n}\n\ndescribe(\"AgentSession Fallback Models\", () => {\n\tit(\"should fallback to the next model when API key validation fails for primary\", async () => {\n\t\tconst rootDir = createTempDir();\n\t\tconst agentDir = join(rootDir, \"agent\");\n\t\tconst cwd = join(rootDir, \"repo\");\n\n\t\tconst authStorage = AuthStorage.create(join(agentDir, \"auth.json\"));\n\t\tconst modelRegistry = new ModelRegistry(authStorage, undefined);\n\n\t\t// Add dummy models\n\t\tconst primaryModel = {\n\t\t\tid: \"primary-1\",\n\t\t\tprovider: \"test-provider\",\n\t\t\tapi: \"openai-chat\",\n\t\t\tbaseUrl: \"url\",\n\t\t\treasoning: false,\n\t\t\tinput: [\"text\"],\n\t\t\tcost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n\t\t\tcontextWindow: 8000,\n\t\t\tmaxTokens: 4000,\n\t\t} as any;\n\t\tconst fallbackModel = {\n\t\t\tid: \"fallback-1\",\n\t\t\tprovider: \"fallback-provider\",\n\t\t\tapi: \"openai-chat\",\n\t\t\tbaseUrl: \"url\",\n\t\t\treasoning: false,\n\t\t\tinput: [\"text\"],\n\t\t\tcost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n\t\t\tcontextWindow: 8000,\n\t\t\tmaxTokens: 4000,\n\t\t} as any;\n\n\t\tmodelRegistry.registerProvider(\"test-provider\", {\n\t\t\tbaseUrl: \"url\",\n\t\t\tapiKey: \"dummy-key\",\n\t\t\tapi: \"openai-chat\",\n\t\t\tmodels: [primaryModel],\n\t\t});\n\t\tmodelRegistry.registerProvider(\"fallback-provider\", {\n\t\t\tbaseUrl: \"url\",\n\t\t\tapiKey: \"valid-key\",\n\t\t\tapi: \"openai-chat\",\n\t\t\tmodels: [fallbackModel],\n\t\t});\n\n\t\t// Remove the API key for the primary model to force failure\n\t\tauthStorage.remove(\"test-provider\");\n\t\t(modelRegistry as any).customProviderApiKeys.delete(\"test-provider\");\n\n\t\tconst settingsManager = SettingsManager.create(cwd, agentDir);\n\t\tconst sessionManager = SessionManager.create(cwd);\n\t\tconst resourceLoader = new DefaultResourceLoader({ cwd, agentDir, settingsManager });\n\n\t\tconst agent = new Agent({\n\t\t\tinitialState: {\n\t\t\t\tsystemPrompt: \"\",\n\t\t\t\tmodel: primaryModel,\n\t\t\t\tthinkingLevel: \"off\",\n\t\t\t\ttools: [],\n\t\t\t},\n\t\t});\n\n\t\tconst session = new AgentSession({\n\t\t\tagent,\n\t\t\tsessionManager,\n\t\t\tsettingsManager,\n\t\t\tcwd,\n\t\t\tfallbackModels: [fallbackModel],\n\t\t\tresourceLoader,\n\t\t\tmodelRegistry,\n\t\t});\n\n\t\t// Mock prompt on agent to avoid real execution\n\t\tlet agentPromptCalled = false;\n\t\tagent.prompt = async () => {\n\t\t\tagentPromptCalled = true;\n\t\t};\n\n\t\t// Should automatically fallback and not throw during validation\n\t\tawait session.prompt(\"Hello\");\n\n\t\texpect(session.model?.id).toBe(\"fallback-1\");\n\t\texpect(agentPromptCalled).toBe(true);\n\n\t\trmSync(rootDir, { recursive: true, force: true });\n\t});\n\n\tit(\"should fallback when retries are exhausted\", async () => {\n\t\tconst rootDir = createTempDir();\n\t\tconst agentDir = join(rootDir, \"agent\");\n\t\tconst cwd = join(rootDir, \"repo\");\n\n\t\tconst authStorage = AuthStorage.create(join(agentDir, \"auth.json\"));\n\t\tconst modelRegistry = new ModelRegistry(authStorage, undefined);\n\n\t\tconst primaryModel = {\n\t\t\tid: \"primary-1\",\n\t\t\tprovider: \"test-provider\",\n\t\t\tapi: \"openai-chat\",\n\t\t\tbaseUrl: \"url\",\n\t\t\treasoning: false,\n\t\t\tinput: [\"text\"],\n\t\t\tcost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n\t\t\tcontextWindow: 8000,\n\t\t\tmaxTokens: 4000,\n\t\t} as any;\n\t\tconst fallbackModel = {\n\t\t\tid: \"fallback-1\",\n\t\t\tprovider: \"fallback-provider\",\n\t\t\tapi: \"openai-chat\",\n\t\t\tbaseUrl: \"url\",\n\t\t\treasoning: false,\n\t\t\tinput: [\"text\"],\n\t\t\tcost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n\t\t\tcontextWindow: 8000,\n\t\t\tmaxTokens: 4000,\n\t\t} as any;\n\n\t\tmodelRegistry.registerProvider(\"test-provider\", {\n\t\t\tbaseUrl: \"url\",\n\t\t\tapiKey: \"valid-key-1\",\n\t\t\tapi: \"openai-chat\",\n\t\t\tmodels: [primaryModel],\n\t\t});\n\t\tmodelRegistry.registerProvider(\"fallback-provider\", {\n\t\t\tbaseUrl: \"url\",\n\t\t\tapiKey: \"valid-key-2\",\n\t\t\tapi: \"openai-chat\",\n\t\t\tmodels: [fallbackModel],\n\t\t});\n\n\t\tconst settingsManager = SettingsManager.create(cwd, agentDir);\n\t\t// Enable retries and set maxRetries to 1\n\t\tsettingsManager.setRetryEnabled(true);\n\t\t// Can't directly set maxRetries via API easily here, but we can rely on default (3) or mock it.\n\t\t// We can just spy on the settings manager:\n\t\tconst getRetrySettings = vi.spyOn(settingsManager, \"getRetrySettings\");\n\t\tgetRetrySettings.mockReturnValue({ enabled: true, maxRetries: 1, maxDelayMs: 1000, baseDelayMs: 10 });\n\n\t\tconst sessionManager = SessionManager.create(cwd);\n\t\tconst resourceLoader = new DefaultResourceLoader({ cwd, agentDir, settingsManager });\n\n\t\tconst agent = new Agent({\n\t\t\tinitialState: {\n\t\t\t\tsystemPrompt: \"\",\n\t\t\t\tmodel: primaryModel,\n\t\t\t\tthinkingLevel: \"off\",\n\t\t\t\ttools: [],\n\t\t\t},\n\t\t});\n\n\t\tconst session = new AgentSession({\n\t\t\tagent,\n\t\t\tsessionManager,\n\t\t\tsettingsManager,\n\t\t\tcwd,\n\t\t\tfallbackModels: [fallbackModel],\n\t\t\tresourceLoader,\n\t\t\tmodelRegistry,\n\t\t});\n\n\t\t// Emit an agent_end with a retryable error message\n\t\tconst messages = [\n\t\t\t{\n\t\t\t\trole: \"assistant\",\n\t\t\t\tstopReason: \"error\",\n\t\t\t\terrorMessage: \"Rate limit exceeded 429\",\n\t\t\t\tcontent: [],\n\t\t\t\tusage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: { total: 0 } },\n\t\t\t\ttimestamp: 0,\n\t\t\t},\n\t\t];\n\n\t\t// Mock agent.continue to just record call\n\t\tlet continueCalled = 0;\n\t\tagent.continue = async () => {\n\t\t\tcontinueCalled++;\n\t\t};\n\t\tagent.state.messages = messages as any;\n\n\t\t// Force a process of agent_end which will trigger retry\n\t\t(session as any)._lastAssistantMessage = messages[0];\n\t\t(session as any)._handleAgentEvent({ type: \"agent_end\", messages } as any);\n\n\t\t// Wait for retry to be processed\n\t\tawait new Promise((r) => setTimeout(r, 50));\n\n\t\t// It should have tried to retry once\n\t\texpect(continueCalled).toBe(1);\n\t\texpect(session.retryAttempt).toBe(1);\n\t\texpect(session.model?.id).toBe(\"primary-1\"); // Still primary\n\n\t\t// Trigger failure again\n\t\t(session as any)._lastAssistantMessage = messages[0];\n\t\t(session as any)._handleAgentEvent({ type: \"agent_end\", messages } as any);\n\n\t\t// Wait for fallback\n\t\tawait new Promise((r) => setTimeout(r, 50));\n\n\t\t// Should have fallen back!\n\t\texpect(session.model?.id).toBe(\"fallback-1\");\n\t\texpect(session.retryAttempt).toBe(0); // Reset for the new model\n\t\t// Continue should have been called again for the fallback\n\t\texpect(continueCalled).toBe(2);\n\n\t\trmSync(rootDir, { recursive: true, force: true });\n\t});\n});\n"]}