mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-10 20:50:08 +02:00
Fix candidate ACPX initialization and symlinked eval entrypoint
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
25ab093921
commit
ac17430b01
4 files changed
+67
-8
No files matched your search
@@ -5,6 +5,8 @@ import { fileURLToPath } from "node:url";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { normalizeAcpxPermission } from "../drivers/acpx/acp-permission-adapter.js";
|
||||
import { ACPX_CAPABILITY_PROFILES } from "../drivers/acpx/capability-profiles.js";
|
||||
import { resolveQualifiedAcpxProfile } from "../drivers/acpx/qualified-profiles.js";
|
||||
import { ACPX_SIDECAR_PROTOCOL_VERSION } from "../drivers/acpx/sidecar-protocol.js";
|
||||
import { canonicalProviderEventsFromAcpxRuntimeEvent } from "../provider-events.js";
|
||||
import {
|
||||
@@ -552,12 +554,27 @@ describe("qualified ACPX runtime sidecar", () => {
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
["pi", "openrouter/deepseek/deepseek-v4-flash-0731"],
|
||||
["cursor", "explicit-cursor-model"],
|
||||
["copilot", "explicit-copilot-model"],
|
||||
] as const)("initializes the declared %s candidate without promoting its profile", async (agent, model) => {
|
||||
const sidecar = startSidecar();
|
||||
sidecar.write(initializeRequest(1, agent, model));
|
||||
const frame = await sidecar.next((value) => value.id === 1);
|
||||
expect(frame).toMatchObject({ id: 1, ok: true });
|
||||
const result = frame.result as Record<string, unknown>;
|
||||
expect(result.profile).toEqual(resolveQualifiedAcpxProfile(agent, model));
|
||||
expect(result.profile).toMatchObject({ reportedModelId: model });
|
||||
expect(ACPX_CAPABILITY_PROFILES[agent].qualification).toBe("pending");
|
||||
});
|
||||
|
||||
it("fails closed after an unsupported provider bootstrap", async () => {
|
||||
const sidecar = startSidecar();
|
||||
sidecar.write(
|
||||
initializeRequest(
|
||||
1,
|
||||
"pi",
|
||||
"unknown-provider",
|
||||
"openrouter/deepseek/deepseek-v4-flash-0731",
|
||||
),
|
||||
);
|
||||
@@ -569,7 +586,7 @@ describe("qualified ACPX runtime sidecar", () => {
|
||||
ok: false,
|
||||
error: {
|
||||
code: "acpx_sidecar_command_failed",
|
||||
message: "ACPX agent must be claude or codex",
|
||||
message: "ACPX agent must be claude, codex, cursor, copilot, or pi",
|
||||
retryable: false,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1321,8 +1321,8 @@ function requireHost(
|
||||
}
|
||||
|
||||
function requireQualifiedAgent(value: unknown): QualifiedAcpxAgent {
|
||||
if (value !== "codex" && value !== "claude") {
|
||||
throw new Error("ACPX agent must be claude or codex");
|
||||
if (value !== "codex" && value !== "claude" && value !== "pi" && value !== "cursor" && value !== "copilot") {
|
||||
throw new Error("ACPX agent must be claude, codex, cursor, copilot, or pi");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import { mkdtemp, realpath, rm, symlink } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
const execute = promisify(execFile);
|
||||
const tsx = fileURLToPath(new URL("../../node_modules/.bin/tsx", import.meta.url));
|
||||
|
||||
it("runs the eval CLI parser through both canonical and symlinked paths", async () => {
|
||||
const cli = await realpath(fileURLToPath(new URL("./eval-session.ts", import.meta.url)));
|
||||
const root = await mkdtemp(join(tmpdir(), "paperclip-eval-cli-entrypoint-"));
|
||||
try {
|
||||
const alias = join(root, "cli-alias");
|
||||
await symlink(dirname(cli), alias, "dir");
|
||||
for (const path of [cli, join(alias, "eval-session.ts")]) {
|
||||
// A skipped main guard silently exits zero. Rejecting the invalid option
|
||||
// proves actual CLI execution without loading credentials or a provider.
|
||||
await expect(execute(tsx, [path, "--unknown", "value"], { timeout: 15_000 }))
|
||||
.rejects.toMatchObject({ code: 1, stderr: "unknown argument: --unknown\n" });
|
||||
}
|
||||
const imported = await execute(tsx, ["--eval", `import(${JSON.stringify(cli)}).then(() => console.log("imported"))`], { timeout: 15_000 });
|
||||
expect(imported.stdout.trim()).toBe("imported");
|
||||
expect(imported.stderr).toBe("");
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
}, 45_000);
|
||||
@@ -1,5 +1,6 @@
|
||||
#!/usr/bin/env node
|
||||
import { createHash } from "node:crypto";
|
||||
import { realpathSync } from "node:fs";
|
||||
import { chmod, mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises";
|
||||
import { resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
@@ -254,12 +255,14 @@ export function boundedEvalSessionUsage(
|
||||
throw new Error("agent turn limit exceeded");
|
||||
}
|
||||
if (
|
||||
usage.estimatedCostNanodollars !== null &&
|
||||
usage.estimatedCostNanodollars >
|
||||
request.limits.maxEstimatedCostNanodollars
|
||||
) {
|
||||
throw new Error("estimated cost limit exceeded");
|
||||
}
|
||||
if (
|
||||
usage.providerReportedCostNanodollars !== null &&
|
||||
usage.providerReportedCostNanodollars >
|
||||
request.limits.maxEstimatedCostNanodollars
|
||||
) {
|
||||
@@ -467,10 +470,18 @@ export async function runEvalSessionCli(
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
process.argv[1] !== undefined &&
|
||||
resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url))
|
||||
) {
|
||||
function isEvalSessionEntrypoint(): boolean {
|
||||
if (process.argv[1] === undefined) return false;
|
||||
try {
|
||||
// Node resolves module URLs through symlinks, including /tmp on macOS and
|
||||
// package-manager bin links. Compare the same physical paths on both sides.
|
||||
return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url));
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (isEvalSessionEntrypoint()) {
|
||||
void runEvalSessionCli(process.argv.slice(2))
|
||||
.then((code) => {
|
||||
process.exitCode = code;
|
||||
|
||||
Reference in new issue
Block a user