{"version":3,"file":"process-manager.test.d.ts","sourceRoot":"","sources":["../../../src/core/tools/process-manager.test.ts"],"names":[],"mappings":"","sourcesContent":["import { existsSync, mkdirSync, rmSync } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { afterEach, beforeEach, describe, expect, it } from \"vitest\";\nimport { createProcessManagerTool, type ProcessManagerOperations } from \"./process-manager.js\";\n\nconst TEST_REGISTRY_DIR = join(tmpdir(), \"jensen-process-registry\");\n\nfunction cleanRegistry(): void {\n\tif (existsSync(TEST_REGISTRY_DIR)) {\n\t\trmSync(TEST_REGISTRY_DIR, { recursive: true, force: true });\n\t}\n\tmkdirSync(TEST_REGISTRY_DIR, { recursive: true });\n}\n\ndescribe(\"process manager tool\", () => {\n\tbeforeEach(() => {\n\t\tcleanRegistry();\n\t});\n\n\tafterEach(() => {\n\t\tcleanRegistry();\n\t});\n\n\tfunction mockOps(stdoutResponses: string[]): ProcessManagerOperations {\n\t\tlet callIndex = 0;\n\t\treturn {\n\t\t\texecPowerShell: async (_command, _cwd, _opts) => {\n\t\t\t\tconst stdout = stdoutResponses[callIndex] ?? \"\";\n\t\t\t\tcallIndex++;\n\t\t\t\treturn { exitCode: 0, stdout, stderr: \"\" };\n\t\t\t},\n\t\t};\n\t}\n\n\tit(\"list returns empty when no processes\", async () => {\n\t\tconst ops = mockOps([]);\n\t\tconst tool = createProcessManagerTool(process.cwd(), { operations: ops });\n\t\tconst result = await tool.execute(\"call_1\", { action: \"list\" as const });\n\t\tconst text = result.content[0]?.type === \"text\" ? result.content[0].text : \"\";\n\t\texpect(text).toBe(\"No managed processes.\");\n\t});\n\n\tit(\"start fails when missing command\", async () => {\n\t\tconst ops = mockOps([]);\n\t\tconst tool = createProcessManagerTool(process.cwd(), { operations: ops });\n\t\tawait expect(\n\t\t\ttool.execute(\"call_2\", { action: \"start\" as const, command: undefined as unknown as string }),\n\t\t).rejects.toThrow(\"command is required\");\n\t});\n\n\tit(\"start registers a process and returns run ID and PID\", async () => {\n\t\t// Responses: start script, alive check, port check, tree verify\n\t\tconst ops = mockOps([\"PID:4242\\n\", \"ALIVE\\n\", \"PID:4242\\n\", \"TREE_OK\\n\"]);\n\n\t\tconst tool = createProcessManagerTool(process.cwd(), { operations: ops });\n\t\tconst result = await tool.execute(\"call_3\", {\n\t\t\taction: \"start\" as const,\n\t\t\tcommand: \"node -e \\\"require('http').createServer((_,r)=>r.end('ok')).listen(0)\\\"\",\n\t\t\texpectedPort: 3000,\n\t\t\treadyTimeout: 5,\n\t\t});\n\n\t\tconst text = result.content[0]?.type === \"text\" ? result.content[0].text : \"\";\n\t\texpect(text).toContain(\"Process started successfully\");\n\t\texpect(text).toContain(\"Root PID: 4242\");\n\t\texpect(text).toContain(\"Run ID:\");\n\t\texpect(text).toContain(\"Listener PID: 4242\");\n\t\texpect(text).toContain(\"Port: 3000\");\n\t});\n\n\tit(\"start fails when process does not bind the expected port within timeout\", async () => {\n\t\tconst responses: string[] = [\"PID:9999\\n\"];\n\t\tfor (let i = 0; i < 20; i++) {\n\t\t\tresponses.push(\"ALIVE\\n\");\n\t\t\tresponses.push(\"NONE\\n\");\n\t\t}\n\t\tconst ops = mockOps(responses);\n\n\t\tconst tool = createProcessManagerTool(process.cwd(), { operations: ops });\n\t\tawait expect(\n\t\t\ttool.execute(\"call_4\", {\n\t\t\t\taction: \"start\" as const,\n\t\t\t\tcommand: \"some-slow-server.exe\",\n\t\t\t\texpectedPort: 9999,\n\t\t\t\treadyTimeout: 0.5,\n\t\t\t}),\n\t\t).rejects.toThrow(\"did not become ready\");\n\t});\n\n\tit(\"start fails when port is owned by a foreign process (conflict)\", async () => {\n\t\tconst ops = mockOps([\"PID:1000\\n\", \"ALIVE\\n\", \"PID:5555\\n\", \"TREE_NOT_FOUND\\n\"]);\n\n\t\tconst tool = createProcessManagerTool(process.cwd(), { operations: ops });\n\t\tawait expect(\n\t\t\ttool.execute(\"call_5\", {\n\t\t\t\taction: \"start\" as const,\n\t\t\t\tcommand: \"my-server.exe\",\n\t\t\t\texpectedPort: 8080,\n\t\t\t\treadyTimeout: 5,\n\t\t\t}),\n\t\t).rejects.toThrow(\"conflict\");\n\t});\n\n\tit(\"status reports process info\", async () => {\n\t\tconst startOps = mockOps([\"PID:7777\\n\", \"ALIVE\\n\"]);\n\t\tconst tool = createProcessManagerTool(process.cwd(), { operations: startOps });\n\t\tconst startResult = await tool.execute(\"call_6\", {\n\t\t\taction: \"start\" as const,\n\t\t\tcommand: \"test-server.exe\",\n\t\t});\n\n\t\tconst startText = startResult.content[0]?.type === \"text\" ? startResult.content[0].text : \"\";\n\t\tconst runIdMatch = startText.match(/Run ID: ([a-f0-9]+)/);\n\t\texpect(runIdMatch).not.toBeNull();\n\t\tconst runId = runIdMatch![1];\n\n\t\tconst statusOps = mockOps([\"ALIVE\\n\"]);\n\t\tconst statusTool = createProcessManagerTool(process.cwd(), { operations: statusOps });\n\t\tconst statusResult = await statusTool.execute(\"call_7\", {\n\t\t\taction: \"status\" as const,\n\t\t\trunId,\n\t\t});\n\n\t\tconst statusText = statusResult.content[0]?.type === \"text\" ? statusResult.content[0].text : \"\";\n\t\texpect(statusText).toContain(`Run ID: ${runId}`);\n\t\texpect(statusText).toContain(\"Status: running\");\n\t\texpect(statusText).toContain(\"Root PID: 7777\");\n\t});\n\n\tit(\"status reports stopped when process is dead\", async () => {\n\t\tconst startOps = mockOps([\"PID:8888\\n\", \"ALIVE\\n\"]);\n\t\tconst tool = createProcessManagerTool(process.cwd(), { operations: startOps });\n\t\tconst startResult = await tool.execute(\"call_8\", {\n\t\t\taction: \"start\" as const,\n\t\t\tcommand: \"ephemeral.exe\",\n\t\t});\n\n\t\tconst startText = startResult.content[0]?.type === \"text\" ? startResult.content[0].text : \"\";\n\t\tconst runIdMatch = startText.match(/Run ID: ([a-f0-9]+)/);\n\t\tconst runId = runIdMatch![1];\n\n\t\tconst statusOps = mockOps([\"DEAD\\n\"]);\n\t\tconst statusTool = createProcessManagerTool(process.cwd(), { operations: statusOps });\n\t\tconst statusResult = await statusTool.execute(\"call_9\", {\n\t\t\taction: \"status\" as const,\n\t\t\trunId,\n\t\t});\n\n\t\tconst statusText = statusResult.content[0]?.type === \"text\" ? statusResult.content[0].text : \"\";\n\t\texpect(statusText).toContain(\"Status: stopped\");\n\t});\n\n\tit(\"stop kills a running process\", async () => {\n\t\tconst startOps = mockOps([\"PID:4444\\n\", \"ALIVE\\n\"]);\n\t\tconst tool = createProcessManagerTool(process.cwd(), { operations: startOps });\n\t\tconst startResult = await tool.execute(\"call_10\", {\n\t\t\taction: \"start\" as const,\n\t\t\tcommand: \"killable.exe\",\n\t\t});\n\n\t\tconst startText = startResult.content[0]?.type === \"text\" ? startResult.content[0].text : \"\";\n\t\tconst runIdMatch = startText.match(/Run ID: ([a-f0-9]+)/);\n\t\tconst runId = runIdMatch![1];\n\n\t\tconst stopOps = mockOps([\"ALIVE\\n\", \"STOP_OK\\n\"]);\n\t\tconst stopTool = createProcessManagerTool(process.cwd(), { operations: stopOps });\n\t\tconst stopResult = await stopTool.execute(\"call_11\", {\n\t\t\taction: \"stop\" as const,\n\t\t\trunId,\n\t\t});\n\n\t\tconst stopText = stopResult.content[0]?.type === \"text\" ? stopResult.content[0].text : \"\";\n\t\texpect(stopText).toContain(\"stopped successfully\");\n\t});\n\n\tit(\"stop reports already stopped for dead process\", async () => {\n\t\tconst startOps = mockOps([\"PID:3333\\n\", \"ALIVE\\n\"]);\n\t\tconst tool = createProcessManagerTool(process.cwd(), { operations: startOps });\n\t\tconst startResult = await tool.execute(\"call_12\", {\n\t\t\taction: \"start\" as const,\n\t\t\tcommand: \"short-lived.exe\",\n\t\t});\n\n\t\tconst startText = startResult.content[0]?.type === \"text\" ? startResult.content[0].text : \"\";\n\t\tconst runIdMatch = startText.match(/Run ID: ([a-f0-9]+)/);\n\t\tconst runId = runIdMatch![1];\n\n\t\tconst stopOps = mockOps([\"DEAD\\n\"]);\n\t\tconst stopTool = createProcessManagerTool(process.cwd(), { operations: stopOps });\n\t\tconst stopResult = await stopTool.execute(\"call_13\", {\n\t\t\taction: \"stop\" as const,\n\t\t\trunId,\n\t\t});\n\n\t\tconst stopText = stopResult.content[0]?.type === \"text\" ? stopResult.content[0].text : \"\";\n\t\texpect(stopText).toContain(\"already stopped\");\n\t});\n\n\tit(\"fails for unknown runId\", async () => {\n\t\tconst ops = mockOps([]);\n\t\tconst tool = createProcessManagerTool(process.cwd(), { operations: ops });\n\t\tawait expect(tool.execute(\"call_14\", { action: \"status\" as const, runId: \"nonexistent\" })).rejects.toThrow(\n\t\t\t\"No process found\",\n\t\t);\n\t});\n\n\tit(\"rejects unknown action\", async () => {\n\t\tconst ops = mockOps([]);\n\t\tconst tool = createProcessManagerTool(process.cwd(), { operations: ops });\n\t\tawait expect(tool.execute(\"call_15\", { action: \"invalid\" as unknown as \"start\" })).rejects.toThrow(\n\t\t\t\"Unknown action\",\n\t\t);\n\t});\n\n\tit(\"is registered in codingTools\", async () => {\n\t\tconst { codingTools } = await import(\"./index.js\");\n\t\tconst names = codingTools.map((t: { name: string }) => t.name);\n\t\texpect(names).toContain(\"process_manager\");\n\t});\n\n\tit(\"is registered in allTools\", async () => {\n\t\tconst { allTools } = await import(\"./index.js\");\n\t\texpect(allTools).toHaveProperty(\"process_manager\");\n\t});\n});\n"]}