fix(coding-agent): use async operations in tools

This commit is contained in:
Mario Zechner
2026-05-23 09:48:51 +02:00
parent 9b62f1f87c
commit e9146a5ff7
17 changed files with 706 additions and 398 deletions

View File

@@ -10,6 +10,18 @@ function delay(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function createDeferred(): { promise: Promise<void>; resolve: () => void } {
let resolve!: () => void;
const promise = new Promise<void>((promiseResolve) => {
resolve = promiseResolve;
});
return { promise, resolve };
}
async function resolvesWithin(promise: Promise<unknown>, ms: number): Promise<boolean> {
return Promise.race([promise.then(() => true), delay(ms).then(() => false)]);
}
const tempDirs: string[] = [];
async function createTempDir(): Promise<string> {
@@ -160,4 +172,103 @@ describe("built-in edit and write tools", () => {
const content = await readFile(filePath, "utf8");
expect(content).toBe("replacement\n");
});
it("keeps write queue locked while an aborted write is still in flight", async () => {
const dir = await createTempDir();
const filePath = join(dir, "abort-write.txt");
const firstWriteStarted = createDeferred();
const finishFirstWrite = createDeferred();
const secondWriteStarted = createDeferred();
let firstWriteSettled = false;
const writeTool = createWriteTool(dir, {
operations: {
mkdir: async () => {},
writeFile: async (path, content) => {
if (content === "first\n") {
firstWriteStarted.resolve();
await finishFirstWrite.promise;
await writeFile(path, content, "utf8");
firstWriteSettled = true;
return;
}
if (content === "second\n") {
expect(firstWriteSettled).toBe(true);
secondWriteStarted.resolve();
}
await writeFile(path, content, "utf8");
},
},
});
const controller = new AbortController();
const firstWrite = writeTool.execute("call-1", { path: filePath, content: "first\n" }, controller.signal);
await firstWriteStarted.promise;
controller.abort();
const secondWrite = writeTool.execute("call-2", { path: filePath, content: "second\n" });
expect(await resolvesWithin(secondWriteStarted.promise, 20)).toBe(false);
finishFirstWrite.resolve();
await expect(firstWrite).rejects.toThrow("Operation aborted");
await secondWrite;
const content = await readFile(filePath, "utf8");
expect(content).toBe("second\n");
});
it("keeps edit queue locked while an aborted edit write is still in flight", async () => {
const dir = await createTempDir();
const filePath = join(dir, "abort-edit.txt");
await writeFile(filePath, "alpha\nbeta\n", "utf8");
const firstWriteStarted = createDeferred();
const finishFirstWrite = createDeferred();
const secondWriteStarted = createDeferred();
let firstWriteSettled = false;
const editTool = createEditTool(dir, {
operations: {
access,
readFile,
writeFile: async (path, content) => {
if (content === "ALPHA\nbeta\n") {
firstWriteStarted.resolve();
await finishFirstWrite.promise;
await writeFile(path, content, "utf8");
firstWriteSettled = true;
return;
}
if (content === "ALPHA\nBETA\n" || content === "alpha\nBETA\n") {
expect(firstWriteSettled).toBe(true);
secondWriteStarted.resolve();
}
await writeFile(path, content, "utf8");
},
},
});
const controller = new AbortController();
const firstEdit = editTool.execute(
"call-1",
{ path: filePath, edits: [{ oldText: "alpha", newText: "ALPHA" }] },
controller.signal,
);
await firstWriteStarted.promise;
controller.abort();
const secondEdit = editTool.execute("call-2", {
path: filePath,
edits: [{ oldText: "beta", newText: "BETA" }],
});
expect(await resolvesWithin(secondWriteStarted.promise, 20)).toBe(false);
finishFirstWrite.resolve();
await expect(firstEdit).rejects.toThrow("Operation aborted");
await secondEdit;
const content = await readFile(filePath, "utf8");
expect(content).toBe("ALPHA\nBETA\n");
});
});