Files
oh-my-pi/packages/coding-agent/test/tools/root-path-alias.test.ts
T
can1357 ea9c46262f feat(coding-agent): introduced /force command and ToolChoiceQueue for directive management
- Added `/force` slash command and `ToolChoiceQueue` system for managing tool-choice directives with lifecycle callbacks and requeue semantics.
- Added `setForcedToolChoice()`, `peekQueueInvoker()`, `buildToolChoice()`, `steer()`, and `getToolChoiceQueue()` methods to AgentSession and ToolSession APIs.
- Refactored tool-choice override mechanism from simple override to queue-based system with generator directives, lifecycle callbacks, and requeue preservation.
- Removed `PendingActionStore` class and replaced with `ToolChoiceQueue`; updated `ResolveTool` and custom tool loader to use queue invokers.
- Fixed tool-choice queue cleanup on agent loop abort and requeue semantics to preserve callbacks across abort cycles.
2026-04-08 22:10:37 +02:00

150 lines
4.9 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it } from "bun:test";
import * as fs from "node:fs/promises";
import * as os from "node:os";
import * as path from "node:path";
import { Settings } from "@oh-my-pi/pi-coding-agent/config/settings";
import { ToolChoiceQueue } from "@oh-my-pi/pi-coding-agent/session/tool-choice-queue";
import { createTools, type ToolSession } from "@oh-my-pi/pi-coding-agent/tools";
import { resolveToCwd } from "@oh-my-pi/pi-coding-agent/tools/path-utils";
function createTestSession(cwd: string, overrides: Partial<ToolSession> = {}): ToolSession {
return {
cwd,
hasUI: false,
getSessionFile: () => null,
getSessionSpawns: () => "*",
settings: Settings.isolated(),
...overrides,
};
}
function getText(result: { content: Array<{ type: string; text?: string }> }): string {
return result.content
.filter(entry => entry.type === "text")
.map(entry => entry.text ?? "")
.join("\n");
}
describe("tool path root alias", () => {
let tempDir: string;
beforeEach(async () => {
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "tool-root-alias-"));
await Bun.write(path.join(tempDir, "search.txt"), "root-alias-needle\n");
await Bun.write(
path.join(tempDir, "sample.ts"),
"const rootAliasSymbol = 1;\nlegacyWrap(rootAliasSymbol, anotherValue);\n",
);
});
afterEach(async () => {
await fs.rm(tempDir, { recursive: true, force: true });
});
it("resolves a bare slash to the session cwd", () => {
expect(resolveToCwd("/", tempDir)).toBe(tempDir);
expect(resolveToCwd("///", tempDir)).toBe(tempDir);
});
it("greps from cwd when path is slash", async () => {
const tools = await createTools(createTestSession(tempDir));
const tool = tools.find(entry => entry.name === "grep");
expect(tool).toBeDefined();
if (!tool) throw new Error("Missing grep tool");
const result = await tool.execute("grep-root-alias", {
pattern: "root-alias-needle",
path: "/",
});
const details = result.details as { scopePath?: string } | undefined;
expect(getText(result)).toContain("search.txt");
expect(details?.scopePath).toBe(".");
});
it("reads cwd when path is slash", async () => {
const tools = await createTools(createTestSession(tempDir));
const tool = tools.find(entry => entry.name === "read");
expect(tool).toBeDefined();
if (!tool) throw new Error("Missing read tool");
const result = await tool.execute("read-root-alias", {
path: "/",
});
const text = getText(result);
expect(text).toContain("search.txt");
expect(text).toContain("sample.ts");
});
it("finds from cwd when pattern is slash", async () => {
const tools = await createTools(createTestSession(tempDir));
const tool = tools.find(entry => entry.name === "find");
expect(tool).toBeDefined();
if (!tool) throw new Error("Missing find tool");
const result = await tool.execute("find-root-alias", {
pattern: "/",
});
const details = result.details as { scopePath?: string } | undefined;
const text = getText(result);
expect(details?.scopePath).toBe(".");
expect(text).toContain("search.txt");
expect(text).toContain("sample.ts");
});
it("ast_grep searches cwd when path is slash", async () => {
const tools = await createTools(createTestSession(tempDir));
const tool = tools.find(entry => entry.name === "ast_grep");
expect(tool).toBeDefined();
if (!tool) throw new Error("Missing ast_grep tool");
const result = await tool.execute("ast-grep-root-alias", {
pat: ["rootAliasSymbol"],
sel: "identifier",
lang: "typescript",
path: "/",
glob: "**/*.ts",
});
const details = result.details as { scopePath?: string } | undefined;
expect(getText(result)).toContain("sample.ts");
expect(details?.scopePath).toBe(".");
});
it("ast_edit rewrites within cwd when path is slash", async () => {
const queue = new ToolChoiceQueue();
const tools = await createTools(
createTestSession(tempDir, {
getToolChoiceQueue: () => queue,
buildToolChoice: () => ({ type: "tool" as const, name: "resolve" }),
steer: () => {},
}),
);
const tool = tools.find(entry => entry.name === "ast_edit");
expect(tool).toBeDefined();
if (!tool) throw new Error("Missing ast_edit tool");
const preview = await tool.execute("ast-edit-root-alias", {
ops: [{ pat: "legacyWrap($A, $B)", out: "modernWrap($A, $B)" }],
lang: "typescript",
path: "/",
glob: "**/*.ts",
});
const details = preview.details as { scopePath?: string; totalReplacements?: number } | undefined;
expect(getText(preview)).toContain("sample.ts");
expect(details?.scopePath).toBe(".");
expect(details?.totalReplacements).toBe(1);
queue.nextToolChoice();
const invoker = queue.peekInFlightInvoker()!;
expect(invoker).toBeDefined();
await invoker({ action: "apply", reason: "apply root alias rewrite" });
expect(await Bun.file(path.join(tempDir, "sample.ts")).text()).toContain(
"modernWrap(rootAliasSymbol, anotherValue)",
);
});
});