refactor: gpt 스트림 오류 최소화

This commit is contained in:
donghyeon-ka
2026-09-17 15:34:01 +09:00
parent d7ceca39a0
commit 0a8c8e55a8
11 changed files with 957 additions and 29 deletions
+158
View File
@@ -0,0 +1,158 @@
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);
});
});