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["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/, ); });