@tickernelz/paperclip-pro-adapter-codex-local 2026.925.0

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 (268) hide show
  1. package/LICENSE +22 -0
  2. package/dist/cli/format-event.d.ts +2 -0
  3. package/dist/cli/format-event.d.ts.map +1 -0
  4. package/dist/cli/format-event.js +218 -0
  5. package/dist/cli/format-event.js.map +1 -0
  6. package/dist/cli/index.d.ts +2 -0
  7. package/dist/cli/index.d.ts.map +1 -0
  8. package/dist/cli/index.js +2 -0
  9. package/dist/cli/index.js.map +1 -0
  10. package/dist/cli/quota-probe.d.ts +3 -0
  11. package/dist/cli/quota-probe.d.ts.map +1 -0
  12. package/dist/cli/quota-probe.js +104 -0
  13. package/dist/cli/quota-probe.js.map +1 -0
  14. package/dist/index.d.ts +21 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +155 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/index.test.d.ts +2 -0
  19. package/dist/index.test.d.ts.map +1 -0
  20. package/dist/index.test.js +58 -0
  21. package/dist/index.test.js.map +1 -0
  22. package/dist/server/acp.d.ts +30 -0
  23. package/dist/server/acp.d.ts.map +1 -0
  24. package/dist/server/acp.js +532 -0
  25. package/dist/server/acp.js.map +1 -0
  26. package/dist/server/acp.test.d.ts +2 -0
  27. package/dist/server/acp.test.d.ts.map +1 -0
  28. package/dist/server/acp.test.js +1087 -0
  29. package/dist/server/acp.test.js.map +1 -0
  30. package/dist/server/adapter-auth-promotion.d.ts +102 -0
  31. package/dist/server/adapter-auth-promotion.d.ts.map +1 -0
  32. package/dist/server/adapter-auth-promotion.js +210 -0
  33. package/dist/server/adapter-auth-promotion.js.map +1 -0
  34. package/dist/server/adapter-auth-promotion.test.d.ts +2 -0
  35. package/dist/server/adapter-auth-promotion.test.d.ts.map +1 -0
  36. package/dist/server/adapter-auth-promotion.test.js +658 -0
  37. package/dist/server/adapter-auth-promotion.test.js.map +1 -0
  38. package/dist/server/auth-check.d.ts +8 -0
  39. package/dist/server/auth-check.d.ts.map +1 -0
  40. package/dist/server/auth-check.js +8 -0
  41. package/dist/server/auth-check.js.map +1 -0
  42. package/dist/server/auth-precedence.d.ts +16 -0
  43. package/dist/server/auth-precedence.d.ts.map +1 -0
  44. package/dist/server/auth-precedence.js +20 -0
  45. package/dist/server/auth-precedence.js.map +1 -0
  46. package/dist/server/auth-precedence.test.d.ts +2 -0
  47. package/dist/server/auth-precedence.test.d.ts.map +1 -0
  48. package/dist/server/auth-precedence.test.js +100 -0
  49. package/dist/server/auth-precedence.test.js.map +1 -0
  50. package/dist/server/codex-args.d.ts +13 -0
  51. package/dist/server/codex-args.d.ts.map +1 -0
  52. package/dist/server/codex-args.js +76 -0
  53. package/dist/server/codex-args.js.map +1 -0
  54. package/dist/server/codex-args.test.d.ts +2 -0
  55. package/dist/server/codex-args.test.d.ts.map +1 -0
  56. package/dist/server/codex-args.test.js +243 -0
  57. package/dist/server/codex-args.test.js.map +1 -0
  58. package/dist/server/codex-auth-cache.d.ts +206 -0
  59. package/dist/server/codex-auth-cache.d.ts.map +1 -0
  60. package/dist/server/codex-auth-cache.js +501 -0
  61. package/dist/server/codex-auth-cache.js.map +1 -0
  62. package/dist/server/codex-auth-cache.test.d.ts +2 -0
  63. package/dist/server/codex-auth-cache.test.d.ts.map +1 -0
  64. package/dist/server/codex-auth-cache.test.js +497 -0
  65. package/dist/server/codex-auth-cache.test.js.map +1 -0
  66. package/dist/server/codex-auth-copyback.d.ts +53 -0
  67. package/dist/server/codex-auth-copyback.d.ts.map +1 -0
  68. package/dist/server/codex-auth-copyback.js +123 -0
  69. package/dist/server/codex-auth-copyback.js.map +1 -0
  70. package/dist/server/codex-auth-copyback.test.d.ts +2 -0
  71. package/dist/server/codex-auth-copyback.test.d.ts.map +1 -0
  72. package/dist/server/codex-auth-copyback.test.js +507 -0
  73. package/dist/server/codex-auth-copyback.test.js.map +1 -0
  74. package/dist/server/codex-auth-merge-decision.cjs +170 -0
  75. package/dist/server/codex-auth-merge-decision.d.ts +22 -0
  76. package/dist/server/codex-auth-merge-decision.d.ts.map +1 -0
  77. package/dist/server/codex-auth-merge-decision.js +58 -0
  78. package/dist/server/codex-auth-merge-decision.js.map +1 -0
  79. package/dist/server/codex-auth-merge-decision.test.d.ts +2 -0
  80. package/dist/server/codex-auth-merge-decision.test.d.ts.map +1 -0
  81. package/dist/server/codex-auth-merge-decision.test.js +230 -0
  82. package/dist/server/codex-auth-merge-decision.test.js.map +1 -0
  83. package/dist/server/codex-auth-merge-extract.sh +73 -0
  84. package/dist/server/codex-auth-merge-scripts.d.ts +21 -0
  85. package/dist/server/codex-auth-merge-scripts.d.ts.map +1 -0
  86. package/dist/server/codex-auth-merge-scripts.js +40 -0
  87. package/dist/server/codex-auth-merge-scripts.js.map +1 -0
  88. package/dist/server/codex-auth-merge.test.d.ts +2 -0
  89. package/dist/server/codex-auth-merge.test.d.ts.map +1 -0
  90. package/dist/server/codex-auth-merge.test.js +714 -0
  91. package/dist/server/codex-auth-merge.test.js.map +1 -0
  92. package/dist/server/codex-auth-seed-write.d.ts +30 -0
  93. package/dist/server/codex-auth-seed-write.d.ts.map +1 -0
  94. package/dist/server/codex-auth-seed-write.js +41 -0
  95. package/dist/server/codex-auth-seed-write.js.map +1 -0
  96. package/dist/server/codex-home.d.ts +162 -0
  97. package/dist/server/codex-home.d.ts.map +1 -0
  98. package/dist/server/codex-home.js +771 -0
  99. package/dist/server/codex-home.js.map +1 -0
  100. package/dist/server/codex-home.test.d.ts +2 -0
  101. package/dist/server/codex-home.test.d.ts.map +1 -0
  102. package/dist/server/codex-home.test.js +1183 -0
  103. package/dist/server/codex-home.test.js.map +1 -0
  104. package/dist/server/config-schema.d.ts +3 -0
  105. package/dist/server/config-schema.d.ts.map +1 -0
  106. package/dist/server/config-schema.js +67 -0
  107. package/dist/server/config-schema.js.map +1 -0
  108. package/dist/server/device-login-export.d.ts +56 -0
  109. package/dist/server/device-login-export.d.ts.map +1 -0
  110. package/dist/server/device-login-export.js +230 -0
  111. package/dist/server/device-login-export.js.map +1 -0
  112. package/dist/server/device-login-export.test.d.ts +2 -0
  113. package/dist/server/device-login-export.test.d.ts.map +1 -0
  114. package/dist/server/device-login-export.test.js +245 -0
  115. package/dist/server/device-login-export.test.js.map +1 -0
  116. package/dist/server/device-login-parse.d.ts +20 -0
  117. package/dist/server/device-login-parse.d.ts.map +1 -0
  118. package/dist/server/device-login-parse.js +155 -0
  119. package/dist/server/device-login-parse.js.map +1 -0
  120. package/dist/server/device-login-parse.test.d.ts +2 -0
  121. package/dist/server/device-login-parse.test.d.ts.map +1 -0
  122. package/dist/server/device-login-parse.test.js +269 -0
  123. package/dist/server/device-login-parse.test.js.map +1 -0
  124. package/dist/server/device-login-runner.d.ts +69 -0
  125. package/dist/server/device-login-runner.d.ts.map +1 -0
  126. package/dist/server/device-login-runner.js +109 -0
  127. package/dist/server/device-login-runner.js.map +1 -0
  128. package/dist/server/device-login-runner.test.d.ts +2 -0
  129. package/dist/server/device-login-runner.test.d.ts.map +1 -0
  130. package/dist/server/device-login-runner.test.js +201 -0
  131. package/dist/server/device-login-runner.test.js.map +1 -0
  132. package/dist/server/engine-availability.test.d.ts +2 -0
  133. package/dist/server/engine-availability.test.d.ts.map +1 -0
  134. package/dist/server/engine-availability.test.js +41 -0
  135. package/dist/server/engine-availability.test.js.map +1 -0
  136. package/dist/server/execute.acp-fallback.test.d.ts +2 -0
  137. package/dist/server/execute.acp-fallback.test.d.ts.map +1 -0
  138. package/dist/server/execute.acp-fallback.test.js +123 -0
  139. package/dist/server/execute.acp-fallback.test.js.map +1 -0
  140. package/dist/server/execute.auth-precedence.test.d.ts +2 -0
  141. package/dist/server/execute.auth-precedence.test.d.ts.map +1 -0
  142. package/dist/server/execute.auth-precedence.test.js +137 -0
  143. package/dist/server/execute.auth-precedence.test.js.map +1 -0
  144. package/dist/server/execute.auth.test.d.ts +2 -0
  145. package/dist/server/execute.auth.test.d.ts.map +1 -0
  146. package/dist/server/execute.auth.test.js +197 -0
  147. package/dist/server/execute.auth.test.js.map +1 -0
  148. package/dist/server/execute.d.ts +40 -0
  149. package/dist/server/execute.d.ts.map +1 -0
  150. package/dist/server/execute.js +1290 -0
  151. package/dist/server/execute.js.map +1 -0
  152. package/dist/server/execute.remote.test.d.ts +2 -0
  153. package/dist/server/execute.remote.test.d.ts.map +1 -0
  154. package/dist/server/execute.remote.test.js +556 -0
  155. package/dist/server/execute.remote.test.js.map +1 -0
  156. package/dist/server/execute.stderr-error.test.d.ts +2 -0
  157. package/dist/server/execute.stderr-error.test.d.ts.map +1 -0
  158. package/dist/server/execute.stderr-error.test.js +142 -0
  159. package/dist/server/execute.stderr-error.test.js.map +1 -0
  160. package/dist/server/execute.test.d.ts +2 -0
  161. package/dist/server/execute.test.d.ts.map +1 -0
  162. package/dist/server/execute.test.js +240 -0
  163. package/dist/server/execute.test.js.map +1 -0
  164. package/dist/server/index.d.ts +18 -0
  165. package/dist/server/index.d.ts.map +1 -0
  166. package/dist/server/index.js +71 -0
  167. package/dist/server/index.js.map +1 -0
  168. package/dist/server/output-inactivity-monitor.d.ts +62 -0
  169. package/dist/server/output-inactivity-monitor.d.ts.map +1 -0
  170. package/dist/server/output-inactivity-monitor.integration.test.d.ts +2 -0
  171. package/dist/server/output-inactivity-monitor.integration.test.d.ts.map +1 -0
  172. package/dist/server/output-inactivity-monitor.integration.test.js +136 -0
  173. package/dist/server/output-inactivity-monitor.integration.test.js.map +1 -0
  174. package/dist/server/output-inactivity-monitor.js +112 -0
  175. package/dist/server/output-inactivity-monitor.js.map +1 -0
  176. package/dist/server/output-inactivity-monitor.test.d.ts +2 -0
  177. package/dist/server/output-inactivity-monitor.test.d.ts.map +1 -0
  178. package/dist/server/output-inactivity-monitor.test.js +284 -0
  179. package/dist/server/output-inactivity-monitor.test.js.map +1 -0
  180. package/dist/server/parse.d.ts +51 -0
  181. package/dist/server/parse.d.ts.map +1 -0
  182. package/dist/server/parse.js +261 -0
  183. package/dist/server/parse.js.map +1 -0
  184. package/dist/server/parse.test.d.ts +2 -0
  185. package/dist/server/parse.test.d.ts.map +1 -0
  186. package/dist/server/parse.test.js +186 -0
  187. package/dist/server/parse.test.js.map +1 -0
  188. package/dist/server/process-activity-monitor.d.ts +21 -0
  189. package/dist/server/process-activity-monitor.d.ts.map +1 -0
  190. package/dist/server/process-activity-monitor.js +103 -0
  191. package/dist/server/process-activity-monitor.js.map +1 -0
  192. package/dist/server/process-activity-monitor.test.d.ts +2 -0
  193. package/dist/server/process-activity-monitor.test.d.ts.map +1 -0
  194. package/dist/server/process-activity-monitor.test.js +82 -0
  195. package/dist/server/process-activity-monitor.test.js.map +1 -0
  196. package/dist/server/quota-spawn-error.test.d.ts +2 -0
  197. package/dist/server/quota-spawn-error.test.d.ts.map +1 -0
  198. package/dist/server/quota-spawn-error.test.js +174 -0
  199. package/dist/server/quota-spawn-error.test.js.map +1 -0
  200. package/dist/server/quota.d.ts +66 -0
  201. package/dist/server/quota.d.ts.map +1 -0
  202. package/dist/server/quota.js +511 -0
  203. package/dist/server/quota.js.map +1 -0
  204. package/dist/server/runtime-config.d.ts +11 -0
  205. package/dist/server/runtime-config.d.ts.map +1 -0
  206. package/dist/server/runtime-config.js +380 -0
  207. package/dist/server/runtime-config.js.map +1 -0
  208. package/dist/server/runtime-config.test.d.ts +2 -0
  209. package/dist/server/runtime-config.test.d.ts.map +1 -0
  210. package/dist/server/runtime-config.test.js +367 -0
  211. package/dist/server/runtime-config.test.js.map +1 -0
  212. package/dist/server/skills.d.ts +8 -0
  213. package/dist/server/skills.d.ts.map +1 -0
  214. package/dist/server/skills.js +26 -0
  215. package/dist/server/skills.js.map +1 -0
  216. package/dist/server/test.d.ts +3 -0
  217. package/dist/server/test.d.ts.map +1 -0
  218. package/dist/server/test.js +458 -0
  219. package/dist/server/test.js.map +1 -0
  220. package/dist/server/test.remote.test.d.ts +2 -0
  221. package/dist/server/test.remote.test.d.ts.map +1 -0
  222. package/dist/server/test.remote.test.js +396 -0
  223. package/dist/server/test.remote.test.js.map +1 -0
  224. package/dist/ui/build-config.d.ts +5 -0
  225. package/dist/ui/build-config.d.ts.map +1 -0
  226. package/dist/ui/build-config.js +229 -0
  227. package/dist/ui/build-config.js.map +1 -0
  228. package/dist/ui/build-config.test.d.ts +2 -0
  229. package/dist/ui/build-config.test.d.ts.map +1 -0
  230. package/dist/ui/build-config.test.js +272 -0
  231. package/dist/ui/build-config.test.js.map +1 -0
  232. package/dist/ui/index.d.ts +4 -0
  233. package/dist/ui/index.d.ts.map +1 -0
  234. package/dist/ui/index.js +8 -0
  235. package/dist/ui/index.js.map +1 -0
  236. package/dist/ui/parse-stdout.d.ts +3 -0
  237. package/dist/ui/parse-stdout.d.ts.map +1 -0
  238. package/dist/ui/parse-stdout.js +265 -0
  239. package/dist/ui/parse-stdout.js.map +1 -0
  240. package/dist/ui/parse-stdout.test.d.ts +2 -0
  241. package/dist/ui/parse-stdout.test.d.ts.map +1 -0
  242. package/dist/ui/parse-stdout.test.js +77 -0
  243. package/dist/ui/parse-stdout.test.js.map +1 -0
  244. package/package.json +61 -0
  245. package/skills/agentmail/SKILL.md +70 -0
  246. package/skills/paperclip/SKILL.md +712 -0
  247. package/skills/paperclip/references/api-reference.md +1675 -0
  248. package/skills/paperclip/references/artifacts.md +158 -0
  249. package/skills/paperclip/references/cases.md +295 -0
  250. package/skills/paperclip/references/company-skills.md +266 -0
  251. package/skills/paperclip/references/issue-workspaces.md +80 -0
  252. package/skills/paperclip/references/routines.md +231 -0
  253. package/skills/paperclip/references/workflows.md +141 -0
  254. package/skills/paperclip/scripts/paperclip-upload-artifact.sh +592 -0
  255. package/skills/paperclip-board/SKILL.md +619 -0
  256. package/skills/paperclip-converting-plans-to-tasks/SKILL.md +60 -0
  257. package/skills/paperclip-create-agent/SKILL.md +179 -0
  258. package/skills/paperclip-create-agent/references/agent-instruction-templates.md +123 -0
  259. package/skills/paperclip-create-agent/references/agents/coder.md +64 -0
  260. package/skills/paperclip-create-agent/references/agents/qa.md +88 -0
  261. package/skills/paperclip-create-agent/references/agents/securityengineer.md +135 -0
  262. package/skills/paperclip-create-agent/references/agents/uxdesigner.md +115 -0
  263. package/skills/paperclip-create-agent/references/api-reference.md +110 -0
  264. package/skills/paperclip-create-agent/references/baseline-role-guide.md +168 -0
  265. package/skills/paperclip-create-agent/references/draft-review-checklist.md +95 -0
  266. package/skills/para-memory-files/SKILL.md +100 -0
  267. package/skills/para-memory-files/references/schemas.md +35 -0
  268. package/skills/slack/SKILL.md +65 -0
@@ -0,0 +1,714 @@
1
+ import { execFile as execFileCallback } from "node:child_process";
2
+ import { lstat, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { promisify } from "node:util";
7
+ import { afterEach, describe, expect, it } from "vitest";
8
+ import { prepareSandboxManagedRuntime, } from "@tickernelz/paperclip-pro-adapter-utils/sandbox-managed-runtime";
9
+ import { buildCodexAuthInboundProvision } from "./codex-auth-merge-scripts.js";
10
+ const execFile = promisify(execFileCallback);
11
+ describe("codex home auth merge on sandbox asset extract", () => {
12
+ const cleanupDirs = [];
13
+ afterEach(async () => {
14
+ while (cleanupDirs.length > 0) {
15
+ const dir = cleanupDirs.pop();
16
+ if (!dir)
17
+ continue;
18
+ await rm(dir, { recursive: true, force: true }).catch(() => undefined);
19
+ }
20
+ });
21
+ function subscriptionAuth(input) {
22
+ return JSON.stringify({
23
+ tokens: {
24
+ id_token: `id-token-${input.marker}`,
25
+ access_token: `access-token-${input.marker}`,
26
+ refresh_token: `refresh-token-${input.marker}`,
27
+ account_id: input.accountId,
28
+ },
29
+ ...(input.lastRefresh ? { last_refresh: input.lastRefresh } : {}),
30
+ }, null, 2);
31
+ }
32
+ function apiKeyAuth(marker) {
33
+ return JSON.stringify({ OPENAI_API_KEY: `sk-${marker}` }, null, 2);
34
+ }
35
+ async function runCodexHomeAssetExtract(input) {
36
+ const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-codex-auth-merge-"));
37
+ cleanupDirs.push(rootDir);
38
+ const localWorkspaceDir = path.join(rootDir, "local-workspace");
39
+ const remoteWorkspaceDir = path.join(rootDir, "remote-workspace");
40
+ const localHomeDir = path.join(rootDir, "local-codex-home");
41
+ const remoteHomeDir = path.join(remoteWorkspaceDir, ".paperclip-runtime", "codex", "home");
42
+ await mkdir(localWorkspaceDir, { recursive: true });
43
+ await mkdir(localHomeDir, { recursive: true });
44
+ await mkdir(remoteHomeDir, { recursive: true });
45
+ await writeFile(path.join(localWorkspaceDir, "README.md"), "workspace\n", "utf8");
46
+ if (input.hostAuth !== undefined) {
47
+ await writeFile(path.join(localHomeDir, "auth.json"), input.hostAuth, { mode: 0o600 });
48
+ }
49
+ await writeFile(path.join(localHomeDir, "config.toml"), "model = \"gpt\"\n", "utf8");
50
+ if (input.sandboxAuth !== undefined) {
51
+ await writeFile(path.join(remoteHomeDir, "auth.json"), input.sandboxAuth, { mode: 0o600 });
52
+ }
53
+ // A fake in-sandbox $HOME whose ~/.codex may carry the image's own login.
54
+ const imageHomeDir = path.join(rootDir, "image-home");
55
+ await mkdir(path.join(imageHomeDir, ".codex"), { recursive: true });
56
+ if (input.imageAuth !== undefined) {
57
+ await writeFile(path.join(imageHomeDir, ".codex", "auth.json"), input.imageAuth, { mode: 0o600 });
58
+ }
59
+ const commands = [];
60
+ const outputs = [];
61
+ const writtenPaths = [];
62
+ const client = {
63
+ makeDir: async (remotePath) => {
64
+ await mkdir(remotePath, { recursive: true });
65
+ },
66
+ writeFile: async (remotePath, bytes) => {
67
+ writtenPaths.push(remotePath);
68
+ await mkdir(path.dirname(remotePath), { recursive: true });
69
+ await writeFile(remotePath, Buffer.from(bytes));
70
+ },
71
+ readFile: async (remotePath) => await readFile(remotePath),
72
+ listFiles: async () => [],
73
+ remove: async (remotePath) => {
74
+ await rm(remotePath, { recursive: true, force: true });
75
+ },
76
+ run: async (command) => {
77
+ commands.push(command);
78
+ const result = await execFile("sh", ["-c", command], {
79
+ maxBuffer: 32 * 1024 * 1024,
80
+ env: { ...process.env, HOME: imageHomeDir },
81
+ });
82
+ outputs.push(result.stdout, result.stderr);
83
+ },
84
+ };
85
+ // Non-native base64-tar fallback `syncIn`: place each file mapping via
86
+ // `writeFile`, then run the operation's ordered `postUploadCommands` — the
87
+ // same seam the command-managed client provides in production. The Codex home
88
+ // asset uploads its tar + the two merge scripts, then runs the auth-merge
89
+ // command as the operation's post-upload command.
90
+ client.syncIn = async (operations) => {
91
+ for (const operation of operations) {
92
+ for (const mapping of operation.files) {
93
+ const bytes = await readFile(mapping.sourcePath);
94
+ await mkdir(path.dirname(mapping.targetPath), { recursive: true });
95
+ await client.writeFile(mapping.targetPath, bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength));
96
+ }
97
+ for (const command of operation.postUploadCommands ?? []) {
98
+ await client.run(command.command, { timeoutMs: 30_000 });
99
+ }
100
+ }
101
+ return { operations: [] };
102
+ };
103
+ await prepareSandboxManagedRuntime({
104
+ spec: {
105
+ transport: "sandbox",
106
+ provider: "test",
107
+ sandboxId: "sandbox-1",
108
+ remoteCwd: remoteWorkspaceDir,
109
+ timeoutMs: 30_000,
110
+ apiKey: null,
111
+ },
112
+ adapterKey: "codex",
113
+ client,
114
+ workspaceLocalDir: localWorkspaceDir,
115
+ assets: [{
116
+ key: "home",
117
+ localDir: localHomeDir,
118
+ followSymlinks: true,
119
+ // The Codex inbound auth-merge rides the generic per-asset `provision`
120
+ // seam. This matrix drives the sandbox core directly, supplying the same
121
+ // contribution the codex adapter (`execute.ts`) attaches in production —
122
+ // proving the seam reproduces inbound behavior.
123
+ provision: buildCodexAuthInboundProvision(),
124
+ }],
125
+ });
126
+ const commandText = commands.find((command) => command.includes("codex-auth-merge-extract.sh")) ?? "";
127
+ const finalAuthPath = path.join(remoteHomeDir, "auth.json");
128
+ return {
129
+ commandText,
130
+ writtenPaths,
131
+ finalAuth: await readFile(finalAuthPath, "utf8"),
132
+ finalMode: (await lstat(finalAuthPath)).mode & 0o777,
133
+ combinedOutput: outputs.join("\n"),
134
+ };
135
+ }
136
+ it("keeps a newer same-account sandbox auth.json and installs it atomically with mode 0600", async () => {
137
+ const sandboxAuth = subscriptionAuth({
138
+ accountId: "acct-same",
139
+ lastRefresh: "2026-07-09T02:00:00Z",
140
+ marker: "sandbox-newer-SENTINEL",
141
+ });
142
+ const hostAuth = subscriptionAuth({
143
+ accountId: "acct-same",
144
+ lastRefresh: "2026-07-09T01:00:00Z",
145
+ marker: "host-older-SENTINEL",
146
+ });
147
+ const result = await runCodexHomeAssetExtract({ sandboxAuth, hostAuth });
148
+ expect(result.finalAuth).toBe(sandboxAuth);
149
+ expect(result.finalMode).toBe(0o600);
150
+ expect(result.combinedOutput).not.toContain("SENTINEL");
151
+ expect(result.commandText).not.toContain("SENTINEL");
152
+ expect(result.commandText).toContain("codex-auth-merge-extract.sh");
153
+ expect(result.commandText).not.toContain("paperclip-extract");
154
+ expect(result.commandText).not.toContain("node -");
155
+ expect(result.commandText).not.toContain("target_tmp=");
156
+ expect(result.commandText).not.toContain("mv -f");
157
+ expect(result.writtenPaths.some((entry) => entry.endsWith("codex-auth-merge-extract.sh"))).toBe(true);
158
+ expect(result.writtenPaths.some((entry) => entry.endsWith("codex-auth-merge-decision.cjs"))).toBe(true);
159
+ });
160
+ it("installs same-account host auth when host last_refresh is strictly newer", async () => {
161
+ const sandboxAuth = subscriptionAuth({
162
+ accountId: "acct-same",
163
+ lastRefresh: "2026-07-09T01:00:00Z",
164
+ marker: "sandbox-older",
165
+ });
166
+ const hostAuth = subscriptionAuth({
167
+ accountId: "acct-same",
168
+ lastRefresh: "2026-07-09T02:00:00Z",
169
+ marker: "host-newer",
170
+ });
171
+ const result = await runCodexHomeAssetExtract({ sandboxAuth, hostAuth });
172
+ expect(result.finalAuth).toBe(hostAuth);
173
+ expect(result.finalMode).toBe(0o600);
174
+ });
175
+ it("installs host auth on identity mismatch, auth-mode mismatch, apikey mode, and unusable sandbox auth", async () => {
176
+ const cases = [
177
+ {
178
+ name: "identity mismatch",
179
+ sandboxAuth: subscriptionAuth({
180
+ accountId: "acct-b",
181
+ lastRefresh: "2026-07-09T03:00:00Z",
182
+ marker: "sandbox-account-b",
183
+ }),
184
+ hostAuth: subscriptionAuth({
185
+ accountId: "acct-a",
186
+ lastRefresh: "2026-07-09T01:00:00Z",
187
+ marker: "host-account-a",
188
+ }),
189
+ },
190
+ {
191
+ name: "auth-mode mismatch",
192
+ sandboxAuth: subscriptionAuth({
193
+ accountId: "acct-a",
194
+ lastRefresh: "2026-07-09T03:00:00Z",
195
+ marker: "sandbox-subscription",
196
+ }),
197
+ hostAuth: apiKeyAuth("host-api-key"),
198
+ },
199
+ {
200
+ name: "both apikey",
201
+ sandboxAuth: apiKeyAuth("sandbox-api-key"),
202
+ hostAuth: apiKeyAuth("host-api-key"),
203
+ },
204
+ {
205
+ name: "sandbox account id missing",
206
+ sandboxAuth: JSON.stringify({
207
+ tokens: {
208
+ id_token: "id-token-sandbox",
209
+ access_token: "access-token-sandbox",
210
+ refresh_token: "refresh-token-sandbox",
211
+ },
212
+ last_refresh: "2026-07-09T03:00:00Z",
213
+ }),
214
+ hostAuth: subscriptionAuth({
215
+ accountId: "acct-a",
216
+ lastRefresh: "2026-07-09T01:00:00Z",
217
+ marker: "host-account-a",
218
+ }),
219
+ },
220
+ ];
221
+ for (const entry of cases) {
222
+ const result = await runCodexHomeAssetExtract({
223
+ sandboxAuth: entry.sandboxAuth,
224
+ hostAuth: entry.hostAuth,
225
+ });
226
+ expect(result.finalAuth, entry.name).toBe(entry.hostAuth);
227
+ expect(result.finalMode, entry.name).toBe(0o600);
228
+ }
229
+ });
230
+ it("keeps host auth when the sandbox copy is not strictly newer (equal, missing, or unparseable freshness)", async () => {
231
+ // The extract path stages the sandbox copy as `source` and the host copy as
232
+ // `destination`. The decision predicate only adopts the source when it is
233
+ // strictly fresher, so a tie, a missing last_refresh on either side, or an
234
+ // unparseable stamp all fall through to the host destination.
235
+ const cases = [
236
+ {
237
+ name: "equal last_refresh",
238
+ sandboxAuth: subscriptionAuth({
239
+ accountId: "acct-same",
240
+ lastRefresh: "2026-07-09T02:00:00Z",
241
+ marker: "sandbox-equal",
242
+ }),
243
+ hostAuth: subscriptionAuth({
244
+ accountId: "acct-same",
245
+ lastRefresh: "2026-07-09T02:00:00Z",
246
+ marker: "host-equal",
247
+ }),
248
+ },
249
+ {
250
+ name: "missing sandbox last_refresh",
251
+ sandboxAuth: subscriptionAuth({
252
+ accountId: "acct-same",
253
+ marker: "sandbox-missing-refresh",
254
+ }),
255
+ hostAuth: subscriptionAuth({
256
+ accountId: "acct-same",
257
+ lastRefresh: "2026-07-09T02:00:00Z",
258
+ marker: "host-refresh",
259
+ }),
260
+ },
261
+ {
262
+ name: "missing host last_refresh",
263
+ sandboxAuth: subscriptionAuth({
264
+ accountId: "acct-same",
265
+ lastRefresh: "2026-07-09T02:00:00Z",
266
+ marker: "sandbox-refresh",
267
+ }),
268
+ hostAuth: subscriptionAuth({
269
+ accountId: "acct-same",
270
+ marker: "host-missing-refresh",
271
+ }),
272
+ },
273
+ {
274
+ name: "unparseable sandbox last_refresh",
275
+ sandboxAuth: subscriptionAuth({
276
+ accountId: "acct-same",
277
+ lastRefresh: "not-a-date",
278
+ marker: "sandbox-bad-refresh",
279
+ }),
280
+ hostAuth: subscriptionAuth({
281
+ accountId: "acct-same",
282
+ lastRefresh: "2026-07-09T02:00:00Z",
283
+ marker: "host-refresh",
284
+ }),
285
+ },
286
+ {
287
+ name: "unparseable host last_refresh",
288
+ sandboxAuth: subscriptionAuth({
289
+ accountId: "acct-same",
290
+ lastRefresh: "2026-07-09T02:00:00Z",
291
+ marker: "sandbox-refresh",
292
+ }),
293
+ hostAuth: subscriptionAuth({
294
+ accountId: "acct-same",
295
+ lastRefresh: "not-a-date",
296
+ marker: "host-bad-refresh",
297
+ }),
298
+ },
299
+ ];
300
+ for (const entry of cases) {
301
+ const result = await runCodexHomeAssetExtract({
302
+ sandboxAuth: entry.sandboxAuth,
303
+ hostAuth: entry.hostAuth,
304
+ });
305
+ expect(result.finalAuth, entry.name).toBe(entry.hostAuth);
306
+ expect(result.finalMode, entry.name).toBe(0o600);
307
+ }
308
+ });
309
+ it("installs unusable host auth instead of serving leftover sandbox auth", async () => {
310
+ const sandboxAuth = subscriptionAuth({
311
+ accountId: "acct-a",
312
+ lastRefresh: "2026-07-09T03:00:00Z",
313
+ marker: "sandbox-valid-SENTINEL",
314
+ });
315
+ const cases = [
316
+ {
317
+ name: "invalid JSON",
318
+ hostAuth: "{not valid json",
319
+ },
320
+ {
321
+ name: "partial subscription",
322
+ hostAuth: JSON.stringify({
323
+ tokens: {
324
+ account_id: "acct-b",
325
+ },
326
+ last_refresh: "2026-07-09T02:00:00Z",
327
+ }),
328
+ },
329
+ {
330
+ name: "top-level access token",
331
+ hostAuth: JSON.stringify({
332
+ access_token: "top-level-parser-differential-token",
333
+ }),
334
+ },
335
+ ];
336
+ for (const entry of cases) {
337
+ const result = await runCodexHomeAssetExtract({
338
+ sandboxAuth,
339
+ hostAuth: entry.hostAuth,
340
+ });
341
+ expect(result.finalAuth, entry.name).toBe(entry.hostAuth);
342
+ expect(result.finalAuth, entry.name).not.toBe(sandboxAuth);
343
+ expect(result.finalMode, entry.name).toBe(0o600);
344
+ expect(result.combinedOutput, entry.name).not.toContain("SENTINEL");
345
+ expect(result.commandText, entry.name).not.toContain("SENTINEL");
346
+ }
347
+ });
348
+ it("falls back to the sandbox image's own login when neither host nor prior asset has auth", async () => {
349
+ const imageAuth = subscriptionAuth({
350
+ accountId: "acct-image",
351
+ lastRefresh: "2026-07-01T00:00:00Z",
352
+ marker: "image",
353
+ });
354
+ const result = await runCodexHomeAssetExtract({
355
+ imageAuth,
356
+ });
357
+ expect(result.finalAuth).toBe(imageAuth);
358
+ expect(result.finalMode).toBe(0o600);
359
+ });
360
+ it("prefers shipped host auth over the image's own login", async () => {
361
+ const hostAuth = subscriptionAuth({
362
+ accountId: "acct-host",
363
+ lastRefresh: "2026-07-02T00:00:00Z",
364
+ marker: "host",
365
+ });
366
+ const imageAuth = subscriptionAuth({
367
+ accountId: "acct-image",
368
+ lastRefresh: "2026-07-03T00:00:00Z",
369
+ marker: "image",
370
+ });
371
+ const result = await runCodexHomeAssetExtract({
372
+ hostAuth,
373
+ imageAuth,
374
+ });
375
+ expect(result.finalAuth).toBe(hostAuth);
376
+ });
377
+ it("prefers a preserved newer prior-lease credential over the image's own login", async () => {
378
+ const hostAuth = subscriptionAuth({
379
+ accountId: "acct-1",
380
+ lastRefresh: "2026-07-01T00:00:00Z",
381
+ marker: "host",
382
+ });
383
+ const sandboxAuth = subscriptionAuth({
384
+ accountId: "acct-1",
385
+ lastRefresh: "2026-07-05T00:00:00Z",
386
+ marker: "prior-lease",
387
+ });
388
+ const imageAuth = subscriptionAuth({
389
+ accountId: "acct-image",
390
+ lastRefresh: "2026-07-06T00:00:00Z",
391
+ marker: "image",
392
+ });
393
+ const result = await runCodexHomeAssetExtract({
394
+ hostAuth,
395
+ sandboxAuth,
396
+ imageAuth,
397
+ });
398
+ expect(result.finalAuth).toBe(sandboxAuth);
399
+ });
400
+ it("routes the Codex home asset through a single native syncIn operation whose post-command is the auth-merge (#4, C5/C6)", async () => {
401
+ const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-codex-native-route-"));
402
+ cleanupDirs.push(rootDir);
403
+ const localWorkspaceDir = path.join(rootDir, "local-workspace");
404
+ const remoteWorkspaceDir = path.join(rootDir, "remote-workspace");
405
+ const localHomeDir = path.join(rootDir, "local-codex-home");
406
+ const remoteHomeDir = path.join(remoteWorkspaceDir, ".paperclip-runtime", "codex", "home");
407
+ await mkdir(localWorkspaceDir, { recursive: true });
408
+ await mkdir(localHomeDir, { recursive: true });
409
+ await mkdir(remoteHomeDir, { recursive: true });
410
+ await writeFile(path.join(localWorkspaceDir, "README.md"), "workspace\n", "utf8");
411
+ // Host credential is strictly newer → wins the merge (same identity).
412
+ const hostAuth = subscriptionAuth({
413
+ accountId: "acct-native",
414
+ lastRefresh: "2026-07-10T02:00:00Z",
415
+ marker: "host-newer-SENTINEL",
416
+ });
417
+ const sandboxAuth = subscriptionAuth({
418
+ accountId: "acct-native",
419
+ lastRefresh: "2026-07-10T01:00:00Z",
420
+ marker: "sandbox-older-SENTINEL",
421
+ });
422
+ await writeFile(path.join(localHomeDir, "auth.json"), hostAuth, { mode: 0o600 });
423
+ await writeFile(path.join(localHomeDir, "config.toml"), "model = \"gpt\"\n", "utf8");
424
+ await writeFile(path.join(remoteHomeDir, "auth.json"), sandboxAuth, { mode: 0o600 });
425
+ // A native runner: the orchestrator must make NO direct writeFile/run — every
426
+ // byte (incl. auth.json) rides `syncIn` (native uploadFiles), and the merge
427
+ // runs as the operation's ordered post-upload command.
428
+ const directWrites = [];
429
+ const directRuns = [];
430
+ const captured = [];
431
+ const client = {
432
+ makeDir: async (remotePath) => {
433
+ await mkdir(remotePath, { recursive: true });
434
+ },
435
+ writeFile: async (remotePath, bytes) => {
436
+ directWrites.push(remotePath);
437
+ await mkdir(path.dirname(remotePath), { recursive: true });
438
+ await writeFile(remotePath, Buffer.from(bytes));
439
+ },
440
+ readFile: async (remotePath) => await readFile(remotePath),
441
+ listFiles: async () => [],
442
+ remove: async (remotePath) => {
443
+ await rm(remotePath, { recursive: true, force: true });
444
+ },
445
+ run: async (command) => {
446
+ directRuns.push(command);
447
+ await execFile("sh", ["-c", command], { maxBuffer: 32 * 1024 * 1024 });
448
+ },
449
+ };
450
+ client.syncIn = async (operations) => {
451
+ for (const operation of operations) {
452
+ captured.push(operation);
453
+ for (const mapping of operation.files) {
454
+ const bytes = await readFile(mapping.sourcePath);
455
+ await mkdir(path.dirname(mapping.targetPath), { recursive: true });
456
+ await writeFile(mapping.targetPath, bytes);
457
+ if (mapping.mode != null)
458
+ await lstat(mapping.targetPath);
459
+ }
460
+ for (const command of operation.postUploadCommands ?? []) {
461
+ await execFile("sh", ["-c", command.command], { maxBuffer: 32 * 1024 * 1024 });
462
+ }
463
+ }
464
+ return { operations: [] };
465
+ };
466
+ await prepareSandboxManagedRuntime({
467
+ spec: {
468
+ transport: "sandbox",
469
+ provider: "test",
470
+ sandboxId: "sandbox-1",
471
+ remoteCwd: remoteWorkspaceDir,
472
+ timeoutMs: 30_000,
473
+ apiKey: null,
474
+ },
475
+ adapterKey: "codex",
476
+ client,
477
+ workspaceLocalDir: localWorkspaceDir,
478
+ assets: [{
479
+ key: "home",
480
+ localDir: localHomeDir,
481
+ followSymlinks: true,
482
+ provision: buildCodexAuthInboundProvision(),
483
+ }],
484
+ });
485
+ // 0 direct exec/writeFile — pure native delegation.
486
+ expect(directWrites).toEqual([]);
487
+ expect(directRuns).toEqual([]);
488
+ // One operation carries the home asset: the home tar + the two merge scripts
489
+ // as `files` mappings, and the auth-merge as the ordered post-upload command.
490
+ const homeOp = captured.find((op) => op.files.some((mapping) => mapping.targetPath.endsWith("home-upload.tar")));
491
+ expect(homeOp).toBeDefined();
492
+ const targets = homeOp.files.map((mapping) => path.posix.basename(mapping.targetPath)).sort();
493
+ expect(targets).toEqual([
494
+ "codex-auth-merge-decision.cjs",
495
+ "codex-auth-merge-extract.sh",
496
+ "home-upload.tar",
497
+ ]);
498
+ expect(homeOp.files.every((mapping) => mapping.kind === "file")).toBe(true);
499
+ expect(homeOp.postUploadCommands).toHaveLength(1);
500
+ // The post-command is the auth-merge script, NOT a plain `tar -xf` (C6).
501
+ const mergeCommand = homeOp.postUploadCommands[0].command;
502
+ expect(mergeCommand).toContain("codex-auth-merge-extract.sh");
503
+ expect(mergeCommand).not.toMatch(/^\s*tar -xf/);
504
+ // C5: no token/credential material leaks into the operation metadata.
505
+ const opJson = JSON.stringify(captured);
506
+ expect(opJson).not.toContain("SENTINEL");
507
+ expect(opJson).not.toContain("refresh-token");
508
+ // C6: newer host credential won, installed atomically at mode 0600.
509
+ const finalAuthPath = path.join(remoteHomeDir, "auth.json");
510
+ expect(await readFile(finalAuthPath, "utf8")).toBe(hostAuth);
511
+ expect((await lstat(finalAuthPath)).mode & 0o777).toBe(0o600);
512
+ });
513
+ });
514
+ // The extract shell script consumes the decision predicate as a child process
515
+ // and only branches on its exit code (10 = use the source auth.json, 20 = keep
516
+ // the destination auth.json). The predicate is direction-agnostic: the caller
517
+ // decides which file is `source` and which is `destination` by argument order
518
+ // (first = source, second = destination), so there is no `--direction` flag.
519
+ // This suite drives the `.cjs` directly the same way, asserting the exit code
520
+ // per row. Both the inbound restore (extract.sh: source = the sandbox copy,
521
+ // destination = the host copy) and the future outbound copy-back guard reduce to
522
+ // the same single question — adopt the source only when it is strictly newer,
523
+ // same-identity, subscription-kind.
524
+ describe("codex-auth-merge-decision predicate (source/destination)", () => {
525
+ const cleanupDirs = [];
526
+ afterEach(async () => {
527
+ while (cleanupDirs.length > 0) {
528
+ const dir = cleanupDirs.pop();
529
+ if (!dir)
530
+ continue;
531
+ await rm(dir, { recursive: true, force: true }).catch(() => undefined);
532
+ }
533
+ });
534
+ const decisionScriptPath = fileURLToPath(new URL("./codex-auth-merge-decision.cjs", import.meta.url));
535
+ const KEEP_DESTINATION = 20;
536
+ const USE_SOURCE = 10;
537
+ const IMPLAUSIBLE_LAST_REFRESH = 22;
538
+ function subscriptionAuth(input) {
539
+ const suffix = input.marker ?? input.accountId;
540
+ return JSON.stringify({
541
+ tokens: {
542
+ id_token: `id-token-${suffix}`,
543
+ access_token: `access-token-${suffix}`,
544
+ refresh_token: `refresh-token-${suffix}`,
545
+ account_id: input.accountId,
546
+ },
547
+ ...(input.lastRefresh ? { last_refresh: input.lastRefresh } : {}),
548
+ });
549
+ }
550
+ function apiKeyAuth(marker) {
551
+ return JSON.stringify({ OPENAI_API_KEY: `sk-${marker}` });
552
+ }
553
+ async function runDecision(input) {
554
+ const dir = await mkdtemp(path.join(os.tmpdir(), "paperclip-codex-auth-decision-"));
555
+ cleanupDirs.push(dir);
556
+ const sourcePath = path.join(dir, "source-auth.json");
557
+ const destinationPath = path.join(dir, "destination-auth.json");
558
+ await writeFile(sourcePath, input.sourceAuth, { mode: 0o600 });
559
+ await writeFile(destinationPath, input.destinationAuth, { mode: 0o600 });
560
+ // Arg order is the whole contract: first = source, second = destination.
561
+ const args = [decisionScriptPath, sourcePath, destinationPath];
562
+ try {
563
+ const result = await execFile("node", args);
564
+ return { code: 0, output: `${result.stdout}\n${result.stderr}` };
565
+ }
566
+ catch (error) {
567
+ const failure = error;
568
+ const output = `${failure.stdout ?? ""}\n${failure.stderr ?? ""}`;
569
+ if (typeof failure.code === "number")
570
+ return { code: failure.code, output };
571
+ throw error;
572
+ }
573
+ }
574
+ const NEWER = "2026-07-09T02:00:00Z";
575
+ const OLDER = "2026-07-09T01:00:00Z";
576
+ const cases = [
577
+ {
578
+ name: "source strictly newer, same identity → use source",
579
+ sourceAuth: subscriptionAuth({ accountId: "acct", lastRefresh: NEWER }),
580
+ destinationAuth: subscriptionAuth({ accountId: "acct", lastRefresh: OLDER }),
581
+ expected: USE_SOURCE,
582
+ },
583
+ {
584
+ name: "equal last_refresh (tie) → keep destination",
585
+ sourceAuth: subscriptionAuth({ accountId: "acct", lastRefresh: NEWER, marker: "src" }),
586
+ destinationAuth: subscriptionAuth({ accountId: "acct", lastRefresh: NEWER, marker: "dst" }),
587
+ expected: KEEP_DESTINATION,
588
+ },
589
+ {
590
+ name: "source older → keep destination",
591
+ sourceAuth: subscriptionAuth({ accountId: "acct", lastRefresh: OLDER }),
592
+ destinationAuth: subscriptionAuth({ accountId: "acct", lastRefresh: NEWER }),
593
+ expected: KEEP_DESTINATION,
594
+ },
595
+ {
596
+ name: "identity mismatch even when source newer → keep destination",
597
+ sourceAuth: subscriptionAuth({ accountId: "acct-source", lastRefresh: NEWER }),
598
+ destinationAuth: subscriptionAuth({ accountId: "acct-destination", lastRefresh: OLDER }),
599
+ expected: KEEP_DESTINATION,
600
+ },
601
+ {
602
+ name: "source last_refresh missing → keep destination",
603
+ sourceAuth: subscriptionAuth({ accountId: "acct" }),
604
+ destinationAuth: subscriptionAuth({ accountId: "acct", lastRefresh: OLDER }),
605
+ expected: KEEP_DESTINATION,
606
+ },
607
+ {
608
+ name: "destination last_refresh missing → keep destination",
609
+ sourceAuth: subscriptionAuth({ accountId: "acct", lastRefresh: NEWER }),
610
+ destinationAuth: subscriptionAuth({ accountId: "acct" }),
611
+ expected: KEEP_DESTINATION,
612
+ },
613
+ {
614
+ name: "both last_refresh missing → keep destination",
615
+ sourceAuth: subscriptionAuth({ accountId: "acct" }),
616
+ destinationAuth: subscriptionAuth({ accountId: "acct" }),
617
+ expected: KEEP_DESTINATION,
618
+ },
619
+ {
620
+ name: "source last_refresh unparseable → keep destination",
621
+ sourceAuth: subscriptionAuth({ accountId: "acct", lastRefresh: "not-a-date" }),
622
+ destinationAuth: subscriptionAuth({ accountId: "acct", lastRefresh: OLDER }),
623
+ expected: KEEP_DESTINATION,
624
+ },
625
+ {
626
+ name: "destination last_refresh unparseable → keep destination",
627
+ sourceAuth: subscriptionAuth({ accountId: "acct", lastRefresh: NEWER }),
628
+ destinationAuth: subscriptionAuth({ accountId: "acct", lastRefresh: "not-a-date" }),
629
+ expected: KEEP_DESTINATION,
630
+ },
631
+ {
632
+ name: "source apikey → keep destination",
633
+ sourceAuth: apiKeyAuth("source"),
634
+ destinationAuth: subscriptionAuth({ accountId: "acct", lastRefresh: OLDER }),
635
+ expected: KEEP_DESTINATION,
636
+ },
637
+ {
638
+ name: "destination apikey → keep destination",
639
+ sourceAuth: subscriptionAuth({ accountId: "acct", lastRefresh: NEWER }),
640
+ destinationAuth: apiKeyAuth("destination"),
641
+ expected: KEEP_DESTINATION,
642
+ },
643
+ {
644
+ name: "both apikey → keep destination",
645
+ sourceAuth: apiKeyAuth("source"),
646
+ destinationAuth: apiKeyAuth("destination"),
647
+ expected: KEEP_DESTINATION,
648
+ },
649
+ {
650
+ name: "kind mismatch (source subscription, destination apikey) → keep destination",
651
+ sourceAuth: subscriptionAuth({ accountId: "acct", lastRefresh: NEWER }),
652
+ destinationAuth: apiKeyAuth("destination"),
653
+ expected: KEEP_DESTINATION,
654
+ },
655
+ {
656
+ name: "source unusable JSON → keep destination",
657
+ sourceAuth: "{not valid json",
658
+ destinationAuth: subscriptionAuth({ accountId: "acct", lastRefresh: OLDER }),
659
+ expected: KEEP_DESTINATION,
660
+ },
661
+ {
662
+ name: "destination unusable JSON → keep destination",
663
+ sourceAuth: subscriptionAuth({ accountId: "acct", lastRefresh: NEWER }),
664
+ destinationAuth: "{not valid json",
665
+ expected: KEEP_DESTINATION,
666
+ },
667
+ {
668
+ // 400 days ahead is comfortably beyond the 5-minute skew allowance and
669
+ // any subprocess-spawn scheduling delay, so this integration-level
670
+ // check never depends on millisecond timing.
671
+ name: "source last_refresh implausibly far in the future → keep destination",
672
+ sourceAuth: subscriptionAuth({
673
+ accountId: "acct",
674
+ lastRefresh: new Date(Date.now() + 400 * 24 * 60 * 60 * 1000).toISOString(),
675
+ }),
676
+ destinationAuth: subscriptionAuth({ accountId: "acct", lastRefresh: OLDER }),
677
+ expected: IMPLAUSIBLE_LAST_REFRESH,
678
+ },
679
+ ];
680
+ for (const entry of cases) {
681
+ it(entry.name, async () => {
682
+ const result = await runDecision({
683
+ sourceAuth: entry.sourceAuth,
684
+ destinationAuth: entry.destinationAuth,
685
+ });
686
+ expect(result.code).toBe(entry.expected);
687
+ });
688
+ }
689
+ it("never emits source token bytes", async () => {
690
+ const result = await runDecision({
691
+ sourceAuth: subscriptionAuth({
692
+ accountId: "acct",
693
+ lastRefresh: NEWER,
694
+ marker: "SECRET-SENTINEL",
695
+ }),
696
+ destinationAuth: subscriptionAuth({ accountId: "acct", lastRefresh: OLDER }),
697
+ });
698
+ expect(result.code).toBe(USE_SOURCE);
699
+ expect(result.output).not.toContain("SENTINEL");
700
+ });
701
+ it("never emits source token bytes for an implausibly-future last_refresh", async () => {
702
+ const result = await runDecision({
703
+ sourceAuth: subscriptionAuth({
704
+ accountId: "acct",
705
+ lastRefresh: new Date(Date.now() + 400 * 24 * 60 * 60 * 1000).toISOString(),
706
+ marker: "SECRET-SENTINEL",
707
+ }),
708
+ destinationAuth: subscriptionAuth({ accountId: "acct", lastRefresh: OLDER }),
709
+ });
710
+ expect(result.code).toBe(IMPLAUSIBLE_LAST_REFRESH);
711
+ expect(result.output).not.toContain("SENTINEL");
712
+ });
713
+ });
714
+ //# sourceMappingURL=codex-auth-merge.test.js.map