{"version":3,"file":"rpc-working-context.test.d.ts","sourceRoot":"","sources":["../../../src/modes/rpc/rpc-working-context.test.ts"],"names":[],"mappings":"","sourcesContent":["import { mkdtempSync, rmSync } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { PassThrough } from \"node:stream\";\nimport { Agent, type AgentEvent } from \"@apholdings/jensen-agent-core\";\nimport { afterAll, beforeAll, describe, expect, it, vi } from \"vitest\";\nimport { AgentSession } from \"../../core/agent-session.js\";\nimport { AuthStorage } from \"../../core/auth-storage.js\";\nimport { SESSION_MEMORY_CUSTOM_TYPE, SESSION_TASKS_CUSTOM_TYPE, SESSION_TODOS_CUSTOM_TYPE } from \"../../core/memory.js\";\nimport { ModelRegistry } from \"../../core/model-registry.js\";\nimport { DefaultResourceLoader } from \"../../core/resource-loader.js\";\nimport { SessionManager } from \"../../core/session-manager.js\";\nimport { SettingsManager } from \"../../core/settings-manager.js\";\nimport { buildWorkingContext } from \"../../core/working-context.js\";\nimport { attachJsonlLineReader, serializeJsonLine } from \"./jsonl.js\";\nimport { runRpcMode } from \"./rpc-mode.js\";\nimport type { RpcCommand, RpcResponse } from \"./rpc-types.js\";\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\nfunction createSession(sessionManager = SessionManager.inMemory(\"/tmp/project\"), cwd = \"/tmp/project\"): AgentSession {\n\tconst settingsManager = SettingsManager.inMemory();\n\tconst resourceLoader = new DefaultResourceLoader({\n\t\tcwd,\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,\n\t\tresourceLoader,\n\t\tmodelRegistry,\n\t});\n}\n\nfunction createPersistedSessionManager(\n\tcwd: string,\n\tsessionDir: string,\n\toptions: {\n\t\tmemoryItems: Array<{ key: string; value: string; timestamp: string }>;\n\t\ttodos: Array<{ content: string; activeForm: string; status: string }>;\n\t},\n): SessionManager {\n\tconst sessionManager = SessionManager.create(cwd, sessionDir);\n\tsessionManager.appendCustomEntry(SESSION_MEMORY_CUSTOM_TYPE, options.memoryItems);\n\tsessionManager.appendCustomEntry(SESSION_TODOS_CUSTOM_TYPE, options.todos);\n\tsessionManager.appendMessage({\n\t\trole: \"assistant\",\n\t\tapi: \"openai-chat\",\n\t\tprovider: \"test-provider\",\n\t\tmodel: \"test-model\",\n\t\tstopReason: \"stop\",\n\t\tcontent: [{ type: \"text\", text: \"seed persisted session state\" }],\n\t\tusage: {\n\t\t\tinput: 0,\n\t\t\toutput: 0,\n\t\t\tcacheRead: 0,\n\t\t\tcacheWrite: 0,\n\t\t\ttotalTokens: 0,\n\t\t\tcost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },\n\t\t},\n\t\ttimestamp: Date.now(),\n\t});\n\treturn sessionManager;\n}\n\nasync function emitSessionEvent(session: AgentSession, event: AgentEvent): Promise<void> {\n\tconst sessionWithInternals = session as unknown as {\n\t\t_handleAgentEvent: (agentEvent: AgentEvent) => void;\n\t\t_agentEventQueue: Promise<void>;\n\t};\n\tsessionWithInternals._handleAgentEvent(event);\n\tawait sessionWithInternals._agentEventQueue;\n}\n\nfunction waitForRpcReader(stdin: PassThrough): Promise<void> {\n\tif (stdin.listenerCount(\"data\") > 0) {\n\t\treturn Promise.resolve();\n\t}\n\n\treturn new Promise((resolve, reject) => {\n\t\tconst timeout = setTimeout(() => {\n\t\t\tstdin.off(\"newListener\", onNewListener);\n\t\t\treject(new Error(\"RPC mode did not attach a stdin reader\"));\n\t\t}, 250);\n\n\t\tconst onNewListener = (eventName: string | symbol) => {\n\t\t\tif (eventName !== \"data\") {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tclearTimeout(timeout);\n\t\t\tstdin.off(\"newListener\", onNewListener);\n\t\t\tresolve();\n\t\t};\n\n\t\tstdin.on(\"newListener\", onNewListener);\n\t});\n}\n\nasync function runRpcCommand(session: AgentSession, command: RpcCommand): Promise<RpcResponse> {\n\tconst fakeStdin = new PassThrough();\n\tconst fakeStdout = new PassThrough();\n\tconst stdinDescriptor = Object.getOwnPropertyDescriptor(process, \"stdin\");\n\tconst stdoutDescriptor = Object.getOwnPropertyDescriptor(process, \"stdout\");\n\n\tif (!stdinDescriptor || !stdoutDescriptor) {\n\t\tthrow new Error(\"Failed to capture process stdio descriptors\");\n\t}\n\n\tlet resolveResponse: ((response: RpcResponse) => void) | undefined;\n\tlet rejectResponse: ((error: Error) => void) | undefined;\n\tconst responsePromise = new Promise<RpcResponse>((resolve, reject) => {\n\t\tresolveResponse = resolve;\n\t\trejectResponse = reject;\n\t});\n\tconst responseTimeout = setTimeout(() => {\n\t\trejectResponse?.(new Error(`Timed out waiting for RPC response to ${command.type}`));\n\t}, 250);\n\tconst detachOutputReader = attachJsonlLineReader(fakeStdout, (line) => {\n\t\tconst parsed = JSON.parse(line) as RpcResponse;\n\t\tif (parsed.type === \"response\") {\n\t\t\tclearTimeout(responseTimeout);\n\t\t\tresolveResponse?.(parsed);\n\t\t}\n\t});\n\n\tObject.defineProperty(process, \"stdin\", { configurable: true, value: fakeStdin });\n\tObject.defineProperty(process, \"stdout\", { configurable: true, value: fakeStdout });\n\n\ttry {\n\t\tconst readerReady = waitForRpcReader(fakeStdin);\n\t\tvoid runRpcMode(session);\n\t\tawait readerReady;\n\t\tfakeStdin.write(serializeJsonLine(command));\n\t\treturn await responsePromise;\n\t} finally {\n\t\tclearTimeout(responseTimeout);\n\t\tdetachOutputReader();\n\t\tfakeStdin.end();\n\t\tfakeStdout.end();\n\t\tObject.defineProperty(process, \"stdin\", stdinDescriptor);\n\t\tObject.defineProperty(process, \"stdout\", stdoutDescriptor);\n\t}\n}\n\ndescribe(\"RPC working-context payload\", () => {\n\tit(\"is JSON-serializable with explicit persisted vs runtime scope markers\", () => {\n\t\tconst payload = buildWorkingContext({\n\t\t\tmemoryItems: [{ key: \"branch\", value: \"feature/refactor\", timestamp: \"2026-04-02T10:00:00.000Z\" }],\n\t\t\ttodos: [{ content: \"Ship RPC surface\", activeForm: \"Shipping RPC surface\", status: \"in_progress\" }],\n\t\t\ttasks: [],\n\t\t\tdelegatedWorkSummary: {\n\t\t\t\tactive: [\n\t\t\t\t\t{\n\t\t\t\t\t\ttoolCallId: \"call_worker\",\n\t\t\t\t\t\tagent: \"worker\",\n\t\t\t\t\t\tagentSource: \"user\",\n\t\t\t\t\t\ttask: \"Implement RPC working context\",\n\t\t\t\t\t\tmode: \"single\",\n\t\t\t\t\t\tstatus: \"active\",\n\t\t\t\t\t\ttimestamp: Date.now(),\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t\tcompleted: [],\n\t\t\t\tfailed: [],\n\t\t\t\ttotal: 1,\n\t\t\t\tisSessionState: true,\n\t\t\t\tnote: \"ephemeral current-session state; not persisted across sessions or branches\",\n\t\t\t},\n\t\t});\n\n\t\tconst serialized = serializeJsonLine(payload);\n\t\tconst parsed = JSON.parse(serialized);\n\n\t\texpect(parsed.memory.isPersisted).toBe(true);\n\t\texpect(parsed.memory.scope).toBe(\"current_branch_session_state\");\n\t\texpect(parsed.todo.isPersisted).toBe(true);\n\t\texpect(parsed.todo.scope).toBe(\"current_branch_session_state\");\n\t\texpect(parsed.delegatedWork.isPersisted).toBe(false);\n\t\texpect(parsed.delegatedWork.scope).toBe(\"current_process_runtime_state\");\n\t\texpect(parsed.delegatedWork.note).toContain(\"not persisted\");\n\t});\n\n\tit(\"represents the no-delegation case honestly with zero counts\", () => {\n\t\tconst payload = buildWorkingContext({\n\t\t\tmemoryItems: [],\n\t\t\ttodos: [],\n\t\t\ttasks: [],\n\t\t\tdelegatedWorkSummary: {\n\t\t\t\tactive: [],\n\t\t\t\tcompleted: [],\n\t\t\t\tfailed: [],\n\t\t\t\ttotal: 0,\n\t\t\t\tisSessionState: true,\n\t\t\t\tnote: \"ephemeral current-session state; not persisted across sessions or branches\",\n\t\t\t},\n\t\t});\n\n\t\texpect(payload.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 live working context through the RPC JSONL command path\", async () => {\n\t\tconst sessionManager = SessionManager.inMemory(\"/tmp/project\");\n\t\tsessionManager.appendCustomEntry(SESSION_MEMORY_CUSTOM_TYPE, [\n\t\t\t{ key: \"project.goal\", value: \"cover runtime RPC path\", timestamp: \"2026-04-02T12:00:00.000Z\" },\n\t\t]);\n\t\tsessionManager.appendCustomEntry(SESSION_TODOS_CUSTOM_TYPE, [\n\t\t\t{ content: \"Add runtime RPC test\", activeForm: \"Adding runtime RPC test\", status: \"in_progress\" },\n\t\t]);\n\t\tconst session = createSession(sessionManager);\n\n\t\tawait emitSessionEvent(session, {\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: \"Verify live delegated work summary\" },\n\t\t});\n\n\t\tconst response = await runRpcCommand(session, { id: \"wc-1\", type: \"get_working_context\" });\n\n\t\texpect(response).toEqual({\n\t\t\tid: \"wc-1\",\n\t\t\ttype: \"response\",\n\t\t\tcommand: \"get_working_context\",\n\t\t\tsuccess: true,\n\t\t\tdata: {\n\t\t\t\tmemory: {\n\t\t\t\t\titemCount: 1,\n\t\t\t\t\tstaleCount: 0,\n\t\t\t\t\tkeyPreview: [\"project.goal\"],\n\t\t\t\t\tisPersisted: true,\n\t\t\t\t\tscope: \"current_branch_session_state\",\n\t\t\t\t},\n\t\t\t\ttodo: {\n\t\t\t\t\ttotal: 1,\n\t\t\t\t\tcompleted: 0,\n\t\t\t\t\tinProgress: \"Adding runtime RPC test\",\n\t\t\t\t\tisPersisted: true,\n\t\t\t\t\tscope: \"current_branch_session_state\",\n\t\t\t\t},\n\t\t\t\ttasks: {\n\t\t\t\t\ttotal: 0,\n\t\t\t\t\tpending: 0,\n\t\t\t\t\tinProgress: 0,\n\t\t\t\t\tcompleted: 0,\n\t\t\t\t\tinProgressTask: undefined,\n\t\t\t\t\tisPersisted: true,\n\t\t\t\t\tscope: \"current_branch_session_state\",\n\t\t\t\t},\n\t\t\t\tdelegatedWork: {\n\t\t\t\t\tactiveCount: 1,\n\t\t\t\t\tcompletedCount: 0,\n\t\t\t\t\tfailedCount: 0,\n\t\t\t\t\tactiveAgents: [\"worker\"],\n\t\t\t\t\tfailurePreview: [],\n\t\t\t\t\tisPersisted: false,\n\t\t\t\t\tscope: \"current_process_runtime_state\",\n\t\t\t\t\tnote: \"live current-process state only; not persisted and resets on session switch/resume\",\n\t\t\t\t},\n\t\t\t},\n\t\t});\n\t});\n\n\tit(\"proves honest reset boundary: delegated-work resets on session switch while persisted memory and todos remain available\", async () => {\n\t\tconst rootDir = mkdtempSync(join(tmpdir(), \"jensen-rpc-working-context-\"));\n\t\tconst cwd = join(rootDir, \"repo\");\n\t\tconst sessionDir = join(rootDir, \"sessions\");\n\n\t\ttry {\n\t\t\tconst sourceSessionManager = createPersistedSessionManager(cwd, sessionDir, {\n\t\t\t\tmemoryItems: [{ key: \"session.source\", value: \"source state\", timestamp: \"2026-04-02T12:00:00.000Z\" }],\n\t\t\t\ttodos: [{ content: \"Source task\", activeForm: \"Working source task\", status: \"in_progress\" }],\n\t\t\t});\n\t\t\tsourceSessionManager.appendCustomEntry(SESSION_TASKS_CUSTOM_TYPE, [\n\t\t\t\t{ id: \"src_task_1\", subject: \"Source session task\", description: \"source desc\", status: \"pending\" },\n\t\t\t\t{\n\t\t\t\t\tid: \"src_task_2\",\n\t\t\t\t\tsubject: \"Source in progress task\",\n\t\t\t\t\tdescription: \"source desc 2\",\n\t\t\t\t\tstatus: \"in_progress\",\n\t\t\t\t},\n\t\t\t]);\n\t\t\tconst targetSessionManager = createPersistedSessionManager(cwd, sessionDir, {\n\t\t\t\tmemoryItems: [{ key: \"session.target\", value: \"target state\", timestamp: \"2026-04-02T12:05:00.000Z\" }],\n\t\t\t\ttodos: [{ content: \"Target task\", activeForm: \"Working target task\", status: \"in_progress\" }],\n\t\t\t});\n\t\t\ttargetSessionManager.appendCustomEntry(SESSION_TASKS_CUSTOM_TYPE, [\n\t\t\t\t{ id: \"tgt_task_1\", subject: \"Target session task\", description: \"target desc\", status: \"pending\" },\n\t\t\t]);\n\t\t\tconst targetSessionFile = targetSessionManager.getSessionFile();\n\t\t\tif (!targetSessionFile) {\n\t\t\t\tthrow new Error(\"Expected target persisted session file\");\n\t\t\t}\n\n\t\t\tconst session = createSession(sourceSessionManager, cwd);\n\t\t\tawait emitSessionEvent(session, {\n\t\t\t\ttype: \"tool_execution_start\",\n\t\t\t\ttoolCallId: \"call_worker\",\n\t\t\t\ttoolName: \"subagent\",\n\t\t\t\targs: { agent: \"worker\", task: \"Verify reset boundary honesty\" },\n\t\t\t});\n\n\t\t\tconst beforeResponse = await runRpcCommand(session, { id: \"wc-before\", type: \"get_working_context\" });\n\t\t\texpect(beforeResponse).toEqual({\n\t\t\t\tid: \"wc-before\",\n\t\t\t\ttype: \"response\",\n\t\t\t\tcommand: \"get_working_context\",\n\t\t\t\tsuccess: true,\n\t\t\t\tdata: {\n\t\t\t\t\tmemory: {\n\t\t\t\t\t\titemCount: 1,\n\t\t\t\t\t\tstaleCount: 0,\n\t\t\t\t\t\tkeyPreview: [\"session.source\"],\n\t\t\t\t\t\tisPersisted: true,\n\t\t\t\t\t\tscope: \"current_branch_session_state\",\n\t\t\t\t\t},\n\t\t\t\t\ttodo: {\n\t\t\t\t\t\ttotal: 1,\n\t\t\t\t\t\tcompleted: 0,\n\t\t\t\t\t\tinProgress: \"Working source task\",\n\t\t\t\t\t\tisPersisted: true,\n\t\t\t\t\t\tscope: \"current_branch_session_state\",\n\t\t\t\t\t},\n\t\t\t\t\ttasks: {\n\t\t\t\t\t\ttotal: 2,\n\t\t\t\t\t\tpending: 1,\n\t\t\t\t\t\tinProgress: 1,\n\t\t\t\t\t\tcompleted: 0,\n\t\t\t\t\t\tinProgressTask: { id: \"src_task_2\", subject: \"Source in progress task\", activeForm: undefined },\n\t\t\t\t\t\tisPersisted: true,\n\t\t\t\t\t\tscope: \"current_branch_session_state\",\n\t\t\t\t\t},\n\t\t\t\t\tdelegatedWork: {\n\t\t\t\t\t\tactiveCount: 1,\n\t\t\t\t\t\tcompletedCount: 0,\n\t\t\t\t\t\tfailedCount: 0,\n\t\t\t\t\t\tactiveAgents: [\"worker\"],\n\t\t\t\t\t\tfailurePreview: [],\n\t\t\t\t\t\tisPersisted: false,\n\t\t\t\t\t\tscope: \"current_process_runtime_state\",\n\t\t\t\t\t\tnote: \"live current-process state only; not persisted and resets on session switch/resume\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t});\n\n\t\t\tconst switchResponse = await runRpcCommand(session, {\n\t\t\t\tid: \"switch-1\",\n\t\t\t\ttype: \"switch_session\",\n\t\t\t\tsessionPath: targetSessionFile,\n\t\t\t});\n\t\t\texpect(switchResponse).toEqual({\n\t\t\t\tid: \"switch-1\",\n\t\t\t\ttype: \"response\",\n\t\t\t\tcommand: \"switch_session\",\n\t\t\t\tsuccess: true,\n\t\t\t\tdata: { cancelled: false },\n\t\t\t});\n\n\t\t\tconst afterResponse = await runRpcCommand(session, { id: \"wc-after\", type: \"get_working_context\" });\n\t\t\texpect(afterResponse).toEqual({\n\t\t\t\tid: \"wc-after\",\n\t\t\t\ttype: \"response\",\n\t\t\t\tcommand: \"get_working_context\",\n\t\t\t\tsuccess: true,\n\t\t\t\tdata: {\n\t\t\t\t\tmemory: {\n\t\t\t\t\t\titemCount: 1,\n\t\t\t\t\t\tstaleCount: 0,\n\t\t\t\t\t\tkeyPreview: [\"session.target\"],\n\t\t\t\t\t\tisPersisted: true,\n\t\t\t\t\t\tscope: \"current_branch_session_state\",\n\t\t\t\t\t},\n\t\t\t\t\ttodo: {\n\t\t\t\t\t\ttotal: 1,\n\t\t\t\t\t\tcompleted: 0,\n\t\t\t\t\t\tinProgress: \"Working target task\",\n\t\t\t\t\t\tisPersisted: true,\n\t\t\t\t\t\tscope: \"current_branch_session_state\",\n\t\t\t\t\t},\n\t\t\t\t\ttasks: {\n\t\t\t\t\t\ttotal: 1,\n\t\t\t\t\t\tpending: 1,\n\t\t\t\t\t\tinProgress: 0,\n\t\t\t\t\t\tcompleted: 0,\n\t\t\t\t\t\tinProgressTask: undefined,\n\t\t\t\t\t\tisPersisted: true,\n\t\t\t\t\t\tscope: \"current_branch_session_state\",\n\t\t\t\t\t},\n\t\t\t\t\tdelegatedWork: {\n\t\t\t\t\t\tactiveCount: 0,\n\t\t\t\t\t\tcompletedCount: 0,\n\t\t\t\t\t\tfailedCount: 0,\n\t\t\t\t\t\tactiveAgents: [],\n\t\t\t\t\t\tfailurePreview: [],\n\t\t\t\t\t\tisPersisted: false,\n\t\t\t\t\t\tscope: \"current_process_runtime_state\",\n\t\t\t\t\t\tnote: \"live current-process state only; not persisted and resets on session switch/resume\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t});\n\t\t} finally {\n\t\t\trmSync(rootDir, { recursive: true, force: true });\n\t\t}\n\t});\n});\n"]}