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
+58
View File
@@ -14,20 +14,24 @@ import { createServices } from "../src/mcp-server.js";
const ALL_TOOLS = [
"apply_patch",
"cancel_managed_task",
"chmod_path",
"copy_path",
"download_file",
"exec_command",
"hash_file",
"list_directory",
"list_managed_tasks",
"list_processes",
"make_directory",
"move_path",
"read_file",
"read_managed_task",
"read_process",
"remove_path",
"replace_in_file",
"run_script",
"start_managed_task",
"stat_path",
"terminate_process",
"upload_file",
@@ -40,20 +44,24 @@ type ToolResult = Awaited<ReturnType<Client["callTool"]>>;
const EXPECTED_ANNOTATIONS = {
apply_patch: [false, true, false, false],
cancel_managed_task: [false, true, false, false],
chmod_path: [false, true, true, false],
copy_path: [false, true, true, false],
download_file: [true, false, true, false],
exec_command: [false, true, false, true],
hash_file: [true, false, true, false],
list_directory: [true, false, true, false],
list_managed_tasks: [true, false, true, false],
list_processes: [true, false, true, false],
make_directory: [false, false, true, false],
move_path: [false, true, true, false],
read_file: [true, false, true, false],
read_managed_task: [true, false, true, false],
read_process: [true, false, true, false],
remove_path: [false, true, true, false],
replace_in_file: [false, true, false, false],
run_script: [false, true, false, true],
start_managed_task: [false, true, false, true],
stat_path: [true, false, true, false],
terminate_process: [false, true, false, false],
upload_file: [false, true, true, false],
@@ -336,6 +344,56 @@ describe.sequential("all registered MCP tools", () => {
waitMs: 2000,
});
expect(terminated).toMatchObject({ running: false, completed: true, signal: "SIGTERM" });
const managed = await callOk("start_managed_task", {
taskKey: "e2e-managed-task",
cmd: "printf 'managed-start\n'; sleep 0.1; printf 'BUILD SUCCESSFUL\n'",
workdir: testRoot,
initialWaitMs: 0,
});
expect(managed).toMatchObject({
taskKey: "e2e-managed-task",
running: true,
reused: false,
});
const reusedManaged = await callOk("start_managed_task", {
taskKey: "e2e-managed-task",
cmd: "printf 'managed-start\n'; sleep 0.1; printf 'BUILD SUCCESSFUL\n'",
workdir: testRoot,
initialWaitMs: 0,
});
expect(reusedManaged).toMatchObject({
sessionId: managed.sessionId,
reused: true,
});
const listedManaged = await callOk("list_managed_tasks");
expect(listedManaged.tasks).toEqual(
expect.arrayContaining([
expect.objectContaining({ taskKey: "e2e-managed-task" }),
]),
);
const finishedManaged = await callOk("read_managed_task", {
taskKey: "e2e-managed-task",
waitMs: 3000,
});
expect(finishedManaged).toMatchObject({
completed: true,
status: "succeeded",
exitCode: 0,
});
expect(String(finishedManaged.tail)).toContain("BUILD SUCCESSFUL");
await callOk("start_managed_task", {
taskKey: "e2e-managed-cancel",
cmd: "sleep 10",
workdir: testRoot,
initialWaitMs: 0,
});
await callOk("cancel_managed_task", {
taskKey: "e2e-managed-cancel",
signal: "SIGTERM",
graceMs: 1000,
});
}, 30_000);
it("handles text, metadata, listings, permissions, and unified patches", async () => {
+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);
});
});
+4 -4
View File
@@ -37,7 +37,7 @@ describe("tool authentication metadata", () => {
MCP_OAUTH_RESOURCE: "https://mcp.example.com/mcp",
});
expect(tools).toHaveLength(20);
expect(tools).toHaveLength(24);
for (const tool of tools) {
expect(tool._meta, tool.name).toEqual({
securitySchemes: [{ type: "oauth2", scopes: ["mcp:tools"] }],
@@ -48,7 +48,7 @@ describe("tool authentication metadata", () => {
it("does not infer noauth from the internal authentication bypass", async () => {
const tools = await listTools({ MCP_ALLOW_NO_AUTH: "true" });
expect(tools).toHaveLength(20);
expect(tools).toHaveLength(24);
for (const tool of tools) {
expect(tool._meta, tool.name).toBeUndefined();
}
@@ -57,7 +57,7 @@ describe("tool authentication metadata", () => {
it("does not mislabel static bearer authentication as noauth or OAuth", async () => {
const tools = await listTools({ MCP_AUTH_TOKEN: "static-secret" });
expect(tools).toHaveLength(20);
expect(tools).toHaveLength(24);
for (const tool of tools) {
expect(tool._meta, tool.name).toBeUndefined();
}
@@ -81,7 +81,7 @@ describe("client-facing metadata accuracy", () => {
it("describes every tool and every input field", async () => {
const tools = await listTools({ MCP_AUTH_TOKEN: "static-secret" });
expect(tools).toHaveLength(20);
expect(tools).toHaveLength(24);
for (const tool of tools) {
expect(tool.title?.trim().length, tool.name).toBeGreaterThan(0);
expect(tool.description?.trim().length, tool.name).toBeGreaterThan(0);