{"version":3,"file":"agent-session-ultraplan.test.d.ts","sourceRoot":"","sources":["../../src/core/agent-session-ultraplan.test.ts"],"names":[],"mappings":"","sourcesContent":["import { Agent } from \"@apholdings/jensen-agent-core\";\nimport { Type } from \"@sinclair/typebox\";\nimport { describe, expect, it } from \"vitest\";\nimport { AgentSession } from \"./agent-session.js\";\nimport { AuthStorage } from \"./auth-storage.js\";\nimport type { ToolDefinition } from \"./extensions/index.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 { SESSION_ULTRAPLAN_CUSTOM_TYPE } from \"./ultraplan.js\";\n\nfunction createSession(options?: { sessionManager?: SessionManager; customTools?: ToolDefinition[] }): 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: options?.sessionManager ?? SessionManager.inMemory(\"/tmp/project\"),\n\t\tsettingsManager,\n\t\tcwd: \"/tmp/project\",\n\t\tresourceLoader,\n\t\tmodelRegistry,\n\t\tcustomTools: options?.customTools,\n\t});\n}\n\nfunction createSubagentTool(\n\thandler: (params: { agent: string; task: string; agentScope?: string }) => string,\n): ToolDefinition {\n\treturn {\n\t\tname: \"subagent\",\n\t\tlabel: \"subagent\",\n\t\tdescription: \"Fake subagent tool for Ultraplan tests\",\n\t\tparameters: Type.Object({}),\n\t\texecute: async (_toolCallId, params) => {\n\t\t\tconst typedParams = params as unknown as { agent: string; task: string; agentScope?: string };\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: \"text\", text: handler(typedParams) }],\n\t\t\t\tdetails: undefined,\n\t\t\t};\n\t\t},\n\t};\n}\n\ndescribe(\"AgentSession.runUltraplan()\", () => {\n\tit(\"runs a local planner-backed pass, persists a structured artifact, and does not auto-execute\", async () => {\n\t\tlet capturedParams: { agent: string; task: string; agentScope?: string } | undefined;\n\t\tconst session = createSession({\n\t\t\tcustomTools: [\n\t\t\t\tcreateSubagentTool((params) => {\n\t\t\t\t\tcapturedParams = params;\n\t\t\t\t\treturn JSON.stringify({\n\t\t\t\t\t\tobjective: \"Ship local-first Ultraplan\",\n\t\t\t\t\t\tassumptions: [\"Subagent planner is available locally\"],\n\t\t\t\t\t\tconstraints: [\"Do not auto-execute the plan\"],\n\t\t\t\t\t\tphases: [\n\t\t\t\t\t\t\t{ title: \"Command entrypoint\", steps: [\"Add /ultraplan\", \"Wire it to AgentSession.runUltraplan\"] },\n\t\t\t\t\t\t],\n\t\t\t\t\t\trisks: [\"Planner output may be malformed JSON\"],\n\t\t\t\t\t\trecommendedExecutionOrder: [\"Plan\", \"Review\", \"Execute later\"],\n\t\t\t\t\t\tactionableNextSteps: [\"Review the stored plan\", \"Choose a first execution step\"],\n\t\t\t\t\t});\n\t\t\t\t}),\n\t\t\t],\n\t\t});\n\n\t\tconst result = await session.runUltraplan(\"Ship local-first Ultraplan\");\n\n\t\texpect(capturedParams).toMatchObject({\n\t\t\tagent: \"planner\",\n\t\t\tagentScope: \"user\",\n\t\t});\n\t\texpect(capturedParams?.task).toContain(\"Objective: Ship local-first Ultraplan\");\n\t\texpect(result.artifact.objective).toBe(\"Ship local-first Ultraplan\");\n\t\texpect(result.artifact.phases).toEqual([\n\t\t\t{ title: \"Command entrypoint\", steps: [\"Add /ultraplan\", \"Wire it to AgentSession.runUltraplan\"] },\n\t\t]);\n\t\texpect(result.displayText).toContain(\"Execution: planning only; no execution has started\");\n\t\texpect(result.displayText).toContain(\"Persisted as session-owned Ultraplan state on the current branch.\");\n\t\texpect(session.getLatestUltraplanPlan()?.objective).toBe(\"Ship local-first Ultraplan\");\n\t\texpect(session.getDelegatedWorkSummary().total).toBe(0);\n\t\texpect(session.state.messages).toHaveLength(0);\n\t\texpect(\n\t\t\tsession.sessionManager\n\t\t\t\t.getBranch()\n\t\t\t\t.some((entry) => entry.type === \"custom\" && entry.customType === SESSION_ULTRAPLAN_CUSTOM_TYPE),\n\t\t).toBe(true);\n\t});\n\n\tit(\"restores the persisted Ultraplan artifact from session-owned state\", async () => {\n\t\tconst sessionManager = SessionManager.inMemory(\"/tmp/project\");\n\t\tconst writer = createSession({\n\t\t\tsessionManager,\n\t\t\tcustomTools: [\n\t\t\t\tcreateSubagentTool(() =>\n\t\t\t\t\tJSON.stringify({\n\t\t\t\t\t\tobjective: \"Restore persisted plan\",\n\t\t\t\t\t\tassumptions: [],\n\t\t\t\t\t\tconstraints: [\"Persist on current branch\"],\n\t\t\t\t\t\tphases: [{ title: \"Persist\", steps: [\"Write structured artifact\"] }],\n\t\t\t\t\t\trisks: [],\n\t\t\t\t\t\trecommendedExecutionOrder: [\"Inspect\", \"Execute later\"],\n\t\t\t\t\t\tactionableNextSteps: [\"Show the stored plan\"],\n\t\t\t\t\t}),\n\t\t\t\t),\n\t\t\t],\n\t\t});\n\n\t\tawait writer.runUltraplan(\"Restore persisted plan\");\n\t\tconst restored = createSession({ sessionManager });\n\n\t\texpect(restored.getLatestUltraplanPlan()).toMatchObject({\n\t\t\tobjective: \"Restore persisted plan\",\n\t\t\texecutionState: \"plan_only\",\n\t\t\tplannerMode: \"local_subagent\",\n\t\t});\n\t});\n\n\tit(\"fails honestly when no local subagent tool is available\", async () => {\n\t\tconst session = createSession();\n\n\t\tawait expect(session.runUltraplan(\"Plan without a planner tool\")).rejects.toThrow(\n\t\t\t\"Ultraplan requires the local subagent tool to be available.\",\n\t\t);\n\t});\n});\n\ndescribe(\"AgentSession.runUltraplanRevise()\", () => {\n\tit(\"uses latest artifact as context and passes revision instruction to planner\", async () => {\n\t\tconst sessionManager = SessionManager.inMemory(\"/tmp/project\");\n\t\tlet capturedParams: { agent: string; task: string; agentScope?: string } | undefined;\n\n\t\t// Create initial plan\n\t\tconst planner = createSession({\n\t\t\tsessionManager,\n\t\t\tcustomTools: [\n\t\t\t\tcreateSubagentTool(() =>\n\t\t\t\t\tJSON.stringify({\n\t\t\t\t\t\tobjective: \"Initial plan\",\n\t\t\t\t\t\tassumptions: [\"Assumption one\"],\n\t\t\t\t\t\tconstraints: [],\n\t\t\t\t\t\tphases: [{ title: \"Phase 1\", steps: [\"Step A\", \"Step B\"] }],\n\t\t\t\t\t\trisks: [],\n\t\t\t\t\t\trecommendedExecutionOrder: [],\n\t\t\t\t\t\tactionableNextSteps: [\"Step A\", \"Step B\"],\n\t\t\t\t\t}),\n\t\t\t\t),\n\t\t\t],\n\t\t});\n\n\t\tawait planner.runUltraplan(\"Initial plan\");\n\n\t\t// Create session that will call revise\n\t\tconst reviser = createSession({\n\t\t\tsessionManager,\n\t\t\tcustomTools: [\n\t\t\t\tcreateSubagentTool((params) => {\n\t\t\t\t\tcapturedParams = params;\n\t\t\t\t\treturn JSON.stringify({\n\t\t\t\t\t\tobjective: \"Revised plan\",\n\t\t\t\t\t\tassumptions: [\"Assumption one\"],\n\t\t\t\t\t\tconstraints: [],\n\t\t\t\t\t\tphases: [{ title: \"Phase 1\", steps: [\"Step A\", \"Step B\", \"Step C\"] }],\n\t\t\t\t\t\trisks: [],\n\t\t\t\t\t\trecommendedExecutionOrder: [],\n\t\t\t\t\t\tactionableNextSteps: [\"Step A\", \"Step B\", \"Step C\"],\n\t\t\t\t\t});\n\t\t\t\t}),\n\t\t\t],\n\t\t});\n\n\t\tconst result = await reviser.runUltraplanRevise(\"Add a third step to Phase 1\");\n\n\t\t// Verify revision instruction was passed to planner\n\t\texpect(capturedParams).toMatchObject({\n\t\t\tagent: \"planner\",\n\t\t\tagentScope: \"user\",\n\t\t});\n\t\texpect(capturedParams?.task).toContain(\"Revision instruction: Add a third step to Phase 1\");\n\t\texpect(capturedParams?.task).toContain('\"objective\": \"Initial plan\"');\n\t\texpect(capturedParams?.task).toContain(\"Phase 1\");\n\n\t\t// Verify revised artifact\n\t\texpect(result.artifact.objective).toBe(\"Revised plan\");\n\t\texpect(result.artifact.phases[0].steps).toContain(\"Step C\");\n\t\texpect(result.displayText).toContain(\"Persisted as a new latest session-owned Ultraplan state\");\n\t\texpect(result.displayText).toContain(\"Prior plan artifacts remain preserved separately\");\n\t\texpect(result.displayText).toContain(\"Apply remains manual; no execution has started\");\n\t});\n\n\tit(\"persists revised plan as new latest, preserving append-only semantics\", async () => {\n\t\tconst sessionManager = SessionManager.inMemory(\"/tmp/project\");\n\n\t\tconst planner = createSession({\n\t\t\tsessionManager,\n\t\t\tcustomTools: [\n\t\t\t\tcreateSubagentTool(() =>\n\t\t\t\t\tJSON.stringify({\n\t\t\t\t\t\tobjective: \"Original plan\",\n\t\t\t\t\t\tassumptions: [],\n\t\t\t\t\t\tconstraints: [],\n\t\t\t\t\t\tphases: [{ title: \"Phase\", steps: [\"Original step\"] }],\n\t\t\t\t\t\trisks: [],\n\t\t\t\t\t\trecommendedExecutionOrder: [],\n\t\t\t\t\t\tactionableNextSteps: [\"Original step\"],\n\t\t\t\t\t}),\n\t\t\t\t),\n\t\t\t],\n\t\t});\n\n\t\tawait planner.runUltraplan(\"Original plan\");\n\n\t\tconst reviser = createSession({\n\t\t\tsessionManager,\n\t\t\tcustomTools: [\n\t\t\t\tcreateSubagentTool(() =>\n\t\t\t\t\tJSON.stringify({\n\t\t\t\t\t\tobjective: \"Revised plan\",\n\t\t\t\t\t\tassumptions: [],\n\t\t\t\t\t\tconstraints: [],\n\t\t\t\t\t\tphases: [{ title: \"Phase\", steps: [\"Revised step\"] }],\n\t\t\t\t\t\trisks: [],\n\t\t\t\t\t\trecommendedExecutionOrder: [],\n\t\t\t\t\t\tactionableNextSteps: [\"Revised step\"],\n\t\t\t\t\t}),\n\t\t\t\t),\n\t\t\t],\n\t\t});\n\n\t\tawait reviser.runUltraplanRevise(\"Update the step\");\n\n\t\t// Verify new latest is the revised plan\n\t\tconst latest = reviser.getLatestUltraplanPlan();\n\t\texpect(latest?.objective).toBe(\"Revised plan\");\n\n\t\t// Verify branch still contains both plans (append-only)\n\t\tconst branch = sessionManager.getBranch();\n\t\tconst ultraplanEntries = branch.filter(\n\t\t\t(entry) => entry.type === \"custom\" && entry.customType === SESSION_ULTRAPLAN_CUSTOM_TYPE,\n\t\t);\n\t\texpect(ultraplanEntries).toHaveLength(2);\n\t});\n\n\tit(\"does not auto-apply or auto-execute when revising\", async () => {\n\t\tconst sessionManager = SessionManager.inMemory(\"/tmp/project\");\n\n\t\tconst planner = createSession({\n\t\t\tsessionManager,\n\t\t\tcustomTools: [\n\t\t\t\tcreateSubagentTool(() =>\n\t\t\t\t\tJSON.stringify({\n\t\t\t\t\t\tobjective: \"Plan for apply test\",\n\t\t\t\t\t\tassumptions: [],\n\t\t\t\t\t\tconstraints: [],\n\t\t\t\t\t\tphases: [{ title: \"Phase\", steps: [\"Actionable step\"] }],\n\t\t\t\t\t\trisks: [],\n\t\t\t\t\t\trecommendedExecutionOrder: [],\n\t\t\t\t\t\tactionableNextSteps: [\"Actionable step\"],\n\t\t\t\t\t}),\n\t\t\t\t),\n\t\t\t],\n\t\t});\n\n\t\tawait planner.runUltraplan(\"Plan for apply test\");\n\n\t\tconst reviser = createSession({\n\t\t\tsessionManager,\n\t\t\tcustomTools: [\n\t\t\t\tcreateSubagentTool(() =>\n\t\t\t\t\tJSON.stringify({\n\t\t\t\t\t\tobjective: \"Revised plan\",\n\t\t\t\t\t\tassumptions: [],\n\t\t\t\t\t\tconstraints: [],\n\t\t\t\t\t\tphases: [{ title: \"Phase\", steps: [\"Revised step\"] }],\n\t\t\t\t\t\trisks: [],\n\t\t\t\t\t\trecommendedExecutionOrder: [],\n\t\t\t\t\t\tactionableNextSteps: [\"Revised step\"],\n\t\t\t\t\t}),\n\t\t\t\t),\n\t\t\t],\n\t\t});\n\n\t\tawait reviser.runUltraplanRevise(\"Revise it\");\n\n\t\t// Verify no auto-apply: todos should be empty after revision\n\t\texpect(reviser.getTodos()).toHaveLength(0);\n\t\texpect(reviser.getDelegatedWorkSummary().total).toBe(0);\n\t\texpect(reviser.state.messages).toHaveLength(0);\n\n\t\t// Verify apply is still separate/manual\n\t\tconst applyResult = reviser.applyUltraplan();\n\t\texpect(applyResult.applied).toHaveLength(1);\n\t\texpect(applyResult.displayText).toContain(\"Apply did not start execution\");\n\t});\n\n\tit(\"fails honestly when no existing plan to revise\", async () => {\n\t\tconst session = createSession({\n\t\t\tcustomTools: [createSubagentTool(() => JSON.stringify({}))],\n\t\t});\n\n\t\tawait expect(session.runUltraplanRevise(\"Try to revise without a plan\")).rejects.toThrow(\n\t\t\t\"No persisted Ultraplan plan found. Run /ultraplan <objective> first.\",\n\t\t);\n\t});\n\n\tit(\"fails honestly with empty revision instruction\", async () => {\n\t\tconst sessionManager = SessionManager.inMemory(\"/tmp/project\");\n\n\t\tconst planner = createSession({\n\t\t\tsessionManager,\n\t\t\tcustomTools: [\n\t\t\t\tcreateSubagentTool(() =>\n\t\t\t\t\tJSON.stringify({\n\t\t\t\t\t\tobjective: \"Existing plan\",\n\t\t\t\t\t\tassumptions: [],\n\t\t\t\t\t\tconstraints: [],\n\t\t\t\t\t\tphases: [{ title: \"Phase\", steps: [\"Step\"] }],\n\t\t\t\t\t\trisks: [],\n\t\t\t\t\t\trecommendedExecutionOrder: [],\n\t\t\t\t\t\tactionableNextSteps: [\"Step\"],\n\t\t\t\t\t}),\n\t\t\t\t),\n\t\t\t],\n\t\t});\n\n\t\tawait planner.runUltraplan(\"Existing plan\");\n\n\t\tconst reviser = createSession({\n\t\t\tsessionManager,\n\t\t\tcustomTools: [createSubagentTool(() => JSON.stringify({}))],\n\t\t});\n\n\t\tawait expect(reviser.runUltraplanRevise(\"   \")).rejects.toThrow(\n\t\t\t\"Ultraplan revision requires a non-empty instruction.\",\n\t\t);\n\t});\n});\n"]}