Files
paperclip/server/src/__tests__/gemini-local-execute.test.ts
Aaron 6bfe0b8422 Default Gemini adapter to yolo mode and add API access prompt note
Gemini CLI only registers run_shell_command in --approval-mode yolo.
Non-yolo modes don't expose it at all, making Paperclip API calls
impossible. Always pass --approval-mode yolo and remove the now-unused
policy engine code, approval mode config, and UI toggles.

Add a "Paperclip API access note" to the prompt with curl examples
via run_shell_command, since the universal SKILL.md is tool-agnostic.

Also extract structured question events from Gemini assistant messages
to support interactive approval flows.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 01:45:08 +00:00

169 lines
5.4 KiB
TypeScript

import { describe, expect, it } from "vitest";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { execute } from "@paperclipai/adapter-gemini-local/server";
async function writeFakeGeminiCommand(commandPath: string): Promise<void> {
const script = `#!/usr/bin/env node
const fs = require("node:fs");
const capturePath = process.env.PAPERCLIP_TEST_CAPTURE_PATH;
const payload = {
argv: process.argv.slice(2),
paperclipEnvKeys: Object.keys(process.env)
.filter((key) => key.startsWith("PAPERCLIP_"))
.sort(),
};
if (capturePath) {
fs.writeFileSync(capturePath, JSON.stringify(payload), "utf8");
}
console.log(JSON.stringify({
type: "system",
subtype: "init",
session_id: "gemini-session-1",
model: "gemini-2.5-pro",
}));
console.log(JSON.stringify({
type: "assistant",
message: { content: [{ type: "output_text", text: "hello" }] },
}));
console.log(JSON.stringify({
type: "result",
subtype: "success",
session_id: "gemini-session-1",
result: "ok",
}));
`;
await fs.writeFile(commandPath, script, "utf8");
await fs.chmod(commandPath, 0o755);
}
type CapturePayload = {
argv: string[];
paperclipEnvKeys: string[];
};
describe("gemini execute", () => {
it("passes prompt as final argument and injects paperclip env vars", async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-gemini-execute-"));
const workspace = path.join(root, "workspace");
const commandPath = path.join(root, "gemini");
const capturePath = path.join(root, "capture.json");
await fs.mkdir(workspace, { recursive: true });
await writeFakeGeminiCommand(commandPath);
const previousHome = process.env.HOME;
process.env.HOME = root;
let invocationPrompt = "";
try {
const result = await execute({
runId: "run-1",
agent: {
id: "agent-1",
companyId: "company-1",
name: "Gemini Coder",
adapterType: "gemini_local",
adapterConfig: {},
},
runtime: {
sessionId: null,
sessionParams: null,
sessionDisplayId: null,
taskKey: null,
},
config: {
command: commandPath,
cwd: workspace,
model: "gemini-2.5-pro",
env: {
PAPERCLIP_TEST_CAPTURE_PATH: capturePath,
},
promptTemplate: "Follow the paperclip heartbeat.",
},
context: {},
authToken: "run-jwt-token",
onLog: async () => {},
onMeta: async (meta) => {
invocationPrompt = meta.prompt ?? "";
},
});
expect(result.exitCode).toBe(0);
expect(result.errorMessage).toBeNull();
const capture = JSON.parse(await fs.readFile(capturePath, "utf8")) as CapturePayload;
expect(capture.argv).toContain("--output-format");
expect(capture.argv).toContain("stream-json");
expect(capture.argv).toContain("--approval-mode");
expect(capture.argv).toContain("yolo");
expect(capture.argv.at(-1)).toContain("Follow the paperclip heartbeat.");
expect(capture.argv.at(-1)).toContain("Paperclip runtime note:");
expect(capture.paperclipEnvKeys).toEqual(
expect.arrayContaining([
"PAPERCLIP_AGENT_ID",
"PAPERCLIP_API_KEY",
"PAPERCLIP_API_URL",
"PAPERCLIP_COMPANY_ID",
"PAPERCLIP_RUN_ID",
]),
);
expect(invocationPrompt).toContain("Paperclip runtime note:");
expect(invocationPrompt).toContain("PAPERCLIP_API_URL");
expect(invocationPrompt).toContain("Paperclip API access note:");
expect(invocationPrompt).toContain("run_shell_command");
expect(result.question).toBeNull();
} finally {
if (previousHome === undefined) {
delete process.env.HOME;
} else {
process.env.HOME = previousHome;
}
await fs.rm(root, { recursive: true, force: true });
}
});
it("always passes --approval-mode yolo", async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-gemini-yolo-"));
const workspace = path.join(root, "workspace");
const commandPath = path.join(root, "gemini");
const capturePath = path.join(root, "capture.json");
await fs.mkdir(workspace, { recursive: true });
await writeFakeGeminiCommand(commandPath);
const previousHome = process.env.HOME;
process.env.HOME = root;
try {
await execute({
runId: "run-yolo",
agent: { id: "a1", companyId: "c1", name: "G", adapterType: "gemini_local", adapterConfig: {} },
runtime: { sessionId: null, sessionParams: null, sessionDisplayId: null, taskKey: null },
config: {
command: commandPath,
cwd: workspace,
env: { PAPERCLIP_TEST_CAPTURE_PATH: capturePath },
},
context: {},
authToken: "t",
onLog: async () => {},
});
const capture = JSON.parse(await fs.readFile(capturePath, "utf8")) as CapturePayload;
expect(capture.argv).toContain("--approval-mode");
expect(capture.argv).toContain("yolo");
expect(capture.argv).not.toContain("--policy");
expect(capture.argv).not.toContain("--allow-all");
expect(capture.argv).not.toContain("--allow-read");
} finally {
if (previousHome === undefined) {
delete process.env.HOME;
} else {
process.env.HOME = previousHome;
}
await fs.rm(root, { recursive: true, force: true });
}
});
});