@tickernelz/paperclip-pro-adapter-utils 2026.1006.0 → 2026.1006.2
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/README.md +13 -0
- package/dist/acpx-engine/ephemeral-session-environment.d.ts +4 -0
- package/dist/acpx-engine/ephemeral-session-environment.d.ts.map +1 -0
- package/dist/acpx-engine/ephemeral-session-environment.js +29 -0
- package/dist/acpx-engine/ephemeral-session-environment.js.map +1 -0
- package/dist/acpx-engine/ephemeral-session-environment.test.d.ts +2 -0
- package/dist/acpx-engine/ephemeral-session-environment.test.d.ts.map +1 -0
- package/dist/acpx-engine/ephemeral-session-environment.test.js +177 -0
- package/dist/acpx-engine/ephemeral-session-environment.test.js.map +1 -0
- package/dist/acpx-engine/execute.d.ts +2 -6
- package/dist/acpx-engine/execute.d.ts.map +1 -1
- package/dist/acpx-engine/execute.js +125 -62
- package/dist/acpx-engine/execute.js.map +1 -1
- package/dist/acpx-engine/execute.test.js +299 -3
- package/dist/acpx-engine/execute.test.js.map +1 -1
- package/dist/acpx-engine/session-codec.d.ts.map +1 -1
- package/dist/acpx-engine/session-codec.js +1 -0
- package/dist/acpx-engine/session-codec.js.map +1 -1
- package/dist/acpx-engine/settlement-characterization.test.js +105 -0
- package/dist/acpx-engine/settlement-characterization.test.js.map +1 -1
- package/dist/acpx-engine/spawn-smoke.test.js +16 -7
- package/dist/acpx-engine/spawn-smoke.test.js.map +1 -1
- package/dist/acpx-engine/startup-cancellation.d.ts +1 -0
- package/dist/acpx-engine/startup-cancellation.d.ts.map +1 -1
- package/dist/acpx-engine/startup-cancellation.js +7 -2
- package/dist/acpx-engine/startup-cancellation.js.map +1 -1
- package/dist/acpx-engine/terminal-session-failure.d.ts +18 -0
- package/dist/acpx-engine/terminal-session-failure.d.ts.map +1 -0
- package/dist/acpx-engine/terminal-session-failure.js +106 -0
- package/dist/acpx-engine/terminal-session-failure.js.map +1 -0
- package/dist/acpx-engine/terminal-session-failure.test.d.ts +2 -0
- package/dist/acpx-engine/terminal-session-failure.test.d.ts.map +1 -0
- package/dist/acpx-engine/terminal-session-failure.test.js +139 -0
- package/dist/acpx-engine/terminal-session-failure.test.js.map +1 -0
- package/dist/acpx-engine/turn-characterization.test.js +4 -3
- package/dist/acpx-engine/turn-characterization.test.js.map +1 -1
- package/dist/command-managed-runtime.d.ts +2 -0
- package/dist/command-managed-runtime.d.ts.map +1 -1
- package/dist/command-managed-runtime.js +2 -0
- package/dist/command-managed-runtime.js.map +1 -1
- package/dist/command-redaction.d.ts +2 -0
- package/dist/command-redaction.d.ts.map +1 -1
- package/dist/command-redaction.js +20 -10
- package/dist/command-redaction.js.map +1 -1
- package/dist/command-redaction.test.js +20 -1
- package/dist/command-redaction.test.js.map +1 -1
- package/dist/directory-merge-lock.test.d.ts +2 -0
- package/dist/directory-merge-lock.test.d.ts.map +1 -0
- package/dist/directory-merge-lock.test.js +166 -0
- package/dist/directory-merge-lock.test.js.map +1 -0
- package/dist/execution-phase.d.ts +10 -0
- package/dist/execution-phase.d.ts.map +1 -0
- package/dist/execution-phase.js +27 -0
- package/dist/execution-phase.js.map +1 -0
- package/dist/execution-phase.test.d.ts +2 -0
- package/dist/execution-phase.test.d.ts.map +1 -0
- package/dist/execution-phase.test.js +26 -0
- package/dist/execution-phase.test.js.map +1 -0
- package/dist/execution-target-sandbox.test.js +49 -7
- package/dist/execution-target-sandbox.test.js.map +1 -1
- package/dist/execution-target-stdin-race.test.js +245 -2
- package/dist/execution-target-stdin-race.test.js.map +1 -1
- package/dist/execution-target.d.ts +6 -0
- package/dist/execution-target.d.ts.map +1 -1
- package/dist/execution-target.js +74 -22
- package/dist/execution-target.js.map +1 -1
- package/dist/git-workspace-sync.d.ts +15 -5
- package/dist/git-workspace-sync.d.ts.map +1 -1
- package/dist/git-workspace-sync.js +267 -124
- package/dist/git-workspace-sync.js.map +1 -1
- package/dist/git-workspace-sync.test.js +296 -20
- package/dist/git-workspace-sync.test.js.map +1 -1
- package/dist/github-launcher.d.ts.map +1 -1
- package/dist/github-launcher.js +26 -11
- package/dist/github-launcher.js.map +1 -1
- package/dist/github-launcher.test.js +34 -1
- package/dist/github-launcher.test.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/paperclip-runner-permissions.d.ts +28 -1
- package/dist/paperclip-runner-permissions.d.ts.map +1 -1
- package/dist/paperclip-runner-permissions.js +9 -1
- package/dist/paperclip-runner-permissions.js.map +1 -1
- package/dist/prompt-sections.test.d.ts +2 -0
- package/dist/prompt-sections.test.d.ts.map +1 -0
- package/dist/prompt-sections.test.js +53 -0
- package/dist/prompt-sections.test.js.map +1 -0
- package/dist/provider-stopped-boundary.d.ts +7 -0
- package/dist/provider-stopped-boundary.d.ts.map +1 -0
- package/dist/provider-stopped-boundary.js +22 -0
- package/dist/provider-stopped-boundary.js.map +1 -0
- package/dist/provider-stopped-boundary.test.d.ts +2 -0
- package/dist/provider-stopped-boundary.test.d.ts.map +1 -0
- package/dist/provider-stopped-boundary.test.js +76 -0
- package/dist/provider-stopped-boundary.test.js.map +1 -0
- package/dist/remote-managed-runtime.d.ts +2 -0
- package/dist/remote-managed-runtime.d.ts.map +1 -1
- package/dist/remote-managed-runtime.js +3 -1
- package/dist/remote-managed-runtime.js.map +1 -1
- package/dist/remote-managed-runtime.test.js +25 -0
- package/dist/remote-managed-runtime.test.js.map +1 -1
- package/dist/sandbox-callback-bridge.d.ts +2 -0
- package/dist/sandbox-callback-bridge.d.ts.map +1 -1
- package/dist/sandbox-callback-bridge.js +34 -10
- package/dist/sandbox-callback-bridge.js.map +1 -1
- package/dist/sandbox-callback-bridge.test.js +106 -3
- 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 +749 -718
- package/dist/sandbox-managed-runtime.js.map +1 -1
- package/dist/sandbox-managed-runtime.test.js +171 -2
- package/dist/sandbox-managed-runtime.test.js.map +1 -1
- package/dist/server-utils.d.ts +43 -1
- package/dist/server-utils.d.ts.map +1 -1
- package/dist/server-utils.js +103 -31
- package/dist/server-utils.js.map +1 -1
- package/dist/server-utils.test.js +235 -45
- package/dist/server-utils.test.js.map +1 -1
- package/dist/session-compaction.d.ts.map +1 -1
- package/dist/session-compaction.js +6 -0
- package/dist/session-compaction.js.map +1 -1
- package/dist/ssh-fixture.test.js +22 -0
- package/dist/ssh-fixture.test.js.map +1 -1
- package/dist/ssh.d.ts +2 -0
- package/dist/ssh.d.ts.map +1 -1
- package/dist/ssh.js +2 -2
- package/dist/ssh.js.map +1 -1
- package/dist/test-fixtures/prompt-context.d.ts +54 -0
- package/dist/test-fixtures/prompt-context.d.ts.map +1 -0
- package/dist/test-fixtures/prompt-context.js +55 -0
- package/dist/test-fixtures/prompt-context.js.map +1 -0
- package/dist/types.d.ts +12 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/workspace-git-stream.d.ts +19 -0
- package/dist/workspace-git-stream.d.ts.map +1 -0
- package/dist/workspace-git-stream.js +99 -0
- package/dist/workspace-git-stream.js.map +1 -0
- package/dist/workspace-git-stream.test.d.ts +2 -0
- package/dist/workspace-git-stream.test.d.ts.map +1 -0
- package/dist/workspace-git-stream.test.js +71 -0
- package/dist/workspace-git-stream.test.js.map +1 -0
- package/dist/workspace-manifest.d.ts +61 -0
- package/dist/workspace-manifest.d.ts.map +1 -0
- package/dist/workspace-manifest.js +257 -0
- package/dist/workspace-manifest.js.map +1 -0
- package/dist/workspace-manifest.test.d.ts +2 -0
- package/dist/workspace-manifest.test.d.ts.map +1 -0
- package/dist/workspace-manifest.test.js +160 -0
- package/dist/workspace-manifest.test.js.map +1 -0
- package/dist/workspace-restore-diagnostics.d.ts +34 -0
- package/dist/workspace-restore-diagnostics.d.ts.map +1 -0
- package/dist/workspace-restore-diagnostics.js +238 -0
- package/dist/workspace-restore-diagnostics.js.map +1 -0
- package/dist/workspace-restore-diagnostics.test.d.ts +2 -0
- package/dist/workspace-restore-diagnostics.test.d.ts.map +1 -0
- package/dist/workspace-restore-diagnostics.test.js +258 -0
- package/dist/workspace-restore-diagnostics.test.js.map +1 -0
- package/dist/workspace-restore-merge.d.ts +47 -6
- package/dist/workspace-restore-merge.d.ts.map +1 -1
- package/dist/workspace-restore-merge.js +382 -114
- package/dist/workspace-restore-merge.js.map +1 -1
- package/dist/workspace-restore-merge.test.js +253 -4
- package/dist/workspace-restore-merge.test.js.map +1 -1
- package/dist/workspace-restore-result.d.ts +6 -0
- package/dist/workspace-restore-result.d.ts.map +1 -0
- package/dist/workspace-restore-result.js +72 -0
- package/dist/workspace-restore-result.js.map +1 -0
- package/dist/workspace-restore-result.test.d.ts +2 -0
- package/dist/workspace-restore-result.test.d.ts.map +1 -0
- package/dist/workspace-restore-result.test.js +74 -0
- package/dist/workspace-restore-result.test.js.map +1 -0
- package/dist/workspace-restore-teardown.d.ts.map +1 -1
- package/dist/workspace-restore-teardown.js +5 -3
- package/dist/workspace-restore-teardown.js.map +1 -1
- package/dist/workspace-restore-teardown.test.js +24 -0
- package/dist/workspace-restore-teardown.test.js.map +1 -1
- package/node_modules/acpx/dist/live-checkpoint-ClPCSdrW.js +4 -3
- package/package.json +2 -2
|
@@ -102,7 +102,7 @@ describe("stdin file race (parent PAP-4037)", () => {
|
|
|
102
102
|
async function waitFor(check, timeoutMs = 4_000) {
|
|
103
103
|
const deadline = Date.now() + timeoutMs;
|
|
104
104
|
while (Date.now() < deadline) {
|
|
105
|
-
if (check())
|
|
105
|
+
if (await check())
|
|
106
106
|
return;
|
|
107
107
|
await delay(20);
|
|
108
108
|
}
|
|
@@ -407,7 +407,250 @@ describe("stdin file race (parent PAP-4037)", () => {
|
|
|
407
407
|
await bridge?.stop();
|
|
408
408
|
}
|
|
409
409
|
});
|
|
410
|
+
it.each([
|
|
411
|
+
...["prepare", "append", "finalize", "late-finalize"].map((stage) => [stage, "Request failed with status code 502"]),
|
|
412
|
+
...[502, 503, 504].map((status) => ["finalize", `Cloudflare sandbox bridge request failed with HTTP ${status}.`]),
|
|
413
|
+
])("recovers a transient %s failure (%s) without repeating or reordering stdin", async (stage, failure) => {
|
|
414
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-stdin-retry-"));
|
|
415
|
+
cleanupDirs.push(rootDir);
|
|
416
|
+
const childPath = path.join(rootDir, "echo-child.mjs");
|
|
417
|
+
await writeFile(childPath, "process.stdin.on('data', (c) => process.stdout.write(c));\n", "utf8");
|
|
418
|
+
const first = "first-" + "x".repeat(70_000);
|
|
419
|
+
let delivered = "";
|
|
420
|
+
let injected = false;
|
|
421
|
+
let lateFinalize;
|
|
422
|
+
const local = createLocalSandboxRunner();
|
|
423
|
+
const runner = {
|
|
424
|
+
execute: async (input) => {
|
|
425
|
+
const script = input.args?.[1] ?? "";
|
|
426
|
+
const matches = script.includes("/stdin/000000000001.json") && (stage === "prepare" ? script.includes("mkdir -p") :
|
|
427
|
+
stage === "append" ? script.startsWith("printf") : script.startsWith("base64 -d"));
|
|
428
|
+
if (matches && !injected) {
|
|
429
|
+
injected = true;
|
|
430
|
+
if (stage === "late-finalize")
|
|
431
|
+
lateFinalize = () => local.execute(input);
|
|
432
|
+
else if (stage !== "prepare")
|
|
433
|
+
await local.execute(input);
|
|
434
|
+
// The provider can lose the response after the receiver consumed
|
|
435
|
+
// the file. A retry must not repeat those bytes on the ACP stream.
|
|
436
|
+
if (stage === "finalize")
|
|
437
|
+
await waitFor(() => delivered === first, 8_000);
|
|
438
|
+
throw new Error(failure);
|
|
439
|
+
}
|
|
440
|
+
return local.execute(input);
|
|
441
|
+
},
|
|
442
|
+
};
|
|
443
|
+
const bridge = await startAdapterExecutionTargetProcessSessionBridge({
|
|
444
|
+
runId: "run-stdin-retry",
|
|
445
|
+
target: { kind: "remote", transport: "sandbox", remoteCwd: rootDir, runner },
|
|
446
|
+
runtimeRootDir: path.join(rootDir, "runtime"),
|
|
447
|
+
adapterKey: "acpx", command: process.execPath, args: [childPath], cwd: rootDir, env: {},
|
|
448
|
+
});
|
|
449
|
+
let peer;
|
|
450
|
+
try {
|
|
451
|
+
const source = await readFile(bridge.agentCommand, "utf8");
|
|
452
|
+
const port = Number(/port: (\d+)/.exec(source)[1]);
|
|
453
|
+
const token = JSON.parse(/const token = (".*?");/.exec(source)[1]);
|
|
454
|
+
peer = net.createConnection({ host: "127.0.0.1", port });
|
|
455
|
+
peer.on("error", () => { });
|
|
456
|
+
peer.setEncoding("utf8");
|
|
457
|
+
let buffer = "";
|
|
458
|
+
peer.on("data", (chunk) => {
|
|
459
|
+
buffer += chunk;
|
|
460
|
+
const lines = buffer.split("\n");
|
|
461
|
+
buffer = lines.pop();
|
|
462
|
+
for (const line of lines) {
|
|
463
|
+
const frame = JSON.parse(line);
|
|
464
|
+
delivered += collectDelivered([frame]);
|
|
465
|
+
}
|
|
466
|
+
});
|
|
467
|
+
await new Promise((resolve) => peer.once("connect", resolve));
|
|
468
|
+
for (const text of [first, "-second"])
|
|
469
|
+
peer.write(JSON.stringify({ token, type: "stdin", data: Buffer.from(text).toString("base64") }) + "\n");
|
|
470
|
+
await waitFor(() => delivered.endsWith("-second"), 10_000);
|
|
471
|
+
expect(injected).toBe(true);
|
|
472
|
+
expect(delivered).toBe(first + "-second");
|
|
473
|
+
if (lateFinalize) {
|
|
474
|
+
// A provider can return 502 while its original finalize still runs.
|
|
475
|
+
// Cleanup may invalidate its private upload, but it cannot touch
|
|
476
|
+
// the retry's data or repeat input after newer messages arrived.
|
|
477
|
+
await lateFinalize();
|
|
478
|
+
peer.write(JSON.stringify({ token, type: "stdin", data: Buffer.from("-third").toString("base64") }) + "\n");
|
|
479
|
+
await waitFor(() => delivered.endsWith("-third"), 8_000);
|
|
480
|
+
expect(delivered).toBe(first + "-second-third");
|
|
481
|
+
}
|
|
482
|
+
await waitFor(async () => {
|
|
483
|
+
const files = await readdir(path.join(rootDir, "runtime", "process-sessions"), { recursive: true });
|
|
484
|
+
return files.every((file) => !file.endsWith(".paperclip-upload.b64") && !file.endsWith(".paperclip-upload.decoded"));
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
finally {
|
|
488
|
+
peer?.destroy();
|
|
489
|
+
await bridge?.stop();
|
|
490
|
+
}
|
|
491
|
+
}, 20_000);
|
|
492
|
+
it.each([
|
|
493
|
+
["Request failed with status code 502", 3],
|
|
494
|
+
["Request failed with status code 503", 3],
|
|
495
|
+
["Request failed with status code 504", 3],
|
|
496
|
+
["Cloudflare sandbox bridge request failed with HTTP 502.", 3],
|
|
497
|
+
["Cloudflare sandbox bridge request failed with HTTP 503.", 3],
|
|
498
|
+
["Cloudflare sandbox bridge request failed with HTTP 504.", 3],
|
|
499
|
+
["Request failed with status code 403", 1],
|
|
500
|
+
["Cloudflare sandbox bridge request failed with HTTP 403.", 1],
|
|
501
|
+
["Remote command failed: Request failed with status code 502", 1],
|
|
502
|
+
["Cloudflare sandbox bridge request failed with HTTP 502. sensitive-input", 1],
|
|
503
|
+
["Remote command failed: sensitive-input", 1],
|
|
504
|
+
["Provider never responds", 1],
|
|
505
|
+
])("bounds input failure %s to %i attempts and stops later writes", async (failure, expectedAttempts) => {
|
|
506
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-stdin-failed-"));
|
|
507
|
+
cleanupDirs.push(rootDir);
|
|
508
|
+
let attempts = 0;
|
|
509
|
+
let laterWrite = false;
|
|
510
|
+
let stderr = "";
|
|
511
|
+
const runner = createLocalSandboxRunner(async (script) => {
|
|
512
|
+
if (!script.startsWith("mkdir -p"))
|
|
513
|
+
return;
|
|
514
|
+
if (script.includes("/stdin/000000000002.json"))
|
|
515
|
+
laterWrite = true;
|
|
516
|
+
if (script.includes("/stdin/000000000001.json")) {
|
|
517
|
+
attempts += 1;
|
|
518
|
+
if (failure === "Provider never responds")
|
|
519
|
+
await new Promise(() => { });
|
|
520
|
+
throw new Error(failure);
|
|
521
|
+
}
|
|
522
|
+
});
|
|
523
|
+
const bridge = await startAdapterExecutionTargetProcessSessionBridge({
|
|
524
|
+
runId: "run-stdin-failed",
|
|
525
|
+
timeoutSec: failure === "Provider never responds" ? 5 : undefined,
|
|
526
|
+
target: { kind: "remote", transport: "sandbox", remoteCwd: rootDir, runner },
|
|
527
|
+
runtimeRootDir: path.join(rootDir, "runtime"),
|
|
528
|
+
adapterKey: "acpx", command: "cat", args: [], cwd: rootDir, env: {},
|
|
529
|
+
onLog: async (stream, chunk) => { if (stream === "stderr")
|
|
530
|
+
stderr += chunk; },
|
|
531
|
+
});
|
|
532
|
+
let peer;
|
|
533
|
+
try {
|
|
534
|
+
const source = await readFile(bridge.agentCommand, "utf8");
|
|
535
|
+
const port = Number(/port: (\d+)/.exec(source)[1]);
|
|
536
|
+
const token = JSON.parse(/const token = (".*?");/.exec(source)[1]);
|
|
537
|
+
peer = net.createConnection({ host: "127.0.0.1", port });
|
|
538
|
+
peer.setEncoding("utf8");
|
|
539
|
+
peer.on("error", () => { });
|
|
540
|
+
let output = "";
|
|
541
|
+
peer.on("data", (chunk) => { output += chunk; });
|
|
542
|
+
const closed = new Promise((resolve) => peer.on("close", () => resolve()));
|
|
543
|
+
await new Promise((resolve) => peer.once("connect", resolve));
|
|
544
|
+
for (const text of ["first", "second"])
|
|
545
|
+
peer.write(JSON.stringify({ token, type: "stdin", data: Buffer.from(text).toString("base64") }) + "\n");
|
|
546
|
+
await closed;
|
|
547
|
+
expect(attempts).toBe(expectedAttempts);
|
|
548
|
+
expect(laterWrite).toBe(false);
|
|
549
|
+
expect(JSON.parse(output)).toEqual({ type: "error", message: "ACP process session input delivery failed." });
|
|
550
|
+
expect(stderr).toContain("ACP process session input delivery failed.");
|
|
551
|
+
expect(stderr).not.toContain(failure);
|
|
552
|
+
}
|
|
553
|
+
finally {
|
|
554
|
+
peer?.destroy();
|
|
555
|
+
await bridge?.stop();
|
|
556
|
+
}
|
|
557
|
+
}, 15_000);
|
|
558
|
+
it("stops after exhausted input retries even when failure logging stalls", async () => {
|
|
559
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-stdin-log-stall-"));
|
|
560
|
+
cleanupDirs.push(rootDir);
|
|
561
|
+
let attempts = 0;
|
|
562
|
+
let loggingStarted = false;
|
|
563
|
+
let releaseLog;
|
|
564
|
+
const stalledLog = new Promise((resolve) => { releaseLog = resolve; });
|
|
565
|
+
const runner = createLocalSandboxRunner(async (script) => {
|
|
566
|
+
if (script.startsWith("mkdir -p") && script.includes("/stdin/000000000001.json")) {
|
|
567
|
+
attempts += 1;
|
|
568
|
+
throw new Error("Request failed with status code 502");
|
|
569
|
+
}
|
|
570
|
+
});
|
|
571
|
+
const bridge = await startAdapterExecutionTargetProcessSessionBridge({
|
|
572
|
+
runId: "run-stdin-log-stall",
|
|
573
|
+
target: { kind: "remote", transport: "sandbox", remoteCwd: rootDir, runner },
|
|
574
|
+
runtimeRootDir: path.join(rootDir, "runtime"),
|
|
575
|
+
adapterKey: "acpx", command: "cat", args: [], cwd: rootDir, env: {},
|
|
576
|
+
onLog: async (stream) => {
|
|
577
|
+
if (stream === "stderr") {
|
|
578
|
+
loggingStarted = true;
|
|
579
|
+
await stalledLog;
|
|
580
|
+
}
|
|
581
|
+
},
|
|
582
|
+
});
|
|
583
|
+
let peer;
|
|
584
|
+
let stop;
|
|
585
|
+
try {
|
|
586
|
+
const source = await readFile(bridge.agentCommand, "utf8");
|
|
587
|
+
const port = Number(/port: (\d+)/.exec(source)[1]);
|
|
588
|
+
const token = JSON.parse(/const token = (".*?");/.exec(source)[1]);
|
|
589
|
+
peer = net.createConnection({ host: "127.0.0.1", port });
|
|
590
|
+
peer.setEncoding("utf8");
|
|
591
|
+
peer.on("error", () => { });
|
|
592
|
+
let output = "";
|
|
593
|
+
peer.on("data", (chunk) => { output += chunk; });
|
|
594
|
+
const closed = new Promise((resolve) => peer.once("close", resolve));
|
|
595
|
+
await new Promise((resolve) => peer.once("connect", resolve));
|
|
596
|
+
peer.write(JSON.stringify({ token, type: "stdin", data: Buffer.from("input").toString("base64") }) + "\n");
|
|
597
|
+
await closed;
|
|
598
|
+
expect(attempts).toBe(3);
|
|
599
|
+
expect(loggingStarted).toBe(true);
|
|
600
|
+
expect(JSON.parse(output)).toEqual({ type: "error", message: "ACP process session input delivery failed." });
|
|
601
|
+
let stopped = false;
|
|
602
|
+
stop = bridge.stop().then(() => { stopped = true; });
|
|
603
|
+
// Teardown has a three-second acknowledgement budget. It must finish
|
|
604
|
+
// while the run-log promise remains unresolved, including local cleanup.
|
|
605
|
+
await waitFor(() => stopped, 6_000);
|
|
606
|
+
await expect(lstat(bridge.agentCommand)).rejects.toMatchObject({ code: "ENOENT" });
|
|
607
|
+
}
|
|
608
|
+
finally {
|
|
609
|
+
releaseLog();
|
|
610
|
+
peer?.destroy();
|
|
611
|
+
await (stop ?? bridge?.stop());
|
|
612
|
+
}
|
|
613
|
+
}, 15_000);
|
|
410
614
|
// ---- Host atomic-write tests ------------------------------------------
|
|
615
|
+
it.each(["fails", "stalls"])("preserves the upload failure when best-effort cleanup %s", async (cleanupMode) => {
|
|
616
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-upload-cleanup-"));
|
|
617
|
+
cleanupDirs.push(rootDir);
|
|
618
|
+
const local = createLocalSandboxRunner();
|
|
619
|
+
const uploadFailure = new Error("Request failed with status code 502");
|
|
620
|
+
let cleanupAttempted = false;
|
|
621
|
+
let rejectCleanup;
|
|
622
|
+
const stalledCleanup = new Promise((_resolve, reject) => { rejectCleanup = reject; });
|
|
623
|
+
// Observe the test-owned promise even in the immediate-failure case.
|
|
624
|
+
void stalledCleanup.catch(() => { });
|
|
625
|
+
const client = createCommandManagedSandboxCallbackBridgeQueueClient({
|
|
626
|
+
remoteCwd: rootDir,
|
|
627
|
+
runner: {
|
|
628
|
+
execute: async (input) => {
|
|
629
|
+
const script = input.args?.[1] ?? "";
|
|
630
|
+
if (script.startsWith("rm -f")) {
|
|
631
|
+
cleanupAttempted = true;
|
|
632
|
+
if (cleanupMode === "stalls")
|
|
633
|
+
return stalledCleanup;
|
|
634
|
+
throw new Error("Request failed with status code 403");
|
|
635
|
+
}
|
|
636
|
+
const result = await local.execute(input);
|
|
637
|
+
if (script.startsWith("printf"))
|
|
638
|
+
throw uploadFailure;
|
|
639
|
+
return result;
|
|
640
|
+
},
|
|
641
|
+
},
|
|
642
|
+
});
|
|
643
|
+
try {
|
|
644
|
+
await expect(Promise.race([
|
|
645
|
+
client.writeTextFile(path.join(rootDir, "message.json"), "test input"),
|
|
646
|
+
delay(1_000).then(() => { throw new Error("Upload waited for stalled cleanup"); }),
|
|
647
|
+
])).rejects.toBe(uploadFailure);
|
|
648
|
+
expect(cleanupAttempted).toBe(true);
|
|
649
|
+
}
|
|
650
|
+
finally {
|
|
651
|
+
rejectCleanup(new Error("Cleanup unavailable"));
|
|
652
|
+
}
|
|
653
|
+
});
|
|
411
654
|
// A runner that executes each bridge shell script on the local filesystem,
|
|
412
655
|
// so the test exercises the real command-managed `writeTextFile` script.
|
|
413
656
|
function createLocalShellRunner(scripts) {
|
|
@@ -475,7 +718,7 @@ describe("stdin file race (parent PAP-4037)", () => {
|
|
|
475
718
|
expect(finalizeScript).toBeDefined();
|
|
476
719
|
expect(finalizeScript).toContain(`mv `);
|
|
477
720
|
expect(finalizeScript).not.toContain(`> '${jsonPath}'`);
|
|
478
|
-
expect(finalizeScript).
|
|
721
|
+
expect(finalizeScript).toMatch(/> '[^']+\.paperclip-upload\.decoded'/);
|
|
479
722
|
});
|
|
480
723
|
it("never exposes a partial .json file under a concurrent reader (command-managed host write)", async () => {
|
|
481
724
|
const remoteRoot = await mkdtemp(path.join(os.tmpdir(), "paperclip-stdin-host-race-"));
|