{"version":3,"file":"jobs.test.d.ts","sourceRoot":"","sources":["../../../src/core/jobs/jobs.test.ts"],"names":[],"mappings":"","sourcesContent":["import { mkdtemp, readdir } from \"node:fs/promises\";\nimport { tmpdir } from \"node:os\";\nimport nodePath from \"node:path\";\nimport { afterEach, beforeEach, describe, expect, it } from \"vitest\";\nimport { BackgroundJobRegistry } from \"./index.js\";\n\nlet dir: string;\n\nbeforeEach(async () => {\n\tdir = await mkdtemp(nodePath.join(tmpdir(), \"jensen-jobs-test-\"));\n});\n\nafterEach(async () => {\n\t// no-op\n});\n\nconst sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));\n\ndescribe(\"background job registry\", () => {\n\tit(\"registers a durable record and starts the process\", async () => {\n\t\tconst reg = new BackgroundJobRegistry({ storageDir: dir, workspaceId: \"w\", ownerRunId: \"r\" });\n\t\tawait reg.init();\n\t\tconst rec = await reg.start({\n\t\t\texecutable: process.execPath,\n\t\t\targs: [\"-e\", \"setTimeout(()=>{}, 5000)\"],\n\t\t\tcwd: dir,\n\t\t});\n\t\texpect(rec.state).toBe(\"running\");\n\t\texpect(rec.jobId).toMatch(/^job-/);\n\t\texpect(Number(rec.processIdentity)).toBeGreaterThan(0);\n\t\t// Durable on disk\n\t\texpect(await readdir(nodePath.join(dir, \"jobs\"))).not.toHaveLength(0);\n\t\tawait reg.stop(rec.jobId);\n\t\tawait reg.shutdown();\n\t});\n\n\tit(\"status classifies a live running job as alive with matching identity\", async () => {\n\t\tconst reg = new BackgroundJobRegistry({ storageDir: dir, workspaceId: \"w\" });\n\t\tawait reg.init();\n\t\tconst rec = await reg.start({ executable: process.execPath, args: [\"-e\", \"setTimeout(()=>{}, 5000)\"], cwd: dir });\n\t\tconst status = await reg.status(rec.jobId);\n\t\texpect(status?.kind).toBe(\"recorded_running_and_alive\");\n\t\tawait reg.stop(rec.jobId);\n\t});\n\n\tit(\"classifies a missing process as recorded_running_but_missing\", async () => {\n\t\tconst reg = new BackgroundJobRegistry({ storageDir: dir, workspaceId: \"w\" });\n\t\tawait reg.init();\n\t\tconst rec = await reg.start({ executable: process.execPath, args: [\"-e\", \"process.exit(0)\"], cwd: dir });\n\t\t// Wait for it to exit.\n\t\tawait sleep(600);\n\t\t// Force state to running (simulate stale durable state) then check.\n\t\tconst forced = { ...rec, state: \"running\" as const };\n\t\tconst { writeFile } = await import(\"node:fs/promises\");\n\t\tawait writeFile(nodePath.join(dir, \"jobs\", `${rec.jobId}.json`), JSON.stringify(forced));\n\t\tconst status = await reg.status(rec.jobId);\n\t\texpect(status?.kind).toBe(\"recorded_running_but_missing\");\n\t});\n\n\tit(\"stops a running process and marks it stopped (idempotent)\", async () => {\n\t\tconst reg = new BackgroundJobRegistry({ storageDir: dir, workspaceId: \"w\" });\n\t\tawait reg.init();\n\t\tconst rec = await reg.start({\n\t\t\texecutable: process.execPath,\n\t\t\targs: [\"-e\", \"setTimeout(()=>{}, 10000)\"],\n\t\t\tcwd: dir,\n\t\t});\n\t\tconst stopped = await reg.stop(rec.jobId);\n\t\texpect(stopped?.state).toBe(\"stopped\");\n\t\tconst again = await reg.stop(rec.jobId);\n\t\texpect(again?.state).toBe(\"stopped\");\n\t\tawait reg.shutdown();\n\t});\n\n\tit(\"preserves restart lineage\", async () => {\n\t\tconst reg = new BackgroundJobRegistry({ storageDir: dir, workspaceId: \"w\" });\n\t\tawait reg.init();\n\t\tconst rec = await reg.start({ executable: process.execPath, args: [\"-e\", \"setTimeout(()=>{}, 5000)\"], cwd: dir });\n\t\tconst restarted = await reg.restart(rec.jobId, \"test\");\n\t\texpect(restarted?.restartCount).toBe(1);\n\t\texpect(restarted?.restarts?.[0]?.previousProcessIdentity).toBe(rec.processIdentity);\n\t\tawait reg.shutdown();\n\t});\n\n\tit(\"bounds log output\", async () => {\n\t\tconst reg = new BackgroundJobRegistry({ storageDir: dir, workspaceId: \"w\" });\n\t\tawait reg.init();\n\t\tconst rec = await reg.start({\n\t\t\texecutable: process.execPath,\n\t\t\targs: [\"-e\", \"console.log('hello world')\"],\n\t\t\tcwd: dir,\n\t\t});\n\t\tawait sleep(600);\n\t\tconst logs = await reg.logs({ jobId: rec.jobId, tailLines: 200, maxBytes: 65536 });\n\t\texpect(logs.stdout).toContain(\"hello world\");\n\t\tawait reg.shutdown();\n\t});\n\n\tit(\"refuses adoption without matching identity evidence\", async () => {\n\t\tconst reg = new BackgroundJobRegistry({ storageDir: dir, workspaceId: \"w\" });\n\t\tawait reg.init();\n\t\tconst rec = await reg.start({ executable: process.execPath, args: [\"-e\", \"setTimeout(()=>{}, 5000)\"], cwd: dir });\n\t\tconst adopted = await reg.adopt(rec.jobId, {\n\t\t\texecutable: \"definitely-not-the-executable\",\n\t\t\targuments: [],\n\t\t\tcwd: dir,\n\t\t\tcommandLine: \"no match\",\n\t\t});\n\t\texpect(adopted).toBeNull();\n\t\tawait reg.stop(rec.jobId);\n\t});\n\n\tit(\"gateStepCompletion blocks unresolved running job\", async () => {\n\t\tconst reg = new BackgroundJobRegistry({ storageDir: dir, workspaceId: \"w\" });\n\t\tawait reg.init();\n\t\tconst rec = await reg.start({ executable: process.execPath, args: [\"-e\", \"setTimeout(()=>{}, 5000)\"], cwd: dir });\n\t\tconst gate = await reg.gateStepCompletion(rec.jobId, \"exited\");\n\t\texpect(gate.canComplete).toBe(false);\n\t\tawait reg.stop(rec.jobId);\n\t\tawait reg.shutdown();\n\t});\n});\n"]}