@tickernelz/paperclip-pro-adapter-utils 2026.1011.3 → 2026.1012.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/acpx-engine/execute.d.ts.map +1 -1
- package/dist/acpx-engine/execute.js +77 -8
- package/dist/acpx-engine/execute.js.map +1 -1
- package/dist/acpx-engine/execute.test.js +186 -14
- package/dist/acpx-engine/execute.test.js.map +1 -1
- package/dist/acpx-engine/filesystem-protocol.test.d.ts +2 -0
- package/dist/acpx-engine/filesystem-protocol.test.d.ts.map +1 -0
- package/dist/acpx-engine/filesystem-protocol.test.js +99 -0
- package/dist/acpx-engine/filesystem-protocol.test.js.map +1 -0
- package/dist/acpx-engine/run-contracts.d.ts +1 -0
- package/dist/acpx-engine/run-contracts.d.ts.map +1 -1
- package/dist/billing.d.ts +7 -0
- package/dist/billing.d.ts.map +1 -1
- package/dist/billing.js +25 -2
- package/dist/billing.js.map +1 -1
- package/dist/billing.test.js +17 -1
- package/dist/billing.test.js.map +1 -1
- package/dist/chat-file-delivery.d.ts +1 -1
- package/dist/chat-file-delivery.d.ts.map +1 -1
- package/dist/chat-file-delivery.js +4 -3
- package/dist/chat-file-delivery.js.map +1 -1
- package/dist/chat-file-delivery.test.js +2 -2
- package/dist/chat-file-delivery.test.js.map +1 -1
- package/dist/command-managed-runtime.d.ts +1 -0
- package/dist/command-managed-runtime.d.ts.map +1 -1
- package/dist/command-managed-runtime.js +3 -1
- package/dist/command-managed-runtime.js.map +1 -1
- package/dist/connection-failure.d.ts +33 -0
- package/dist/connection-failure.d.ts.map +1 -0
- package/dist/connection-failure.js +43 -0
- package/dist/connection-failure.js.map +1 -0
- package/dist/connection-failure.test.d.ts +2 -0
- package/dist/connection-failure.test.d.ts.map +1 -0
- package/dist/connection-failure.test.js +94 -0
- package/dist/connection-failure.test.js.map +1 -0
- package/dist/directory-merge-lock.test.js +4 -2
- package/dist/directory-merge-lock.test.js.map +1 -1
- package/dist/execution-target-sandbox.test.js +228 -0
- package/dist/execution-target-sandbox.test.js.map +1 -1
- package/dist/execution-target.d.ts +4 -0
- package/dist/execution-target.d.ts.map +1 -1
- package/dist/execution-target.js +46 -13
- package/dist/execution-target.js.map +1 -1
- package/dist/execution-target.test.js +1 -1
- package/dist/execution-target.test.js.map +1 -1
- package/dist/github-launcher-environment.test.js +37 -0
- package/dist/github-launcher-environment.test.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/paperclip-runner-permissions.d.ts +15 -4
- package/dist/paperclip-runner-permissions.d.ts.map +1 -1
- package/dist/paperclip-runner-permissions.js +29 -3
- package/dist/paperclip-runner-permissions.js.map +1 -1
- package/dist/paperclip-runner-permissions.test.js +34 -1
- package/dist/paperclip-runner-permissions.test.js.map +1 -1
- package/dist/remote-managed-runtime.d.ts.map +1 -1
- package/dist/remote-managed-runtime.js +18 -2
- package/dist/remote-managed-runtime.js.map +1 -1
- package/dist/sandbox-callback-bridge.d.ts.map +1 -1
- package/dist/sandbox-callback-bridge.js +1 -0
- package/dist/sandbox-callback-bridge.js.map +1 -1
- package/dist/sandbox-callback-bridge.test.js +113 -16
- package/dist/sandbox-callback-bridge.test.js.map +1 -1
- package/dist/sandbox-managed-runtime.d.ts +3 -0
- package/dist/sandbox-managed-runtime.d.ts.map +1 -1
- package/dist/sandbox-managed-runtime.js +8 -1
- package/dist/sandbox-managed-runtime.js.map +1 -1
- package/dist/server-utils.d.ts +6 -3
- package/dist/server-utils.d.ts.map +1 -1
- package/dist/server-utils.js +39 -9
- package/dist/server-utils.js.map +1 -1
- package/dist/server-utils.test.js +35 -1
- package/dist/server-utils.test.js.map +1 -1
- package/dist/ssh-fixture.test.js +172 -0
- package/dist/ssh-fixture.test.js.map +1 -1
- package/dist/ssh.d.ts +5 -0
- package/dist/ssh.d.ts.map +1 -1
- package/dist/ssh.js +138 -20
- package/dist/ssh.js.map +1 -1
- package/dist/types.d.ts +76 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/usage-checkpoint.d.ts +11 -0
- package/dist/usage-checkpoint.d.ts.map +1 -0
- package/dist/usage-checkpoint.js +165 -0
- package/dist/usage-checkpoint.js.map +1 -0
- package/dist/usage-checkpoint.test.d.ts +2 -0
- package/dist/usage-checkpoint.test.d.ts.map +1 -0
- package/dist/usage-checkpoint.test.js +131 -0
- package/dist/usage-checkpoint.test.js.map +1 -0
- package/dist/workspace-restore-diagnostics.d.ts +8 -1
- package/dist/workspace-restore-diagnostics.d.ts.map +1 -1
- package/dist/workspace-restore-diagnostics.js +35 -8
- package/dist/workspace-restore-diagnostics.js.map +1 -1
- package/dist/workspace-restore-diagnostics.test.js +31 -1
- package/dist/workspace-restore-diagnostics.test.js.map +1 -1
- package/node_modules/acpx/dist/live-checkpoint-ClPCSdrW.js +3 -0
- package/package.json +31 -3
- package/node_modules/@esbuild/linux-x64/README.md +0 -3
- package/node_modules/@esbuild/linux-x64/bin/esbuild +0 -0
- package/node_modules/@esbuild/linux-x64/package.json +0 -20
|
@@ -29,12 +29,14 @@ import { withWorkspaceRestoreDiagnostics, withWorkspaceRestoreStep } from "../wo
|
|
|
29
29
|
import { resolveReferencedSourceIgnore } from "../sandbox-managed-runtime.js";
|
|
30
30
|
import { getActiveStepContext, runWithRuntimeParent, SANDBOX_STARTUP_SPAN_ATTRS, } from "./startup-timing.js";
|
|
31
31
|
const tempRoots = [];
|
|
32
|
+
let defaultGeminiTestHome;
|
|
32
33
|
async function makeTempRoot() {
|
|
33
34
|
const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-acpx-skills-"));
|
|
34
35
|
tempRoots.push(root);
|
|
35
36
|
return root;
|
|
36
37
|
}
|
|
37
38
|
afterEach(async () => {
|
|
39
|
+
defaultGeminiTestHome = undefined;
|
|
38
40
|
// A remote run stages a process-session bridge whose detached event writer can
|
|
39
41
|
// still be flushing a trailing event file into `.../process-sessions/<id>/events`
|
|
40
42
|
// when the run's own best-effort `client.remove(sessionDir)` (which production
|
|
@@ -114,6 +116,13 @@ function buildRuntime(onSetConfigOption, onEnsureSession, onStartTurn, onSetMode
|
|
|
114
116
|
};
|
|
115
117
|
}
|
|
116
118
|
async function runExecutor(config, options = {}) {
|
|
119
|
+
// Skill reconciliation must never inspect or modify the developer's real
|
|
120
|
+
// Gemini home. Keep one private home across this test's resumed turns.
|
|
121
|
+
const configuredEnv = config.env;
|
|
122
|
+
if (config.agent === "gemini" && !(typeof configuredEnv?.HOME === "string" && configuredEnv.HOME.trim())) {
|
|
123
|
+
defaultGeminiTestHome ??= await makeTempRoot();
|
|
124
|
+
config = { ...config, env: { ...configuredEnv, HOME: defaultGeminiTestHome } };
|
|
125
|
+
}
|
|
117
126
|
const runtimeOptions = [];
|
|
118
127
|
const configOptions = [];
|
|
119
128
|
const sessionModes = [];
|
|
@@ -122,6 +131,7 @@ async function runExecutor(config, options = {}) {
|
|
|
122
131
|
const meta = [];
|
|
123
132
|
const logs = [];
|
|
124
133
|
const events = [];
|
|
134
|
+
const receipts = [];
|
|
125
135
|
const execute = createAcpxEngineExecutor({
|
|
126
136
|
...(options.prepareRemoteManagedHome
|
|
127
137
|
? { prepareRemoteManagedHome: options.prepareRemoteManagedHome }
|
|
@@ -142,6 +152,7 @@ async function runExecutor(config, options = {}) {
|
|
|
142
152
|
context: options.context ?? {},
|
|
143
153
|
executionTransport: options.executionTransport,
|
|
144
154
|
authToken: options.authToken,
|
|
155
|
+
agentIdentity: options.agentIdentity,
|
|
145
156
|
executionTarget: options.executionTarget,
|
|
146
157
|
runtimeMcp: options.runtimeMcp,
|
|
147
158
|
startupTraceContext: options.startupTraceContext,
|
|
@@ -154,9 +165,10 @@ async function runExecutor(config, options = {}) {
|
|
|
154
165
|
onEvent: async (event) => {
|
|
155
166
|
events.push(event);
|
|
156
167
|
},
|
|
168
|
+
onUsage: async (receipt) => { receipts.push(receipt); },
|
|
157
169
|
});
|
|
158
170
|
expect(result.exitCode).toBe(0);
|
|
159
|
-
return { logs, meta, events, runtimeOptions, configOptions, sessionModes, sessionInputs, turnInputs, result };
|
|
171
|
+
return { logs, meta, events, receipts, runtimeOptions, configOptions, sessionModes, sessionInputs, turnInputs, result };
|
|
160
172
|
}
|
|
161
173
|
// Under `vi.useFakeTimers()`, setup before `ensureSession` still performs real
|
|
162
174
|
// filesystem work. Advancing the fake clock before that work reaches the
|
|
@@ -438,7 +450,7 @@ describe("shared ACPX engine runtime behavior", () => {
|
|
|
438
450
|
expect(turnStartedBeforeProcessIdentity).toBe(false);
|
|
439
451
|
});
|
|
440
452
|
it("sets Codex model, effort, and fast mode through CODEX_CONFIG without session config calls", async () => {
|
|
441
|
-
const { configOptions, meta } = await runExecutor({
|
|
453
|
+
const { configOptions, meta, receipts, result } = await runExecutor({
|
|
442
454
|
agent: "codex",
|
|
443
455
|
model: "gpt-5.6-sol",
|
|
444
456
|
modelReasoningEffort: "high",
|
|
@@ -451,8 +463,23 @@ describe("shared ACPX engine runtime behavior", () => {
|
|
|
451
463
|
features: { fast_mode: true },
|
|
452
464
|
});
|
|
453
465
|
expect(configOptions).toEqual([]);
|
|
466
|
+
expect(result.pricingContext).toEqual({ serviceTier: "fast" });
|
|
467
|
+
expect(receipts).toContainEqual(expect.objectContaining({ pricingContext: { serviceTier: "fast" }, complete: true }));
|
|
454
468
|
expect(meta[0]?.commandNotes).toContain("Requested ACPX model: gpt-5.6-sol (set via CODEX_CONFIG at startup).");
|
|
455
469
|
});
|
|
470
|
+
it("keeps identity and scoped API access without exposing configured service tokens to Codex shells", async () => {
|
|
471
|
+
const { meta } = await runExecutor({ agent: "codex", env: { MY_SERVICE_TOKEN: "assigned-tool-token" } }, {
|
|
472
|
+
authToken: "assigned-run-token",
|
|
473
|
+
agentIdentity: { keyId: "sha256:test", publicKeyPem: "public", privateKeyPem: "private" },
|
|
474
|
+
});
|
|
475
|
+
const config = JSON.parse(String((meta[0]?.env).CODEX_CONFIG));
|
|
476
|
+
expect(config.shell_environment_policy.include_only).toEqual(expect.arrayContaining([
|
|
477
|
+
"PAPERCLIP_API_KEY", "PAPERCLIP_AGENT_PRIVATE_KEY",
|
|
478
|
+
]));
|
|
479
|
+
expect(config.shell_environment_policy.include_only).not.toContain("MY_SERVICE_TOKEN");
|
|
480
|
+
expect(JSON.stringify(config)).not.toContain("assigned-run-token");
|
|
481
|
+
expect(JSON.stringify(config)).not.toContain("assigned-tool-token");
|
|
482
|
+
});
|
|
456
483
|
it("forwards arbitrary Codex model IDs verbatim without picker-dependent session config", async () => {
|
|
457
484
|
const arbitraryModel = "gpt-999-test-does-not-exist";
|
|
458
485
|
const { configOptions, meta } = await runExecutor({
|
|
@@ -500,7 +527,7 @@ describe("shared ACPX engine runtime behavior", () => {
|
|
|
500
527
|
text: "[paperclip] Ignoring invalid user CODEX_CONFIG while applying runtime Codex settings; expected a JSON object.\n",
|
|
501
528
|
});
|
|
502
529
|
});
|
|
503
|
-
it("
|
|
530
|
+
it("sets Claude and Gemini models at startup without unsupported picker calls", async () => {
|
|
504
531
|
const claude = await runExecutor({ agent: "claude", model: "claude-opus-4-7" });
|
|
505
532
|
expect((claude.meta[0]?.env).ANTHROPIC_MODEL).toBe("claude-opus-4-7");
|
|
506
533
|
expect(claude.configOptions).toEqual([]);
|
|
@@ -508,9 +535,10 @@ describe("shared ACPX engine runtime behavior", () => {
|
|
|
508
535
|
agent: "gemini",
|
|
509
536
|
model: "gemini-2.5-pro",
|
|
510
537
|
thinkingEffort: "high",
|
|
538
|
+
env: { GEMINI_MODEL: "stale-model" },
|
|
511
539
|
});
|
|
540
|
+
expect((gemini.meta[0]?.env).GEMINI_MODEL).toBe("gemini-2.5-pro");
|
|
512
541
|
expect(gemini.configOptions).toEqual([
|
|
513
|
-
{ key: "model", value: "gemini-2.5-pro" },
|
|
514
542
|
{ key: "effort", value: "high" },
|
|
515
543
|
]);
|
|
516
544
|
});
|
|
@@ -1293,6 +1321,45 @@ describe("shared ACPX engine runtime behavior", () => {
|
|
|
1293
1321
|
const statusLine = logs.find((entry) => entry.text.includes('"acpx.status"'));
|
|
1294
1322
|
expect(statusLine?.text).toContain('"cost"');
|
|
1295
1323
|
});
|
|
1324
|
+
it.each(["checkpoint", "terminal"])("returns received usage after the %s receipt sink rejects and still closes the runtime", async (failedSave) => {
|
|
1325
|
+
const root = await makeTempRoot();
|
|
1326
|
+
const close = vi.fn(async () => { });
|
|
1327
|
+
const cancel = vi.fn(async () => { });
|
|
1328
|
+
let reads = 0;
|
|
1329
|
+
const execute = createAcpxEngineExecutor({
|
|
1330
|
+
resolveBillingIdentity: () => ({ provider: "anthropic", biller: "anthropic", billingType: "api" }),
|
|
1331
|
+
createRuntime: () => ({
|
|
1332
|
+
...buildRuntime(),
|
|
1333
|
+
getStatus: async () => ++reads === 1
|
|
1334
|
+
? { usage: { cost: { amount: 0.4, currency: "USD" } } }
|
|
1335
|
+
: { usage: { cumulative: { inputTokens: 120, outputTokens: 4500, cachedReadTokens: 900, cachedWriteTokens: 30 }, cost: { amount: 1.15, currency: "USD" } } },
|
|
1336
|
+
startTurn: () => ({
|
|
1337
|
+
events: (async function* () {
|
|
1338
|
+
yield { type: "status", text: "usage", tag: "usage_update",
|
|
1339
|
+
breakdown: { inputTokens: 100, outputTokens: 4000, cachedReadTokens: 800, cachedWriteTokens: 20 }, cost: { amount: 1.1, currency: "USD" } };
|
|
1340
|
+
})(),
|
|
1341
|
+
result: Promise.resolve({ status: "completed", stopReason: "end_turn" }), cancel,
|
|
1342
|
+
}), close,
|
|
1343
|
+
}),
|
|
1344
|
+
});
|
|
1345
|
+
const onUsage = vi.fn(async (receipt) => {
|
|
1346
|
+
if (failedSave === "checkpoint" || receipt.complete)
|
|
1347
|
+
throw new Error("Receipt storage unavailable");
|
|
1348
|
+
});
|
|
1349
|
+
const result = await execute({ runId: `failed-${failedSave}-save`, agent: { id: "agent-1", companyId: "company-1" },
|
|
1350
|
+
runtime: {}, config: { agent: "custom", agentCommand: "node ./fake-acp.js", stateDir: path.join(root, "state") },
|
|
1351
|
+
context: {}, onMeta: async () => { }, onLog: async () => { }, onUsage });
|
|
1352
|
+
expect(result).toMatchObject({ exitCode: 1, usageBasis: "per_run", usageComplete: failedSave === "terminal",
|
|
1353
|
+
provider: "anthropic", biller: "anthropic", billingType: "api" });
|
|
1354
|
+
expect(result.errorMessage).toContain("Receipt storage unavailable");
|
|
1355
|
+
expect(result.usage).toEqual(failedSave === "terminal"
|
|
1356
|
+
? { inputTokens: 150, outputTokens: 4500, cachedInputTokens: 900 }
|
|
1357
|
+
: { inputTokens: 120, outputTokens: 4000, cachedInputTokens: 800 });
|
|
1358
|
+
expect(result.costUsd).toBeCloseTo(failedSave === "terminal" ? 0.75 : 0.7);
|
|
1359
|
+
expect(onUsage).toHaveBeenCalledTimes(failedSave === "terminal" ? 2 : 1);
|
|
1360
|
+
expect(cancel).toHaveBeenCalledTimes(1);
|
|
1361
|
+
expect(close).toHaveBeenCalledTimes(1);
|
|
1362
|
+
});
|
|
1296
1363
|
it("falls back to usage_update events when the runtime lacks getStatus", async () => {
|
|
1297
1364
|
const root = await makeTempRoot();
|
|
1298
1365
|
const stateDir = path.join(root, "state");
|
|
@@ -1339,6 +1406,55 @@ describe("shared ACPX engine runtime behavior", () => {
|
|
|
1339
1406
|
expect(result.provider).toBe("acpx");
|
|
1340
1407
|
expect(result.billingType).toBe("unknown");
|
|
1341
1408
|
});
|
|
1409
|
+
it.each(["failed", "cancelled"])("keeps %s in-stream usage provisional without a final receipt", async (status) => {
|
|
1410
|
+
const root = await makeTempRoot();
|
|
1411
|
+
const stateDir = path.join(root, "state");
|
|
1412
|
+
const execute = createAcpxEngineExecutor({
|
|
1413
|
+
createRuntime: () => ({
|
|
1414
|
+
ensureSession: async () => ({
|
|
1415
|
+
backendSessionId: "backend-session",
|
|
1416
|
+
agentSessionId: "agent-session",
|
|
1417
|
+
runtimeSessionName: "runtime-session",
|
|
1418
|
+
}),
|
|
1419
|
+
startTurn: () => ({
|
|
1420
|
+
events: (async function* () {
|
|
1421
|
+
yield {
|
|
1422
|
+
type: "status",
|
|
1423
|
+
text: "usage",
|
|
1424
|
+
tag: "usage_update",
|
|
1425
|
+
cost: { amount: 0.31, currency: "USD" },
|
|
1426
|
+
breakdown: { inputTokens: 40, outputTokens: 700, cachedReadTokens: 60 },
|
|
1427
|
+
};
|
|
1428
|
+
})(),
|
|
1429
|
+
result: Promise.resolve({ status, error: { code: "INTERNAL", message: "fixture failure" } }),
|
|
1430
|
+
cancel: async () => { },
|
|
1431
|
+
}),
|
|
1432
|
+
close: async () => { },
|
|
1433
|
+
}),
|
|
1434
|
+
});
|
|
1435
|
+
const onUsage = vi.fn();
|
|
1436
|
+
const result = await execute({
|
|
1437
|
+
runId: "run-usage-event-fallback",
|
|
1438
|
+
agent: {
|
|
1439
|
+
id: "agent-1",
|
|
1440
|
+
companyId: "company-1",
|
|
1441
|
+
},
|
|
1442
|
+
runtime: {},
|
|
1443
|
+
config: { agent: "custom", agentCommand: "node ./fake-acp.js", stateDir },
|
|
1444
|
+
context: {},
|
|
1445
|
+
onLog: async () => { },
|
|
1446
|
+
onMeta: async () => { },
|
|
1447
|
+
onUsage,
|
|
1448
|
+
});
|
|
1449
|
+
expect(result.exitCode).not.toBe(0);
|
|
1450
|
+
expect(result.usageComplete).toBe(false);
|
|
1451
|
+
expect(onUsage).toHaveBeenLastCalledWith(expect.objectContaining({ complete: false, costUsd: 0.31 }));
|
|
1452
|
+
expect(result.usage).toEqual({ inputTokens: 40, outputTokens: 700, cachedInputTokens: 60 });
|
|
1453
|
+
expect(result.usageBasis).toBe("per_run");
|
|
1454
|
+
expect(result.costUsd).toBeCloseTo(0.31);
|
|
1455
|
+
expect(result.provider).toBe("acpx");
|
|
1456
|
+
expect(result.billingType).toBe("unknown");
|
|
1457
|
+
});
|
|
1342
1458
|
it.skipIf(process.platform === "win32")("materializes ACPX Claude skills without symlinked descendants", async () => {
|
|
1343
1459
|
const root = await makeTempRoot();
|
|
1344
1460
|
const skillRoot = path.join(root, "skills");
|
|
@@ -1411,6 +1527,29 @@ describe("shared ACPX engine runtime behavior", () => {
|
|
|
1411
1527
|
});
|
|
1412
1528
|
expect(await pathExists(path.join(codexHome, "skills", operational.runtimeName, "SKILL.md"))).toBe(true);
|
|
1413
1529
|
});
|
|
1530
|
+
it.each(["codex", "gemini"])("refreshes the %s skill root when a connection home changes between turns", async (agent) => {
|
|
1531
|
+
const root = await makeTempRoot();
|
|
1532
|
+
const operational = {
|
|
1533
|
+
...await createSkill(path.join(root, "skills"), "paperclip"),
|
|
1534
|
+
key: "paperclipai/paperclip/paperclip",
|
|
1535
|
+
};
|
|
1536
|
+
const homes = [path.join(root, "first-home"), path.join(root, "next-home")];
|
|
1537
|
+
for (const home of homes) {
|
|
1538
|
+
const { meta } = await runExecutor({
|
|
1539
|
+
agent,
|
|
1540
|
+
stateDir: path.join(root, "state"),
|
|
1541
|
+
env: agent === "codex" ? { CODEX_HOME: home } : { HOME: home },
|
|
1542
|
+
paperclipRuntimeSkills: [operational],
|
|
1543
|
+
paperclipSkillSync: { desiredSkills: [] },
|
|
1544
|
+
});
|
|
1545
|
+
const skillsHome = agent === "codex" ? path.join(home, "skills") : path.join(home, ".gemini", "skills");
|
|
1546
|
+
expect(String(meta[0]?.prompt ?? "")).toContain(`Skill root for this run: ${skillsHome}`);
|
|
1547
|
+
expect(String(meta[0]?.prompt ?? "")).toContain("Resolve referenced scripts and files relative to that skill");
|
|
1548
|
+
if (home === homes[1])
|
|
1549
|
+
expect(String(meta[0]?.prompt ?? "")).not.toContain(homes[0]);
|
|
1550
|
+
await fs.rm(home, { recursive: true, force: true });
|
|
1551
|
+
}
|
|
1552
|
+
});
|
|
1414
1553
|
it.skipIf(process.platform === "win32")("removes legacy ACPX Codex skill symlinks when a skill is no longer desired", async () => {
|
|
1415
1554
|
const root = await makeTempRoot();
|
|
1416
1555
|
const skillRoot = path.join(root, "skills");
|
|
@@ -2378,7 +2517,9 @@ describe("shared ACPX engine runtime behavior", () => {
|
|
|
2378
2517
|
});
|
|
2379
2518
|
it.each(["claude", "codex", "grok", "gemini", "kimi"])("resumes %s with added and removed MCP servers and current credentials", async (agent) => {
|
|
2380
2519
|
const root = await makeTempRoot();
|
|
2381
|
-
|
|
2520
|
+
// This fixture injects its runtime. Keep session identity independent of
|
|
2521
|
+
// host CLI discovery/version probes while testing MCP credential changes.
|
|
2522
|
+
const config = { agent, agentCommand: "node ./fake-acp.js", cwd: root, stateDir: path.join(root, "state"), paperclipRuntimeSkills: [], paperclipSkillSync: { desiredSkills: [] } };
|
|
2382
2523
|
const first = await runExecutor(config);
|
|
2383
2524
|
const server = { name: "github", url: "https://example.test/github/mcp", connectionId: "github", token: "current-token" };
|
|
2384
2525
|
const second = await runExecutor(config, {
|
|
@@ -2469,6 +2610,20 @@ describe("findAncestorBin", () => {
|
|
|
2469
2610
|
});
|
|
2470
2611
|
});
|
|
2471
2612
|
describe("gemini ACP flag selection", () => {
|
|
2613
|
+
it("uses the canonical local Gemini workspace for ACP filesystem requests", async () => {
|
|
2614
|
+
const root = await makeTempRoot();
|
|
2615
|
+
const workspace = path.join(root, "workspace");
|
|
2616
|
+
const alias = path.join(root, "workspace-alias");
|
|
2617
|
+
await fs.mkdir(workspace);
|
|
2618
|
+
await fs.symlink(workspace, alias, process.platform === "win32" ? "junction" : "dir");
|
|
2619
|
+
const { runtimeOptions, sessionInputs } = await runExecutor({
|
|
2620
|
+
agent: "gemini", agentCommand: "node ./fake-acp.js", cwd: alias,
|
|
2621
|
+
stateDir: path.join(root, "state"),
|
|
2622
|
+
});
|
|
2623
|
+
const canonical = await fs.realpath(workspace);
|
|
2624
|
+
expect(runtimeOptions[0].cwd).toBe(canonical);
|
|
2625
|
+
expect(sessionInputs[0].cwd).toBe(canonical);
|
|
2626
|
+
});
|
|
2472
2627
|
it("parses semantic version parts from gemini --version output", () => {
|
|
2473
2628
|
expect(parseGeminiVersionParts("0.30.0")).toEqual([0, 30, 0]);
|
|
2474
2629
|
expect(parseGeminiVersionParts("gemini-cli v1.2.3\n")).toEqual([1, 2, 3]);
|
|
@@ -3043,6 +3198,12 @@ describe("ACPX engine remote sandbox staging seam (PR 1: workspace + cwd)", () =
|
|
|
3043
3198
|
expect(sessionInputs[0]?.cwd).toBe(remoteCwd);
|
|
3044
3199
|
expect(sessionInputs[0]?.cwd).not.toBe(localCwd);
|
|
3045
3200
|
});
|
|
3201
|
+
it("preserves Gemini's in-sandbox workspace without host canonicalization", async () => {
|
|
3202
|
+
const { stateDir, localCwd, remoteCwd, executionTarget } = await setupRemoteSandbox();
|
|
3203
|
+
const { sessionInputs, runtimeOptions } = await runExecutor({ agent: "gemini", agentCommand: "node ./fake-acp.js", stateDir, cwd: localCwd }, { authToken: "real-run-jwt", executionTarget });
|
|
3204
|
+
expect(runtimeOptions[0]?.cwd).toBe(remoteCwd);
|
|
3205
|
+
expect(sessionInputs[0]?.cwd).toBe(remoteCwd);
|
|
3206
|
+
});
|
|
3046
3207
|
it("test_remote_warm_handle_reused_after_cwd_change", async () => {
|
|
3047
3208
|
const { stateDir, localCwd, remoteCwd, executionTarget } = await setupRemoteSandbox();
|
|
3048
3209
|
const ensureInputs = [];
|
|
@@ -5388,9 +5549,10 @@ describe("ACPX engine run lifecycle corrections (F1: settle every failure after
|
|
|
5388
5549
|
// next same-session run.
|
|
5389
5550
|
expect(stagingLocks.size).toBe(0);
|
|
5390
5551
|
});
|
|
5391
|
-
it("
|
|
5552
|
+
it.each(["prompt", "metadata", "dispatch"])("records whether provider work could have started on %s failure", async (failure) => {
|
|
5392
5553
|
const root = await makeTempRoot();
|
|
5393
5554
|
const close = vi.fn(async () => { });
|
|
5555
|
+
const startTurn = vi.fn(() => { throw new Error("dispatch failed"); });
|
|
5394
5556
|
const execute = createAcpxEngineExecutor({
|
|
5395
5557
|
createRuntime: () => ({
|
|
5396
5558
|
ensureSession: async () => ({
|
|
@@ -5398,11 +5560,7 @@ describe("ACPX engine run lifecycle corrections (F1: settle every failure after
|
|
|
5398
5560
|
agentSessionId: "agent-session",
|
|
5399
5561
|
runtimeSessionName: "runtime-session",
|
|
5400
5562
|
}),
|
|
5401
|
-
startTurn
|
|
5402
|
-
events: (async function* () { })(),
|
|
5403
|
-
result: Promise.resolve({ status: "completed", stopReason: "end_turn" }),
|
|
5404
|
-
cancel: async () => { },
|
|
5405
|
-
}),
|
|
5563
|
+
startTurn,
|
|
5406
5564
|
close,
|
|
5407
5565
|
}),
|
|
5408
5566
|
});
|
|
@@ -5412,7 +5570,9 @@ describe("ACPX engine run lifecycle corrections (F1: settle every failure after
|
|
|
5412
5570
|
Object.defineProperty(context, "paperclipSessionHandoffMarkdown", {
|
|
5413
5571
|
enumerable: false,
|
|
5414
5572
|
get() {
|
|
5415
|
-
|
|
5573
|
+
if (failure === "prompt")
|
|
5574
|
+
throw new Error("prompt build boom");
|
|
5575
|
+
return undefined;
|
|
5416
5576
|
},
|
|
5417
5577
|
});
|
|
5418
5578
|
const result = await execute({
|
|
@@ -5422,10 +5582,20 @@ describe("ACPX engine run lifecycle corrections (F1: settle every failure after
|
|
|
5422
5582
|
config: { agent: "custom", agentCommand: "node ./fake-acp.js", stateDir: path.join(root, "state") },
|
|
5423
5583
|
context,
|
|
5424
5584
|
onLog: async () => { },
|
|
5425
|
-
onMeta: async () => {
|
|
5585
|
+
onMeta: async () => { if (failure === "metadata")
|
|
5586
|
+
throw new Error("metadata failed"); },
|
|
5426
5587
|
});
|
|
5427
5588
|
expect(result.exitCode).toBe(1);
|
|
5428
|
-
|
|
5589
|
+
if (failure === "dispatch") {
|
|
5590
|
+
expect(startTurn).toHaveBeenCalledTimes(1);
|
|
5591
|
+
expect(result.executionRecovery).toBeUndefined();
|
|
5592
|
+
expect(result.usageComplete).toBe(false);
|
|
5593
|
+
}
|
|
5594
|
+
else {
|
|
5595
|
+
expect(result.resultJson?.phase).toBe("prepare_turn");
|
|
5596
|
+
expect(startTurn).not.toHaveBeenCalled();
|
|
5597
|
+
expect(result.executionRecovery).toEqual({ kind: "bootstrap", providerWorkStarted: false });
|
|
5598
|
+
}
|
|
5429
5599
|
// The established runtime is closed on the pre-turn failure, so no session
|
|
5430
5600
|
// leaks.
|
|
5431
5601
|
expect(close).toHaveBeenCalledTimes(1);
|
|
@@ -6284,6 +6454,7 @@ describe("ACPX engine sandbox bridge run-disposition seam (fail-closed)", () =>
|
|
|
6284
6454
|
// The lost channel overrides the nominally completed terminal to a failure.
|
|
6285
6455
|
expect(result.exitCode).not.toBe(0);
|
|
6286
6456
|
expect(result.errorCode).toBe("duplex_channel_lost");
|
|
6457
|
+
expect(result.usageComplete).toBe(false);
|
|
6287
6458
|
// The message carries only the typed loss reason, not raw provider text.
|
|
6288
6459
|
expect(result.errorMessage).toContain("provider_exit");
|
|
6289
6460
|
expect(result.resultJson).toMatchObject({ status: "failed" });
|
|
@@ -6300,6 +6471,7 @@ describe("ACPX engine sandbox bridge run-disposition seam (fail-closed)", () =>
|
|
|
6300
6471
|
const result = await runRemote(fake.handle, runtime, sandbox);
|
|
6301
6472
|
expect(result.exitCode).toBe(0);
|
|
6302
6473
|
expect(result.errorCode ?? null).toBeNull();
|
|
6474
|
+
expect(result.usageComplete).toBe(true);
|
|
6303
6475
|
// The atomic settle step marked the orderly completion for the
|
|
6304
6476
|
// success-eligible terminal.
|
|
6305
6477
|
expect(fake.settleRunDisposition).toHaveBeenCalledTimes(1);
|