{"version":3,"file":"todo-runtime-registry.test.d.ts","sourceRoot":"","sources":["../../src/core/todo-runtime-registry.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG","sourcesContent":["/**\n * Tests for the runtime registration surface of todo_write, todo_read, and todo_update.\n *\n * V01 – Superficie completa\n * V02 – Schema del modelo\n * V03 – Dispatch\n * V04 – Prompt/tool consistency\n * V05 – Defaults\n * V06 – Persisted settings compatibility (backward compat)\n * V07 – Disabled tools\n * V08 – Exact black-box surface regression\n *\n * Plus:\n * - Integrated dispatcher simulation (section 10)\n * - Truthful tool sequence (section 11)\n * - Session isolation\n */\n\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport type { Model } from \"@apholdings/jensen-ai\";\nimport { afterEach, beforeEach, describe, expect, it } from \"vitest\";\nimport { AuthStorage } from \"./auth-storage.js\";\nimport { ModelRegistry } from \"./model-registry.js\";\nimport { DefaultResourceLoader } from \"./resource-loader.js\";\nimport { createAgentSession } from \"./sdk.js\";\nimport { SessionManager } from \"./session-manager.js\";\nimport { SettingsManager } from \"./settings-manager.js\";\nimport { allTools } from \"./tools/index.js\";\nimport { TodoLoopGuard } from \"./tools/todo-loop-guard.js\";\nimport { createTodoReadTool } from \"./tools/todo-read.js\";\nimport { createTodoUpdateTool } from \"./tools/todo-update.js\";\nimport { createTodoWriteTool, type TodoItem } from \"./tools/todo-write.js\";\n\n/** Extract joined text content from a tool result's content array */\nfunction resultText(result: { content: Array<{ type: string; text?: string }> }): string {\n\treturn result.content.map((c) => (c.type === \"text\" ? (c.text ?? \"\") : \"\")).join(\"\");\n}\n\nfunction createTestModel(): Model<\"openai-chat\"> {\n\treturn {\n\t\tid: \"todo-registry-test-model\",\n\t\tname: \"Todo Registry Test Model\",\n\t\tprovider: \"todo-registry-test-provider\",\n\t\tapi: \"openai-chat\",\n\t\tbaseUrl: \"https://example.test\",\n\t\treasoning: true,\n\t\tinput: [\"text\"],\n\t\tcost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n\t\tcontextWindow: 200_000,\n\t\tmaxTokens: 8_000,\n\t};\n}\n\nfunction setupTestEnv() {\n\tconst rootDir = require(\"fs\").mkdtempSync(join(tmpdir(), \"jensen-todo-registry-\"));\n\tconst cwd = join(rootDir, \"repo\");\n\tconst agentDir = join(rootDir, \"agent\");\n\tconst sessionDir = join(rootDir, \"sessions\");\n\n\trequire(\"fs\").mkdirSync(cwd, { recursive: true });\n\trequire(\"fs\").mkdirSync(agentDir, { recursive: true });\n\trequire(\"fs\").mkdirSync(sessionDir, { recursive: true });\n\n\tconst authStorage = AuthStorage.create(join(agentDir, \"auth.json\"));\n\tconst modelRegistry = new ModelRegistry(authStorage);\n\tconst testModel = createTestModel();\n\tmodelRegistry.registerProvider(testModel.provider, {\n\t\tapi: testModel.api,\n\t\tapiKey: \"test-api-key\",\n\t\tbaseUrl: testModel.baseUrl,\n\t\tmodels: [\n\t\t\t{\n\t\t\t\tid: testModel.id,\n\t\t\t\tname: testModel.name,\n\t\t\t\tapi: testModel.api,\n\t\t\t\treasoning: testModel.reasoning,\n\t\t\t\tinput: testModel.input,\n\t\t\t\tcost: testModel.cost,\n\t\t\t\tcontextWindow: testModel.contextWindow,\n\t\t\t\tmaxTokens: testModel.maxTokens,\n\t\t\t},\n\t\t],\n\t});\n\n\treturn { rootDir, cwd, agentDir, sessionDir, authStorage, modelRegistry, testModel };\n}\n\ndescribe(\"todo runtime tool registration\", () => {\n\tlet rootDir: string;\n\tlet cwd: string;\n\tlet agentDir: string;\n\n\tbeforeEach(() => {\n\t\tconst env = setupTestEnv();\n\t\trootDir = env.rootDir;\n\t\tcwd = env.cwd;\n\t\tagentDir = env.agentDir;\n\t});\n\n\tafterEach(() => {\n\t\trequire(\"fs\").rmSync(rootDir, { recursive: true, force: true });\n\t});\n\n\t// =========================================================================\n\t// V01 — Superficie completa\n\t// =========================================================================\n\tdescribe(\"V01 — Surface completo\", () => {\n\t\tit(\"exposes todo_write, todo_read, and todo_update in the default session\", async () => {\n\t\t\tconst env = setupTestEnv();\n\t\t\trootDir = env.rootDir;\n\t\t\tcwd = env.cwd;\n\t\t\tagentDir = env.agentDir;\n\n\t\t\ttry {\n\t\t\t\tconst settingsManager = SettingsManager.inMemory({});\n\t\t\t\tconst sessionManager = SessionManager.inMemory();\n\t\t\t\tconst resourceLoader = new DefaultResourceLoader({\n\t\t\t\t\tcwd,\n\t\t\t\t\tagentDir,\n\t\t\t\t\tsettingsManager,\n\t\t\t\t});\n\t\t\t\tawait resourceLoader.reload();\n\n\t\t\t\tconst { session } = await createAgentSession({\n\t\t\t\t\tcwd,\n\t\t\t\t\tagentDir,\n\t\t\t\t\tsettingsManager,\n\t\t\t\t\tauthStorage: env.authStorage,\n\t\t\t\t\tmodelRegistry: env.modelRegistry,\n\t\t\t\t\tresourceLoader,\n\t\t\t\t\tsessionManager,\n\t\t\t\t});\n\n\t\t\t\tconst activeTools = session.getActiveToolNames();\n\t\t\t\texpect(activeTools).toContain(\"todo_write\");\n\t\t\t\texpect(activeTools).toContain(\"todo_read\");\n\t\t\t\texpect(activeTools).toContain(\"todo_update\");\n\t\t\t} finally {\n\t\t\t\trequire(\"fs\").rmSync(rootDir, { recursive: true, force: true });\n\t\t\t}\n\t\t});\n\n\t\tit(\"all three todo tools are present in allTools map\", () => {\n\t\t\texpect(allTools).toHaveProperty(\"todo_write\");\n\t\t\texpect(allTools).toHaveProperty(\"todo_read\");\n\t\t\texpect(allTools).toHaveProperty(\"todo_update\");\n\t\t});\n\n\t\tit(\"all three todo tools have correct names\", () => {\n\t\t\texpect(allTools.todo_write.name).toBe(\"todo_write\");\n\t\t\texpect(allTools.todo_read.name).toBe(\"todo_read\");\n\t\t\texpect(allTools.todo_update.name).toBe(\"todo_update\");\n\t\t});\n\t});\n\n\t// =========================================================================\n\t// V02 — Schema del modelo\n\t// =========================================================================\n\tdescribe(\"V02 — Schema del modelo\", () => {\n\t\tit(\"all three todo tools emit correct parameter schemas\", async () => {\n\t\t\tconst env = setupTestEnv();\n\t\t\trootDir = env.rootDir;\n\t\t\tcwd = env.cwd;\n\t\t\tagentDir = env.agentDir;\n\n\t\t\ttry {\n\t\t\t\tconst settingsManager = SettingsManager.inMemory({});\n\t\t\t\tconst sessionManager = SessionManager.inMemory();\n\t\t\t\tconst resourceLoader = new DefaultResourceLoader({\n\t\t\t\t\tcwd,\n\t\t\t\t\tagentDir,\n\t\t\t\t\tsettingsManager,\n\t\t\t\t});\n\t\t\t\tawait resourceLoader.reload();\n\n\t\t\t\tconst { session } = await createAgentSession({\n\t\t\t\t\tcwd,\n\t\t\t\t\tagentDir,\n\t\t\t\t\tsettingsManager,\n\t\t\t\t\tauthStorage: env.authStorage,\n\t\t\t\t\tmodelRegistry: env.modelRegistry,\n\t\t\t\t\tresourceLoader,\n\t\t\t\t\tsessionManager,\n\t\t\t\t});\n\n\t\t\t\tconst allToolInfos = session.getAllTools();\n\t\t\t\tconst todoWrite = allToolInfos.find((t) => t.name === \"todo_write\");\n\t\t\t\tconst todoRead = allToolInfos.find((t) => t.name === \"todo_read\");\n\t\t\t\tconst todoUpdate = allToolInfos.find((t) => t.name === \"todo_update\");\n\n\t\t\t\texpect(todoWrite).toBeDefined();\n\t\t\t\texpect(todoRead).toBeDefined();\n\t\t\t\texpect(todoUpdate).toBeDefined();\n\n\t\t\t\t// todo_write has a parameters schema\n\t\t\t\texpect(todoWrite!.parameters).toBeDefined();\n\n\t\t\t\t// todo_read has a parameters schema (empty object schema)\n\t\t\t\texpect(todoRead!.parameters).toBeDefined();\n\n\t\t\t\t// todo_update has updates array + expectedRevision\n\t\t\t\texpect(todoUpdate!.parameters).toBeDefined();\n\t\t\t} finally {\n\t\t\t\trequire(\"fs\").rmSync(rootDir, { recursive: true, force: true });\n\t\t\t}\n\t\t});\n\t});\n\n\t// =========================================================================\n\t// V03 — Dispatch\n\t// =========================================================================\n\tdescribe(\"V03 — Dispatch\", () => {\n\t\tit(\"todo_write dispatches to a write handler that sets todos\", async () => {\n\t\t\tlet todos: TodoItem[] = [];\n\t\t\tlet revision = 0;\n\n\t\t\tconst writeTool = createTodoWriteTool(\n\t\t\t\t() => todos,\n\t\t\t\t(newTodos) => {\n\t\t\t\t\ttodos = newTodos;\n\t\t\t\t\trevision++;\n\t\t\t\t},\n\t\t\t\tnew TodoLoopGuard(),\n\t\t\t\t() => revision,\n\t\t\t);\n\n\t\t\texpect(writeTool.name).toBe(\"todo_write\");\n\n\t\t\tconst result = await writeTool.execute(\"call_1\", {\n\t\t\t\ttodos: [\n\t\t\t\t\t{ content: \"Task 1\", activeForm: \"Doing Task 1\", status: \"pending\" },\n\t\t\t\t\t{ content: \"Task 2\", activeForm: \"Doing Task 2\", status: \"pending\" },\n\t\t\t\t],\n\t\t\t});\n\n\t\t\texpect(todos.length).toBe(2);\n\t\t\texpect(revision).toBeGreaterThan(0);\n\t\t\texpect(resultText(result)).toContain(\"updated\");\n\t\t});\n\n\t\tit(\"todo_read dispatches to a read handler that returns todos\", async () => {\n\t\t\tconst todos: TodoItem[] = [\n\t\t\t\t{ id: \"t1\", content: \"Task 1\", activeForm: \"Doing 1\", status: \"in_progress\" },\n\t\t\t\t{ id: \"t2\", content: \"Task 2\", activeForm: \"Doing 2\", status: \"pending\" },\n\t\t\t];\n\n\t\t\tconst readTool = createTodoReadTool(\n\t\t\t\t() => todos,\n\t\t\t\t() => 5,\n\t\t\t);\n\n\t\t\texpect(readTool.name).toBe(\"todo_read\");\n\n\t\t\tconst result = await readTool.execute(\"call_1\", {});\n\t\t\tconst text = resultText(result);\n\n\t\t\texpect(text).toContain(\"Doing 1\");\n\t\t\texpect(text).toContain(\"Task 2\");\n\t\t\texpect(result.details).toBeDefined();\n\t\t\texpect(result.details!.revision).toBe(5);\n\t\t});\n\n\t\tit(\"todo_read reports empty state correctly\", async () => {\n\t\t\tconst readTool = createTodoReadTool(\n\t\t\t\t() => [],\n\t\t\t\t() => 0,\n\t\t\t);\n\n\t\t\tconst result = await readTool.execute(\"call_1\", {});\n\n\t\t\texpect(resultText(result)).toContain(\"empty\");\n\t\t\texpect(result.details).toBeDefined();\n\t\t\texpect(result.details!.todos).toEqual([]);\n\t\t});\n\n\t\tit(\"todo_update dispatches to an update handler that applies partial changes\", async () => {\n\t\t\tconst todos: TodoItem[] = [\n\t\t\t\t{ id: \"t1\", content: \"Task 1\", activeForm: \"Doing 1\", status: \"pending\" },\n\t\t\t\t{ id: \"t2\", content: \"Task 2\", activeForm: \"Doing 2\", status: \"pending\" },\n\t\t\t];\n\t\t\tlet revision = 3;\n\n\t\t\tconst updateTool = createTodoUpdateTool(\n\t\t\t\t() => todos,\n\t\t\t\t(newTodos) => {\n\t\t\t\t\ttodos.length = 0;\n\t\t\t\t\ttodos.push(...newTodos);\n\t\t\t\t\trevision++;\n\t\t\t\t},\n\t\t\t\t() => revision,\n\t\t\t\tnew TodoLoopGuard(),\n\t\t\t\t{\n\t\t\t\t\tgetSnapshot: () => ({ revision: 3, timestamp: Date.now() }),\n\t\t\t\t\tinvalidateSnapshot: () => {},\n\t\t\t\t},\n\t\t\t);\n\n\t\t\texpect(updateTool.name).toBe(\"todo_update\");\n\n\t\t\tconst result = await updateTool.execute(\"call_1\", {\n\t\t\t\tupdates: [\n\t\t\t\t\t{ id: \"t1\", status: \"in_progress\" },\n\t\t\t\t\t{ id: \"t2\", status: \"completed\" },\n\t\t\t\t],\n\t\t\t\texpectedRevision: 3,\n\t\t\t});\n\n\t\t\texpect(resultText(result)).toContain(\"updated\");\n\t\t\texpect(todos[0].status).toBe(\"in_progress\");\n\t\t\texpect(todos[1].status).toBe(\"completed\");\n\t\t\texpect(revision).toBe(4);\n\t\t});\n\n\t\tit(\"todo_update rejects stale revision\", async () => {\n\t\t\tconst todos: TodoItem[] = [{ id: \"t1\", content: \"Task 1\", activeForm: \"Doing 1\", status: \"pending\" }];\n\t\t\tconst revision = 3;\n\t\t\tlet mutated = false;\n\n\t\t\tconst updateTool = createTodoUpdateTool(\n\t\t\t\t() => todos,\n\t\t\t\t() => {\n\t\t\t\t\tmutated = true;\n\t\t\t\t},\n\t\t\t\t() => revision,\n\t\t\t\tnew TodoLoopGuard(),\n\t\t\t\t{\n\t\t\t\t\tgetSnapshot: () => ({ revision: 3, timestamp: Date.now() }),\n\t\t\t\t\tinvalidateSnapshot: () => {},\n\t\t\t\t},\n\t\t\t);\n\n\t\t\tconst staleResult = await updateTool.execute(\"call_1\", {\n\t\t\t\tupdates: [{ id: \"t1\", status: \"completed\" }],\n\t\t\t\texpectedRevision: 2, // stale\n\t\t\t});\n\t\t\tconst staleText = staleResult.content[0];\n\t\t\tif (staleText.type !== \"text\") throw new Error(\"expected text content\");\n\t\t\t// Stale revisions are now recovered internally: the non-conflicting\n\t\t\t// update applies automatically with no TODO_READ_REQUIRED and no\n\t\t\t// run termination.\n\t\t\texpect(staleText.text).not.toContain(\"TODO_READ_REQUIRED\");\n\t\t\texpect(staleText.text).toContain(\"rebase\");\n\t\t\texpect(mutated).toBe(true);\n\t\t});\n\t});\n\n\t// =========================================================================\n\t// V04 — Prompt/tool consistency\n\t// =========================================================================\n\tdescribe(\"V04 — Prompt/tool consistency\", () => {\n\t\tit(\"system prompt does not reference tools absent from the active surface\", async () => {\n\t\t\tconst env = setupTestEnv();\n\t\t\trootDir = env.rootDir;\n\t\t\tcwd = env.cwd;\n\t\t\tagentDir = env.agentDir;\n\n\t\t\ttry {\n\t\t\t\tconst settingsManager = SettingsManager.inMemory({\n\t\t\t\t\ttools: { defaultActiveToolNames: [\"read\", \"bash\"] },\n\t\t\t\t});\n\t\t\t\tconst sessionManager = SessionManager.inMemory();\n\t\t\t\tconst resourceLoader = new DefaultResourceLoader({\n\t\t\t\t\tcwd,\n\t\t\t\t\tagentDir,\n\t\t\t\t\tsettingsManager,\n\t\t\t\t});\n\t\t\t\tawait resourceLoader.reload();\n\n\t\t\t\tconst { session } = await createAgentSession({\n\t\t\t\t\tcwd,\n\t\t\t\t\tagentDir,\n\t\t\t\t\tsettingsManager,\n\t\t\t\t\tauthStorage: env.authStorage,\n\t\t\t\t\tmodelRegistry: env.modelRegistry,\n\t\t\t\t\tresourceLoader,\n\t\t\t\t\tsessionManager,\n\t\t\t\t});\n\n\t\t\t\tconst activeTools = session.getActiveToolNames();\n\t\t\t\tconst systemPrompt = session.agent.state.systemPrompt.toLowerCase();\n\n\t\t\t\t// No active todo tool should not generate instructions to use absent tools\n\t\t\t\tif (!activeTools.includes(\"todo_write\")) {\n\t\t\t\t\texpect(systemPrompt).not.toContain(\"Use todo_write\");\n\t\t\t\t}\n\t\t\t\tif (!activeTools.includes(\"todo_read\")) {\n\t\t\t\t\texpect(systemPrompt).not.toContain(\"Use todo_read\");\n\t\t\t\t}\n\t\t\t\tif (!activeTools.includes(\"todo_update\")) {\n\t\t\t\t\texpect(systemPrompt).not.toContain(\"Use todo_update\");\n\t\t\t\t}\n\t\t\t} finally {\n\t\t\t\trequire(\"fs\").rmSync(rootDir, { recursive: true, force: true });\n\t\t\t}\n\t\t});\n\n\t\tit(\"active todo tools generate consistent tool descriptions reflecting all three\", async () => {\n\t\t\tconst env = setupTestEnv();\n\t\t\trootDir = env.rootDir;\n\t\t\tcwd = env.cwd;\n\t\t\tagentDir = env.agentDir;\n\n\t\t\ttry {\n\t\t\t\tconst settingsManager = SettingsManager.inMemory({});\n\t\t\t\tconst sessionManager = SessionManager.inMemory();\n\t\t\t\tconst resourceLoader = new DefaultResourceLoader({\n\t\t\t\t\tcwd,\n\t\t\t\t\tagentDir,\n\t\t\t\t\tsettingsManager,\n\t\t\t\t});\n\t\t\t\tawait resourceLoader.reload();\n\n\t\t\t\tconst { session } = await createAgentSession({\n\t\t\t\t\tcwd,\n\t\t\t\t\tagentDir,\n\t\t\t\t\tsettingsManager,\n\t\t\t\t\tauthStorage: env.authStorage,\n\t\t\t\t\tmodelRegistry: env.modelRegistry,\n\t\t\t\t\tresourceLoader,\n\t\t\t\t\tsessionManager,\n\t\t\t\t});\n\n\t\t\t\tconst allToolInfos = session.getAllTools();\n\t\t\t\tconst todoWrite = allToolInfos.find((t) => t.name === \"todo_write\");\n\t\t\t\tconst todoRead = allToolInfos.find((t) => t.name === \"todo_read\");\n\t\t\t\tconst todoUpdate = allToolInfos.find((t) => t.name === \"todo_update\");\n\n\t\t\t\texpect(todoWrite).toBeDefined();\n\t\t\t\texpect(todoRead).toBeDefined();\n\t\t\t\texpect(todoUpdate).toBeDefined();\n\n\t\t\t\t// The descriptions should cross-reference the other tools\n\t\t\t\texpect(todoWrite!.description).toBeDefined();\n\t\t\t\texpect(todoRead!.description).toBeDefined();\n\t\t\t\texpect(todoUpdate!.description).toBeDefined();\n\t\t\t} finally {\n\t\t\t\trequire(\"fs\").rmSync(rootDir, { recursive: true, force: true });\n\t\t\t}\n\t\t});\n\t});\n\n\t// =========================================================================\n\t// V05 — Defaults\n\t// =========================================================================\n\tdescribe(\"V05 — Defaults\", () => {\n\t\tit(\"a new session with default config exposes all three todo tools\", async () => {\n\t\t\tconst env = setupTestEnv();\n\t\t\trootDir = env.rootDir;\n\t\t\tcwd = env.cwd;\n\t\t\tagentDir = env.agentDir;\n\n\t\t\ttry {\n\t\t\t\tconst settingsManager = SettingsManager.inMemory({});\n\t\t\t\tconst sessionManager = SessionManager.inMemory();\n\t\t\t\tconst resourceLoader = new DefaultResourceLoader({\n\t\t\t\t\tcwd,\n\t\t\t\t\tagentDir,\n\t\t\t\t\tsettingsManager,\n\t\t\t\t});\n\t\t\t\tawait resourceLoader.reload();\n\n\t\t\t\tconst { session } = await createAgentSession({\n\t\t\t\t\tcwd,\n\t\t\t\t\tagentDir,\n\t\t\t\t\tsettingsManager,\n\t\t\t\t\tauthStorage: env.authStorage,\n\t\t\t\t\tmodelRegistry: env.modelRegistry,\n\t\t\t\t\tresourceLoader,\n\t\t\t\t\tsessionManager,\n\t\t\t\t});\n\n\t\t\t\tconst activeTools = session.getActiveToolNames();\n\t\t\t\texpect(activeTools).toContain(\"todo_write\");\n\t\t\t\texpect(activeTools).toContain(\"todo_read\");\n\t\t\t\texpect(activeTools).toContain(\"todo_update\");\n\t\t\t\texpect(activeTools).toContain(\"memory_write\");\n\t\t\t} finally {\n\t\t\t\trequire(\"fs\").rmSync(rootDir, { recursive: true, force: true });\n\t\t\t}\n\t\t});\n\t});\n\n\t// =========================================================================\n\t// V06 — Persisted settings compatibility\n\t// =========================================================================\n\tdescribe(\"V06 — Persisted settings compatibility\", () => {\n\t\tit(\"old config with only todo_write does not silently remove todo_read or todo_update\", async () => {\n\t\t\tconst env = setupTestEnv();\n\t\t\trootDir = env.rootDir;\n\t\t\tcwd = env.cwd;\n\t\t\tagentDir = env.agentDir;\n\n\t\t\ttry {\n\t\t\t\t// Simulate an old config that only knew about todo_write\n\t\t\t\tconst settingsManager = SettingsManager.inMemory({\n\t\t\t\t\ttools: { defaultActiveToolNames: [\"todo_write\"] },\n\t\t\t\t});\n\t\t\t\tconst sessionManager = SessionManager.inMemory();\n\t\t\t\tconst resourceLoader = new DefaultResourceLoader({\n\t\t\t\t\tcwd,\n\t\t\t\t\tagentDir,\n\t\t\t\t\tsettingsManager,\n\t\t\t\t});\n\t\t\t\tawait resourceLoader.reload();\n\n\t\t\t\tconst { session } = await createAgentSession({\n\t\t\t\t\tcwd,\n\t\t\t\t\tagentDir,\n\t\t\t\t\tsettingsManager,\n\t\t\t\t\tauthStorage: env.authStorage,\n\t\t\t\t\tmodelRegistry: env.modelRegistry,\n\t\t\t\t\tresourceLoader,\n\t\t\t\t\tsessionManager,\n\t\t\t\t});\n\n\t\t\t\tconst activeTools = session.getActiveToolNames();\n\t\t\t\t// User explicitly configured only todo_write — respect their config\n\t\t\t\texpect(activeTools).toContain(\"todo_write\");\n\t\t\t\t// But the old config shouldn't block access to todo_read/todo_update\n\t\t\t\t// if they add them later\n\t\t\t\texpect(session.getAllTools().some((t) => t.name === \"todo_read\")).toBe(true);\n\t\t\t\texpect(session.getAllTools().some((t) => t.name === \"todo_update\")).toBe(true);\n\t\t\t} finally {\n\t\t\t\trequire(\"fs\").rmSync(rootDir, { recursive: true, force: true });\n\t\t\t}\n\t\t});\n\t});\n\n\t// =========================================================================\n\t// V07 — Disabled tools\n\t// =========================================================================\n\tdescribe(\"V07 — Disabled tools\", () => {\n\t\tit(\"explicitly disabling a tool removes it from the active surface\", async () => {\n\t\t\tconst env = setupTestEnv();\n\t\t\trootDir = env.rootDir;\n\t\t\tcwd = env.cwd;\n\t\t\tagentDir = env.agentDir;\n\n\t\t\ttry {\n\t\t\t\t// User explicitly only wants read and bash\n\t\t\t\tconst settingsManager = SettingsManager.inMemory({\n\t\t\t\t\ttools: { defaultActiveToolNames: [\"read\", \"bash\"] },\n\t\t\t\t});\n\t\t\t\tconst sessionManager = SessionManager.inMemory();\n\t\t\t\tconst resourceLoader = new DefaultResourceLoader({\n\t\t\t\t\tcwd,\n\t\t\t\t\tagentDir,\n\t\t\t\t\tsettingsManager,\n\t\t\t\t});\n\t\t\t\tawait resourceLoader.reload();\n\n\t\t\t\tconst { session } = await createAgentSession({\n\t\t\t\t\tcwd,\n\t\t\t\t\tagentDir,\n\t\t\t\t\tsettingsManager,\n\t\t\t\t\tauthStorage: env.authStorage,\n\t\t\t\t\tmodelRegistry: env.modelRegistry,\n\t\t\t\t\tresourceLoader,\n\t\t\t\t\tsessionManager,\n\t\t\t\t});\n\n\t\t\t\tconst activeTools = session.getActiveToolNames();\n\t\t\t\texpect(activeTools).toContain(\"read\");\n\t\t\t\texpect(activeTools).toContain(\"bash\");\n\t\t\t\texpect(activeTools).not.toContain(\"todo_write\");\n\t\t\t\texpect(activeTools).not.toContain(\"todo_read\");\n\t\t\t\texpect(activeTools).not.toContain(\"todo_update\");\n\t\t\t} finally {\n\t\t\t\trequire(\"fs\").rmSync(rootDir, { recursive: true, force: true });\n\t\t\t}\n\t\t});\n\t});\n\n\t// =========================================================================\n\t// V08 — Exact black-box surface regression\n\t// =========================================================================\n\tdescribe(\"V08 — Black-box surface regression\", () => {\n\t\tit(\"default session does NOT have the incident symptom (todo_read/todo_update missing)\", async () => {\n\t\t\tconst env = setupTestEnv();\n\t\t\trootDir = env.rootDir;\n\t\t\tcwd = env.cwd;\n\t\t\tagentDir = env.agentDir;\n\n\t\t\ttry {\n\t\t\t\tconst settingsManager = SettingsManager.inMemory({});\n\t\t\t\tconst sessionManager = SessionManager.inMemory();\n\t\t\t\tconst resourceLoader = new DefaultResourceLoader({\n\t\t\t\t\tcwd,\n\t\t\t\t\tagentDir,\n\t\t\t\t\tsettingsManager,\n\t\t\t\t});\n\t\t\t\tawait resourceLoader.reload();\n\n\t\t\t\tconst { session } = await createAgentSession({\n\t\t\t\t\tcwd,\n\t\t\t\t\tagentDir,\n\t\t\t\t\tsettingsManager,\n\t\t\t\t\tauthStorage: env.authStorage,\n\t\t\t\t\tmodelRegistry: env.modelRegistry,\n\t\t\t\t\tresourceLoader,\n\t\t\t\t\tsessionManager,\n\t\t\t\t});\n\n\t\t\t\tconst activeTools = session.getActiveToolNames();\n\t\t\t\tconst allToolInfos = session.getAllTools();\n\n\t\t\t\t// Incident: todo_write was present\n\t\t\t\texpect(activeTools).toContain(\"todo_write\");\n\t\t\t\t// Incident: todo_read was NOT present — regression check\n\t\t\t\texpect(activeTools).toContain(\"todo_read\");\n\t\t\t\t// Incident: todo_update was NOT present — regression check\n\t\t\t\texpect(activeTools).toContain(\"todo_update\");\n\n\t\t\t\t// Incident: model tried todo_read and got \"Tool todo_read not found\"\n\t\t\t\tconst todoReadInRegistry = allToolInfos.some((t) => t.name === \"todo_read\");\n\t\t\t\texpect(todoReadInRegistry).toBe(true);\n\n\t\t\t\t// Incident: todo_update was absent from model-visible tool list\n\t\t\t\tconst todoUpdateInRegistry = allToolInfos.some((t) => t.name === \"todo_update\");\n\t\t\t\texpect(todoUpdateInRegistry).toBe(true);\n\t\t\t} finally {\n\t\t\t\trequire(\"fs\").rmSync(rootDir, { recursive: true, force: true });\n\t\t\t}\n\t\t});\n\t});\n});\n\n// =========================================================================\n// Section 10 — Integrated dispatcher simulation\n// =========================================================================\ndescribe(\"integrated todo tool dispatcher simulation\", () => {\n\tit(\"full lifecycle: write → read → update → read → stale rejection\", async () => {\n\t\tlet todos: TodoItem[] = [];\n\t\tlet revision = 0;\n\n\t\tconst writeTool = createTodoWriteTool(\n\t\t\t() => todos,\n\t\t\t(newTodos) => {\n\t\t\t\ttodos = [...newTodos];\n\t\t\t\trevision++;\n\t\t\t},\n\t\t\tnew TodoLoopGuard(),\n\t\t\t() => revision,\n\t\t);\n\n\t\tconst readTool = createTodoReadTool(\n\t\t\t() => todos,\n\t\t\t() => revision,\n\t\t);\n\n\t\tconst updateTool = createTodoUpdateTool(\n\t\t\t() => todos,\n\t\t\t(newTodos) => {\n\t\t\t\ttodos = [...newTodos];\n\t\t\t\trevision++;\n\t\t\t},\n\t\t\t() => revision,\n\t\t\tnew TodoLoopGuard(),\n\t\t\t{\n\t\t\t\tgetSnapshot: () => ({ revision, timestamp: Date.now() }),\n\t\t\t\tinvalidateSnapshot: () => {},\n\t\t\t},\n\t\t);\n\n\t\t// 1. todo_write with 7 items\n\t\tconst writeResult = await writeTool.execute(\"call_1\", {\n\t\t\ttodos: Array.from({ length: 7 }, (_, i) => ({\n\t\t\t\tcontent: `Task ${i + 1}`,\n\t\t\t\tactiveForm: `Doing ${i + 1}`,\n\t\t\t\tstatus: \"pending\" as const,\n\t\t\t})),\n\t\t});\n\n\t\texpect(todos.length).toBe(7);\n\t\texpect(resultText(writeResult)).toBeDefined();\n\n\t\t// 2. Enumerate todos: confirm all three tools exist\n\t\texpect(writeTool.name).toBe(\"todo_write\");\n\t\texpect(readTool.name).toBe(\"todo_read\");\n\t\texpect(updateTool.name).toBe(\"todo_update\");\n\n\t\t// 3. todo_read\n\t\tconst readResult1 = await readTool.execute(\"call_2\", {});\n\t\texpect(resultText(readResult1)).toContain(\"Task 1\");\n\t\texpect(readResult1.details).toBeDefined();\n\t\texpect(readResult1.details!.todos.length).toBe(7);\n\t\tconst readRevision = readResult1.details!.revision as number;\n\t\texpect(readRevision).toBeGreaterThan(0);\n\n\t\t// 4. Extract IDs and revision\n\t\tconst ids = (readResult1.details!.todos as Array<{ id: string }>).map((t) => t.id);\n\t\texpect(ids.length).toBe(7);\n\n\t\t// 5. todo_update: complete first, activate second\n\t\tconst updateResult = await updateTool.execute(\"call_3\", {\n\t\t\tupdates: [\n\t\t\t\t{ id: ids[0], status: \"completed\" },\n\t\t\t\t{ id: ids[1], status: \"in_progress\" },\n\t\t\t],\n\t\t\texpectedRevision: readRevision,\n\t\t});\n\n\t\texpect(resultText(updateResult)).toContain(\"updated\");\n\t\texpect(todos[0].status).toBe(\"completed\");\n\t\texpect(todos[1].status).toBe(\"in_progress\");\n\n\t\t// 6. todo_read again — confirm state\n\t\tconst readResult2 = await readTool.execute(\"call_4\", {});\n\t\texpect(readResult2.details!.todos.length).toBe(7);\n\n\t\t// 7. Stale revision update is auto-recovered (internal rebase), not rejected\n\t\tconst staleResult = await updateTool.execute(\"call_5\", {\n\t\t\tupdates: [{ id: ids[2], status: \"completed\" }],\n\t\t\texpectedRevision: readRevision, // stale\n\t\t});\n\t\tconst staleText = staleResult.content[0];\n\t\tif (staleText.type !== \"text\") throw new Error(\"expected text content\");\n\t\texpect(staleText.text).not.toContain(\"TODO_READ_REQUIRED\");\n\n\t\t// 8. The stale non-conflicting update was applied automatically\n\t\texpect(todos[2].status).toBe(\"completed\");\n\t});\n});\n\n// =========================================================================\n// Section 11 — Truthful tool sequence\n// =========================================================================\ndescribe(\"truthful tool result sequence\", () => {\n\tit(\"produces correct success/failure sequence for each tool call\", async () => {\n\t\tlet todos: TodoItem[] = [];\n\t\tlet revision = 0;\n\n\t\tconst writeTool = createTodoWriteTool(\n\t\t\t() => todos,\n\t\t\t(newTodos) => {\n\t\t\t\ttodos = [...newTodos];\n\t\t\t\trevision++;\n\t\t\t},\n\t\t\tnew TodoLoopGuard(),\n\t\t\t() => revision,\n\t\t);\n\n\t\tconst readTool = createTodoReadTool(\n\t\t\t() => todos,\n\t\t\t() => revision,\n\t\t);\n\n\t\tconst updateTool = createTodoUpdateTool(\n\t\t\t() => todos,\n\t\t\t(newTodos) => {\n\t\t\t\ttodos = [...newTodos];\n\t\t\t\trevision++;\n\t\t\t},\n\t\t\t() => revision,\n\t\t\tnew TodoLoopGuard(),\n\t\t\t{\n\t\t\t\tgetSnapshot: () => ({ revision, timestamp: Date.now() }),\n\t\t\t\tinvalidateSnapshot: () => {},\n\t\t\t},\n\t\t);\n\n\t\tconst results: Array<{\n\t\t\tnumber: number;\n\t\t\ttool: string;\n\t\t\tactualResult: string;\n\t\t\tsuccess: boolean;\n\t\t}> = [];\n\n\t\t// 1. Write 7 items\n\t\tconst r1 = await writeTool.execute(\"c1\", {\n\t\t\ttodos: Array.from({ length: 7 }, (_, i) => ({\n\t\t\t\tcontent: `T${i + 1}`,\n\t\t\t\tactiveForm: `D${i + 1}`,\n\t\t\t\tstatus: \"pending\" as const,\n\t\t\t})),\n\t\t});\n\t\tconst r1Text = resultText(r1);\n\t\tresults.push({\n\t\t\tnumber: 1,\n\t\t\ttool: \"todo_write\",\n\t\t\tactualResult: r1Text,\n\t\t\tsuccess: r1Text.includes(\"updated\"),\n\t\t});\n\n\t\t// 2. Read\n\t\tconst r2 = await readTool.execute(\"c2\", {});\n\t\tconst rev = (r2.details!.revision as number) ?? 0;\n\t\tresults.push({\n\t\t\tnumber: 2,\n\t\t\ttool: \"todo_read\",\n\t\t\tactualResult: resultText(r2),\n\t\t\tsuccess: r2.details!.todos.length === 7,\n\t\t});\n\n\t\t// 3. Valid update\n\t\tconst ids = (r2.details!.todos as Array<{ id: string }>).map((t) => t.id);\n\t\tconst r3 = await updateTool.execute(\"c3\", {\n\t\t\tupdates: [{ id: ids[0], status: \"in_progress\" }],\n\t\t\texpectedRevision: rev,\n\t\t});\n\t\tconst r3Text = resultText(r3);\n\t\tresults.push({\n\t\t\tnumber: 3,\n\t\t\ttool: \"todo_update\",\n\t\t\tactualResult: r3Text,\n\t\t\tsuccess: r3Text.includes(\"updated\"),\n\t\t});\n\n\t\t// 4. Stale update → rejected\n\t\ttry {\n\t\t\tawait updateTool.execute(\"c4\", {\n\t\t\t\tupdates: [{ id: ids[1], status: \"completed\" }],\n\t\t\t\texpectedRevision: rev, // stale — was already incremented\n\t\t\t});\n\t\t\tthrow new Error(\"Expected throw was not thrown\");\n\t\t} catch (err) {\n\t\t\tresults.push({\n\t\t\t\tnumber: 4,\n\t\t\t\ttool: \"todo_update\",\n\t\t\t\tactualResult: (err as Error).message,\n\t\t\t\tsuccess: false, // stale revision → false\n\t\t\t});\n\t\t}\n\n\t\t// 5. Rejected clear attempt (clear without confirmClear)\n\t\tconst r5 = await writeTool.execute(\"c5\", {\n\t\t\ttodos: [],\n\t\t\tconfirmClear: false,\n\t\t});\n\t\tconst r5Text = resultText(r5);\n\t\tresults.push({\n\t\t\tnumber: 5,\n\t\t\ttool: \"todo_write\",\n\t\t\tactualResult: r5Text,\n\t\t\tsuccess: false, // clear without confirmClear → rejected\n\t\t});\n\n\t\t// Verify the sequence\n\t\texpect(results).toEqual([\n\t\t\t{ number: 1, tool: \"todo_write\", actualResult: expect.any(String), success: true },\n\t\t\t{ number: 2, tool: \"todo_read\", actualResult: expect.any(String), success: true },\n\t\t\t{ number: 3, tool: \"todo_update\", actualResult: expect.any(String), success: true },\n\t\t\t{ number: 4, tool: \"todo_update\", actualResult: expect.any(String), success: false },\n\t\t\t{ number: 5, tool: \"todo_write\", actualResult: expect.any(String), success: false },\n\t\t]);\n\t});\n});\n\n// =========================================================================\n// Session isolation tests\n// =========================================================================\ndescribe(\"session isolation for todo tools\", () => {\n\tit(\"todo_read sees only its own session's todos\", () => {\n\t\tconst storeA: TodoItem[] = [{ id: \"a1\", content: \"A Task\", activeForm: \"Doing A\", status: \"pending\" }];\n\t\tconst storeB: TodoItem[] = [{ id: \"b1\", content: \"B Task\", activeForm: \"Doing B\", status: \"pending\" }];\n\n\t\tconst readA = createTodoReadTool(\n\t\t\t() => storeA,\n\t\t\t() => 1,\n\t\t);\n\t\tconst readB = createTodoReadTool(\n\t\t\t() => storeB,\n\t\t\t() => 2,\n\t\t);\n\n\t\t// Can't easily test different \"sessions\" with the same instance,\n\t\t// but we can test that different factories see different stores\n\t\tconst resultA = readA.execute(\"call_1\", {});\n\t\tconst resultB = readB.execute(\"call_1\", {});\n\n\t\t// Use Promise.all to compare\n\t\treturn Promise.all([resultA, resultB]).then(([rA, rB]) => {\n\t\t\texpect(resultText(rA)).toContain(\"A Task\");\n\t\t\texpect(resultText(rB)).toContain(\"B Task\");\n\t\t});\n\t});\n\n\tit(\"todo_update in one session does not affect another\", async () => {\n\t\tconst storeA: TodoItem[] = [{ id: \"a1\", content: \"A Task\", activeForm: \"Doing A\", status: \"pending\" }];\n\t\tconst storeB: TodoItem[] = [{ id: \"b1\", content: \"B Task\", activeForm: \"Doing B\", status: \"pending\" }];\n\n\t\tlet revA = 1;\n\t\tconst updateA = createTodoUpdateTool(\n\t\t\t() => storeA,\n\t\t\t(newTodos) => {\n\t\t\t\tstoreA.length = 0;\n\t\t\t\tstoreA.push(...newTodos);\n\t\t\t\trevA++;\n\t\t\t},\n\t\t\t() => revA,\n\t\t\tnew TodoLoopGuard(),\n\t\t\t{\n\t\t\t\tgetSnapshot: () => ({ revision: 1, timestamp: Date.now() }),\n\t\t\t\tinvalidateSnapshot: () => {},\n\t\t\t},\n\t\t);\n\n\t\tawait updateA.execute(\"call_1\", {\n\t\t\tupdates: [{ id: \"a1\", status: \"completed\" }],\n\t\t\texpectedRevision: 1,\n\t\t});\n\n\t\t// Store B should be unaffected\n\t\texpect(storeB[0].status).toBe(\"pending\");\n\t\texpect(storeA[0].status).toBe(\"completed\");\n\t});\n});\n"]}