143 lines
4.9 KiB
TypeScript
143 lines
4.9 KiB
TypeScript
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { Agent } from "@mariozechner/pi-agent-core";
|
|
import type { Model } from "@mariozechner/pi-ai";
|
|
import { loadSkills } from "@mariozechner/pi-coding-agent";
|
|
import { describe, expect, it } from "vitest";
|
|
import { buildAgentHooks } from "../../src/main/agent-runtime/agent-hooks";
|
|
import {
|
|
createPiAgent,
|
|
resolveThinkingLevel,
|
|
} from "../../src/main/worker/runtime/pi-agent-factory";
|
|
import {
|
|
createRunScopedBashOptions,
|
|
createRunScopedCodingTools,
|
|
} from "../../src/main/worker/runtime/run-tools";
|
|
import type { WorkerRpcClient } from "../../src/main/worker/entry/rpc";
|
|
|
|
function createModel(): Model<"openai-responses"> {
|
|
return {
|
|
id: "contract-model",
|
|
name: "Contract Model",
|
|
api: "openai-responses",
|
|
provider: "openai",
|
|
baseUrl: "https://example.invalid",
|
|
reasoning: false,
|
|
input: ["text"],
|
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
contextWindow: 8192,
|
|
maxTokens: 2048,
|
|
};
|
|
}
|
|
|
|
function createRpc(): WorkerRpcClient {
|
|
return {
|
|
invoke: async () => {
|
|
throw new Error("Unexpected RPC call in pi runtime contract test.");
|
|
},
|
|
} as unknown as WorkerRpcClient;
|
|
}
|
|
|
|
describe("pi runtime integration contract", () => {
|
|
it("builds the AgentCompany pi Agent boundary with sequential execution and hooks", () => {
|
|
const agent = createPiAgent({
|
|
runId: "run-contract",
|
|
taskSessionId: "session-contract",
|
|
systemPrompt: "Contract prompt",
|
|
model: createModel(),
|
|
tools: [],
|
|
priorMessages: [],
|
|
thinkingLevel: resolveThinkingLevel("high"),
|
|
apiKey: "contract-key",
|
|
extraHeaders: { "x-contract": "true" },
|
|
rpc: createRpc(),
|
|
});
|
|
|
|
expect(agent).toBeInstanceOf(Agent);
|
|
expect(agent.sessionId).toBe("session-contract");
|
|
expect(agent.toolExecution).toBe("sequential");
|
|
expect(agent.state.systemPrompt).toBe("Contract prompt");
|
|
expect(agent.state.thinkingLevel).toBe("high");
|
|
expect(agent.state.tools).toEqual([]);
|
|
expect(agent.state.messages).toEqual([]);
|
|
expect(typeof agent.beforeToolCall).toBe("function");
|
|
expect(typeof agent.transformContext).toBe("function");
|
|
expect(agent.afterToolCall).toBeUndefined();
|
|
});
|
|
|
|
it("keeps the coding-tool surface expected by worker runs", () => {
|
|
const workspaceDir = mkdtempSync(join(tmpdir(), "agentcompany-pi-tools-"));
|
|
try {
|
|
const tools = createRunScopedCodingTools(workspaceDir, { AGENTCOMPANY_CONTRACT: "1" });
|
|
expect(tools.map((tool) => tool.name)).toEqual(["read", "bash", "edit", "write"]);
|
|
for (const tool of tools) {
|
|
expect(typeof tool.execute).toBe("function");
|
|
}
|
|
} finally {
|
|
rmSync(workspaceDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it("preserves run-scoped environment through the bash spawn hook", () => {
|
|
const options = createRunScopedBashOptions({
|
|
AGENTCOMPANY_CONTRACT: "1",
|
|
PATH: "/contract/bin",
|
|
});
|
|
const result = options.spawnHook?.({
|
|
command: "pwd",
|
|
cwd: "/tmp",
|
|
env: {
|
|
HOME: "/home/contract",
|
|
PATH: "/base/bin",
|
|
},
|
|
});
|
|
|
|
expect(result?.command).toBe("pwd");
|
|
expect(result?.cwd).toBe("/tmp");
|
|
expect(result?.env.HOME).toBe("/home/contract");
|
|
expect(result?.env.PATH).toBe("/contract/bin");
|
|
expect(result?.env.AGENTCOMPANY_CONTRACT).toBe("1");
|
|
});
|
|
|
|
it("loads progressive-disclosure skills through the pinned pi runtime", () => {
|
|
const root = mkdtempSync(join(tmpdir(), "agentcompany-pi-skills-"));
|
|
const skillDir = join(root, "contract-skill");
|
|
mkdirSync(skillDir);
|
|
writeFileSync(
|
|
join(skillDir, "SKILL.md"),
|
|
[
|
|
"---",
|
|
"name: contract-skill",
|
|
"description: Contract skill loaded by the pinned pi runtime.",
|
|
"---",
|
|
"# Contract Skill",
|
|
"",
|
|
"Use this skill to prove the runtime loader contract.",
|
|
].join("\n"),
|
|
"utf8",
|
|
);
|
|
|
|
try {
|
|
const result = loadSkills({ skillPaths: [skillDir], includeDefaults: false });
|
|
expect(result.diagnostics).toEqual([]);
|
|
expect(result.skills).toHaveLength(1);
|
|
expect(result.skills[0]?.name).toBe("contract-skill");
|
|
expect(result.skills[0]?.description).toBe("Contract skill loaded by the pinned pi runtime.");
|
|
expect(result.skills[0]?.filePath).toBe(join(skillDir, "SKILL.md"));
|
|
} finally {
|
|
rmSync(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it("keeps the aggregated hook bundle assignable to pi Agent options at runtime", () => {
|
|
const hooks = buildAgentHooks({ runId: "run-contract", rpc: createRpc() });
|
|
expect(Object.keys(hooks).sort()).toEqual([
|
|
"beforeToolCall",
|
|
"transformContext",
|
|
]);
|
|
expect(typeof hooks.beforeToolCall).toBe("function");
|
|
expect(typeof hooks.transformContext).toBe("function");
|
|
});
|
|
});
|