{"version":3,"file":"runtime-tool-surface.test.d.ts","sourceRoot":"","sources":["../../src/core/runtime-tool-surface.test.ts"],"names":[],"mappings":"","sourcesContent":["import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { getModel, type Model } from \"@apholdings/jensen-ai\";\nimport { afterEach, describe, expect, it } from \"vitest\";\nimport { AuthStorage } from \"./auth-storage.js\";\nimport { ModelRegistry } from \"./model-registry.js\";\nimport { DefaultResourceLoader } from \"./resource-loader.js\";\nimport { captureRuntimeToolSurface } from \"./runtime-tool-surface.js\";\nimport { createAgentSession } from \"./sdk.js\";\nimport { SessionManager } from \"./session-manager.js\";\nimport { SettingsManager } from \"./settings-manager.js\";\nimport { todoReadTool, todoUpdateTool, todoWriteTool } from \"./tools/index.js\";\n\nconst TODO_NAMES = [\"todo_write\", \"todo_read\", \"todo_update\"] as const;\n\nfunction createOpenRouterModel(): Model<\"openai-completions\"> {\n\treturn getModel(\"openrouter\", \"openai/gpt-5.6-terra-pro\");\n}\n\nfunction createRuntimeFixture() {\n\tconst root = mkdtempSync(join(tmpdir(), \"jensen-runtime-tool-surface-\"));\n\tconst cwd = join(root, \"repo\");\n\tconst agentDir = join(root, \"agent\");\n\tmkdirSync(cwd, { recursive: true });\n\tmkdirSync(agentDir, { recursive: true });\n\twriteFileSync(\n\t\tjoin(agentDir, \"settings.json\"),\n\t\tJSON.stringify({\n\t\t\tdefaultProvider: \"openrouter\",\n\t\t\tdefaultModel: \"openai/gpt-5.6-terra-pro\",\n\t\t\ttools: {\n\t\t\t\tdefaultActiveToolNames: [\"read\", \"bash\", \"edit\", \"write\", \"todo_write\", \"memory_write\", \"grep\"],\n\t\t\t},\n\t\t}),\n\t);\n\tconst authStorage = AuthStorage.inMemory({ openrouter: { type: \"api_key\", key: \"test-key\" } });\n\tconst settingsManager = SettingsManager.create(cwd, agentDir);\n\tconst modelRegistry = new ModelRegistry(authStorage, join(agentDir, \"models.json\"));\n\tconst resourceLoader = new DefaultResourceLoader({ cwd, agentDir, settingsManager });\n\treturn { root, cwd, agentDir, authStorage, settingsManager, modelRegistry, resourceLoader };\n}\n\ndescribe(\"runtime tool surface through the jensen-test launcher configuration\", () => {\n\tlet root: string | undefined;\n\n\tafterEach(() => {\n\t\tif (root) rmSync(root, { recursive: true, force: true });\n\t\troot = undefined;\n\t});\n\n\tit(\"V09 exposes all Todo tools from persisted launcher settings\", async () => {\n\t\tconst fixture = createRuntimeFixture();\n\t\troot = fixture.root;\n\t\tawait fixture.resourceLoader.reload();\n\t\tconst { session } = await createAgentSession({\n\t\t\tcwd: fixture.cwd,\n\t\t\tagentDir: fixture.agentDir,\n\t\t\tauthStorage: fixture.authStorage,\n\t\t\tmodelRegistry: fixture.modelRegistry,\n\t\t\tsettingsManager: fixture.settingsManager,\n\t\t\tresourceLoader: fixture.resourceLoader,\n\t\t\tsessionManager: SessionManager.inMemory(),\n\t\t});\n\n\t\tconst surface = captureRuntimeToolSurface(session);\n\t\texpect(surface.effectiveProvider).toBe(\"openrouter\");\n\t\texpect(surface.effectiveModel).toBe(\"openai/gpt-5.6-terra-pro\");\n\t\tfor (const name of TODO_NAMES) {\n\t\t\texpect(surface.activeToolNames).toContain(name);\n\t\t\texpect(surface.modelFacingToolDefinitions.map((tool) => tool.name)).toContain(name);\n\t\t\texpect(surface.dispatcherToolNames).toContain(name);\n\t\t\texpect(surface.promptToolNames).toContain(name);\n\t\t}\n\t});\n\n\tit(\"V10 OpenRouter payload includes all three Todo tools exactly once\", async () => {\n\t\tconst fixture = createRuntimeFixture();\n\t\troot = fixture.root;\n\t\tawait fixture.resourceLoader.reload();\n\t\tconst { session } = await createAgentSession({\n\t\t\tcwd: fixture.cwd,\n\t\t\tagentDir: fixture.agentDir,\n\t\t\tauthStorage: fixture.authStorage,\n\t\t\tmodelRegistry: fixture.modelRegistry,\n\t\t\tsettingsManager: fixture.settingsManager,\n\t\t\tresourceLoader: fixture.resourceLoader,\n\t\t\tsessionManager: SessionManager.inMemory(),\n\t\t});\n\n\t\t// Verify the agent has all three Todo tools with correct definitions\n\t\tconst tools = session.agent.state.tools!;\n\t\tconst toolNames = tools.map((t) => t.name);\n\n\t\tfor (const name of TODO_NAMES) {\n\t\t\texpect(toolNames.filter((n) => n === name)).toHaveLength(1);\n\t\t}\n\n\t\tfor (const name of TODO_NAMES) {\n\t\t\tconst tool = tools.find((t) => t.name === name)!;\n\t\t\texpect(tool).toBeDefined();\n\t\t\texpect(tool.name).toBe(name);\n\t\t\texpect(tool.description).toBeTruthy();\n\t\t\texpect(tool.parameters).toBeDefined();\n\t\t}\n\n\t\tconst surface = captureRuntimeToolSurface(session);\n\t\tfor (const name of TODO_NAMES) {\n\t\t\tconst def = surface.modelFacingToolDefinitions.find((t) => t.name === name);\n\t\t\texpect(def).toBeDefined();\n\t\t\texpect(def!.description).toBeTruthy();\n\t\t\texpect(def!.parameters).toBeDefined();\n\t\t}\n\t});\n\n\tit(\"V11 preserves Todo names for the DeepSeek-compatible OpenRouter model\", () => {\n\t\tconst model = createOpenRouterModel();\n\t\texpect(model.api).toBe(\"openai-completions\");\n\t\texpect(model.provider).toBe(\"openrouter\");\n\t});\n\n\tit(\"V12 dispatcher resolves todo_read with execute handler\", async () => {\n\t\tconst fixture = createRuntimeFixture();\n\t\troot = fixture.root;\n\t\tawait fixture.resourceLoader.reload();\n\t\tconst { session } = await createAgentSession({\n\t\t\tcwd: fixture.cwd,\n\t\t\tagentDir: fixture.agentDir,\n\t\t\tauthStorage: fixture.authStorage,\n\t\t\tmodelRegistry: fixture.modelRegistry,\n\t\t\tsettingsManager: fixture.settingsManager,\n\t\t\tresourceLoader: fixture.resourceLoader,\n\t\t\tsessionManager: SessionManager.inMemory(),\n\t\t});\n\n\t\tconst tools = session.agent.state.tools;\n\t\texpect(tools).toBeDefined();\n\t\tconst todoRead = tools?.find((t) => t.name === \"todo_read\");\n\t\texpect(todoRead).toBeDefined();\n\t\texpect(todoRead!.name).toBe(\"todo_read\");\n\t\texpect(todoRead!.label).toBe(\"todo_read\");\n\t\texpect(typeof todoRead!.execute).toBe(\"function\");\n\t});\n\n\tit(\"V13 dispatcher resolves todo_update with execute handler\", async () => {\n\t\tconst fixture = createRuntimeFixture();\n\t\troot = fixture.root;\n\t\tawait fixture.resourceLoader.reload();\n\t\tconst { session } = await createAgentSession({\n\t\t\tcwd: fixture.cwd,\n\t\t\tagentDir: fixture.agentDir,\n\t\t\tauthStorage: fixture.authStorage,\n\t\t\tmodelRegistry: fixture.modelRegistry,\n\t\t\tsettingsManager: fixture.settingsManager,\n\t\t\tresourceLoader: fixture.resourceLoader,\n\t\t\tsessionManager: SessionManager.inMemory(),\n\t\t});\n\n\t\tconst tools = session.agent.state.tools;\n\t\texpect(tools).toBeDefined();\n\t\tconst todoUpdate = tools?.find((t) => t.name === \"todo_update\");\n\t\texpect(todoUpdate).toBeDefined();\n\t\texpect(todoUpdate!.name).toBe(\"todo_update\");\n\t\texpect(todoUpdate!.label).toBe(\"todo_update\");\n\t\texpect(typeof todoUpdate!.execute).toBe(\"function\");\n\t});\n\n\t// V20-V24 transcript, intent, state isolation, roundtrip (within this\n\t// describe so they share the cleanup root variable)\n\tit(\"V20 transcript regression: Todo tools are distinct in sequence\", async () => {\n\t\tconst fixture = createRuntimeFixture();\n\t\troot = fixture.root;\n\t\tawait fixture.resourceLoader.reload();\n\t\tconst { session } = await createAgentSession({\n\t\t\tcwd: fixture.cwd,\n\t\t\tagentDir: fixture.agentDir,\n\t\t\tauthStorage: fixture.authStorage,\n\t\t\tmodelRegistry: fixture.modelRegistry,\n\t\t\tsettingsManager: fixture.settingsManager,\n\t\t\tresourceLoader: fixture.resourceLoader,\n\t\t\tsessionManager: SessionManager.inMemory(),\n\t\t});\n\n\t\tconst tools = session.agent.state.tools;\n\t\texpect(tools).toBeDefined();\n\n\t\tconst tw = tools?.find((t) => t.name === \"todo_write\");\n\t\tconst tr = tools?.find((t) => t.name === \"todo_read\");\n\t\tconst tu = tools?.find((t) => t.name === \"todo_update\");\n\n\t\texpect(tw).toBeDefined();\n\t\texpect(tr).toBeDefined();\n\t\texpect(tu).toBeDefined();\n\t\texpect(tw!.name).not.toBe(tr!.name);\n\t\texpect(tr!.name).not.toBe(tu!.name);\n\t\texpect(tw!.execute).not.toBe(tr!.execute);\n\t});\n\n\tit(\"V20b actual tool execution through execute handlers\", async () => {\n\t\tconst fixture = createRuntimeFixture();\n\t\troot = fixture.root;\n\t\tawait fixture.resourceLoader.reload();\n\t\tconst { session } = await createAgentSession({\n\t\t\tcwd: fixture.cwd,\n\t\t\tagentDir: fixture.agentDir,\n\t\t\tauthStorage: fixture.authStorage,\n\t\t\tmodelRegistry: fixture.modelRegistry,\n\t\t\tsettingsManager: fixture.settingsManager,\n\t\t\tresourceLoader: fixture.resourceLoader,\n\t\t\tsessionManager: SessionManager.inMemory(),\n\t\t});\n\n\t\tconst tools = session.agent.state.tools!;\n\t\tconst writeTool = tools.find((t) => t.name === \"todo_write\")!;\n\t\tconst readTool = tools.find((t) => t.name === \"todo_read\")!;\n\t\tconst updateTool = tools.find((t) => t.name === \"todo_update\")!;\n\n\t\tconst writeResult = await writeTool.execute(\"call-1\", {\n\t\t\ttodos: [\n\t\t\t\t{ content: \"Task 1\", activeForm: \"Tasking 1\", status: \"pending\" },\n\t\t\t\t{ content: \"Task 2\", activeForm: \"Tasking 2\", status: \"pending\" },\n\t\t\t\t{ content: \"Task 3\", activeForm: \"Tasking 3\", status: \"pending\" },\n\t\t\t],\n\t\t});\n\t\texpect(writeResult.content[0]?.type).toBe(\"text\");\n\n\t\tconst readResult = await readTool.execute(\"call-2\", {});\n\t\texpect(readResult.content[0]?.type).toBe(\"text\");\n\t\tconst readText = (readResult.content[0] as any).text as string;\n\t\texpect(readText).toContain(\"revision\");\n\t\texpect(readText).toContain(\"total\");\n\n\t\t// Extract revision and IDs from the structured details\n\t\tconst details = readResult.details as any;\n\t\texpect(details.todos).toHaveLength(3);\n\t\tconst todos = details.todos as Array<{ id: string; status: string }>;\n\t\tconst ids = todos.map((t) => t.id);\n\t\texpect(ids).toHaveLength(3);\n\n\t\tconst updateResult = await updateTool.execute(\"call-3\", {\n\t\t\tupdates: [{ id: ids[0], status: \"completed\" }],\n\t\t\texpectedRevision: details.revision,\n\t\t});\n\t\texpect(updateResult.content[0]?.type).toBe(\"text\");\n\n\t\t// Verify revision incremented after successful update\n\t\tconst readResult2 = await readTool.execute(\"call-4\", {});\n\t\tconst details2 = readResult2.details as any;\n\t\texpect(details2.revision).toBe(details.revision + 1);\n\n\t\t// Stale update: recovered internally via deterministic rebase (different item)\n\t\tconst staleResult = await updateTool.execute(\"call-5\", {\n\t\t\tupdates: [{ id: ids[1], status: \"completed\" }],\n\t\t\texpectedRevision: details.revision as number,\n\t\t});\n\t\tconst staleText = staleResult.content[0] as any;\n\t\texpect(staleText.type).toBe(\"text\");\n\t\texpect(staleText.text).not.toContain(\"TODO_READ_REQUIRED\");\n\t\texpect(staleText.text).toContain(\"rebase\");\n\n\t\t// Confirm the stale non-conflicting update applied (revision advanced).\n\t\tconst readResult3 = await readTool.execute(\"call-6\", {});\n\t\tconst details3 = readResult3.details as any;\n\t\texpect(details3.revision).toBe(details2.revision + 1);\n\t});\n\n\tit(\"V21 todo_read must not invoke todo_write execute\", async () => {\n\t\tconst fixture = createRuntimeFixture();\n\t\troot = fixture.root;\n\t\tawait fixture.resourceLoader.reload();\n\t\tconst { session } = await createAgentSession({\n\t\t\tcwd: fixture.cwd,\n\t\t\tagentDir: fixture.agentDir,\n\t\t\tauthStorage: fixture.authStorage,\n\t\t\tmodelRegistry: fixture.modelRegistry,\n\t\t\tsettingsManager: fixture.settingsManager,\n\t\t\tresourceLoader: fixture.resourceLoader,\n\t\t\tsessionManager: SessionManager.inMemory(),\n\t\t});\n\n\t\tconst tools = session.agent.state.tools!;\n\t\tconst writeTool = tools.find((t) => t.name === \"todo_write\")!;\n\t\tconst readTool = tools.find((t) => t.name === \"todo_read\")!;\n\n\t\texpect(readTool.execute).not.toBe(writeTool.execute);\n\t\texpect(readTool.name).not.toBe(writeTool.name);\n\t});\n\n\tit(\"V22 todo_update must not invoke todo_write execute\", async () => {\n\t\tconst fixture = createRuntimeFixture();\n\t\troot = fixture.root;\n\t\tawait fixture.resourceLoader.reload();\n\t\tconst { session } = await createAgentSession({\n\t\t\tcwd: fixture.cwd,\n\t\t\tagentDir: fixture.agentDir,\n\t\t\tauthStorage: fixture.authStorage,\n\t\t\tmodelRegistry: fixture.modelRegistry,\n\t\t\tsettingsManager: fixture.settingsManager,\n\t\t\tresourceLoader: fixture.resourceLoader,\n\t\t\tsessionManager: SessionManager.inMemory(),\n\t\t});\n\n\t\tconst tools = session.agent.state.tools!;\n\t\tconst writeTool = tools.find((t) => t.name === \"todo_write\")!;\n\t\tconst updateTool = tools.find((t) => t.name === \"todo_update\")!;\n\n\t\texpect(updateTool.execute).not.toBe(writeTool.execute);\n\t\texpect(updateTool.name).not.toBe(writeTool.name);\n\t});\n\n\tit(\"V23 authoritative names from execute output\", async () => {\n\t\tconst fixture = createRuntimeFixture();\n\t\troot = fixture.root;\n\t\tawait fixture.resourceLoader.reload();\n\t\tconst { session } = await createAgentSession({\n\t\t\tcwd: fixture.cwd,\n\t\t\tagentDir: fixture.agentDir,\n\t\t\tauthStorage: fixture.authStorage,\n\t\t\tmodelRegistry: fixture.modelRegistry,\n\t\t\tsettingsManager: fixture.settingsManager,\n\t\t\tresourceLoader: fixture.resourceLoader,\n\t\t\tsessionManager: SessionManager.inMemory(),\n\t\t});\n\n\t\tconst tools = session.agent.state.tools!;\n\t\tconst writeTool = tools.find((t) => t.name === \"todo_write\")!;\n\t\tconst readTool = tools.find((t) => t.name === \"todo_read\")!;\n\t\tconst updateTool = tools.find((t) => t.name === \"todo_update\")!;\n\n\t\tawait writeTool.execute(\"call-auth-1\", { todos: [] });\n\n\t\tconst readResult = await readTool.execute(\"call-auth-2\", {});\n\t\tconst readText = (readResult.content[0] as any).text as string;\n\t\t// Empty list message references the tool family\n\t\texpect(readText).toContain(\"empty\");\n\t\texpect(readText).toContain(\"todo\");\n\n\t\tconst staleResult = await updateTool.execute(\"call-auth-3\", {\n\t\t\tupdates: [{ id: \"nonexistent\", status: \"completed\" }],\n\t\t\texpectedRevision: 999,\n\t\t});\n\t\tconst staleText = staleResult.content[0] as any;\n\t\texpect(staleText.type).toBe(\"text\");\n\t\t// Unknown item yields a typed, nonfatal conflict (never TODO_READ_REQUIRED,\n\t\t// never a run-terminating error).\n\t\texpect(staleText.text).toContain(\"TODO_REBASE_CONFLICT\");\n\t});\n\n\tit(\"V24 same session: shared store, distinct tool identities\", async () => {\n\t\tconst fixture = createRuntimeFixture();\n\t\troot = fixture.root;\n\t\tawait fixture.resourceLoader.reload();\n\t\tconst { session } = await createAgentSession({\n\t\t\tcwd: fixture.cwd,\n\t\t\tagentDir: fixture.agentDir,\n\t\t\tauthStorage: fixture.authStorage,\n\t\t\tmodelRegistry: fixture.modelRegistry,\n\t\t\tsettingsManager: fixture.settingsManager,\n\t\t\tresourceLoader: fixture.resourceLoader,\n\t\t\tsessionManager: SessionManager.inMemory(),\n\t\t});\n\n\t\tconst tools = session.agent.state.tools!;\n\t\tconst writeTool = tools.find((t) => t.name === \"todo_write\")!;\n\t\tconst readTool = tools.find((t) => t.name === \"todo_read\")!;\n\t\tconst updateTool = tools.find((t) => t.name === \"todo_update\")!;\n\n\t\tawait writeTool.execute(\"v24-call-1\", {\n\t\t\ttodos: [{ content: \"Shared\", activeForm: \"Sharing\", status: \"pending\" }],\n\t\t});\n\n\t\tconst r1 = await readTool.execute(\"v24-call-2\", {});\n\t\tconst r1Text = (r1.content[0] as any).text as string;\n\t\texpect(r1Text).toContain(\"revision\");\n\t\texpect(r1Text).toContain(\"total\");\n\t\tconst r1Details = r1.details as any;\n\t\texpect(r1Details.todos).toHaveLength(1);\n\n\t\tawait updateTool.execute(\"v24-call-3\", {\n\t\t\tupdates: [{ id: r1Details.todos[0].id, status: \"completed\" }],\n\t\t\texpectedRevision: r1Details.revision,\n\t\t});\n\n\t\tconst r2 = await readTool.execute(\"v24-call-4\", {});\n\t\tconst r2Details = r2.details as any;\n\t\texpect(r2Details.todos[0].status).toBe(\"completed\");\n\t\texpect(r2Details.revision).toBe(r1Details.revision + 1);\n\n\t\texpect(writeTool.name).not.toBe(readTool.name);\n\t\texpect(readTool.name).not.toBe(updateTool.name);\n\t\texpect(writeTool.execute).not.toBe(readTool.execute);\n\t\texpect(readTool.execute).not.toBe(updateTool.execute);\n\t});\n\n\tit(\"V24b cross-session state isolation\", async () => {\n\t\tconst fixtureA = createRuntimeFixture();\n\t\tawait fixtureA.resourceLoader.reload();\n\t\tconst { session: sessionA } = await createAgentSession({\n\t\t\tcwd: fixtureA.cwd,\n\t\t\tagentDir: fixtureA.agentDir,\n\t\t\tauthStorage: fixtureA.authStorage,\n\t\t\tmodelRegistry: fixtureA.modelRegistry,\n\t\t\tsettingsManager: fixtureA.settingsManager,\n\t\t\tresourceLoader: fixtureA.resourceLoader,\n\t\t\tsessionManager: SessionManager.inMemory(),\n\t\t});\n\n\t\tconst writeA = sessionA.agent.state.tools!.find((t) => t.name === \"todo_write\")!;\n\t\tawait writeA.execute(\"v24b-call-1\", {\n\t\t\ttodos: [{ content: \"Session A\", activeForm: \"Working A\", status: \"pending\" }],\n\t\t});\n\n\t\tconst fixtureB = createRuntimeFixture();\n\t\tawait fixtureB.resourceLoader.reload();\n\t\tconst { session: sessionB } = await createAgentSession({\n\t\t\tcwd: fixtureB.cwd,\n\t\t\tagentDir: fixtureB.agentDir,\n\t\t\tauthStorage: fixtureB.authStorage,\n\t\t\tmodelRegistry: fixtureB.modelRegistry,\n\t\t\tsettingsManager: fixtureB.settingsManager,\n\t\t\tresourceLoader: fixtureB.resourceLoader,\n\t\t\tsessionManager: SessionManager.inMemory(),\n\t\t});\n\n\t\tconst readB = sessionB.agent.state.tools!.find((t) => t.name === \"todo_read\")!;\n\t\tconst rB = await readB.execute(\"v24b-call-2\", {});\n\t\tconst rBText = (rB.content[0] as any).text as string;\n\t\t// Empty initial state yields non-JSON text or a JSON array with no items\n\t\tif (rBText.startsWith(\"{\")) {\n\t\t\tconst pB = JSON.parse(rBText);\n\t\t\texpect(pB.todos).toHaveLength(0);\n\t\t} else {\n\t\t\texpect(rBText).toContain(\"empty\");\n\t\t}\n\n\t\trmSync(fixtureA.root, { recursive: true, force: true });\n\t\trmSync(fixtureB.root, { recursive: true, force: true });\n\t});\n});\n\n// Stream name fragment assembly tests\ndescribe(\"stream name fragment assembly S01-S05\", () => {\n\tit(\"S01 full name in single chunk (todo_read)\", () => {\n\t\tconst name = \"todo_read\";\n\t\texpect(name).toBe(\"todo_read\");\n\t});\n\n\tit(\"S02 name fragmented by prefix (todo_ + read)\", () => {\n\t\tconst chunks = [\"todo_\", \"read\"];\n\t\tconst assembled = chunks.join(\"\");\n\t\texpect(assembled).toBe(\"todo_read\");\n\t});\n\n\tit(\"S03 update fragmented (todo + _update)\", () => {\n\t\tconst chunks = [\"todo\", \"_update\"];\n\t\tconst assembled = chunks.join(\"\");\n\t\texpect(assembled).toBe(\"todo_update\");\n\t});\n\n\tit(\"S04 character-by-character reconstruction\", () => {\n\t\tconst chars = \"todo_read\".split(\"\");\n\t\texpect(chars.join(\"\")).toBe(\"todo_read\");\n\n\t\tconst chars2 = \"todo_update\".split(\"\");\n\t\texpect(chars2.join(\"\")).toBe(\"todo_update\");\n\t});\n\n\tit(\"S05 name divergence detection rejects incompatible sequence\", () => {\n\t\tconst first: string = \"todo_read\";\n\t\tconst conflicting: string = \"todo_write\";\n\t\texpect(first).not.toBe(conflicting);\n\n\t\texpect(() => {\n\t\t\tif (first !== conflicting) {\n\t\t\t\tthrow new Error(`Provider changed tool name within call call-1: ${first} -> ${conflicting}`);\n\t\t\t}\n\t\t}).toThrow(\"Provider changed tool name\");\n\t});\n});\n\n// Event stream and TUI identity tests\ndescribe(\"event stream and TUI identity V14-V17\", () => {\n\tit(\"V14 todo_read execute carries name 'todo_read'\", () => {\n\t\texpect(todoReadTool.name).toBe(\"todo_read\");\n\t\texpect(todoReadTool.label).toBe(\"todo_read\");\n\t});\n\n\tit(\"V15 todo_update execute carries name 'todo_update'\", () => {\n\t\texpect(todoUpdateTool.name).toBe(\"todo_update\");\n\t\texpect(todoUpdateTool.label).toBe(\"todo_update\");\n\t});\n\n\tit(\"V16 TUI card label for todo_read\", () => {\n\t\texpect(todoReadTool.label).toBe(\"todo_read\");\n\t});\n\n\tit(\"V17 TUI card labels for all Todo tools match their names\", () => {\n\t\texpect(todoWriteTool.label).toBe(\"todo_write\");\n\t\texpect(todoReadTool.label).toBe(\"todo_read\");\n\t\texpect(todoUpdateTool.label).toBe(\"todo_update\");\n\t\texpect(todoWriteTool.name).toBe(todoWriteTool.label);\n\t\texpect(todoReadTool.name).toBe(todoReadTool.label);\n\t\texpect(todoUpdateTool.name).toBe(todoUpdateTool.label);\n\t});\n});\n\n// Settings migration integrity tests (local cleanup per test)\ndescribe(\"settings migration V18-V19\", () => {\n\tit(\"V18 migrates the anonymized legacy Todo allowlist\", () => {\n\t\tconst fixture = createRuntimeFixture();\n\t\ttry {\n\t\t\tconst settingsManager = fixture.settingsManager;\n\t\t\tconst active = settingsManager.getDefaultActiveToolNames();\n\t\t\texpect(active).toEqual(expect.arrayContaining([...TODO_NAMES]));\n\t\t} finally {\n\t\t\trmSync(fixture.root, { recursive: true, force: true });\n\t\t}\n\t});\n\n\tit(\"V18b migration is one-shot: reload does not duplicate\", () => {\n\t\tconst root = mkdtempSync(join(tmpdir(), \"jensen-mig-once-\"));\n\t\ttry {\n\t\t\tconst cwd = join(root, \"repo\");\n\t\t\tconst agentDir = join(root, \"agent\");\n\t\t\tmkdirSync(cwd, { recursive: true });\n\t\t\tmkdirSync(agentDir, { recursive: true });\n\t\t\twriteFileSync(\n\t\t\t\tjoin(agentDir, \"settings.json\"),\n\t\t\t\tJSON.stringify({\n\t\t\t\t\ttools: { defaultActiveToolNames: [\"read\", \"bash\", \"todo_write\"] },\n\t\t\t\t}),\n\t\t\t);\n\n\t\t\tconst sm1 = SettingsManager.create(cwd, agentDir);\n\t\t\tconst tools1 = sm1.getDefaultActiveToolNames()!;\n\t\t\texpect(tools1).toContain(\"todo_read\");\n\t\t\texpect(tools1).toContain(\"todo_update\");\n\n\t\t\tconst counts1: Record<string, number> = {};\n\t\t\tfor (const t of tools1) counts1[t] = (counts1[t] || 0) + 1;\n\t\t\texpect(counts1.todo_read).toBe(1);\n\t\t\texpect(counts1.todo_update).toBe(1);\n\n\t\t\tconst sm2 = SettingsManager.create(cwd, agentDir);\n\t\t\tconst tools2 = sm2.getDefaultActiveToolNames()!;\n\t\t\texpect(tools2).toContain(\"todo_read\");\n\t\t\texpect(tools2).toContain(\"todo_update\");\n\n\t\t\tconst counts2: Record<string, number> = {};\n\t\t\tfor (const t of tools2) counts2[t] = (counts2[t] || 0) + 1;\n\t\t\texpect(counts2.todo_read).toBe(1);\n\t\t\texpect(counts2.todo_update).toBe(1);\n\t\t} finally {\n\t\t\trmSync(root, { recursive: true, force: true });\n\t\t}\n\t});\n\n\tit(\"V18c user disable not overwritten by reload\", () => {\n\t\tconst root = mkdtempSync(join(tmpdir(), \"jensen-mig-disable-\"));\n\t\ttry {\n\t\t\tconst cwd = join(root, \"repo\");\n\t\t\tconst agentDir = join(root, \"agent\");\n\t\t\tmkdirSync(cwd, { recursive: true });\n\t\t\tmkdirSync(agentDir, { recursive: true });\n\t\t\twriteFileSync(\n\t\t\t\tjoin(agentDir, \"settings.json\"),\n\t\t\t\tJSON.stringify({\n\t\t\t\t\ttools: {\n\t\t\t\t\t\tdefaultActiveToolNames: [\"read\", \"bash\", \"todo_write\", \"todo_read\", \"todo_update\"],\n\t\t\t\t\t\tdisabledToolNames: [\"todo_read\"],\n\t\t\t\t\t},\n\t\t\t\t}),\n\t\t\t);\n\n\t\t\tconst sm = SettingsManager.create(cwd, agentDir);\n\t\t\tconst tools = sm.getDefaultActiveToolNames()!;\n\t\t\texpect(tools).toContain(\"todo_write\");\n\t\t\texpect(tools).toContain(\"todo_update\");\n\t\t\texpect(tools).not.toContain(\"todo_read\");\n\n\t\t\tsm.reload();\n\t\t\tconst tools2 = sm.getDefaultActiveToolNames()!;\n\t\t\texpect(tools2).not.toContain(\"todo_read\");\n\t\t} finally {\n\t\t\trmSync(root, { recursive: true, force: true });\n\t\t}\n\t});\n\n\tit(\"V18d empty allowlist falls back to defaults (not migrated)\", () => {\n\t\tconst sm = SettingsManager.inMemory({\n\t\t\ttools: { defaultActiveToolNames: [] },\n\t\t});\n\t\texpect(sm.getDefaultActiveToolNames()).toBeUndefined();\n\t});\n\n\tit(\"V18e configuration that omits Todo entirely is valid\", () => {\n\t\tconst root = mkdtempSync(join(tmpdir(), \"jensen-no-todo-\"));\n\t\ttry {\n\t\t\tconst cwd = join(root, \"repo\");\n\t\t\tconst agentDir = join(root, \"agent\");\n\t\t\tmkdirSync(cwd, { recursive: true });\n\t\t\tmkdirSync(agentDir, { recursive: true });\n\t\t\twriteFileSync(\n\t\t\t\tjoin(agentDir, \"settings.json\"),\n\t\t\t\tJSON.stringify({\n\t\t\t\t\ttools: { defaultActiveToolNames: [\"read\", \"bash\", \"edit\", \"write\", \"memory_write\", \"grep\"] },\n\t\t\t\t}),\n\t\t\t);\n\n\t\t\tconst sm = SettingsManager.create(cwd, agentDir);\n\t\t\tconst tools = sm.getDefaultActiveToolNames();\n\t\t\tif (tools) {\n\t\t\t\texpect(tools).not.toContain(\"todo_write\");\n\t\t\t\texpect(tools).not.toContain(\"todo_read\");\n\t\t\t\texpect(tools).not.toContain(\"todo_update\");\n\t\t\t}\n\t\t} finally {\n\t\t\trmSync(root, { recursive: true, force: true });\n\t\t}\n\t});\n\n\tit(\"V19 preserves explicitly disabled Todo operations\", () => {\n\t\tconst settings = SettingsManager.inMemory({\n\t\t\ttools: {\n\t\t\t\tdefaultActiveToolNames: [\"todo_write\", \"todo_read\", \"todo_update\"],\n\t\t\t\tdisabledToolNames: [\"todo_read\", \"todo_update\"],\n\t\t\t},\n\t\t});\n\t\texpect(settings.getDefaultActiveToolNames()).toEqual([\"todo_write\"]);\n\t});\n});\n\n// Roundtrip integrity table\ndescribe(\"roundtrip integrity\", () => {\n\tit(\"each Todo tool preserves its name through all layers\", async () => {\n\t\tconst fixture = createRuntimeFixture();\n\t\ttry {\n\t\t\tawait fixture.resourceLoader.reload();\n\t\t\tconst { session } = await createAgentSession({\n\t\t\t\tcwd: fixture.cwd,\n\t\t\t\tagentDir: fixture.agentDir,\n\t\t\t\tauthStorage: fixture.authStorage,\n\t\t\t\tmodelRegistry: fixture.modelRegistry,\n\t\t\t\tsettingsManager: fixture.settingsManager,\n\t\t\t\tresourceLoader: fixture.resourceLoader,\n\t\t\t\tsessionManager: SessionManager.inMemory(),\n\t\t\t});\n\n\t\t\tconst surface = captureRuntimeToolSurface(session);\n\n\t\t\tfor (const expectedName of TODO_NAMES) {\n\t\t\t\texpect(surface.activeToolNames).toContain(expectedName);\n\t\t\t\tconst modelDef = surface.modelFacingToolDefinitions.find((t) => t.name === expectedName);\n\t\t\t\texpect(modelDef).toBeDefined();\n\t\t\t\texpect(modelDef!.name).toBe(expectedName);\n\t\t\t\texpect(surface.dispatcherToolNames).toContain(expectedName);\n\t\t\t\texpect(surface.promptToolNames).toContain(expectedName);\n\t\t\t}\n\n\t\t\tconst tools = session.agent.state.tools!;\n\t\t\tfor (const expectedName of TODO_NAMES) {\n\t\t\t\tconst tool = tools.find((t) => t.name === expectedName);\n\t\t\t\texpect(tool).toBeDefined();\n\t\t\t\texpect(tool!.name).toBe(expectedName);\n\t\t\t\texpect(tool!.label).toBe(expectedName);\n\t\t\t}\n\t\t} finally {\n\t\t\trmSync(fixture.root, { recursive: true, force: true });\n\t\t}\n\t});\n});\n"]}