@ikon85/agent-workflow-kit 0.42.1 → 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 (133) 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 +268 -7
  24. package/agent-workflow-kit.package.json +42 -42
  25. package/package.json +16 -2
  26. package/scripts/marker_lib.py +3 -0
  27. package/scripts/profile_globs.py +26 -45
  28. package/scripts/release-delta-guard.mjs +29 -4
  29. package/scripts/worktree-lifecycle/README.md +57 -154
  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 -1289
  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/domain.md +0 -7
  53. package/docs/agents/issue-tracker.md +0 -23
  54. package/docs/agents/skills/local-ci.md +0 -90
  55. package/docs/agents/skills/orchestrate-wave.md +0 -166
  56. package/docs/agents/skills/spec-self-critique.md +0 -18
  57. package/docs/agents/triage-labels.md +0 -10
  58. package/docs/agents/workflow-capabilities.json +0 -28
  59. package/docs/conventions/spec-completeness.md +0 -23
  60. package/docs/research/agent-task-taxonomy-benchmark-coverage.md +0 -451
  61. package/docs/research/benchlm-routing-source.md +0 -198
  62. package/docs/research/consumer-owned-protocol-files.md +0 -238
  63. package/docs/research/frontend-agent-benchmarks.md +0 -282
  64. package/docs/research/model-effort-routing-benchmarks.md +0 -261
  65. package/docs/research/provider-neutral-agent-routing.md +0 -207
  66. package/docs/research/wave-152-consumer-acceptance.md +0 -98
  67. package/docs/research/wave-43-script-hook-census.md +0 -128
  68. package/scripts/build-kit.mjs +0 -117
  69. package/scripts/build-kit.test.mjs +0 -204
  70. package/scripts/census/census.test.mjs +0 -252
  71. package/scripts/census/state.test.mjs +0 -15
  72. package/scripts/census/transaction.test.mjs +0 -103
  73. package/scripts/census-contract.test.mjs +0 -227
  74. package/scripts/check-kit-staleness.mjs +0 -45
  75. package/scripts/check-kit-staleness.test.mjs +0 -19
  76. package/scripts/codex-exec-scenarios/fake-codex.mjs +0 -152
  77. package/scripts/codex-exec.test.mjs +0 -871
  78. package/scripts/grill-census-wiring-guard.mjs +0 -87
  79. package/scripts/grill-census-wiring-guard.test.mjs +0 -107
  80. package/scripts/kit-release.test.mjs +0 -273
  81. package/scripts/kit-update-pr.test.mjs +0 -151
  82. package/scripts/lib/audit-refs.mjs +0 -146
  83. package/scripts/lib/scrub.mjs +0 -113
  84. package/scripts/lib/scrub.test.mjs +0 -187
  85. package/scripts/memory-lifecycle/memory-lifecycle.test.mjs +0 -170
  86. package/scripts/portability_profile_scan.py +0 -53
  87. package/scripts/release-delta-guard.test.mjs +0 -151
  88. package/scripts/release-parity.test.mjs +0 -90
  89. package/scripts/release-state.test.mjs +0 -360
  90. package/scripts/test_anchor_table.py +0 -69
  91. package/scripts/test_board_bootstrap.py +0 -348
  92. package/scripts/test_board_sync.py +0 -208
  93. package/scripts/test_board_sync_create_idempotency.py +0 -117
  94. package/scripts/test_board_sync_wave_title.py +0 -120
  95. package/scripts/test_census_backstop.py +0 -712
  96. package/scripts/test_census_forward_contract.py +0 -91
  97. package/scripts/test_census_update_contract.test.mjs +0 -454
  98. package/scripts/test_codex_adapter_sync_contract.py +0 -169
  99. package/scripts/test_dist_kit_smoke.py +0 -220
  100. package/scripts/test_drift_guard_diagnostics.py +0 -295
  101. package/scripts/test_issue_claim_contract.py +0 -169
  102. package/scripts/test_kit_docs_language_census.py +0 -178
  103. package/scripts/test_marker_lib.py +0 -162
  104. package/scripts/test_orchestrate_wave_contract.py +0 -381
  105. package/scripts/test_pr_body_check.py +0 -264
  106. package/scripts/test_profile_globs.py +0 -280
  107. package/scripts/test_program_planning_contract.py +0 -133
  108. package/scripts/test_release_authorization_contract.py +0 -101
  109. package/scripts/test_render_anchor.py +0 -267
  110. package/scripts/test_retro_wrapup_contract.py +0 -153
  111. package/scripts/test_skill_code_review_seed.py +0 -89
  112. package/scripts/test_skill_codex_exec_lifecycle.py +0 -123
  113. package/scripts/test_skill_frontmatter_lint.py +0 -234
  114. package/scripts/test_skill_gh_lint.py +0 -124
  115. package/scripts/test_skill_language_census.py +0 -254
  116. package/scripts/test_skill_optional_readiness.py +0 -171
  117. package/scripts/test_skill_portability_lint.py +0 -866
  118. package/scripts/test_skill_precommit_template.py +0 -30
  119. package/scripts/test_skill_publish_audit.py +0 -358
  120. package/scripts/test_skill_readiness_contract.py +0 -264
  121. package/scripts/test_skill_readiness_preflight.py +0 -180
  122. package/scripts/test_skill_required_readiness.py +0 -233
  123. package/scripts/test_skill_selfcontainment_lint.py +0 -255
  124. package/scripts/test_skill_setup_workflow_seeds.py +0 -691
  125. package/scripts/test_skill_stale_name_lint.py +0 -126
  126. package/scripts/test_skill_surface_refs.py +0 -300
  127. package/scripts/test_skill_trailing_artifact_lint.py +0 -71
  128. package/scripts/test_tdd_contract.py +0 -78
  129. package/scripts/test_worktree_ignore_seed.py +0 -320
  130. package/scripts/test_worktree_setup_base_guard.py +0 -140
  131. package/scripts/test_worktree_wrapup_contract.py +0 -1648
  132. package/scripts/test_wrapup_land.py +0 -428
  133. 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 running = spawn(helper, launchArgs('build'), {
373
- cwd: root, encoding: 'utf8', env: {
374
- ...process.env, CODEX_EXEC_STATE_ROOT: fx.stateRoot,
375
- FAKE_CODEX_SCENARIO: 'group-hang', FAKE_CODEX_PAUSE_MS: '5000',
376
- FAKE_CODEX_LAUNCH_LOG: fx.launchLog,
377
- },
378
- });
379
- running.stdout.on('data', () => {});
380
- try {
381
- const stateName = await waitFor(() => exists(fx.stateRoot) && readdirSync(fx.stateRoot)
382
- .find((name) => readJson(join(fx.stateRoot, name, 'runtime.json'))?.phase === 'running'));
383
- assert.ok(stateName);
384
- const stateDir = join(fx.stateRoot, stateName);
385
- const runId = readFileSync(join(stateDir, 'run-id'), 'utf8').trim();
386
- const invalidAge = invoke(fx, launchArgs(), { CODEX_EXEC_STALE_SECONDS: '-1' });
387
- assert.equal(invalidAge.output.error, 'INVALID_STALE_CONFIG');
388
- const invalidCount = invoke(fx, launchArgs(), { CODEX_EXEC_STALE_MAX_DELETE: '1.5' });
389
- assert.equal(invalidCount.output.error, 'INVALID_STALE_CONFIG');
390
- assert.equal(exists(join(stateDir, 'runtime.json')), true);
391
- assert.equal(readFileSync(fx.launchLog, 'utf8').trim().split('\n').length, 1);
392
- assert.equal(invoke(fx, ['abort', runId]).output.status, 'OK');
393
- assert.ok(await waitFor(() => running.exitCode !== null, 3_000));
394
- } finally {
395
- running.kill('SIGKILL');
396
- }
397
- });
398
-
399
- test('stale cleanup tolerates a state disappearing after candidate listing', async () => {
400
- const fx = fixture();
401
- mkdirSync(fx.stateRoot, { recursive: true, mode: 0o700 });
402
- const disappearing = join(fx.stateRoot, 'codex-exec.disappearing');
403
- mkdirSync(disappearing, { mode: 0o700 });
404
- writeFileSync(join(disappearing, 'run-id'), 'disappearing\n');
405
- utimesSync(disappearing, 0, 0);
406
- const marker = join(fx.dir, 'cleanup-listed');
407
- const running = spawn(helper, launchArgs(), {
408
- cwd: root, encoding: 'utf8', env: {
409
- ...process.env,
410
- CODEX_EXEC_STATE_ROOT: fx.stateRoot,
411
- CODEX_EXEC_STALE_SECONDS: '1',
412
- CODEX_EXEC_TEST_CLEANUP_PAUSE_MARKER: marker,
413
- FAKE_CODEX_LAUNCH_LOG: fx.launchLog,
414
- },
415
- });
416
- let stdout = '';
417
- running.stdout.on('data', (chunk) => { stdout += chunk; });
418
- try {
419
- assert.ok(await waitFor(() => exists(marker)));
420
- rmSync(disappearing, { recursive: true });
421
- assert.ok(await waitFor(() => running.exitCode !== null, 3_000));
422
- assert.equal(running.exitCode, 0);
423
- assert.equal(JSON.parse(stdout.trim().split('\n').at(-1)).status, 'OK');
424
- } finally {
425
- running.kill('SIGKILL');
426
- }
427
- });
428
-
429
- test('stale cleanup cannot delete a run whose resume acquired the shared lease', async () => {
430
- const fx = fixture();
431
- const original = invoke(fx, launchArgs()).output;
432
- utimesSync(original.stateDir, 0, 0);
433
- const marker = join(fx.dir, 'cleanup-listed');
434
- const cleanupRun = spawn(helper, launchArgs(), {
435
- cwd: root, encoding: 'utf8', env: {
436
- ...process.env,
437
- CODEX_EXEC_STATE_ROOT: fx.stateRoot,
438
- CODEX_EXEC_STALE_SECONDS: '1',
439
- CODEX_EXEC_TEST_CLEANUP_PAUSE_MARKER: marker,
440
- FAKE_CODEX_LAUNCH_LOG: fx.launchLog,
441
- },
442
- });
443
- cleanupRun.stdout.on('data', () => {});
444
- let resumed;
445
- try {
446
- assert.ok(await waitFor(() => exists(marker)));
447
- resumed = spawn(helper, [
448
- 'resume', original.runId, '--codex-bin', fake, '--prompt', 'Again',
449
- '--timeout', '5', '--probe-timeout', '1',
450
- ], {
451
- cwd: root, encoding: 'utf8', env: {
452
- ...process.env,
453
- CODEX_EXEC_STATE_ROOT: fx.stateRoot,
454
- FAKE_CODEX_SCENARIO: 'group-hang',
455
- FAKE_CODEX_PAUSE_MS: '5000',
456
- FAKE_CODEX_LAUNCH_LOG: fx.launchLog,
457
- },
458
- });
459
- resumed.stdout.on('data', () => {});
460
- assert.ok(await waitFor(() => readJson(join(original.stateDir, 'runtime.json'))?.round === 2));
461
- assert.ok(await waitFor(() => cleanupRun.exitCode !== null, 3_000));
462
- assert.equal(cleanupRun.exitCode, 0);
463
- assert.equal(exists(join(original.stateDir, 'runtime.json')), true);
464
- assert.equal(invoke(fx, ['abort', original.runId]).output.status, 'OK');
465
- assert.ok(await waitFor(() => resumed.exitCode !== null, 3_000));
466
- } finally {
467
- cleanupRun.kill('SIGKILL');
468
- resumed?.kill('SIGKILL');
469
- }
470
- });
471
-
472
- test('stale cleanup removes abandoned runtime state without signaling its persisted pgid', () => {
473
- const fx = fixture();
474
- const decoy = spawn(process.execPath, ['-e', 'setInterval(() => {}, 1000)'], { detached: true });
475
- try {
476
- const stateDir = join(fx.stateRoot, 'codex-exec.abandoned');
477
- mkdirSync(stateDir, { recursive: true, mode: 0o700 });
478
- writeFileSync(join(stateDir, 'run-id'), 'abandoned\n');
479
- writeFileSync(join(stateDir, 'runtime.json'), JSON.stringify({
480
- token: 'b'.repeat(64), round: 1, phase: 'running', heartbeat: 0,
481
- pid: decoy.pid, pgid: decoy.pid,
482
- }));
483
- utimesSync(stateDir, 0, 0);
484
- const result = invoke(fx, launchArgs(), {
485
- CODEX_EXEC_STALE_SECONDS: '1', CODEX_EXEC_STALE_MAX_DELETE: '8',
486
- });
487
- assert.equal(result.output.status, 'OK');
488
- assert.equal(exists(stateDir), false);
489
- assert.equal(alive(decoy.pid), true);
490
- } finally {
491
- decoy.kill('SIGKILL');
492
- }
493
- });
494
-
495
- for (const [scenario, status] of [
496
- ['malformed', 'MALFORMED-JSON'],
497
- ['missing-thread', 'NO-THREAD'],
498
- ['missing-verdict', 'NO-VERDICT'],
499
- ['exec-fail', 'EXEC_FAILED'],
500
- ['signal', 'SIGNALLED'],
501
- ]) {
502
- test(`${scenario} maps to ${status} and retains state`, () => {
503
- const fx = fixture();
504
- const result = invoke(fx, launchArgs(), { FAKE_CODEX_SCENARIO: scenario });
505
- assert.equal(result.output.status, status);
506
- assert.ok(exists(result.output.stateDir));
507
- if (scenario === 'exec-fail') {
508
- assert.equal(result.output.originalExitStatus, 23);
509
- assert.doesNotMatch(readFileSync(join(result.output.stateDir, 'round-1.stderr.log'), 'utf8'), /super-secret/);
510
- }
511
- if (scenario === 'signal') assert.equal(result.output.signal, 'SIGTERM');
512
- });
513
- }
514
-
515
- for (const scenario of [
516
- 'json-null', 'json-array', 'json-scalar', 'item-null', 'item-array',
517
- 'event-type-null', 'event-type-array', 'item-type-null', 'item-type-array',
518
- 'thread-non-string', 'verdict-non-string',
519
- ]) {
520
- test(`${scenario} is a structured MALFORMED-JSON result without traceback`, () => {
521
- const fx = fixture();
522
- const result = invoke(fx, launchArgs(), { FAKE_CODEX_SCENARIO: scenario });
523
- assert.equal(result.output.status, 'MALFORMED-JSON');
524
- assert.equal(readFileSync(join(result.output.stateDir, 'latest'), 'utf8').trim(), 'round-1.result.json');
525
- assert.doesNotMatch(result.stderr, /Traceback/);
526
- });
527
- }
528
-
529
- test('auth failure is classified before launch and leaves no run state', () => {
530
- const fx = fixture();
531
- const result = invoke(fx, launchArgs(), { FAKE_CODEX_SCENARIO: 'auth-fail' });
532
- assert.equal(result.output.status, 'AUTH');
533
- assert.equal(result.output.originalExitStatus, 1);
534
- assert.equal(result.output.signal, null);
535
- assert.equal(exists(fx.launchLog), false);
536
- assert.equal(exists(fx.stateRoot) ? readdirSync(fx.stateRoot).length : 0, 0);
537
- });
538
-
539
- test('stderr redaction spans process-read boundaries', () => {
540
- const fx = fixture();
541
- const result = invoke(fx, launchArgs(), { FAKE_CODEX_SCENARIO: 'split-secret' });
542
- assert.equal(result.output.status, 'EXEC_FAILED');
543
- const log = readFileSync(join(result.output.stateDir, 'round-1.stderr.log'), 'utf8');
544
- assert.doesNotMatch(log, /super-secret|er-secret/);
545
- assert.match(log, /token=\[REDACTED\]/);
546
- });
547
-
548
- test('review profile declares a silent pre-thread process HUNG', () => {
549
- const fx = fixture();
550
- const result = invoke(fx, launchArgs(), {
551
- FAKE_CODEX_SCENARIO: 'silent', FAKE_CODEX_PAUSE_MS: '2000',
552
- });
553
- assert.equal(result.output.status, 'HUNG');
554
- });
555
-
556
- test('pre-thread activity resets the HUNG probe but later silence still becomes HUNG', () => {
557
- const fx = fixture();
558
- const result = invoke(fx, launchArgs(), {
559
- FAKE_CODEX_SCENARIO: 'startup-byte-silence', FAKE_CODEX_PAUSE_MS: '1000',
560
- });
561
- assert.equal(result.output.status, 'HUNG');
562
- });
563
-
564
- test('post-thread silence is governed only by the overall timeout', () => {
565
- const fx = fixture();
566
- const args = launchArgs('build').map((value, index, all) => all[index - 1] === '--timeout' ? '0.35' : value);
567
- const result = invoke(fx, args, {
568
- FAKE_CODEX_SCENARIO: 'quiet-post-thread', FAKE_CODEX_PAUSE_MS: '1000',
569
- });
570
- assert.equal(result.output.status, 'TIMEOUT');
571
- });
572
-
573
- test('timeout kills only the owned group and leaves a decoy sibling alive', () => {
574
- const fx = fixture();
575
- const childPid = join(fx.dir, 'child.pid');
576
- const decoy = spawn(process.execPath, ['-e', 'setInterval(() => {}, 1000)']);
577
- try {
578
- const args = launchArgs().map((value, index, all) => all[index - 1] === '--timeout' ? '0.35' : value);
579
- const result = invoke(fx, args, {
580
- FAKE_CODEX_SCENARIO: 'group-hang', FAKE_CODEX_PAUSE_MS: '2000', FAKE_CODEX_CHILD_PID: childPid,
581
- });
582
- assert.equal(result.output.status, 'TIMEOUT');
583
- assert.equal(exists(result.output.stateDir), true);
584
- assert.equal(readFileSync(join(result.output.stateDir, 'latest'), 'utf8').trim(), 'round-1.result.json');
585
- assert.equal(alive(decoy.pid), true);
586
- assert.equal(alive(Number(readFileSync(childPid, 'utf8'))), false);
587
- } finally {
588
- decoy.kill('SIGKILL');
589
- }
590
- });
591
-
592
- test('a concurrent resume and finalize cannot steal an owned round lease', async () => {
593
- const fx = fixture();
594
- const first = invoke(fx, launchArgs()).output;
595
- const running = spawn(helper, [
596
- 'resume', first.runId, '--codex-bin', fake, '--prompt', 'Again',
597
- '--timeout', '5', '--probe-timeout', '1',
598
- ], {
599
- cwd: root, encoding: 'utf8', env: {
600
- ...process.env, CODEX_EXEC_STATE_ROOT: fx.stateRoot,
601
- FAKE_CODEX_SCENARIO: 'group-hang', FAKE_CODEX_PAUSE_MS: '5000',
602
- FAKE_CODEX_LAUNCH_LOG: fx.launchLog,
603
- },
604
- });
605
- running.stdout.on('data', () => {});
606
- try {
607
- assert.ok(await waitFor(() => readJson(join(first.stateDir, 'runtime.json'))?.round === 2));
608
- const duplicate = invoke(fx, [
609
- 'resume', first.runId, '--codex-bin', fake, '--prompt', 'Duplicate',
610
- ]);
611
- assert.equal(duplicate.output.error, 'ACTIVE_RUN');
612
- const finalized = invoke(fx, ['finalize', first.runId]);
613
- assert.equal(finalized.output.error, 'ACTIVE_RUN');
614
- const roundTwoLaunches = readFileSync(fx.launchLog, 'utf8').trim().split('\n')
615
- .map((line) => JSON.parse(line))
616
- .filter((args) => args[0] === 'exec' && args[1] === 'resume');
617
- assert.equal(roundTwoLaunches.length, 1);
618
- assert.equal(invoke(fx, ['abort', first.runId]).output.status, 'OK');
619
- assert.ok(await waitFor(() => running.exitCode !== null, 3_000));
620
- } finally {
621
- running.kill('SIGKILL');
622
- }
623
- });
624
-
625
- test('a broken result output sink cannot strand runtime or lease ownership', () => {
626
- if (process.platform !== 'linux') return;
627
- const fx = fixture();
628
- const full = openSync('/dev/full', 'w');
629
- try {
630
- spawnSync(helper, launchArgs(), {
631
- cwd: root,
632
- stdio: ['ignore', full, 'pipe'],
633
- timeout: 8_000,
634
- env: {
635
- ...process.env,
636
- CODEX_EXEC_STATE_ROOT: fx.stateRoot,
637
- FAKE_CODEX_LAUNCH_LOG: fx.launchLog,
638
- },
639
- });
640
- } finally {
641
- closeSync(full);
642
- }
643
- const [stateName] = readdirSync(fx.stateRoot);
644
- const stateDir = join(fx.stateRoot, stateName);
645
- assert.equal(exists(join(stateDir, 'round-1.result.json')), true);
646
- assert.equal(exists(join(stateDir, 'latest')), true);
647
- assert.equal(exists(join(stateDir, 'runtime.json')), false);
648
- assert.equal(exists(join(stateDir, 'round.lease')), false);
649
- });
650
-
651
- test('a runtime publication failure reaps the owned child before releasing its lease', async () => {
652
- const fx = fixture();
653
- const childPid = join(fx.dir, 'supervised.pid');
654
- const result = invoke(fx, launchArgs('build'), {
655
- CODEX_EXEC_TEST_CHILD_PID_FILE: childPid,
656
- CODEX_EXEC_TEST_RUNTIME_WRITE_FAIL: '1',
657
- FAKE_CODEX_SCENARIO: 'group-hang',
658
- FAKE_CODEX_PAUSE_MS: '5000',
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_PAUSE_MS: '5000', 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', FAKE_CODEX_PAUSE_MS: '5000',
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
- });