summaryrefslogtreecommitdiffhomepage
path: root/packages/tool-write-file/src/write-file.test.ts
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-25 14:06:23 +0900
committerAdam Malczewski <[email protected]>2026-06-25 14:06:23 +0900
commit1ff0eac44cd44751af979c51c746a1774c268e8a (patch)
treebf1c4563595e5b4c23f63e1d5b0782400be7e025 /packages/tool-write-file/src/write-file.test.ts
parent54db4583e66134010375a1fa94256f36034ffdff (diff)
downloaddispatch-1ff0eac44cd44751af979c51c746a1774c268e8a.tar.gz
dispatch-1ff0eac44cd44751af979c51c746a1774c268e8a.zip
feat(ssh): wave 2 — route filesystem/shell tools behind ExecBackend
Wave 2 of transparent SSH support (4 parallel owner-agents on disjoint tool packages). The tools now resolve an ExecBackend per-call from ctx.computerId and call backend.spawn / backend.readFile / etc. instead of node:fs and node:child_process directly — so they are transport-agnostic (local now; remote over SSH later, transparent to the agent). Still LOCAL-ONLY this wave (computerId always undefined -> LocalExecBackend, behavior-identical). - tool-shell: factory takes resolveBackend; execute calls backend.spawn. spawn.ts DELETED (realSpawn was a verbatim duplicate of exec-backend's LocalExecBackend.spawn — logic moved to the sanctioned shared package). manifest dependsOn:[exec-backend]; host.getService at activation. - tool-read-file: readFile/stat/readdir -> backend.* (pure logic untouched; ENOENT .code branches kept). - tool-write-file: exists/stat/writeFile -> backend.* (pure logic untouched). - tool-edit-file: readFile/writeFile -> backend.* + forward-compatible REMOTE diagnostics skip (ctx.computerId set -> skip LSP, return empty — plan §6.1; local path byte-identical to today). LSP lookup stays lazy. - orchestrator: pre-wired @dispatch/exec-backend dep into the 4 tool package.jsons + bun install (build/config, my lane) so isolated verify resolved cleanly; agents added the ../exec-backend tsconfig ref. Verified: tsc -b EXIT 0, biome clean, 1599 vitest pass (was 1592). Refs: notes/ssh-support-plan.md (decisions §0.5/§13). No merge or push.
Diffstat (limited to 'packages/tool-write-file/src/write-file.test.ts')
-rw-r--r--packages/tool-write-file/src/write-file.test.ts32
1 files changed, 21 insertions, 11 deletions
diff --git a/packages/tool-write-file/src/write-file.test.ts b/packages/tool-write-file/src/write-file.test.ts
index 6b316bc..d157eb2 100644
--- a/packages/tool-write-file/src/write-file.test.ts
+++ b/packages/tool-write-file/src/write-file.test.ts
@@ -1,6 +1,7 @@
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
+import { localExecBackend } from "@dispatch/exec-backend";
import { createLogger, type ToolExecuteContext } from "@dispatch/kernel";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { createWriteFileTool, decideOverwrite, validateArgs } from "./write-file.js";
@@ -19,6 +20,15 @@ function stubCtx(overrides?: Partial<ToolExecuteContext>): ToolExecuteContext {
};
}
+/**
+ * Build a write_file tool wired to the real local ExecBackend (node:fs,
+ * behavior-identical to today's inline calls). No `@dispatch/*` mocking — the
+ * real fs edge is exercised, matching the constitution's strict-core rule.
+ */
+function makeTool(workdir: string) {
+ return createWriteFileTool({ resolveBackend: () => localExecBackend, workdir });
+}
+
let workdir: string;
beforeEach(async () => {
@@ -116,7 +126,7 @@ describe("validateArgs", () => {
describe("createWriteFileTool", () => {
it("creates a new file when overwrite is unset and the file is absent", async () => {
- const tool = createWriteFileTool(workdir);
+ const tool = makeTool(workdir);
const result = await tool.execute({ path: "new-file.txt", content: "hello world" }, stubCtx());
expect(result.isError).toBeUndefined();
@@ -128,7 +138,7 @@ describe("createWriteFileTool", () => {
it("errors when the file exists and overwrite is unset", async () => {
await writeFile(join(workdir, "existing.txt"), "old content", "utf8");
- const tool = createWriteFileTool(workdir);
+ const tool = makeTool(workdir);
const result = await tool.execute({ path: "existing.txt", content: "new content" }, stubCtx());
expect(result.isError).toBe(true);
@@ -141,7 +151,7 @@ describe("createWriteFileTool", () => {
it("overwrites an existing file when overwrite is true", async () => {
await writeFile(join(workdir, "existing.txt"), "old content", "utf8");
- const tool = createWriteFileTool(workdir);
+ const tool = makeTool(workdir);
const result = await tool.execute(
{ path: "existing.txt", content: "new content", overwrite: true },
stubCtx(),
@@ -154,7 +164,7 @@ describe("createWriteFileTool", () => {
});
it("errors when overwrite is true but the file is absent", async () => {
- const tool = createWriteFileTool(workdir);
+ const tool = makeTool(workdir);
const result = await tool.execute(
{ path: "nonexistent.txt", content: "data", overwrite: true },
stubCtx(),
@@ -165,7 +175,7 @@ describe("createWriteFileTool", () => {
});
it("errors when the parent directory does not exist", async () => {
- const tool = createWriteFileTool(workdir);
+ const tool = makeTool(workdir);
const result = await tool.execute({ path: "no/such/dir/file.txt", content: "data" }, stubCtx());
expect(result.isError).toBe(true);
@@ -173,12 +183,12 @@ describe("createWriteFileTool", () => {
});
it("concurrencySafe is false", () => {
- const tool = createWriteFileTool(workdir);
+ const tool = makeTool(workdir);
expect(tool.concurrencySafe).toBe(false);
});
it("has correct name and parameters shape", () => {
- const tool = createWriteFileTool(workdir);
+ const tool = makeTool(workdir);
expect(tool.name).toBe("write_file");
expect(tool.parameters.type).toBe("object");
expect(tool.parameters.required).toEqual(["path", "content"]);
@@ -188,7 +198,7 @@ describe("createWriteFileTool", () => {
});
it("never throws on bad input (always returns ToolResult)", async () => {
- const tool = createWriteFileTool(workdir);
+ const tool = makeTool(workdir);
const inputs = [null, undefined, 42, "string", {}, { path: "" }, { path: 123 }];
for (const input of inputs) {
const result = await tool.execute(input, stubCtx());
@@ -200,7 +210,7 @@ describe("createWriteFileTool", () => {
it("respects ctx.cwd over baked workdir", async () => {
const ctxDir = await mkdtemp(join(tmpdir(), "ctx-cwd-test-"));
try {
- const tool = createWriteFileTool(workdir);
+ const tool = makeTool(workdir);
const result = await tool.execute(
{ path: "ctx-file.txt", content: "from ctx" },
stubCtx({ cwd: ctxDir }),
@@ -215,7 +225,7 @@ describe("createWriteFileTool", () => {
});
it("writes empty content", async () => {
- const tool = createWriteFileTool(workdir);
+ const tool = makeTool(workdir);
const result = await tool.execute({ path: "empty.txt", content: "" }, stubCtx());
expect(result.isError).toBeUndefined();
@@ -225,7 +235,7 @@ describe("createWriteFileTool", () => {
it("writes content in subdirectory that exists", async () => {
await mkdir(join(workdir, "sub"));
- const tool = createWriteFileTool(workdir);
+ const tool = makeTool(workdir);
const result = await tool.execute({ path: "sub/file.txt", content: "nested" }, stubCtx());
expect(result.isError).toBeUndefined();