{"version":3,"file":"agent-session-delegation-contract.test.d.ts","sourceRoot":"","sources":["../../src/core/agent-session-delegation-contract.test.ts"],"names":[],"mappings":"","sourcesContent":["/**\n * Contract tests for operator state discipline around delegation.\n *\n * These tests prove the infrastructure is ready to track task/todo state around delegation.\n * They validate the co-occurrence rule: \"delegation active implies task/todo entries exist\".\n *\n * The tests are NOT about whether the agent follows guidance (non-deterministic).\n * They ARE about whether the infrastructure CAN detect gaps when they occur.\n */\nimport { mkdtempSync, rmSync } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { Agent } from \"@apholdings/jensen-agent-core\";\nimport type { Model } from \"@apholdings/jensen-ai\";\nimport { describe, expect, it } 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 makeSession(): { rootDir: string; session: AgentSession } {\n\tconst rootDir = mkdtempSync(join(tmpdir(), \"jensen-delegation-contract-\"));\n\tconst agentDir = join(rootDir, \"agent\");\n\tconst cwd = join(rootDir, \"repo\");\n\n\tconst authStorage = AuthStorage.create(join(agentDir, \"auth.json\"));\n\tconst modelRegistry = new ModelRegistry(authStorage, undefined);\n\tconst model: Model<any> = {\n\t\tid: \"test-model\",\n\t\tname: \"Test Model\",\n\t\tprovider: \"test-provider\",\n\t\tapi: \"openai-chat\",\n\t\tbaseUrl: \"https://example.invalid\",\n\t\treasoning: false,\n\t\tinput: [\"text\"],\n\t\tcost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n\t\tcontextWindow: 8000,\n\t\tmaxTokens: 4000,\n\t};\n\tmodelRegistry.registerProvider(\"test-provider\", {\n\t\tbaseUrl: \"https://example.invalid\",\n\t\tapiKey: \"test-key\",\n\t\tapi: \"openai-chat\",\n\t\tmodels: [model],\n\t});\n\n\tconst settingsManager = SettingsManager.create(cwd, agentDir);\n\tconst sessionManager = SessionManager.create(cwd);\n\tconst resourceLoader = new DefaultResourceLoader({ cwd, agentDir, settingsManager });\n\tconst agent = new Agent({\n\t\tinitialState: {\n\t\t\tsystemPrompt: \"\",\n\t\t\tmodel,\n\t\t\tthinkingLevel: \"off\",\n\t\t\ttools: [],\n\t\t},\n\t});\n\n\tconst session = new AgentSession({\n\t\tagent,\n\t\tsessionManager,\n\t\tsettingsManager,\n\t\tcwd,\n\t\tresourceLoader,\n\t\tmodelRegistry,\n\t});\n\n\treturn { rootDir, session };\n}\n\n/** Fire a tool execution start event and wait for queue processing */\nasync function fireDelegationStart(\n\tsession: AgentSession,\n\ttoolCallId: string,\n\targs: Record<string, unknown>,\n): Promise<void> {\n\t(session as unknown as { _handleAgentEvent: (event: unknown) => void })._handleAgentEvent({\n\t\ttype: \"tool_execution_start\",\n\t\ttoolCallId,\n\t\ttoolName: \"subagent\",\n\t\targs,\n\t});\n\tawait (session as unknown as { _agentEventQueue: Promise<void> })._agentEventQueue;\n}\n\n/** Seed task state directly via internal method */\nfunction seedTask(session: AgentSession, subject: string, activeForm: string): void {\n\tconst task = {\n\t\tid: `task_${Date.now()}`,\n\t\tsubject,\n\t\tactiveForm,\n\t\tstatus: \"pending\" as const,\n\t\tcreatedAt: new Date().toISOString(),\n\t\tupdatedAt: new Date().toISOString(),\n\t};\n\t(session as unknown as { _setTasks: (tasks: unknown[]) => void })._setTasks([task]);\n}\n\n/** Seed todo state directly via internal method */\nfunction seedTodo(session: AgentSession, content: string, activeForm: string): void {\n\tconst todo = {\n\t\tcontent,\n\t\tactiveForm,\n\t\tstatus: \"pending\" as const,\n\t};\n\t(session as unknown as { _setTodos: (todos: unknown[]) => void })._setTodos([todo]);\n}\n\ndescribe(\"AgentSession delegation contract: operator state discipline\", () => {\n\tdescribe(\"infrastructure can detect state gaps during delegation\", () => {\n\t\tit(\"delegation without prior task or todo: infrastructure can detect the gap\", async () => {\n\t\t\tconst { rootDir, session } = makeSession();\n\n\t\t\ttry {\n\t\t\t\t// Seed: NO tasks/todos - pure infrastructure state\n\t\t\t\texpect(session.getTasks().length).toBe(0);\n\t\t\t\texpect(session.getTodos().length).toBe(0);\n\n\t\t\t\t// Fire delegation\n\t\t\t\tawait fireDelegationStart(session, \"call_contract_1\", {\n\t\t\t\t\tagent: \"worker\",\n\t\t\t\t\ttask: \"Implement a feature\",\n\t\t\t\t});\n\n\t\t\t\t// Infrastructure proves:\n\t\t\t\t// 1. Delegation is tracked (contract satisfied for tracking)\n\t\t\t\tconst summary = session.getDelegatedWorkSummary();\n\t\t\t\texpect(summary.total).toBe(1);\n\t\t\t\texpect(summary.active).toHaveLength(1);\n\n\t\t\t\t// 2. Gap is visible (contract gap detectable)\n\t\t\t\texpect(session.getTasks().length).toBe(0);\n\t\t\t\texpect(session.getTodos().length).toBe(0);\n\n\t\t\t\t// The advisory can now fire because infrastructure detected the gap\n\t\t\t} finally {\n\t\t\t\trmSync(rootDir, { recursive: true, force: true });\n\t\t\t}\n\t\t});\n\n\t\tit(\"delegation with prior task: infrastructure tracks both correctly\", async () => {\n\t\t\tconst { rootDir, session } = makeSession();\n\n\t\t\ttry {\n\t\t\t\t// Seed with a task (simulating existing task context)\n\t\t\t\tseedTask(session, \"Implement the panel\", \"Implementing the panel\");\n\n\t\t\t\t// Verify seed\n\t\t\t\texpect(session.getTasks().length).toBe(1);\n\t\t\t\texpect(session.getTasks()[0]?.subject).toBe(\"Implement the panel\");\n\n\t\t\t\t// Fire delegation\n\t\t\t\tawait fireDelegationStart(session, \"call_contract_2\", {\n\t\t\t\t\tagent: \"worker\",\n\t\t\t\t\ttask: \"Implement a feature\",\n\t\t\t\t});\n\n\t\t\t\t// Infrastructure proves both are tracked:\n\t\t\t\texpect(session.getTasks().length).toBe(1);\n\t\t\t\tconst summary2 = session.getDelegatedWorkSummary();\n\t\t\t\texpect(summary2.total).toBe(1);\n\t\t\t\texpect(summary2.active).toHaveLength(1);\n\t\t\t} finally {\n\t\t\t\trmSync(rootDir, { recursive: true, force: true });\n\t\t\t}\n\t\t});\n\n\t\tit(\"delegation with prior todo: infrastructure tracks both correctly\", async () => {\n\t\t\tconst { rootDir, session } = makeSession();\n\n\t\t\ttry {\n\t\t\t\t// Seed with a todo (simulating existing todo context)\n\t\t\t\tseedTodo(session, \"Review the PR\", \"Reviewing the PR\");\n\n\t\t\t\t// Verify seed\n\t\t\t\texpect(session.getTodos().length).toBe(1);\n\t\t\t\texpect(session.getTodos()[0]?.content).toBe(\"Review the PR\");\n\n\t\t\t\t// Fire delegation\n\t\t\t\tawait fireDelegationStart(session, \"call_contract_3\", {\n\t\t\t\t\tagent: \"worker\",\n\t\t\t\t\ttask: \"Implement a feature\",\n\t\t\t\t});\n\n\t\t\t\t// Infrastructure proves both are tracked:\n\t\t\t\texpect(session.getTodos().length).toBe(1);\n\t\t\t\texpect(session.getDelegatedWorkSummary().total).toBe(1);\n\t\t\t} finally {\n\t\t\t\trmSync(rootDir, { recursive: true, force: true });\n\t\t\t}\n\t\t});\n\t});\n\n\tdescribe(\"runtime advisory fires correctly\", () => {\n\t\tit(\"advisory fires when delegating without task/todo state\", async () => {\n\t\t\tconst { rootDir, session } = makeSession();\n\n\t\t\ttry {\n\t\t\t\t// Ensure no task/todo state\n\t\t\t\texpect(session.getTasks().length).toBe(0);\n\t\t\t\texpect(session.getTodos().length).toBe(0);\n\n\t\t\t\t// Collect events\n\t\t\t\tconst events: unknown[] = [];\n\t\t\t\tsession.subscribe((event) => {\n\t\t\t\t\tevents.push(event);\n\t\t\t\t});\n\n\t\t\t\t// Fire delegation - the advisory fires via tool_execution_start event handling\n\t\t\t\tawait fireDelegationStart(session, \"call_advisory_1\", {\n\t\t\t\t\tagent: \"worker\",\n\t\t\t\t\ttask: \"Quick task\",\n\t\t\t\t});\n\n\t\t\t\t// The advisory should have been emitted\n\t\t\t\tconst advisoryEvent = events.find((e: unknown) => (e as { type?: string }).type === \"notification\");\n\t\t\t\texpect(advisoryEvent).toBeDefined();\n\t\t\t\texpect((advisoryEvent as { kind?: string }).kind).toBe(\"operator_discipline_advisory\");\n\t\t\t} finally {\n\t\t\t\trmSync(rootDir, { recursive: true, force: true });\n\t\t\t}\n\t\t});\n\n\t\tit(\"advisory does NOT fire when task state exists\", async () => {\n\t\t\tconst { rootDir, session } = makeSession();\n\n\t\t\ttry {\n\t\t\t\t// Seed with a task\n\t\t\t\tseedTask(session, \"Parent task\", \"Working on parent task\");\n\n\t\t\t\texpect(session.getTasks().length).toBe(1);\n\n\t\t\t\t// Collect events\n\t\t\t\tconst events: unknown[] = [];\n\t\t\t\tsession.subscribe((event) => {\n\t\t\t\t\tevents.push(event);\n\t\t\t\t});\n\n\t\t\t\t// Fire delegation with task context\n\t\t\t\tawait fireDelegationStart(session, \"call_advisory_2\", {\n\t\t\t\t\tagent: \"worker\",\n\t\t\t\t\ttask: \"Child task\",\n\t\t\t\t});\n\n\t\t\t\t// No advisory should fire because task state exists\n\t\t\t\tconst advisoryEvent = events.find(\n\t\t\t\t\t(e: unknown) =>\n\t\t\t\t\t\t(e as { type?: string }).type === \"notification\" &&\n\t\t\t\t\t\t(e as { kind?: string }).kind === \"operator_discipline_advisory\",\n\t\t\t\t);\n\t\t\t\texpect(advisoryEvent).toBeUndefined();\n\t\t\t} finally {\n\t\t\t\trmSync(rootDir, { recursive: true, force: true });\n\t\t\t}\n\t\t});\n\n\t\tit(\"advisory does NOT fire when todo state exists\", async () => {\n\t\t\tconst { rootDir, session } = makeSession();\n\n\t\t\ttry {\n\t\t\t\t// Seed with a todo\n\t\t\t\tseedTodo(session, \"Parent todo\", \"Working on parent todo\");\n\n\t\t\t\texpect(session.getTodos().length).toBe(1);\n\n\t\t\t\t// Collect events\n\t\t\t\tconst events: unknown[] = [];\n\t\t\t\tsession.subscribe((event) => {\n\t\t\t\t\tevents.push(event);\n\t\t\t\t});\n\n\t\t\t\t// Fire delegation with todo context\n\t\t\t\tawait fireDelegationStart(session, \"call_advisory_3\", {\n\t\t\t\t\tagent: \"worker\",\n\t\t\t\t\ttask: \"Child task\",\n\t\t\t\t});\n\n\t\t\t\t// No advisory should fire because todo state exists\n\t\t\t\tconst advisoryEvent = events.find(\n\t\t\t\t\t(e: unknown) =>\n\t\t\t\t\t\t(e as { type?: string }).type === \"notification\" &&\n\t\t\t\t\t\t(e as { kind?: string }).kind === \"operator_discipline_advisory\",\n\t\t\t\t);\n\t\t\t\texpect(advisoryEvent).toBeUndefined();\n\t\t\t} finally {\n\t\t\t\trmSync(rootDir, { recursive: true, force: true });\n\t\t\t}\n\t\t});\n\t});\n});\n"]}