import { mkdtemp, readFile, rm } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { ManagedTaskManager } from "../src/managed-task-manager.js"; import { ProcessManager } from "../src/process-manager.js"; function createProcessManager(): ProcessManager { return new ProcessManager({ maxRetainedOutputBytes: 32 * 1024, processRetentionMs: 60_000, maxProcesses: 16, defaultMaxOutputBytes: 64 * 1024, }); } describe("ManagedTaskManager", () => { let temporaryDirectory: string; let processManager: ProcessManager; let tasks: ManagedTaskManager; beforeEach(async () => { temporaryDirectory = await mkdtemp(path.join(os.tmpdir(), "cokacremote-managed-task-test-")); processManager = createProcessManager(); tasks = new ManagedTaskManager(processManager, { logDirectory: path.join(temporaryDirectory, "logs"), defaultTailBytes: 4096, maxTailBytes: 16 * 1024, }); }); afterEach(async () => { await processManager.shutdown(); await rm(temporaryDirectory, { recursive: true, force: true }); }); it("reuses a retained task key instead of executing the same work twice", async () => { const command = "printf run >> marker.txt; sleep 0.2; printf 'done\\n'"; const request = { taskKey: "duplicate-protected", executable: "/bin/bash", args: ["-c", command], commandForDisplay: command, cwd: temporaryDirectory, initialWaitMs: 0, }; const first = await tasks.start(request); const second = await tasks.start(request); expect(first.reused).toBe(false); expect(second.reused).toBe(true); expect(second.sessionId).toBe(first.sessionId); await processManager.waitForExit(first.sessionId, 2000); const completed = await tasks.read("duplicate-protected"); expect(completed).toMatchObject({ running: false, completed: true, exitCode: 0 }); expect(await readFile(path.join(temporaryDirectory, "marker.txt"), "utf8")).toBe("run"); }); it("stores complete output while returning only a bounded diagnostic tail", async () => { const command = "node -e \"process.stdout.write('x'.repeat(100000)); process.stderr.write('\\nERROR final-marker\\n')\""; const started = await tasks.start({ taskKey: "large-output", executable: "/bin/bash", args: ["-c", command], commandForDisplay: command, cwd: temporaryDirectory, initialWaitMs: 2000, }); expect(started.completed).toBe(true); const result = await tasks.read("large-output", { tailBytes: 4096 }); const completeLog = await readFile(result.logPath, "utf8"); expect(completeLog.length).toBeGreaterThan(100_000); expect(result.tail.length).toBeLessThanOrEqual(4096); expect(result.tail).toContain("ERROR final-marker"); expect(result.importantLines).toEqual( expect.arrayContaining([expect.stringContaining("ERROR final-marker")]), ); }); it("keeps important lines from the full log even when they fall outside the returned tail", async () => { const command = "node -e \"console.log('ERROR early-marker'); process.stdout.write('x'.repeat(100000))\""; await tasks.start({ taskKey: "early-error", executable: "/bin/bash", args: ["-c", command], commandForDisplay: command, cwd: temporaryDirectory, initialWaitMs: 2000, }); const result = await tasks.read("early-error", { tailBytes: 4096 }); expect(result.tail).not.toContain("ERROR early-marker"); expect(result.importantLines).toEqual( expect.arrayContaining([expect.stringContaining("ERROR early-marker")]), ); }); it("reuses completed work by default and reruns only when explicitly requested", async () => { const command = "printf x >> restart-marker.txt"; const request = { taskKey: "restartable", executable: "/bin/bash", args: ["-c", command], commandForDisplay: command, cwd: temporaryDirectory, initialWaitMs: 2000, }; const first = await tasks.start(request); const reused = await tasks.start(request); const restarted = await tasks.start({ ...request, restartCompleted: true }); expect(first.completed).toBe(true); expect(reused).toMatchObject({ reused: true, sessionId: first.sessionId }); expect(restarted.reused).toBe(false); expect(restarted.sessionId).not.toBe(first.sessionId); expect(await readFile(path.join(temporaryDirectory, "restart-marker.txt"), "utf8")).toBe( "xx", ); }); it("lists tasks by stable key and can cancel a running task", async () => { const command = "printf started; sleep 10"; const started = await tasks.start({ taskKey: "recover-me", executable: "/bin/bash", args: ["-c", command], commandForDisplay: command, cwd: temporaryDirectory, initialWaitMs: 0, }); expect(await tasks.list()).toEqual( expect.arrayContaining([ expect.objectContaining({ taskKey: "recover-me", sessionId: started.sessionId, running: true, }), ]), ); await tasks.cancel("recover-me", "SIGTERM", 20); await processManager.waitForExit(started.sessionId, 2000); const cancelled = await tasks.read("recover-me"); expect(cancelled.running).toBe(false); expect(cancelled.completed).toBe(true); }); });