{"version":3,"file":"agent-session-working-context.test.d.ts","sourceRoot":"","sources":["../../src/core/agent-session-working-context.test.ts"],"names":[],"mappings":"","sourcesContent":["import { Agent } from \"@apholdings/jensen-agent-core\";\nimport { afterAll, beforeAll, describe, expect, it, vi } from \"vitest\";\n\n// Fixed time within 7 days of test fixture timestamps (2026-04-02) so\n// reviewMemoryItems produces staleCount: 0 deterministically.\nconst FIXED_NOW = new Date(\"2026-04-03T00:00:00.000Z\").getTime();\n\nbeforeAll(() => {\n\tvi.useFakeTimers();\n\tvi.setSystemTime(FIXED_NOW);\n});\n\nafterAll(() => {\n\tvi.useRealTimers();\n});\n\nimport { AgentSession } from \"./agent-session.js\";\nimport { AuthStorage } from \"./auth-storage.js\";\nimport { SESSION_MEMORY_CUSTOM_TYPE, SESSION_TASKS_CUSTOM_TYPE, SESSION_TODOS_CUSTOM_TYPE } from \"./memory.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\";\nimport { buildWorkingContext } from \"./working-context.js\";\n\nfunction createSession(sessionManager = SessionManager.inMemory(\"/tmp/project\")): AgentSession {\n\tconst settingsManager = SettingsManager.inMemory();\n\tconst resourceLoader = new DefaultResourceLoader({\n\t\tcwd: \"/tmp/project\",\n\t\tagentDir: \"/tmp/agent\",\n\t\tsettingsManager,\n\t});\n\tconst authStorage = AuthStorage.create(\"/tmp/agent/auth.json\");\n\tconst modelRegistry = new ModelRegistry(authStorage);\n\tconst agent = new Agent({\n\t\tinitialState: {\n\t\t\tsystemPrompt: \"\",\n\t\t\tthinkingLevel: \"off\",\n\t\t\ttools: [],\n\t\t},\n\t});\n\n\treturn new AgentSession({\n\t\tagent,\n\t\tsessionManager,\n\t\tsettingsManager,\n\t\tcwd: \"/tmp/project\",\n\t\tresourceLoader,\n\t\tmodelRegistry,\n\t});\n}\n\ndescribe(\"AgentSession.getWorkingContext()\", () => {\n\tit(\"reuses the shared working-context builder for persisted memory and todo summaries\", () => {\n\t\tconst sessionManager = SessionManager.inMemory(\"/tmp/project\");\n\t\tsessionManager.appendCustomEntry(SESSION_MEMORY_CUSTOM_TYPE, [\n\t\t\t{ key: \"project.goal\", value: \"ship same-process working context\", timestamp: \"2026-04-02T09:00:00.000Z\" },\n\t\t\t{ key: \"project.scope\", value: \"bounded sdk surface\", timestamp: \"2026-04-02T09:05:00.000Z\" },\n\t\t]);\n\t\tsessionManager.appendCustomEntry(SESSION_TODOS_CUSTOM_TYPE, [\n\t\t\t{ content: \"Add AgentSession API\", activeForm: \"Adding AgentSession API\", status: \"in_progress\" },\n\t\t\t{ content: \"Update SDK docs\", activeForm: \"Updating SDK docs\", status: \"completed\" },\n\t\t]);\n\t\tconst session = createSession(sessionManager);\n\n\t\texpect(session.getWorkingContext()).toEqual(\n\t\t\tbuildWorkingContext({\n\t\t\t\tmemoryItems: session.getMemoryItems(),\n\t\t\t\ttodos: session.getTodos(),\n\t\t\t\ttasks: session.getTasks(),\n\t\t\t\tdelegatedWorkSummary: session.getDelegatedWorkSummary(),\n\t\t\t}),\n\t\t);\n\t});\n\n\tit(\"surfaces persisted memory and todo provenance markers honestly\", () => {\n\t\tconst sessionManager = SessionManager.inMemory(\"/tmp/project\");\n\t\tsessionManager.appendCustomEntry(SESSION_MEMORY_CUSTOM_TYPE, [\n\t\t\t{ key: \"branch\", value: \"feature/sdk-working-context\", timestamp: \"2026-04-02T10:00:00.000Z\" },\n\t\t]);\n\t\tsessionManager.appendCustomEntry(SESSION_TODOS_CUSTOM_TYPE, [\n\t\t\t{ content: \"Document contract\", activeForm: \"Documenting contract\", status: \"in_progress\" },\n\t\t]);\n\t\tconst session = createSession(sessionManager);\n\n\t\tconst context = session.getWorkingContext();\n\t\texpect(context.memory).toEqual({\n\t\t\titemCount: 1,\n\t\t\tstaleCount: 0,\n\t\t\tkeyPreview: [\"branch\"],\n\t\t\tisPersisted: true,\n\t\t\tscope: \"current_branch_session_state\",\n\t\t});\n\t\texpect(context.todo).toEqual({\n\t\t\ttotal: 1,\n\t\t\tcompleted: 0,\n\t\t\tinProgress: \"Documenting contract\",\n\t\t\tisPersisted: true,\n\t\t\tscope: \"current_branch_session_state\",\n\t\t});\n\t});\n\n\tit(\"surfaces live delegated work as current-process runtime state only\", async () => {\n\t\tconst session = createSession();\n\n\t\t(session as unknown as { _handleAgentEvent: (event: unknown) => void })._handleAgentEvent({\n\t\t\ttype: \"tool_execution_start\",\n\t\t\ttoolCallId: \"call_worker\",\n\t\t\ttoolName: \"subagent\",\n\t\t\targs: { agent: \"worker\", task: \"Implement SDK helper\" },\n\t\t});\n\t\tawait (session as unknown as { _agentEventQueue: Promise<void> })._agentEventQueue;\n\n\t\tconst context = session.getWorkingContext();\n\t\texpect(context.delegatedWork).toEqual({\n\t\t\tactiveCount: 1,\n\t\t\tcompletedCount: 0,\n\t\t\tfailedCount: 0,\n\t\t\tactiveAgents: [\"worker\"],\n\t\t\tfailurePreview: [],\n\t\t\tisPersisted: false,\n\t\t\tscope: \"current_process_runtime_state\",\n\t\t\tnote: \"live current-process state only; not persisted and resets on session switch/resume\",\n\t\t});\n\t});\n\n\tit(\"represents the no-delegation case honestly with zero runtime counts\", () => {\n\t\tconst session = createSession();\n\t\tconst context = session.getWorkingContext();\n\n\t\texpect(context.delegatedWork).toEqual({\n\t\t\tactiveCount: 0,\n\t\t\tcompletedCount: 0,\n\t\t\tfailedCount: 0,\n\t\t\tactiveAgents: [],\n\t\t\tfailurePreview: [],\n\t\t\tisPersisted: false,\n\t\t\tscope: \"current_process_runtime_state\",\n\t\t\tnote: \"live current-process state only; not persisted and resets on session switch/resume\",\n\t\t});\n\t});\n\n\tit(\"returns a JSON-serializable current-state payload\", () => {\n\t\tconst sessionManager = SessionManager.inMemory(\"/tmp/project\");\n\t\tsessionManager.appendCustomEntry(SESSION_MEMORY_CUSTOM_TYPE, [\n\t\t\t{ key: \"goal\", value: \"verify serializability\", timestamp: \"2026-04-02T11:00:00.000Z\" },\n\t\t]);\n\t\tconst session = createSession(sessionManager);\n\n\t\tconst parsed = JSON.parse(JSON.stringify(session.getWorkingContext()));\n\t\texpect(parsed.memory.isPersisted).toBe(true);\n\t\texpect(parsed.todo.isPersisted).toBe(true);\n\t\texpect(parsed.delegatedWork.isPersisted).toBe(false);\n\t});\n\n\tit(\"consolidates the same path used by interactive mode and RPC working-context handlers\", () => {\n\t\t// This test proves that both interactive-mode.ts (updateWorkingContextPanel, handleSessionCommand)\n\t\t// and rpc-mode.ts (get_working_context handler) now delegate to session.getWorkingContext()\n\t\t// instead of composing buildWorkingContext directly from session state.\n\t\t//\n\t\t// The consolidation point is AgentSession.getWorkingContext() which:\n\t\t// - calls buildWorkingContext() internally (preserving the shared contract in working-context.ts)\n\t\t// - retrieves state through AgentSession's own accessors (getMemoryItems, getTodos, getDelegatedWorkSummary)\n\t\t// - is the single source of truth for all working-context consumption paths\n\t\t//\n\t\t// This test verifies the consolidation by confirming session.getWorkingContext() returns\n\t\t// the same payload that would be built by direct composition, ensuring no semantics are lost.\n\t\tconst sessionManager = SessionManager.inMemory(\"/tmp/project\");\n\t\tsessionManager.appendCustomEntry(SESSION_MEMORY_CUSTOM_TYPE, [\n\t\t\t{ key: \"interactive.test\", value: \"consolidation proof\", timestamp: \"2026-04-02T12:00:00.000Z\" },\n\t\t]);\n\t\tsessionManager.appendCustomEntry(SESSION_TODOS_CUSTOM_TYPE, [\n\t\t\t{ content: \"Consolidate modes\", activeForm: \"Consolidating modes\", status: \"completed\" },\n\t\t]);\n\t\tconst session = createSession(sessionManager);\n\n\t\t// Direct composition that was previously done in both modes\n\t\tconst directComposition = buildWorkingContext({\n\t\t\tmemoryItems: session.getMemoryItems(),\n\t\t\ttodos: session.getTodos(),\n\t\t\ttasks: session.getTasks(),\n\t\t\tdelegatedWorkSummary: session.getDelegatedWorkSummary(),\n\t\t});\n\n\t\t// Consolidated path used by both modes now\n\t\tconst consolidatedPath = session.getWorkingContext();\n\n\t\t// They must be identical to prove consolidation doesn't change semantics\n\t\texpect(consolidatedPath).toEqual(directComposition);\n\n\t\t// Verify provenance honesty is preserved\n\t\texpect(consolidatedPath.memory.isPersisted).toBe(true);\n\t\texpect(consolidatedPath.memory.scope).toBe(\"current_branch_session_state\");\n\t\texpect(consolidatedPath.todo.isPersisted).toBe(true);\n\t\texpect(consolidatedPath.todo.scope).toBe(\"current_branch_session_state\");\n\t\texpect(consolidatedPath.delegatedWork.isPersisted).toBe(false);\n\t\texpect(consolidatedPath.delegatedWork.scope).toBe(\"current_process_runtime_state\");\n\t});\n\n\tit(\"resets tasks to empty array when newSession() is called\", async () => {\n\t\tconst sessionManager = SessionManager.inMemory(\"/tmp/project\");\n\t\t// Seed tasks via SessionManager (simulates task_create -> persisted)\n\t\tsessionManager.appendCustomEntry(SESSION_TASKS_CUSTOM_TYPE, [\n\t\t\t{ id: \"task_clear_1\", subject: \"Task before new session\", description: \"desc\", status: \"in_progress\" },\n\t\t\t{ id: \"task_clear_2\", subject: \"Completed task\", description: \"desc2\", status: \"completed\" },\n\t\t]);\n\n\t\t// Create session — tasks should be loaded from SessionManager\n\t\tconst session = createSession(sessionManager);\n\t\texpect(session.getTasks()).toHaveLength(2);\n\t\texpect(session.getWorkingContext().tasks.total).toBe(2);\n\n\t\t// newSession() resets all state\n\t\tawait session.newSession();\n\t\texpect(session.getTasks()).toEqual([]);\n\t\texpect(session.getWorkingContext().tasks.total).toBe(0);\n\t\texpect(session.getWorkingContext().tasks.pending).toBe(0);\n\t\texpect(session.getWorkingContext().tasks.inProgress).toBe(0);\n\t\texpect(session.getWorkingContext().tasks.completed).toBe(0);\n\t});\n});\n"]}