159 lines
5.4 KiB
TypeScript
159 lines
5.4 KiB
TypeScript
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);
|
|
});
|
|
});
|