Fix candidate ACPX initialization and symlinked eval entrypoint

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-09-28 14:10:10 -05:00
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;