Files
harness-tools/extensions/bash-cwd/index.test.ts
T

161 lines
4.6 KiB
TypeScript

import assert from "node:assert/strict";
import { execFileSync } from "node:child_process";
import {
access,
chmod,
mkdir,
mkdtemp,
rm,
writeFile,
} from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import test from "node:test";
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
import { createContextualBashTool } from "./index.ts";
function git(cwd: string, ...args: string[]): void {
execFileSync("git", args, { cwd, stdio: "pipe" });
}
function getOutput(
result: Awaited<ReturnType<ReturnType<typeof createContextualBashTool>["execute"]>>,
): string {
return result.content
.filter((item) => item.type === "text")
.map((item) => item.text)
.join("")
.trim();
}
async function executeIn(
sessionCwd: string,
command: string,
cwd?: string,
) {
const params = cwd === undefined ? { command } : { command, cwd };
return createContextualBashTool({}).execute(
"bash-cwd-test",
params,
undefined,
undefined,
{
cwd: sessionCwd,
sessionManager: {
getSessionId: () => "bash-cwd-test-session",
getSessionFile: () => undefined,
},
} as ExtensionContext,
);
}
test("bash honors hidden-worktree and nested-package cwd arguments", async (t) => {
const repository = await mkdtemp(join(tmpdir(), "pi-bash-cwd-"));
t.after(() => rm(repository, { recursive: true, force: true }));
await writeFile(join(repository, ".gitignore"), ".worktrees/\n");
await mkdir(join(repository, "extensions", "fixture"), { recursive: true });
await writeFile(
join(repository, "extensions", "fixture", "package.json"),
'{"private":true}\n',
);
git(repository, "init", "--initial-branch=main");
git(repository, "config", "user.name", "Bash cwd test");
git(repository, "config", "user.email", "bash-cwd@example.invalid");
git(repository, "add", ".");
git(repository, "commit", "-m", "fixture");
const worktree = join(repository, ".worktrees", "feature");
git(repository, "worktree", "add", "-b", "feature", worktree, "main");
const packageDirectory = join(worktree, "extensions", "fixture");
assert.equal(getOutput(await executeIn(repository, "pwd")), repository);
assert.equal(
getOutput(await executeIn(repository, 'printf %s "$PI_SESSION_ID"')),
"bash-cwd-test-session",
);
assert.equal(getOutput(await executeIn(repository, "pwd", worktree)), worktree);
assert.equal(
getOutput(
await executeIn(
repository,
"pwd",
".worktrees/feature/extensions/fixture",
),
),
packageDirectory,
);
const missingDirectory = join(worktree, "missing");
const sentinel = join(repository, "should-not-run");
await assert.rejects(
executeIn(repository, "touch should-not-run", missingDirectory),
/Working directory does not exist/,
);
await assert.rejects(access(sentinel));
});
test("bash preserves configured command prefixes and shell paths", async (t) => {
const directory = await mkdtemp(join(tmpdir(), "pi-bash-cwd-options-"));
t.after(() => rm(directory, { recursive: true, force: true }));
const shellMarker = join(directory, "custom-shell-used");
const shell = join(directory, "custom-shell");
await writeFile(
shell,
`#!/bin/sh\nprintf used > "${shellMarker}"\nexec /bin/bash "$@"\n`,
);
await chmod(shell, 0o755);
await mkdir(join(directory, ".pi"));
await writeFile(
join(directory, ".pi", "settings.json"),
JSON.stringify({
shellCommandPrefix: "printf prefix-",
shellPath: shell,
}),
);
const tool = createContextualBashTool();
const result = await tool.execute(
"bash-cwd-options-test",
{ command: "printf command", cwd: directory },
undefined,
undefined,
{
cwd: directory,
sessionManager: {
getSessionId: () => "bash-cwd-test-session",
getSessionFile: () => undefined,
},
isProjectTrusted: () => true,
} as ExtensionContext,
);
assert.equal(getOutput(result), "prefix-command");
await access(shellMarker);
});
test("bash exposes cwd without changing existing arguments", () => {
const schema = createContextualBashTool({}).parameters;
assert.deepEqual(Object.keys(schema.properties), ["command", "cwd", "timeout"]);
assert.deepEqual(schema.required, ["command"]);
});
test("bash fails before execution when Pi omits the tool context", async () => {
const tool = createContextualBashTool({});
await assert.rejects(
tool.execute(
"bash-cwd-test",
{ command: "pwd" },
undefined,
undefined,
undefined as unknown as ExtensionContext,
),
/Bash execution context is unavailable/,
);
});