import { describe, expect, it } from "@effect/vitest"; import { BunServices } from "@effect/platform-bun"; import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import * as Effect from "effect/Effect"; import { resolveToolInvocation, sanitizeCliOutputText, shellQuoteArg } from "./tooling"; const withTmp = (body: (dir: string) => Effect.Effect): Effect.Effect => Effect.acquireUseRelease( Effect.sync(() => mkdtempSync(join(tmpdir(), "exec-call-"))), body, (dir) => Effect.sync(() => rmSync(dir, { recursive: true, force: true })), ); describe("resolveToolInvocation", () => { it.effect("reads the JSON arg from a file via @path", () => withTmp((dir) => Effect.gen(function* () { const file = join(dir, "input.json"); writeFileSync(file, '{"title":"Hi","n":2}'); const result = yield* resolveToolInvocation({ rawPathParts: ["github", "issues", "create", `@${file}`], }); expect(result.path).toBe("github.issues.create"); expect(result.args).toEqual({ title: "Hi", n: 2 }); }), ).pipe(Effect.provide(BunServices.layer)), ); it.effect("still accepts inline JSON (slice condition regression)", () => Effect.gen(function* () { const result = yield* resolveToolInvocation({ rawPathParts: ["github", "issues", "create", '{"title":"Hi"}'], }); expect(result.path).toBe("github.issues.create"); expect(result.args).toEqual({ title: "Hi" }); }).pipe(Effect.provide(BunServices.layer)), ); it.effect("treats a path with no trailing JSON as empty args", () => Effect.gen(function* () { const result = yield* resolveToolInvocation({ rawPathParts: ["github", "issues", "list"] }); expect(result.path).toBe("github.issues.list"); expect(result.args).toEqual({}); }).pipe(Effect.provide(BunServices.layer)), ); it.effect("fails with a path-bearing message when the @file is missing", () => Effect.gen(function* () { const error = yield* Effect.flip( resolveToolInvocation({ rawPathParts: ["x", "@/no/such/file.json"] }), ); expect(error.message).toContain("/no/such/file.json"); }).pipe(Effect.provide(BunServices.layer)), ); it.effect("rejects an @file whose content is not a JSON object", () => withTmp((dir) => Effect.gen(function* () { const file = join(dir, "bad.json"); writeFileSync(file, "[1,2,3]"); const error = yield* Effect.flip( resolveToolInvocation({ rawPathParts: ["x", `@${file}`] }), ); expect(error.message).toContain("must contain a JSON object"); }), ).pipe(Effect.provide(BunServices.layer)), ); it.effect("rejects a bare '@' with no path", () => Effect.gen(function* () { const error = yield* Effect.flip(resolveToolInvocation({ rawPathParts: ["x", "@"] })); expect(error.message).toContain("requires a file path"); }).pipe(Effect.provide(BunServices.layer)), ); }); describe("shellQuoteArg", () => { it("quotes single quotes without breaking the shell argument", () => { expect(shellQuoteArg(`{"name":"owner's repo"}`)).toBe(`'{"name":"owner'"'"'s repo"}'`); }); it("leaves simple values readable", () => { expect(shellQuoteArg("exec_123")).toBe("exec_123"); }); }); describe("sanitizeCliOutputText", () => { it("removes terminal control sequences from tool metadata", () => { expect(sanitizeCliOutputText("safe\u001b[2J\u001b]0;title\u0007 text\u0000")).toBe("safe text"); }); it("preserves readable multiline content", () => { expect(sanitizeCliOutputText("type Input = {\n\tname: string\n}")).toBe( "type Input = {\n\tname: string\n}", ); }); });