mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-08-28 08:35:57 +00:00
feat: enable permission-aware subagents
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
/**
|
||||
* boot-extension.ts — the mock `pi` / `ctx` pair the wiring tests use to boot the
|
||||
* REAL extension (src/index.ts) and drive its registered tools directly.
|
||||
*
|
||||
* The 14 pre-existing wiring tests each inline their own near-identical copy of
|
||||
* this. They are deliberately NOT migrated: `test/` is outside the tsconfig
|
||||
* `include`, so that churn would be unchecked by `tsc`, and those copies have
|
||||
* small divergences that would have to be reconciled blind. New files use this.
|
||||
*/
|
||||
|
||||
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { vi } from "vitest";
|
||||
|
||||
export interface BootedPi {
|
||||
pi: any;
|
||||
tools: Map<string, any>;
|
||||
lifecycle: Map<string, any>;
|
||||
}
|
||||
|
||||
/** A mock ExtensionAPI that records every tool and lifecycle handler registered. */
|
||||
export function makePi(): BootedPi {
|
||||
const tools = new Map<string, any>();
|
||||
const lifecycle = new Map<string, any>();
|
||||
const pi = {
|
||||
registerMessageRenderer: vi.fn(),
|
||||
registerTool: vi.fn((t: any) => tools.set(t.name, t)),
|
||||
registerCommand: vi.fn(),
|
||||
on: vi.fn((event: string, handler: any) => lifecycle.set(event, handler)),
|
||||
events: {
|
||||
emit: vi.fn(),
|
||||
on: vi.fn(() => vi.fn()),
|
||||
},
|
||||
appendEntry: vi.fn(),
|
||||
sendMessage: vi.fn(),
|
||||
} as any;
|
||||
return { pi, tools, lifecycle };
|
||||
}
|
||||
|
||||
/** A mock ExtensionContext — the second half of what a tool's `execute` receives. */
|
||||
export function ctx(overrides: Record<string, unknown> = {}) {
|
||||
return {
|
||||
// The interactive mode extensions normally run in. Set explicitly because
|
||||
// the `@handle` input hook is TUI-only, so an absent mode would make every
|
||||
// mention test exercise the headless fall-through instead.
|
||||
mode: "tui",
|
||||
hasUI: false,
|
||||
ui: { setStatus: vi.fn(), setWidget: vi.fn(), notify: vi.fn(), addAutocompleteProvider: vi.fn() },
|
||||
cwd: process.cwd(),
|
||||
model: undefined,
|
||||
modelRegistry: { find: vi.fn(), getAvailable: vi.fn(() => []) },
|
||||
sessionManager: { getSessionId: vi.fn(() => "s1"), getBranch: vi.fn(() => []) },
|
||||
getSystemPrompt: vi.fn(() => "parent"),
|
||||
...overrides,
|
||||
} as any;
|
||||
}
|
||||
|
||||
/** Text of a tool result. */
|
||||
export const textOf = (r: any): string => r.content[0].text;
|
||||
|
||||
/** Let queued microtasks run — enough for the manager's internal chaining. */
|
||||
export const flush = async () => {
|
||||
await new Promise((r) => setImmediate(r));
|
||||
await new Promise((r) => setImmediate(r));
|
||||
};
|
||||
|
||||
export interface Hermetic {
|
||||
dir: string;
|
||||
restore: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect cwd, `PI_CODING_AGENT_DIR` and `HOME` into a fresh temp dir, so the
|
||||
* developer's real settings and agent files can't reach the extension under
|
||||
* test. Call BEFORE instantiating the extension — settings are read at boot.
|
||||
*/
|
||||
export function hermeticDir(opts: {
|
||||
settings?: Record<string, unknown>;
|
||||
agentFiles?: Record<string, string>;
|
||||
} = {}): Hermetic {
|
||||
const dir = mkdtempSync(join(tmpdir(), "pi-boot-"));
|
||||
const agentDir = mkdtempSync(join(tmpdir(), "pi-boot-agentdir-"));
|
||||
const prevCwd = process.cwd();
|
||||
const prevAgentDir = process.env.PI_CODING_AGENT_DIR;
|
||||
const prevHome = process.env.HOME;
|
||||
|
||||
mkdirSync(join(dir, ".pi"), { recursive: true });
|
||||
if (opts.settings) {
|
||||
writeFileSync(join(dir, ".pi", "subagents.json"), JSON.stringify(opts.settings));
|
||||
}
|
||||
if (opts.agentFiles) {
|
||||
mkdirSync(join(dir, ".pi", "agents"), { recursive: true });
|
||||
for (const [name, content] of Object.entries(opts.agentFiles)) {
|
||||
writeFileSync(join(dir, ".pi", "agents", `${name}.md`), content);
|
||||
}
|
||||
}
|
||||
|
||||
process.env.PI_CODING_AGENT_DIR = agentDir;
|
||||
process.env.HOME = agentDir;
|
||||
process.chdir(dir);
|
||||
|
||||
return {
|
||||
dir,
|
||||
restore() {
|
||||
process.chdir(prevCwd);
|
||||
if (prevAgentDir == null) delete process.env.PI_CODING_AGENT_DIR;
|
||||
else process.env.PI_CODING_AGENT_DIR = prevAgentDir;
|
||||
if (prevHome == null) delete process.env.HOME;
|
||||
else process.env.HOME = prevHome;
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
rmSync(agentDir, { recursive: true, force: true });
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user