refactor: gpt 스트림 오류 최소화
This commit is contained in:
@@ -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 () => {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user