@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.
Files changed (181) hide show
  1. package/README.md +13 -0
  2. package/dist/acpx-engine/ephemeral-session-environment.d.ts +4 -0
  3. package/dist/acpx-engine/ephemeral-session-environment.d.ts.map +1 -0
  4. package/dist/acpx-engine/ephemeral-session-environment.js +29 -0
  5. package/dist/acpx-engine/ephemeral-session-environment.js.map +1 -0
  6. package/dist/acpx-engine/ephemeral-session-environment.test.d.ts +2 -0
  7. package/dist/acpx-engine/ephemeral-session-environment.test.d.ts.map +1 -0
  8. package/dist/acpx-engine/ephemeral-session-environment.test.js +177 -0
  9. package/dist/acpx-engine/ephemeral-session-environment.test.js.map +1 -0
  10. package/dist/acpx-engine/execute.d.ts +2 -6
  11. package/dist/acpx-engine/execute.d.ts.map +1 -1
  12. package/dist/acpx-engine/execute.js +125 -62
  13. package/dist/acpx-engine/execute.js.map +1 -1
  14. package/dist/acpx-engine/execute.test.js +299 -3
  15. package/dist/acpx-engine/execute.test.js.map +1 -1
  16. package/dist/acpx-engine/session-codec.d.ts.map +1 -1
  17. package/dist/acpx-engine/session-codec.js +1 -0
  18. package/dist/acpx-engine/session-codec.js.map +1 -1
  19. package/dist/acpx-engine/settlement-characterization.test.js +105 -0
  20. package/dist/acpx-engine/settlement-characterization.test.js.map +1 -1
  21. package/dist/acpx-engine/spawn-smoke.test.js +16 -7
  22. package/dist/acpx-engine/spawn-smoke.test.js.map +1 -1
  23. package/dist/acpx-engine/startup-cancellation.d.ts +1 -0
  24. package/dist/acpx-engine/startup-cancellation.d.ts.map +1 -1
  25. package/dist/acpx-engine/startup-cancellation.js +7 -2
  26. package/dist/acpx-engine/startup-cancellation.js.map +1 -1
  27. package/dist/acpx-engine/terminal-session-failure.d.ts +18 -0
  28. package/dist/acpx-engine/terminal-session-failure.d.ts.map +1 -0
  29. package/dist/acpx-engine/terminal-session-failure.js +106 -0
  30. package/dist/acpx-engine/terminal-session-failure.js.map +1 -0
  31. package/dist/acpx-engine/terminal-session-failure.test.d.ts +2 -0
  32. package/dist/acpx-engine/terminal-session-failure.test.d.ts.map +1 -0
  33. package/dist/acpx-engine/terminal-session-failure.test.js +139 -0
  34. package/dist/acpx-engine/terminal-session-failure.test.js.map +1 -0
  35. package/dist/acpx-engine/turn-characterization.test.js +4 -3
  36. package/dist/acpx-engine/turn-characterization.test.js.map +1 -1
  37. package/dist/command-managed-runtime.d.ts +2 -0
  38. package/dist/command-managed-runtime.d.ts.map +1 -1
  39. package/dist/command-managed-runtime.js +2 -0
  40. package/dist/command-managed-runtime.js.map +1 -1
  41. package/dist/command-redaction.d.ts +2 -0
  42. package/dist/command-redaction.d.ts.map +1 -1
  43. package/dist/command-redaction.js +20 -10
  44. package/dist/command-redaction.js.map +1 -1
  45. package/dist/command-redaction.test.js +20 -1
  46. package/dist/command-redaction.test.js.map +1 -1
  47. package/dist/directory-merge-lock.test.d.ts +2 -0
  48. package/dist/directory-merge-lock.test.d.ts.map +1 -0
  49. package/dist/directory-merge-lock.test.js +166 -0
  50. package/dist/directory-merge-lock.test.js.map +1 -0
  51. package/dist/execution-phase.d.ts +10 -0
  52. package/dist/execution-phase.d.ts.map +1 -0
  53. package/dist/execution-phase.js +27 -0
  54. package/dist/execution-phase.js.map +1 -0
  55. package/dist/execution-phase.test.d.ts +2 -0
  56. package/dist/execution-phase.test.d.ts.map +1 -0
  57. package/dist/execution-phase.test.js +26 -0
  58. package/dist/execution-phase.test.js.map +1 -0
  59. package/dist/execution-target-sandbox.test.js +49 -7
  60. package/dist/execution-target-sandbox.test.js.map +1 -1
  61. package/dist/execution-target-stdin-race.test.js +245 -2
  62. package/dist/execution-target-stdin-race.test.js.map +1 -1
  63. package/dist/execution-target.d.ts +6 -0
  64. package/dist/execution-target.d.ts.map +1 -1
  65. package/dist/execution-target.js +74 -22
  66. package/dist/execution-target.js.map +1 -1
  67. package/dist/git-workspace-sync.d.ts +15 -5
  68. package/dist/git-workspace-sync.d.ts.map +1 -1
  69. package/dist/git-workspace-sync.js +267 -124
  70. package/dist/git-workspace-sync.js.map +1 -1
  71. package/dist/git-workspace-sync.test.js +296 -20
  72. package/dist/git-workspace-sync.test.js.map +1 -1
  73. package/dist/github-launcher.d.ts.map +1 -1
  74. package/dist/github-launcher.js +26 -11
  75. package/dist/github-launcher.js.map +1 -1
  76. package/dist/github-launcher.test.js +34 -1
  77. package/dist/github-launcher.test.js.map +1 -1
  78. package/dist/index.d.ts +1 -1
  79. package/dist/index.d.ts.map +1 -1
  80. package/dist/index.js +1 -1
  81. package/dist/index.js.map +1 -1
  82. package/dist/paperclip-runner-permissions.d.ts +28 -1
  83. package/dist/paperclip-runner-permissions.d.ts.map +1 -1
  84. package/dist/paperclip-runner-permissions.js +9 -1
  85. package/dist/paperclip-runner-permissions.js.map +1 -1
  86. package/dist/prompt-sections.test.d.ts +2 -0
  87. package/dist/prompt-sections.test.d.ts.map +1 -0
  88. package/dist/prompt-sections.test.js +53 -0
  89. package/dist/prompt-sections.test.js.map +1 -0
  90. package/dist/provider-stopped-boundary.d.ts +7 -0
  91. package/dist/provider-stopped-boundary.d.ts.map +1 -0
  92. package/dist/provider-stopped-boundary.js +22 -0
  93. package/dist/provider-stopped-boundary.js.map +1 -0
  94. package/dist/provider-stopped-boundary.test.d.ts +2 -0
  95. package/dist/provider-stopped-boundary.test.d.ts.map +1 -0
  96. package/dist/provider-stopped-boundary.test.js +76 -0
  97. package/dist/provider-stopped-boundary.test.js.map +1 -0
  98. package/dist/remote-managed-runtime.d.ts +2 -0
  99. package/dist/remote-managed-runtime.d.ts.map +1 -1
  100. package/dist/remote-managed-runtime.js +3 -1
  101. package/dist/remote-managed-runtime.js.map +1 -1
  102. package/dist/remote-managed-runtime.test.js +25 -0
  103. package/dist/remote-managed-runtime.test.js.map +1 -1
  104. package/dist/sandbox-callback-bridge.d.ts +2 -0
  105. package/dist/sandbox-callback-bridge.d.ts.map +1 -1
  106. package/dist/sandbox-callback-bridge.js +34 -10
  107. package/dist/sandbox-callback-bridge.js.map +1 -1
  108. package/dist/sandbox-callback-bridge.test.js +106 -3
  109. package/dist/sandbox-callback-bridge.test.js.map +1 -1
  110. package/dist/sandbox-managed-runtime.d.ts +3 -0
  111. package/dist/sandbox-managed-runtime.d.ts.map +1 -1
  112. package/dist/sandbox-managed-runtime.js +749 -718
  113. package/dist/sandbox-managed-runtime.js.map +1 -1
  114. package/dist/sandbox-managed-runtime.test.js +171 -2
  115. package/dist/sandbox-managed-runtime.test.js.map +1 -1
  116. package/dist/server-utils.d.ts +43 -1
  117. package/dist/server-utils.d.ts.map +1 -1
  118. package/dist/server-utils.js +103 -31
  119. package/dist/server-utils.js.map +1 -1
  120. package/dist/server-utils.test.js +235 -45
  121. package/dist/server-utils.test.js.map +1 -1
  122. package/dist/session-compaction.d.ts.map +1 -1
  123. package/dist/session-compaction.js +6 -0
  124. package/dist/session-compaction.js.map +1 -1
  125. package/dist/ssh-fixture.test.js +22 -0
  126. package/dist/ssh-fixture.test.js.map +1 -1
  127. package/dist/ssh.d.ts +2 -0
  128. package/dist/ssh.d.ts.map +1 -1
  129. package/dist/ssh.js +2 -2
  130. package/dist/ssh.js.map +1 -1
  131. package/dist/test-fixtures/prompt-context.d.ts +54 -0
  132. package/dist/test-fixtures/prompt-context.d.ts.map +1 -0
  133. package/dist/test-fixtures/prompt-context.js +55 -0
  134. package/dist/test-fixtures/prompt-context.js.map +1 -0
  135. package/dist/types.d.ts +12 -2
  136. package/dist/types.d.ts.map +1 -1
  137. package/dist/workspace-git-stream.d.ts +19 -0
  138. package/dist/workspace-git-stream.d.ts.map +1 -0
  139. package/dist/workspace-git-stream.js +99 -0
  140. package/dist/workspace-git-stream.js.map +1 -0
  141. package/dist/workspace-git-stream.test.d.ts +2 -0
  142. package/dist/workspace-git-stream.test.d.ts.map +1 -0
  143. package/dist/workspace-git-stream.test.js +71 -0
  144. package/dist/workspace-git-stream.test.js.map +1 -0
  145. package/dist/workspace-manifest.d.ts +61 -0
  146. package/dist/workspace-manifest.d.ts.map +1 -0
  147. package/dist/workspace-manifest.js +257 -0
  148. package/dist/workspace-manifest.js.map +1 -0
  149. package/dist/workspace-manifest.test.d.ts +2 -0
  150. package/dist/workspace-manifest.test.d.ts.map +1 -0
  151. package/dist/workspace-manifest.test.js +160 -0
  152. package/dist/workspace-manifest.test.js.map +1 -0
  153. package/dist/workspace-restore-diagnostics.d.ts +34 -0
  154. package/dist/workspace-restore-diagnostics.d.ts.map +1 -0
  155. package/dist/workspace-restore-diagnostics.js +238 -0
  156. package/dist/workspace-restore-diagnostics.js.map +1 -0
  157. package/dist/workspace-restore-diagnostics.test.d.ts +2 -0
  158. package/dist/workspace-restore-diagnostics.test.d.ts.map +1 -0
  159. package/dist/workspace-restore-diagnostics.test.js +258 -0
  160. package/dist/workspace-restore-diagnostics.test.js.map +1 -0
  161. package/dist/workspace-restore-merge.d.ts +47 -6
  162. package/dist/workspace-restore-merge.d.ts.map +1 -1
  163. package/dist/workspace-restore-merge.js +382 -114
  164. package/dist/workspace-restore-merge.js.map +1 -1
  165. package/dist/workspace-restore-merge.test.js +253 -4
  166. package/dist/workspace-restore-merge.test.js.map +1 -1
  167. package/dist/workspace-restore-result.d.ts +6 -0
  168. package/dist/workspace-restore-result.d.ts.map +1 -0
  169. package/dist/workspace-restore-result.js +72 -0
  170. package/dist/workspace-restore-result.js.map +1 -0
  171. package/dist/workspace-restore-result.test.d.ts +2 -0
  172. package/dist/workspace-restore-result.test.d.ts.map +1 -0
  173. package/dist/workspace-restore-result.test.js +74 -0
  174. package/dist/workspace-restore-result.test.js.map +1 -0
  175. package/dist/workspace-restore-teardown.d.ts.map +1 -1
  176. package/dist/workspace-restore-teardown.js +5 -3
  177. package/dist/workspace-restore-teardown.js.map +1 -1
  178. package/dist/workspace-restore-teardown.test.js +24 -0
  179. package/dist/workspace-restore-teardown.test.js.map +1 -1
  180. package/node_modules/acpx/dist/live-checkpoint-ClPCSdrW.js +4 -3
  181. 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).toContain(`> '${jsonPath}.paperclip-upload.decoded'`);
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-"));