161 lines
4.6 KiB
TypeScript
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/,
|
|
);
|
|
});
|