@ikon85/agent-workflow-kit 0.43.0 → 0.44.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 (126) hide show
  1. package/.agents/skills/grill-me/SKILL.md +3 -1
  2. package/.agents/skills/grill-with-docs/SKILL.md +3 -1
  3. package/.agents/skills/kit-update/SKILL.md +9 -11
  4. package/.agents/skills/orchestrate-wave/SKILL.md +8 -8
  5. package/.agents/skills/setup-workflow/SKILL.md +20 -18
  6. package/.agents/skills/setup-workflow/board-sync.md +4 -1
  7. package/.agents/skills/setup-workflow/workflow-advisories.md +3 -2
  8. package/.agents/skills/setup-workflow/worktree-lifecycle.md +36 -31
  9. package/.agents/skills/to-issues/SKILL.md +43 -0
  10. package/.agents/skills/wrapup/SKILL.md +98 -54
  11. package/.claude/skills/grill-me/SKILL.md +3 -1
  12. package/.claude/skills/grill-me-codex/SKILL.md +3 -1
  13. package/.claude/skills/grill-with-docs/SKILL.md +3 -1
  14. package/.claude/skills/grill-with-docs-codex/SKILL.md +3 -1
  15. package/.claude/skills/kit-update/SKILL.md +9 -11
  16. package/.claude/skills/orchestrate-wave/SKILL.md +8 -8
  17. package/.claude/skills/setup-workflow/SKILL.md +20 -18
  18. package/.claude/skills/setup-workflow/board-sync.md +4 -1
  19. package/.claude/skills/setup-workflow/workflow-advisories.md +3 -2
  20. package/.claude/skills/setup-workflow/worktree-lifecycle.md +36 -31
  21. package/.claude/skills/to-issues/SKILL.md +43 -0
  22. package/.claude/skills/wrapup/SKILL.md +98 -54
  23. package/README.md +249 -7
  24. package/agent-workflow-kit.package.json +42 -42
  25. package/package.json +16 -12
  26. package/scripts/marker_lib.py +3 -0
  27. package/scripts/profile_globs.py +26 -45
  28. package/scripts/release-delta-guard.mjs +26 -9
  29. package/scripts/worktree-lifecycle/README.md +57 -160
  30. package/scripts/worktree-lifecycle/capabilities.json +2 -3
  31. package/scripts/worktree-lifecycle/classify.py +480 -0
  32. package/scripts/worktree-lifecycle/cleanup.py +9 -29
  33. package/scripts/worktree-lifecycle/core.py +68 -1429
  34. package/scripts/worktree-lifecycle/ignore_seed.py +1 -1
  35. package/scripts/worktree-lifecycle/profile.py +46 -29
  36. package/scripts/worktree-lifecycle/setup.py +7 -19
  37. package/scripts/wrapup-land.py +1292 -545
  38. package/src/consumer-migrations.json +1 -16
  39. package/src/lib/bundle.mjs +31 -1
  40. package/docs/adr/0001-consumer-divergence-policy.md +0 -53
  41. package/docs/adr/0002-capability-gated-orchestration.md +0 -70
  42. package/docs/adr/0003-kit-core-and-project-extension-lifecycle.md +0 -63
  43. package/docs/adr/0004-release-intent-is-a-version-tag.md +0 -64
  44. package/docs/adr/0005-to-issues-is-the-planning-facade.md +0 -42
  45. package/docs/adr/0006-routing-knowledge-access-and-policy-are-separate.md +0 -95
  46. package/docs/adr/0007-session-teardown-requires-provenance-bound-ownership.md +0 -88
  47. package/docs/adr/0008-planning-ignore-rules-are-offered-never-installed.md +0 -84
  48. package/docs/adr/0009-teardown-authority-is-stateless-repository-classification.md +0 -101
  49. package/docs/adr/0010-model-roster-replaces-the-optimization-dial.md +0 -87
  50. package/docs/agents/board-sync.md +0 -74
  51. package/docs/agents/code-review.md +0 -14
  52. package/docs/agents/workflow-capabilities.json +0 -28
  53. package/docs/research/agent-task-taxonomy-benchmark-coverage.md +0 -451
  54. package/docs/research/benchlm-routing-source.md +0 -198
  55. package/docs/research/consumer-owned-protocol-files.md +0 -238
  56. package/docs/research/frontend-agent-benchmarks.md +0 -282
  57. package/docs/research/model-effort-routing-benchmarks.md +0 -261
  58. package/docs/research/provider-neutral-agent-routing.md +0 -207
  59. package/docs/research/wave-152-consumer-acceptance.md +0 -98
  60. package/docs/research/wave-43-script-hook-census.md +0 -128
  61. package/scripts/build-kit.mjs +0 -117
  62. package/scripts/build-kit.test.mjs +0 -207
  63. package/scripts/census/census.test.mjs +0 -252
  64. package/scripts/census/state.test.mjs +0 -15
  65. package/scripts/census/transaction.test.mjs +0 -103
  66. package/scripts/census-contract.test.mjs +0 -227
  67. package/scripts/check-kit-staleness.mjs +0 -45
  68. package/scripts/check-kit-staleness.test.mjs +0 -19
  69. package/scripts/codex-exec-scenarios/fake-codex.mjs +0 -165
  70. package/scripts/codex-exec.test.mjs +0 -871
  71. package/scripts/grill-census-wiring-guard.mjs +0 -87
  72. package/scripts/grill-census-wiring-guard.test.mjs +0 -107
  73. package/scripts/kit-release.test.mjs +0 -273
  74. package/scripts/kit-update-pr.test.mjs +0 -151
  75. package/scripts/lib/audit-refs.mjs +0 -146
  76. package/scripts/lib/scrub.mjs +0 -113
  77. package/scripts/lib/scrub.test.mjs +0 -187
  78. package/scripts/memory-lifecycle/memory-lifecycle.test.mjs +0 -170
  79. package/scripts/portability_profile_scan.py +0 -53
  80. package/scripts/release-delta-guard.test.mjs +0 -174
  81. package/scripts/release-parity.test.mjs +0 -90
  82. package/scripts/release-state.test.mjs +0 -360
  83. package/scripts/test_anchor_table.py +0 -69
  84. package/scripts/test_board_bootstrap.py +0 -348
  85. package/scripts/test_board_sync.py +0 -208
  86. package/scripts/test_board_sync_create_idempotency.py +0 -117
  87. package/scripts/test_board_sync_wave_title.py +0 -120
  88. package/scripts/test_census_backstop.py +0 -712
  89. package/scripts/test_census_forward_contract.py +0 -91
  90. package/scripts/test_census_update_contract.test.mjs +0 -454
  91. package/scripts/test_codex_adapter_sync_contract.py +0 -169
  92. package/scripts/test_dist_kit_smoke.py +0 -220
  93. package/scripts/test_drift_guard_diagnostics.py +0 -295
  94. package/scripts/test_issue_claim_contract.py +0 -169
  95. package/scripts/test_kit_docs_language_census.py +0 -178
  96. package/scripts/test_marker_lib.py +0 -162
  97. package/scripts/test_orchestrate_wave_contract.py +0 -381
  98. package/scripts/test_pr_body_check.py +0 -264
  99. package/scripts/test_profile_globs.py +0 -280
  100. package/scripts/test_program_planning_contract.py +0 -133
  101. package/scripts/test_release_authorization_contract.py +0 -101
  102. package/scripts/test_render_anchor.py +0 -267
  103. package/scripts/test_retro_wrapup_contract.py +0 -153
  104. package/scripts/test_skill_code_review_seed.py +0 -89
  105. package/scripts/test_skill_codex_exec_lifecycle.py +0 -123
  106. package/scripts/test_skill_frontmatter_lint.py +0 -234
  107. package/scripts/test_skill_gh_lint.py +0 -124
  108. package/scripts/test_skill_language_census.py +0 -254
  109. package/scripts/test_skill_optional_readiness.py +0 -171
  110. package/scripts/test_skill_portability_lint.py +0 -866
  111. package/scripts/test_skill_precommit_template.py +0 -30
  112. package/scripts/test_skill_publish_audit.py +0 -358
  113. package/scripts/test_skill_readiness_contract.py +0 -264
  114. package/scripts/test_skill_readiness_preflight.py +0 -180
  115. package/scripts/test_skill_required_readiness.py +0 -233
  116. package/scripts/test_skill_selfcontainment_lint.py +0 -255
  117. package/scripts/test_skill_setup_workflow_seeds.py +0 -691
  118. package/scripts/test_skill_stale_name_lint.py +0 -126
  119. package/scripts/test_skill_surface_refs.py +0 -300
  120. package/scripts/test_skill_trailing_artifact_lint.py +0 -71
  121. package/scripts/test_tdd_contract.py +0 -78
  122. package/scripts/test_worktree_ignore_seed.py +0 -320
  123. package/scripts/test_worktree_setup_base_guard.py +0 -140
  124. package/scripts/test_worktree_wrapup_contract.py +0 -1737
  125. package/scripts/test_wrapup_land.py +0 -428
  126. package/scripts/worktree-lifecycle/session.py +0 -1857
@@ -1,871 +0,0 @@
1
- import assert from 'node:assert/strict';
2
- import {
3
- closeSync, mkdirSync, mkdtempSync, openSync, readFileSync, readdirSync, rmdirSync, rmSync,
4
- statSync, utimesSync, writeFileSync,
5
- } from 'node:fs';
6
- import { spawn, spawnSync } from 'node:child_process';
7
- import { tmpdir } from 'node:os';
8
- import { join } from 'node:path';
9
- import test from 'node:test';
10
-
11
- const root = new URL('..', import.meta.url).pathname;
12
- const helper = join(root, 'scripts/codex-exec.sh');
13
- const fake = join(root, 'scripts/codex-exec-scenarios/fake-codex.mjs');
14
-
15
- function fixture() {
16
- const dir = mkdtempSync(join(tmpdir(), 'codex-exec-test-'));
17
- return { dir, stateRoot: join(dir, 'state'), launchLog: join(dir, 'launch.log') };
18
- }
19
-
20
- function invoke(fx, args, extraEnv = {}) {
21
- const result = spawnSync(helper, args, {
22
- cwd: root,
23
- encoding: 'utf8',
24
- timeout: 8_000,
25
- env: {
26
- ...process.env,
27
- CODEX_EXEC_STATE_ROOT: fx.stateRoot,
28
- FAKE_CODEX_LAUNCH_LOG: fx.launchLog,
29
- ...extraEnv,
30
- },
31
- });
32
- const lines = result.stdout.trim().split('\n').filter(Boolean);
33
- const output = lines.length ? JSON.parse(lines.at(-1)) : null;
34
- return { ...result, output };
35
- }
36
-
37
- const launchArgs = (profile = 'review') => [
38
- 'new', '--codex-bin', fake, '--profile', profile, '--mode', 'read-only',
39
- '--prompt', 'Return a verdict', '--timeout', '2', '--probe-timeout', '0.15',
40
- ];
41
- const exists = (path) => { try { statSync(path); return true; } catch { return false; } };
42
- const alive = (pid) => { try { process.kill(pid, 0); return true; } catch { return false; } };
43
- const readJson = (path) => { try { return JSON.parse(readFileSync(path, 'utf8')); } catch { return null; } };
44
- const waitFor = async (predicate, timeout = 2_000) => {
45
- const end = Date.now() + timeout;
46
- while (Date.now() < end) {
47
- const value = predicate();
48
- if (value) return value;
49
- await new Promise((resolve) => setTimeout(resolve, 20));
50
- }
51
- return null;
52
- };
53
-
54
- test('preflight accepts only exact tested versions and capabilities before launch', () => {
55
- const fx = fixture();
56
- const ok = invoke(fx, ['preflight', '--codex-bin', fake]);
57
- assert.equal(ok.output.status, 'OK');
58
- assert.match(ok.output.auth, /Logged in/);
59
-
60
- const version = invoke(fx, ['preflight', '--codex-bin', fake], { FAKE_CODEX_VERSION: '9.9.9' });
61
- assert.equal(version.output.error, 'UNTESTED_VERSION');
62
- const capability = invoke(fx, ['preflight', '--codex-bin', fake], {
63
- FAKE_CODEX_MISSING_CAPABILITY: '1',
64
- });
65
- assert.equal(capability.output.error, 'MISSING_CAPABILITY');
66
- const resumeCapability = invoke(fx, ['preflight', '--codex-bin', fake], {
67
- FAKE_CODEX_RESUME_MISSING_CAPABILITY: '1',
68
- });
69
- assert.equal(resumeCapability.output.error, 'MISSING_CAPABILITY');
70
- assert.equal(exists(fx.launchLog), false);
71
- });
72
-
73
- test('new and resume preserve immutable rounds and reject mode drift', () => {
74
- const fx = fixture();
75
- const first = invoke(fx, launchArgs());
76
- assert.equal(first.output.status, 'OK');
77
- assert.ok(first.output.runId);
78
- assert.equal(first.output.threadId, 'fake-thread-1');
79
- assert.equal(statSync(first.output.stateDir).mode & 0o777, 0o700);
80
- assert.ok(exists(join(first.output.stateDir, 'round-1.result.json')));
81
-
82
- const second = invoke(fx, [
83
- 'resume', first.output.runId, '--codex-bin', fake,
84
- '--prompt', 'Again', '--timeout', '2',
85
- ]);
86
- assert.equal(second.output.status, 'OK');
87
- assert.equal(second.output.round, 2);
88
- assert.ok(exists(join(first.output.stateDir, 'round-1.stdout.jsonl')));
89
- assert.ok(exists(join(first.output.stateDir, 'round-2.stdout.jsonl')));
90
- assert.equal(readFileSync(join(first.output.stateDir, 'latest'), 'utf8').trim(), 'round-2.result.json');
91
- const resumeCommand = JSON.parse(readFileSync(fx.launchLog, 'utf8').trim().split('\n').at(-1));
92
- assert.equal(resumeCommand.includes('--sandbox'), false);
93
- assert.deepEqual(resumeCommand.slice(0, 4), ['exec', 'resume', 'fake-thread-1', '-c']);
94
- assert.equal(resumeCommand[4], 'sandbox_mode=read-only');
95
-
96
- const mismatch = invoke(fx, [
97
- 'resume', first.output.runId, '--codex-bin', fake,
98
- '--mode', 'workspace-write', '--prompt', 'No',
99
- ]);
100
- assert.equal(mismatch.output.error, 'MODE_MISMATCH');
101
- assert.equal(invoke(fx, ['resume', '--codex-bin', fake]).output.error, 'RUN_ID_REQUIRED');
102
- });
103
-
104
- test('resume rejects tampered persisted profile and sandbox before launch', () => {
105
- const fx = fixture();
106
- const sandboxRun = invoke(fx, launchArgs()).output;
107
- const launchesBeforeSandboxTamper = readFileSync(fx.launchLog, 'utf8');
108
- writeFileSync(join(sandboxRun.stateDir, 'sandbox'), 'danger-full-access\n');
109
-
110
- const sandboxTamper = invoke(fx, [
111
- 'resume', sandboxRun.runId, '--codex-bin', fake, '--prompt', 'Again',
112
- ]);
113
- assert.equal(sandboxTamper.output.error, 'INVALID_STATE');
114
- assert.equal(readFileSync(fx.launchLog, 'utf8'), launchesBeforeSandboxTamper);
115
-
116
- const profileRun = invoke(fx, launchArgs()).output;
117
- const launchesBeforeProfileTamper = readFileSync(fx.launchLog, 'utf8');
118
- writeFileSync(join(profileRun.stateDir, 'profile'), 'unbounded\n');
119
-
120
- const profileTamper = invoke(fx, [
121
- 'resume', profileRun.runId, '--codex-bin', fake, '--prompt', 'Again',
122
- ]);
123
- assert.equal(profileTamper.output.error, 'INVALID_STATE');
124
- assert.equal(readFileSync(fx.launchLog, 'utf8'), launchesBeforeProfileTamper);
125
- });
126
-
127
- test('invalid timeout and mode inputs fail before launch', () => {
128
- const fx = fixture();
129
- const timeout = invoke(fx, launchArgs().map((value, index, all) => (
130
- all[index - 1] === '--timeout' ? 'NaN' : value
131
- )));
132
- assert.equal(timeout.output.error, 'INVALID_TIMEOUT');
133
- const mode = invoke(fx, launchArgs().map((value, index, all) => (
134
- all[index - 1] === '--mode' ? 'anything-goes' : value
135
- )));
136
- assert.equal(mode.output.error, 'INVALID_MODE');
137
- const danger = invoke(fx, launchArgs().map((value, index, all) => (
138
- all[index - 1] === '--mode' ? 'danger-full-access' : value
139
- )));
140
- assert.equal(danger.output.error, 'DANGER_FULL_ACCESS_REJECTED');
141
- const legacyFlag = invoke(fx, launchArgs().map((value) => value === '--mode' ? '--sandbox' : value));
142
- assert.equal(legacyFlag.output.error, 'INVALID_ARGUMENT');
143
- assert.equal(exists(fx.launchLog), false);
144
- });
145
-
146
- test('approved route passes explicit model and effort controls to Codex', () => {
147
- const fx = fixture();
148
- const result = invoke(fx, [
149
- ...launchArgs(),
150
- '--model', 'coding-model',
151
- '--effort', 'high',
152
- ]);
153
- assert.equal(result.output.status, 'OK');
154
- const command = JSON.parse(readFileSync(fx.launchLog, 'utf8').trim());
155
- assert.ok(command.includes('model=coding-model'));
156
- assert.ok(command.includes('model_reasoning_effort=high'));
157
-
158
- const resumed = invoke(fx, [
159
- 'resume', result.output.runId, '--codex-bin', fake,
160
- '--prompt', 'Again', '--timeout', '2',
161
- ]);
162
- assert.equal(resumed.output.status, 'OK');
163
- const resumeCommand = JSON.parse(readFileSync(fx.launchLog, 'utf8').trim().split('\n').at(-1));
164
- assert.ok(resumeCommand.includes('model=coding-model'));
165
- assert.ok(resumeCommand.includes('model_reasoning_effort=high'));
166
-
167
- const mismatch = invoke(fx, [
168
- 'resume', result.output.runId, '--codex-bin', fake,
169
- '--model', 'other-model', '--effort', 'high', '--prompt', 'No',
170
- ]);
171
- assert.equal(mismatch.output.error, 'ROUTE_CONTROL_MISMATCH');
172
- });
173
-
174
- test('route controls fail closed when incomplete or unsafe', () => {
175
- for (const args of [
176
- [...launchArgs(), '--model', 'coding-model'],
177
- [...launchArgs(), '--effort', 'high'],
178
- [...launchArgs(), '--model', 'unsafe value', '--effort', 'high'],
179
- [...launchArgs(), '--model', 'coding-model', '--effort', 'unknown'],
180
- ]) {
181
- const fx = fixture();
182
- const result = invoke(fx, args);
183
- assert.equal(result.output.error, 'INVALID_ROUTE_CONTROL');
184
- assert.equal(exists(fx.launchLog), false);
185
- }
186
- });
187
-
188
- test('current extended reasoning efforts are passed through explicitly', () => {
189
- for (const effort of ['max', 'ultra']) {
190
- const fx = fixture();
191
- const result = invoke(fx, [
192
- ...launchArgs(),
193
- '--model', 'coding-model',
194
- '--effort', effort,
195
- ]);
196
- assert.equal(result.output.status, 'OK', effort);
197
- const command = JSON.parse(readFileSync(fx.launchLog, 'utf8').trim());
198
- assert.ok(command.includes(`model_reasoning_effort=${effort}`));
199
- }
200
- });
201
-
202
- test('finalize deletes state, debug-retain preserves diagnostics, and finalized resume fails', () => {
203
- const fx = fixture();
204
- const run = invoke(fx, launchArgs()).output;
205
- const finalized = invoke(fx, ['finalize', run.runId]);
206
- assert.equal(finalized.output.status, 'OK');
207
- assert.equal(exists(run.stateDir), false);
208
- assert.equal(invoke(fx, ['resume', run.runId]).output.error, 'RUN_NOT_FOUND');
209
-
210
- const retained = invoke(fx, launchArgs()).output;
211
- invoke(fx, ['finalize', retained.runId, '--debug-retain']);
212
- assert.ok(exists(join(retained.stateDir, 'round-1.prompt.txt')));
213
- assert.ok(exists(join(retained.stateDir, 'round-1.stderr.log')));
214
- assert.equal(invoke(fx, ['resume', retained.runId]).output.error, 'RUN_FINALIZED');
215
- });
216
-
217
- test('handle-failure preserves a failed new result without inventing an abort target', () => {
218
- const fx = fixture();
219
- const failure = {
220
- status: 'AUTH', error: 'AUTH_REQUIRED', message: 'Login required',
221
- originalExitStatus: 23, signal: null, diagnostic: { source: 'preflight' },
222
- };
223
- const handled = invoke(fx, ['handle-failure', '--result', JSON.stringify(failure)]);
224
- assert.notEqual(handled.status, 0);
225
- assert.deepEqual(handled.output, failure);
226
- assert.equal(exists(fx.stateRoot), false);
227
- });
228
-
229
- test('handle-failure prefers a failed resume result run id and aborts that run', () => {
230
- const fx = fixture();
231
- const run = invoke(fx, launchArgs()).output;
232
- const failed = invoke(fx, [
233
- 'resume', run.runId, '--codex-bin', fake, '--prompt', 'Again',
234
- '--timeout', '2', '--probe-timeout', '0.15',
235
- ], { FAKE_CODEX_SCENARIO: 'silent', FAKE_CODEX_PAUSE_MS: '1000' });
236
- assert.equal(failed.output.status, 'HUNG');
237
- assert.equal(exists(run.stateDir), true);
238
-
239
- const handled = invoke(fx, [
240
- 'handle-failure', '--result', JSON.stringify(failed.output), '--run-id', 'wrongFallback',
241
- ]);
242
- assert.notEqual(handled.status, 0);
243
- assert.equal(handled.output.status, 'HUNG');
244
- assert.equal(handled.output.runId, run.runId);
245
- assert.equal(exists(run.stateDir), false);
246
- });
247
-
248
- test('handle-failure aborts the explicit fallback after a valid resume preflight failure', () => {
249
- const fx = fixture();
250
- const run = invoke(fx, launchArgs()).output;
251
- const failed = invoke(fx, [
252
- 'resume', run.runId, '--codex-bin', fake, '--prompt', 'Again',
253
- ], { FAKE_CODEX_VERSION: '9.9.9' });
254
- assert.equal(failed.output.error, 'UNTESTED_VERSION');
255
- assert.equal(failed.output.runId, undefined);
256
-
257
- const handled = invoke(fx, [
258
- 'handle-failure', '--result', JSON.stringify(failed.output), '--run-id', run.runId,
259
- ]);
260
- assert.notEqual(handled.status, 0);
261
- assert.deepEqual(handled.output, failed.output);
262
- assert.equal(exists(run.stateDir), false);
263
- });
264
-
265
- test('handle-failure sanitizes malformed input and aborts only its explicit fallback', () => {
266
- const fx = fixture();
267
- const run = invoke(fx, launchArgs()).output;
268
- const handled = invoke(fx, [
269
- 'handle-failure', '--result', 'not-json super-secret raw bytes', '--run-id', run.runId,
270
- ]);
271
- assert.notEqual(handled.status, 0);
272
- assert.equal(handled.output.status, 'MALFORMED_RESULT');
273
- assert.equal(handled.output.error, 'MALFORMED_RESULT');
274
- assert.doesNotMatch(handled.stdout, /super-secret|raw bytes/);
275
- assert.equal(exists(run.stateDir), false);
276
- });
277
-
278
- test('handle-failure sanitizes an empty result without inventing a cleanup target', () => {
279
- const fx = fixture();
280
- const handled = invoke(fx, ['handle-failure', '--result', '']);
281
- assert.notEqual(handled.status, 0);
282
- assert.equal(handled.output.status, 'MALFORMED_RESULT');
283
- assert.equal(handled.output.error, 'MALFORMED_RESULT');
284
- assert.equal(exists(fx.stateRoot), false);
285
- });
286
-
287
- test('handle-failure rejects an unknown result status as malformed', () => {
288
- const fx = fixture();
289
- const handled = invoke(fx, [
290
- 'handle-failure', '--result', JSON.stringify({
291
- status: 'MYSTERY_FAILURE', message: 'do not surface this arbitrary value',
292
- }),
293
- ]);
294
- assert.notEqual(handled.status, 0);
295
- assert.equal(handled.output.status, 'MALFORMED_RESULT');
296
- assert.equal(handled.output.error, 'MALFORMED_RESULT');
297
- assert.doesNotMatch(handled.stdout, /MYSTERY_FAILURE|arbitrary value/);
298
- });
299
-
300
- test('handle-failure rejects OK results without aborting their run', () => {
301
- const fx = fixture();
302
- const run = invoke(fx, launchArgs()).output;
303
- const handled = invoke(fx, [
304
- 'handle-failure', '--result', JSON.stringify({
305
- status: 'OK', runId: run.runId, message: 42,
306
- }), '--run-id', run.runId,
307
- ]);
308
- assert.notEqual(handled.status, 0);
309
- assert.equal(handled.output.error, 'RESULT_NOT_FAILED');
310
- assert.equal(exists(run.stateDir), true);
311
- });
312
-
313
- test('handle-failure treats an invalid embedded run id as malformed and aborts the valid fallback', () => {
314
- const fx = fixture();
315
- const run = invoke(fx, launchArgs()).output;
316
- const handled = invoke(fx, [
317
- 'handle-failure',
318
- '--result', JSON.stringify({ status: 'HUNG', runId: `../${run.runId}` }),
319
- '--run-id', run.runId,
320
- ]);
321
- assert.notEqual(handled.status, 0);
322
- assert.equal(handled.output.status, 'MALFORMED_RESULT');
323
- assert.equal(handled.output.error, 'MALFORMED_RESULT');
324
- assert.equal(handled.output.runId, undefined);
325
- assert.equal(exists(run.stateDir), false);
326
- });
327
-
328
- test('handle-failure preserves classification when cleanup of a valid result id fails', () => {
329
- const fx = fixture();
330
- const fallback = invoke(fx, launchArgs()).output;
331
- const failure = {
332
- status: 'HUNG', runId: 'validButMissing', originalExitStatus: 17, signal: 'SIGTERM',
333
- };
334
- const handled = invoke(fx, [
335
- 'handle-failure', '--result', JSON.stringify(failure), '--run-id', fallback.runId,
336
- ]);
337
- assert.notEqual(handled.status, 0);
338
- assert.equal(handled.output.status, failure.status);
339
- assert.equal(handled.output.runId, failure.runId);
340
- assert.equal(handled.output.originalExitStatus, failure.originalExitStatus);
341
- assert.equal(handled.output.signal, failure.signal);
342
- assert.equal(handled.output.cleanupStatus, 'FAILED');
343
- assert.equal(handled.output.cleanupError, 'RUN_NOT_FOUND');
344
- assert.equal(exists(fallback.stateDir), true);
345
- });
346
-
347
- test('automatic stale cleanup is bounded and never removes retained or active state', () => {
348
- const fx = fixture();
349
- mkdirSync(fx.stateRoot, { recursive: true, mode: 0o700 });
350
- for (const name of ['old1', 'old2', 'old3', 'retained', 'active']) {
351
- const dir = join(fx.stateRoot, `codex-exec.${name}`);
352
- mkdirSync(dir, { mode: 0o700 });
353
- writeFileSync(join(dir, 'run-id'), `${name}\n`);
354
- if (name === 'retained') writeFileSync(join(dir, 'debug-retain'), '');
355
- if (name === 'active') writeFileSync(join(dir, 'runtime.json'), JSON.stringify({
356
- token: 'a'.repeat(64), round: 1, phase: 'running', heartbeat: Date.now() / 1000,
357
- }));
358
- utimesSync(dir, 0, 0);
359
- }
360
- const result = invoke(fx, launchArgs(), {
361
- CODEX_EXEC_STALE_SECONDS: '1', CODEX_EXEC_STALE_MAX_DELETE: '2',
362
- });
363
- assert.equal(result.output.status, 'OK');
364
- const remaining = readdirSync(fx.stateRoot);
365
- assert.equal(remaining.filter((name) => /^codex-exec\.old/.test(name)).length, 1);
366
- assert.ok(remaining.includes('codex-exec.retained'));
367
- assert.ok(remaining.includes('codex-exec.active'));
368
- });
369
-
370
- test('invalid stale cleanup limits fail before launch and preserve an active run', async () => {
371
- const fx = fixture();
372
- const ready = join(fx.dir, 'group-hang.ready');
373
- const running = spawn(helper, launchArgs('build'), {
374
- cwd: root, encoding: 'utf8', env: {
375
- ...process.env, CODEX_EXEC_STATE_ROOT: fx.stateRoot,
376
- FAKE_CODEX_SCENARIO: 'group-hang', FAKE_CODEX_GROUP_HANG_READY: ready,
377
- FAKE_CODEX_LAUNCH_LOG: fx.launchLog,
378
- },
379
- });
380
- running.stdout.on('data', () => {});
381
- try {
382
- assert.ok(await waitFor(() => exists(ready)));
383
- const stateName = await waitFor(() => exists(fx.stateRoot) && readdirSync(fx.stateRoot)
384
- .find((name) => readJson(join(fx.stateRoot, name, 'runtime.json'))?.phase === 'running'));
385
- assert.ok(stateName);
386
- const stateDir = join(fx.stateRoot, stateName);
387
- const runId = readFileSync(join(stateDir, 'run-id'), 'utf8').trim();
388
- const invalidAge = invoke(fx, launchArgs(), { CODEX_EXEC_STALE_SECONDS: '-1' });
389
- assert.equal(invalidAge.output.error, 'INVALID_STALE_CONFIG');
390
- const invalidCount = invoke(fx, launchArgs(), { CODEX_EXEC_STALE_MAX_DELETE: '1.5' });
391
- assert.equal(invalidCount.output.error, 'INVALID_STALE_CONFIG');
392
- assert.equal(exists(join(stateDir, 'runtime.json')), true);
393
- assert.equal(readFileSync(fx.launchLog, 'utf8').trim().split('\n').length, 1);
394
- assert.equal(invoke(fx, ['abort', runId]).output.status, 'OK');
395
- assert.ok(await waitFor(() => running.exitCode !== null, 3_000));
396
- } finally {
397
- running.kill('SIGKILL');
398
- }
399
- });
400
-
401
- test('stale cleanup tolerates a state disappearing after candidate listing', async () => {
402
- const fx = fixture();
403
- mkdirSync(fx.stateRoot, { recursive: true, mode: 0o700 });
404
- const disappearing = join(fx.stateRoot, 'codex-exec.disappearing');
405
- mkdirSync(disappearing, { mode: 0o700 });
406
- writeFileSync(join(disappearing, 'run-id'), 'disappearing\n');
407
- utimesSync(disappearing, 0, 0);
408
- const marker = join(fx.dir, 'cleanup-listed');
409
- const running = spawn(helper, launchArgs(), {
410
- cwd: root, encoding: 'utf8', env: {
411
- ...process.env,
412
- CODEX_EXEC_STATE_ROOT: fx.stateRoot,
413
- CODEX_EXEC_STALE_SECONDS: '1',
414
- CODEX_EXEC_TEST_CLEANUP_PAUSE_MARKER: marker,
415
- FAKE_CODEX_LAUNCH_LOG: fx.launchLog,
416
- },
417
- });
418
- let stdout = '';
419
- running.stdout.on('data', (chunk) => { stdout += chunk; });
420
- try {
421
- assert.ok(await waitFor(() => exists(marker)));
422
- rmSync(disappearing, { recursive: true });
423
- assert.ok(await waitFor(() => running.exitCode !== null, 3_000));
424
- assert.equal(running.exitCode, 0);
425
- assert.equal(JSON.parse(stdout.trim().split('\n').at(-1)).status, 'OK');
426
- } finally {
427
- running.kill('SIGKILL');
428
- }
429
- });
430
-
431
- test('stale cleanup cannot delete a run whose resume acquired the shared lease', async () => {
432
- const fx = fixture();
433
- const original = invoke(fx, launchArgs()).output;
434
- utimesSync(original.stateDir, 0, 0);
435
- const marker = join(fx.dir, 'cleanup-listed');
436
- const cleanupRun = spawn(helper, launchArgs(), {
437
- cwd: root, encoding: 'utf8', env: {
438
- ...process.env,
439
- CODEX_EXEC_STATE_ROOT: fx.stateRoot,
440
- CODEX_EXEC_STALE_SECONDS: '1',
441
- CODEX_EXEC_TEST_CLEANUP_PAUSE_MARKER: marker,
442
- FAKE_CODEX_LAUNCH_LOG: fx.launchLog,
443
- },
444
- });
445
- cleanupRun.stdout.on('data', () => {});
446
- let resumed;
447
- try {
448
- assert.ok(await waitFor(() => exists(marker)));
449
- resumed = spawn(helper, [
450
- 'resume', original.runId, '--codex-bin', fake, '--prompt', 'Again',
451
- '--timeout', '5', '--probe-timeout', '1',
452
- ], {
453
- cwd: root, encoding: 'utf8', env: {
454
- ...process.env,
455
- CODEX_EXEC_STATE_ROOT: fx.stateRoot,
456
- FAKE_CODEX_SCENARIO: 'group-hang',
457
- FAKE_CODEX_LAUNCH_LOG: fx.launchLog,
458
- },
459
- });
460
- resumed.stdout.on('data', () => {});
461
- assert.ok(await waitFor(() => readJson(join(original.stateDir, 'runtime.json'))?.round === 2));
462
- assert.ok(await waitFor(() => cleanupRun.exitCode !== null, 3_000));
463
- assert.equal(cleanupRun.exitCode, 0);
464
- assert.equal(exists(join(original.stateDir, 'runtime.json')), true);
465
- assert.equal(invoke(fx, ['abort', original.runId]).output.status, 'OK');
466
- assert.ok(await waitFor(() => resumed.exitCode !== null, 3_000));
467
- } finally {
468
- cleanupRun.kill('SIGKILL');
469
- resumed?.kill('SIGKILL');
470
- }
471
- });
472
-
473
- test('stale cleanup removes abandoned runtime state without signaling its persisted pgid', () => {
474
- const fx = fixture();
475
- const decoy = spawn(process.execPath, ['-e', 'setInterval(() => {}, 1000)'], { detached: true });
476
- try {
477
- const stateDir = join(fx.stateRoot, 'codex-exec.abandoned');
478
- mkdirSync(stateDir, { recursive: true, mode: 0o700 });
479
- writeFileSync(join(stateDir, 'run-id'), 'abandoned\n');
480
- writeFileSync(join(stateDir, 'runtime.json'), JSON.stringify({
481
- token: 'b'.repeat(64), round: 1, phase: 'running', heartbeat: 0,
482
- pid: decoy.pid, pgid: decoy.pid,
483
- }));
484
- utimesSync(stateDir, 0, 0);
485
- const result = invoke(fx, launchArgs(), {
486
- CODEX_EXEC_STALE_SECONDS: '1', CODEX_EXEC_STALE_MAX_DELETE: '8',
487
- });
488
- assert.equal(result.output.status, 'OK');
489
- assert.equal(exists(stateDir), false);
490
- assert.equal(alive(decoy.pid), true);
491
- } finally {
492
- decoy.kill('SIGKILL');
493
- }
494
- });
495
-
496
- for (const [scenario, status] of [
497
- ['malformed', 'MALFORMED-JSON'],
498
- ['missing-thread', 'NO-THREAD'],
499
- ['missing-verdict', 'NO-VERDICT'],
500
- ['exec-fail', 'EXEC_FAILED'],
501
- ['signal', 'SIGNALLED'],
502
- ]) {
503
- test(`${scenario} maps to ${status} and retains state`, () => {
504
- const fx = fixture();
505
- const result = invoke(fx, launchArgs(), { FAKE_CODEX_SCENARIO: scenario });
506
- assert.equal(result.output.status, status);
507
- assert.ok(exists(result.output.stateDir));
508
- if (scenario === 'exec-fail') {
509
- assert.equal(result.output.originalExitStatus, 23);
510
- assert.doesNotMatch(readFileSync(join(result.output.stateDir, 'round-1.stderr.log'), 'utf8'), /super-secret/);
511
- }
512
- if (scenario === 'signal') assert.equal(result.output.signal, 'SIGTERM');
513
- });
514
- }
515
-
516
- for (const scenario of [
517
- 'json-null', 'json-array', 'json-scalar', 'item-null', 'item-array',
518
- 'event-type-null', 'event-type-array', 'item-type-null', 'item-type-array',
519
- 'thread-non-string', 'verdict-non-string',
520
- ]) {
521
- test(`${scenario} is a structured MALFORMED-JSON result without traceback`, () => {
522
- const fx = fixture();
523
- const result = invoke(fx, launchArgs(), { FAKE_CODEX_SCENARIO: scenario });
524
- assert.equal(result.output.status, 'MALFORMED-JSON');
525
- assert.equal(readFileSync(join(result.output.stateDir, 'latest'), 'utf8').trim(), 'round-1.result.json');
526
- assert.doesNotMatch(result.stderr, /Traceback/);
527
- });
528
- }
529
-
530
- test('auth failure is classified before launch and leaves no run state', () => {
531
- const fx = fixture();
532
- const result = invoke(fx, launchArgs(), { FAKE_CODEX_SCENARIO: 'auth-fail' });
533
- assert.equal(result.output.status, 'AUTH');
534
- assert.equal(result.output.originalExitStatus, 1);
535
- assert.equal(result.output.signal, null);
536
- assert.equal(exists(fx.launchLog), false);
537
- assert.equal(exists(fx.stateRoot) ? readdirSync(fx.stateRoot).length : 0, 0);
538
- });
539
-
540
- test('stderr redaction spans process-read boundaries', () => {
541
- const fx = fixture();
542
- const result = invoke(fx, launchArgs(), { FAKE_CODEX_SCENARIO: 'split-secret' });
543
- assert.equal(result.output.status, 'EXEC_FAILED');
544
- const log = readFileSync(join(result.output.stateDir, 'round-1.stderr.log'), 'utf8');
545
- assert.doesNotMatch(log, /super-secret|er-secret/);
546
- assert.match(log, /token=\[REDACTED\]/);
547
- });
548
-
549
- test('review profile declares a silent pre-thread process HUNG', () => {
550
- const fx = fixture();
551
- const result = invoke(fx, launchArgs(), {
552
- FAKE_CODEX_SCENARIO: 'silent', FAKE_CODEX_PAUSE_MS: '2000',
553
- });
554
- assert.equal(result.output.status, 'HUNG');
555
- });
556
-
557
- test('pre-thread activity resets the HUNG probe but later silence still becomes HUNG', () => {
558
- const fx = fixture();
559
- const result = invoke(fx, launchArgs(), {
560
- FAKE_CODEX_SCENARIO: 'startup-byte-silence', FAKE_CODEX_PAUSE_MS: '1000',
561
- });
562
- assert.equal(result.output.status, 'HUNG');
563
- });
564
-
565
- test('post-thread silence is governed only by the overall timeout', () => {
566
- const fx = fixture();
567
- const args = launchArgs('build').map((value, index, all) => all[index - 1] === '--timeout' ? '0.35' : value);
568
- const result = invoke(fx, args, {
569
- FAKE_CODEX_SCENARIO: 'quiet-post-thread', FAKE_CODEX_PAUSE_MS: '1000',
570
- });
571
- assert.equal(result.output.status, 'TIMEOUT');
572
- });
573
-
574
- test('timeout kills only the owned group and leaves a decoy sibling alive', () => {
575
- const fx = fixture();
576
- const childPid = join(fx.dir, 'child.pid');
577
- const decoy = spawn(process.execPath, ['-e', 'setInterval(() => {}, 1000)']);
578
- try {
579
- const args = launchArgs().map((value, index, all) => all[index - 1] === '--timeout' ? '0.35' : value);
580
- const result = invoke(fx, args, {
581
- FAKE_CODEX_SCENARIO: 'group-hang', FAKE_CODEX_CHILD_PID: childPid,
582
- });
583
- assert.equal(result.output.status, 'TIMEOUT');
584
- assert.equal(exists(result.output.stateDir), true);
585
- assert.equal(readFileSync(join(result.output.stateDir, 'latest'), 'utf8').trim(), 'round-1.result.json');
586
- assert.equal(alive(decoy.pid), true);
587
- assert.equal(alive(Number(readFileSync(childPid, 'utf8'))), false);
588
- } finally {
589
- decoy.kill('SIGKILL');
590
- }
591
- });
592
-
593
- test('a concurrent resume and finalize cannot steal an owned round lease', async () => {
594
- const fx = fixture();
595
- const first = invoke(fx, launchArgs()).output;
596
- const running = spawn(helper, [
597
- 'resume', first.runId, '--codex-bin', fake, '--prompt', 'Again',
598
- '--timeout', '5', '--probe-timeout', '1',
599
- ], {
600
- cwd: root, encoding: 'utf8', env: {
601
- ...process.env, CODEX_EXEC_STATE_ROOT: fx.stateRoot,
602
- FAKE_CODEX_SCENARIO: 'group-hang',
603
- FAKE_CODEX_LAUNCH_LOG: fx.launchLog,
604
- },
605
- });
606
- running.stdout.on('data', () => {});
607
- try {
608
- assert.ok(await waitFor(() => readJson(join(first.stateDir, 'runtime.json'))?.round === 2));
609
- const duplicate = invoke(fx, [
610
- 'resume', first.runId, '--codex-bin', fake, '--prompt', 'Duplicate',
611
- ]);
612
- assert.equal(duplicate.output.error, 'ACTIVE_RUN');
613
- const finalized = invoke(fx, ['finalize', first.runId]);
614
- assert.equal(finalized.output.error, 'ACTIVE_RUN');
615
- const roundTwoLaunches = readFileSync(fx.launchLog, 'utf8').trim().split('\n')
616
- .map((line) => JSON.parse(line))
617
- .filter((args) => args[0] === 'exec' && args[1] === 'resume');
618
- assert.equal(roundTwoLaunches.length, 1);
619
- assert.equal(invoke(fx, ['abort', first.runId]).output.status, 'OK');
620
- assert.ok(await waitFor(() => running.exitCode !== null, 3_000));
621
- } finally {
622
- running.kill('SIGKILL');
623
- }
624
- });
625
-
626
- test('a broken result output sink cannot strand runtime or lease ownership', () => {
627
- if (process.platform !== 'linux') return;
628
- const fx = fixture();
629
- const full = openSync('/dev/full', 'w');
630
- try {
631
- spawnSync(helper, launchArgs(), {
632
- cwd: root,
633
- stdio: ['ignore', full, 'pipe'],
634
- timeout: 8_000,
635
- env: {
636
- ...process.env,
637
- CODEX_EXEC_STATE_ROOT: fx.stateRoot,
638
- FAKE_CODEX_LAUNCH_LOG: fx.launchLog,
639
- },
640
- });
641
- } finally {
642
- closeSync(full);
643
- }
644
- const [stateName] = readdirSync(fx.stateRoot);
645
- const stateDir = join(fx.stateRoot, stateName);
646
- assert.equal(exists(join(stateDir, 'round-1.result.json')), true);
647
- assert.equal(exists(join(stateDir, 'latest')), true);
648
- assert.equal(exists(join(stateDir, 'runtime.json')), false);
649
- assert.equal(exists(join(stateDir, 'round.lease')), false);
650
- });
651
-
652
- test('a runtime publication failure reaps the owned child before releasing its lease', async () => {
653
- const fx = fixture();
654
- const childPid = join(fx.dir, 'supervised.pid');
655
- const result = invoke(fx, launchArgs('build'), {
656
- CODEX_EXEC_TEST_CHILD_PID_FILE: childPid,
657
- CODEX_EXEC_TEST_RUNTIME_WRITE_FAIL: '1',
658
- FAKE_CODEX_SCENARIO: 'group-hang',
659
- });
660
- assert.notEqual(result.status, 0);
661
- assert.equal(result.output.error, 'ROUND_SUPERVISOR_FAILED');
662
- assert.equal(result.output.originalExitStatus, 1);
663
- assert.equal(result.output.signal, null);
664
- const pid = Number(readFileSync(childPid, 'utf8'));
665
- assert.equal(await waitFor(() => !alive(pid), 1_000), true);
666
- const [stateName] = readdirSync(fx.stateRoot);
667
- const stateDir = join(fx.stateRoot, stateName);
668
- assert.equal(exists(join(stateDir, 'runtime.json')), false);
669
- assert.equal(exists(join(stateDir, 'round.lease')), false);
670
- });
671
-
672
- test('debug-retain marker failure is visible and never reports finalize success', () => {
673
- const fx = fixture();
674
- const run = invoke(fx, launchArgs()).output;
675
- mkdirSync(join(run.stateDir, 'debug-retain'));
676
- const finalized = invoke(fx, ['finalize', run.runId, '--debug-retain']);
677
- assert.notEqual(finalized.status, 0);
678
- assert.equal(finalized.output.error, 'DEBUG_RETAIN_FAILED');
679
- assert.equal(exists(run.stateDir), true);
680
- assert.equal(exists(join(run.stateDir, 'round.lease')), false);
681
- rmdirSync(join(run.stateDir, 'debug-retain'));
682
- assert.equal(invoke(fx, ['abort', run.runId]).output.status, 'OK');
683
- });
684
-
685
- test('lease release failure is structured and cannot masquerade as a successful round', () => {
686
- const fx = fixture();
687
- const result = invoke(fx, launchArgs(), { CODEX_EXEC_TEST_LEASE_RELEASE_FAIL: '1' });
688
- assert.notEqual(result.status, 0);
689
- assert.equal(result.output.status, 'OK');
690
- assert.equal(result.output.cleanupStatus, 'FAILED');
691
- assert.equal(result.output.cleanupError, 'LEASE_RELEASE_FAILED');
692
- assert.ok(result.output.runId);
693
- assert.doesNotMatch(result.stderr, /Traceback/);
694
- assert.equal(result.stdout.trim().split('\n').length, 1);
695
- const stateDir = join(fx.stateRoot, `codex-exec.${result.output.runId}`);
696
- assert.equal(exists(join(stateDir, 'round.lease')), true);
697
- assert.equal(invoke(fx, ['resume', result.output.runId]).output.error, 'ACTIVE_RUN');
698
- const handled = invoke(fx, ['handle-failure', '--result', JSON.stringify(result.output)]);
699
- assert.notEqual(handled.status, 0);
700
- assert.equal(handled.output.status, 'OK');
701
- assert.equal(handled.output.cleanupError, 'LEASE_RELEASE_FAILED');
702
- assert.equal(handled.output.recoveryCleanupStatus, 'FAILED');
703
- assert.equal(handled.output.recoveryCleanupError, 'ABORT_FAILED');
704
- });
705
-
706
- test('lease release failure preserves a non-OK round classification and signal', () => {
707
- const fx = fixture();
708
- const result = invoke(fx, launchArgs(), {
709
- CODEX_EXEC_TEST_LEASE_RELEASE_FAIL: '1',
710
- FAKE_CODEX_SCENARIO: 'silent',
711
- FAKE_CODEX_PAUSE_MS: '1000',
712
- });
713
- assert.notEqual(result.status, 0);
714
- assert.equal(result.output.status, 'HUNG');
715
- assert.equal(result.output.signal, 'SIGTERM');
716
- assert.equal(result.output.cleanupStatus, 'FAILED');
717
- assert.equal(result.output.cleanupError, 'LEASE_RELEASE_FAILED');
718
- });
719
-
720
- test('timeout kills descendants after the process-group leader has exited', () => {
721
- const fx = fixture();
722
- const childPid = join(fx.dir, 'orphan.pid');
723
- const args = launchArgs().map((value, index, all) => {
724
- if (all[index - 1] === '--timeout') return '0.35';
725
- if (all[index - 1] === '--probe-timeout') return '1';
726
- return value;
727
- });
728
- const result = invoke(fx, args, {
729
- FAKE_CODEX_SCENARIO: 'orphan-group', FAKE_CODEX_CHILD_PID: childPid,
730
- });
731
- assert.equal(result.output.status, 'TIMEOUT');
732
- assert.equal(alive(Number(readFileSync(childPid, 'utf8'))), false);
733
- });
734
-
735
- test('abort never signals a pgid copied into stale runtime state', async () => {
736
- const fx = fixture();
737
- const decoy = spawn(process.execPath, ['-e', 'setInterval(() => {}, 1000)'], { detached: true });
738
- try {
739
- await new Promise((resolve) => decoy.once('spawn', resolve));
740
- const stateDir = join(fx.stateRoot, 'codex-exec.stale');
741
- mkdirSync(stateDir, { recursive: true, mode: 0o700 });
742
- writeFileSync(join(stateDir, 'run-id'), 'stale\n');
743
- writeFileSync(join(stateDir, 'runtime.json'), JSON.stringify({
744
- token: 'c'.repeat(64), round: 1, phase: 'running', heartbeat: 0,
745
- pid: decoy.pid, pgid: decoy.pid,
746
- }));
747
- const aborted = invoke(fx, ['abort', 'stale']);
748
- assert.equal(aborted.output.error, 'ABORT_FAILED');
749
- assert.equal(exists(stateDir), true);
750
- assert.equal(alive(decoy.pid), true);
751
- assert.equal(await waitFor(() => decoy.exitCode !== null, 300), null);
752
- } finally {
753
- decoy.kill('SIGKILL');
754
- }
755
- });
756
-
757
- test('settling keeps finalize out and lets abort await a published CANCELLED result', async () => {
758
- const fx = fixture();
759
- const settleMarker = join(fx.dir, 'settling.marker');
760
- const running = spawn(helper, launchArgs('build'), {
761
- cwd: root, encoding: 'utf8', env: {
762
- ...process.env, CODEX_EXEC_STATE_ROOT: fx.stateRoot,
763
- FAKE_CODEX_LAUNCH_LOG: fx.launchLog, CODEX_EXEC_TEST_SETTLE_MARKER: settleMarker,
764
- },
765
- });
766
- let stdout = '';
767
- let stderr = '';
768
- running.stdout.on('data', (chunk) => { stdout += chunk; });
769
- running.stderr.on('data', (chunk) => { stderr += chunk; });
770
- try {
771
- const stateName = await waitFor(() => exists(settleMarker) && exists(fx.stateRoot)
772
- && readdirSync(fx.stateRoot).find((name) => readJson(join(fx.stateRoot, name, 'runtime.json'))?.phase === 'settling'));
773
- assert.ok(stateName);
774
- const stateDir = join(fx.stateRoot, stateName);
775
- const runId = readFileSync(join(stateDir, 'run-id'), 'utf8').trim();
776
- const finalized = invoke(fx, ['finalize', runId]);
777
- assert.equal(finalized.output.error, 'ACTIVE_RUN');
778
- assert.equal(exists(stateDir), true);
779
-
780
- const aborted = invoke(fx, ['abort', runId]);
781
- assert.equal(aborted.output.status, 'OK');
782
- const exited = await waitFor(() => running.exitCode !== null, 3_000);
783
- assert.equal(exited, true);
784
- assert.equal(JSON.parse(stdout.trim().split('\n').at(-1)).status, 'CANCELLED');
785
- assert.doesNotMatch(stderr, /Traceback|FileNotFoundError/);
786
- assert.equal(exists(stateDir), false);
787
- } finally {
788
- running.kill('SIGKILL');
789
- }
790
- });
791
-
792
- test('abort cancels an active run, deletes its state, and spares a decoy', async () => {
793
- const fx = fixture();
794
- const decoy = spawn(process.execPath, ['-e', 'setInterval(() => {}, 1000)']);
795
- const running = spawn(helper, launchArgs('build'), {
796
- cwd: root, encoding: 'utf8', env: {
797
- ...process.env, CODEX_EXEC_STATE_ROOT: fx.stateRoot, FAKE_CODEX_SCENARIO: 'group-hang',
798
- FAKE_CODEX_LAUNCH_LOG: fx.launchLog,
799
- },
800
- });
801
- let stdout = '';
802
- running.stdout.on('data', (chunk) => { stdout += chunk; });
803
- try {
804
- const stateName = await waitFor(() => exists(fx.stateRoot) && readdirSync(fx.stateRoot)
805
- .find((name) => exists(join(fx.stateRoot, name, 'run-id'))));
806
- assert.ok(stateName);
807
- const runId = readFileSync(join(fx.stateRoot, stateName, 'run-id'), 'utf8').trim();
808
- const aborted = invoke(fx, ['abort', runId]);
809
- assert.equal(aborted.output.status, 'OK');
810
- const exit = await waitFor(() => running.exitCode !== null && running.exitCode, 3_000);
811
- assert.notEqual(exit, null);
812
- assert.equal(JSON.parse(stdout.trim().split('\n').at(-1)).status, 'CANCELLED');
813
- assert.equal(exists(join(fx.stateRoot, stateName)), false);
814
- assert.equal(alive(decoy.pid), true);
815
- } finally {
816
- running.kill('SIGKILL');
817
- decoy.kill('SIGKILL');
818
- }
819
- });
820
-
821
- test('handle-failure selects one of two simultaneous live runs and leaves the other intact', async () => {
822
- const fx = fixture();
823
- const start = () => spawn(helper, launchArgs('build'), {
824
- cwd: root, encoding: 'utf8', env: {
825
- ...process.env, CODEX_EXEC_STATE_ROOT: fx.stateRoot,
826
- FAKE_CODEX_SCENARIO: 'group-hang',
827
- FAKE_CODEX_LAUNCH_LOG: fx.launchLog,
828
- },
829
- });
830
- const first = start();
831
- const second = start();
832
- try {
833
- const states = await waitFor(() => {
834
- if (!exists(fx.stateRoot)) return null;
835
- const names = readdirSync(fx.stateRoot)
836
- .filter((name) => exists(join(fx.stateRoot, name, 'runtime.json')));
837
- return names.length === 2 ? names.sort() : null;
838
- }, 3_000);
839
- assert.ok(states);
840
- const selectedDir = join(fx.stateRoot, states[0]);
841
- const otherDir = join(fx.stateRoot, states[1]);
842
- const selectedId = readFileSync(join(selectedDir, 'run-id'), 'utf8').trim();
843
- const otherId = readFileSync(join(otherDir, 'run-id'), 'utf8').trim();
844
-
845
- const handled = invoke(fx, [
846
- 'handle-failure', '--result', JSON.stringify({ status: 'HUNG', runId: selectedId }),
847
- '--run-id', otherId,
848
- ]);
849
- assert.equal(handled.output.status, 'HUNG');
850
- assert.equal(exists(selectedDir), false);
851
- assert.equal(exists(join(otherDir, 'runtime.json')), true);
852
- assert.ok(await waitFor(() => (
853
- [first, second].filter((child) => child.exitCode === null).length === 1
854
- ), 2_000));
855
-
856
- const cleanup = invoke(fx, ['abort', otherId]);
857
- assert.equal(cleanup.output.status, 'OK');
858
- assert.ok(await waitFor(() => first.exitCode !== null && second.exitCode !== null, 3_000));
859
- } finally {
860
- first.kill('SIGKILL');
861
- second.kill('SIGKILL');
862
- }
863
- });
864
-
865
- test('0.137 JSON pipe regression drains both streams without deadlock', () => {
866
- const fx = fixture();
867
- const result = invoke(fx, launchArgs(), {
868
- FAKE_CODEX_SCENARIO: 'pipe-burst', FAKE_CODEX_VERSION: '0.137.0',
869
- });
870
- assert.equal(result.output.status, 'OK');
871
- });