oh-my-codex 0.18.9 → 0.18.11

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 (195) hide show
  1. package/Cargo.lock +6 -6
  2. package/Cargo.toml +1 -1
  3. package/README.md +6 -6
  4. package/dist/autopilot/__tests__/deep-interview-gate.test.d.ts +2 -0
  5. package/dist/autopilot/__tests__/deep-interview-gate.test.d.ts.map +1 -0
  6. package/dist/autopilot/__tests__/deep-interview-gate.test.js +215 -0
  7. package/dist/autopilot/__tests__/deep-interview-gate.test.js.map +1 -0
  8. package/dist/autopilot/__tests__/ralplan-gate.test.js +148 -0
  9. package/dist/autopilot/__tests__/ralplan-gate.test.js.map +1 -1
  10. package/dist/autopilot/deep-interview-gate.d.ts.map +1 -1
  11. package/dist/autopilot/deep-interview-gate.js +140 -0
  12. package/dist/autopilot/deep-interview-gate.js.map +1 -1
  13. package/dist/catalog/__tests__/schema.test.js +6 -0
  14. package/dist/catalog/__tests__/schema.test.js.map +1 -1
  15. package/dist/cli/__tests__/auth.test.js +36 -3
  16. package/dist/cli/__tests__/auth.test.js.map +1 -1
  17. package/dist/cli/__tests__/codex-feature-probe.test.d.ts +2 -0
  18. package/dist/cli/__tests__/codex-feature-probe.test.d.ts.map +1 -0
  19. package/dist/cli/__tests__/codex-feature-probe.test.js +46 -0
  20. package/dist/cli/__tests__/codex-feature-probe.test.js.map +1 -0
  21. package/dist/cli/__tests__/doctor-spark-routing.test.d.ts +2 -0
  22. package/dist/cli/__tests__/doctor-spark-routing.test.d.ts.map +1 -0
  23. package/dist/cli/__tests__/doctor-spark-routing.test.js +79 -0
  24. package/dist/cli/__tests__/doctor-spark-routing.test.js.map +1 -0
  25. package/dist/cli/__tests__/doctor-warning-copy.test.js +2 -0
  26. package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
  27. package/dist/cli/__tests__/explore.test.js +47 -1175
  28. package/dist/cli/__tests__/explore.test.js.map +1 -1
  29. package/dist/cli/__tests__/index.test.js +276 -6
  30. package/dist/cli/__tests__/index.test.js.map +1 -1
  31. package/dist/cli/__tests__/launch-fallback.test.js +19 -5
  32. package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
  33. package/dist/cli/__tests__/nested-help-routing.test.js +1 -1
  34. package/dist/cli/__tests__/nested-help-routing.test.js.map +1 -1
  35. package/dist/cli/__tests__/package-bin-contract.test.js +19 -6
  36. package/dist/cli/__tests__/package-bin-contract.test.js.map +1 -1
  37. package/dist/cli/__tests__/setup-refresh.test.js +6 -2
  38. package/dist/cli/__tests__/setup-refresh.test.js.map +1 -1
  39. package/dist/cli/__tests__/sparkshell-packaging.test.js +45 -2
  40. package/dist/cli/__tests__/sparkshell-packaging.test.js.map +1 -1
  41. package/dist/cli/__tests__/team-decompose.test.js +10 -5
  42. package/dist/cli/__tests__/team-decompose.test.js.map +1 -1
  43. package/dist/cli/__tests__/team.test.js +45 -1
  44. package/dist/cli/__tests__/team.test.js.map +1 -1
  45. package/dist/cli/__tests__/ultragoal.test.js +75 -0
  46. package/dist/cli/__tests__/ultragoal.test.js.map +1 -1
  47. package/dist/cli/auth.d.ts.map +1 -1
  48. package/dist/cli/auth.js +25 -1
  49. package/dist/cli/auth.js.map +1 -1
  50. package/dist/cli/codex-feature-probe.d.ts +5 -2
  51. package/dist/cli/codex-feature-probe.d.ts.map +1 -1
  52. package/dist/cli/codex-feature-probe.js +25 -9
  53. package/dist/cli/codex-feature-probe.js.map +1 -1
  54. package/dist/cli/doctor.d.ts +17 -0
  55. package/dist/cli/doctor.d.ts.map +1 -1
  56. package/dist/cli/doctor.js +121 -0
  57. package/dist/cli/doctor.js.map +1 -1
  58. package/dist/cli/explore.d.ts +2 -21
  59. package/dist/cli/explore.d.ts.map +1 -1
  60. package/dist/cli/explore.js +25 -624
  61. package/dist/cli/explore.js.map +1 -1
  62. package/dist/cli/index.d.ts +34 -2
  63. package/dist/cli/index.d.ts.map +1 -1
  64. package/dist/cli/index.js +166 -89
  65. package/dist/cli/index.js.map +1 -1
  66. package/dist/cli/setup.d.ts.map +1 -1
  67. package/dist/cli/setup.js +9 -1
  68. package/dist/cli/setup.js.map +1 -1
  69. package/dist/cli/team.d.ts +4 -0
  70. package/dist/cli/team.d.ts.map +1 -1
  71. package/dist/cli/team.js +43 -4
  72. package/dist/cli/team.js.map +1 -1
  73. package/dist/cli/ultragoal.d.ts.map +1 -1
  74. package/dist/cli/ultragoal.js +29 -0
  75. package/dist/cli/ultragoal.js.map +1 -1
  76. package/dist/hooks/__tests__/agents-overlay.test.js +8 -10
  77. package/dist/hooks/__tests__/agents-overlay.test.js.map +1 -1
  78. package/dist/hooks/__tests__/autopilot-skill-contract.test.js +15 -0
  79. package/dist/hooks/__tests__/autopilot-skill-contract.test.js.map +1 -1
  80. package/dist/hooks/__tests__/deep-interview-contract.test.js +16 -0
  81. package/dist/hooks/__tests__/deep-interview-contract.test.js.map +1 -1
  82. package/dist/hooks/__tests__/explore-routing.test.js +5 -8
  83. package/dist/hooks/__tests__/explore-routing.test.js.map +1 -1
  84. package/dist/hooks/__tests__/explore-sparkshell-guidance-contract.test.js +6 -6
  85. package/dist/hooks/__tests__/explore-sparkshell-guidance-contract.test.js.map +1 -1
  86. package/dist/hooks/__tests__/skill-guidance-contract.test.js +14 -5
  87. package/dist/hooks/__tests__/skill-guidance-contract.test.js.map +1 -1
  88. package/dist/hooks/agents-overlay.d.ts.map +1 -1
  89. package/dist/hooks/agents-overlay.js +2 -1
  90. package/dist/hooks/agents-overlay.js.map +1 -1
  91. package/dist/hooks/explore-routing.d.ts +1 -1
  92. package/dist/hooks/explore-routing.d.ts.map +1 -1
  93. package/dist/hooks/explore-routing.js +3 -8
  94. package/dist/hooks/explore-routing.js.map +1 -1
  95. package/dist/hooks/extensibility/__tests__/plugin-runner.test.js +112 -1
  96. package/dist/hooks/extensibility/__tests__/plugin-runner.test.js.map +1 -1
  97. package/dist/hooks/extensibility/plugin-runner-stdin.d.ts +2 -0
  98. package/dist/hooks/extensibility/plugin-runner-stdin.d.ts.map +1 -0
  99. package/dist/hooks/extensibility/plugin-runner-stdin.js +16 -0
  100. package/dist/hooks/extensibility/plugin-runner-stdin.js.map +1 -0
  101. package/dist/hooks/extensibility/plugin-runner.js +2 -4
  102. package/dist/hooks/extensibility/plugin-runner.js.map +1 -1
  103. package/dist/hud/__tests__/index.test.js +23 -2
  104. package/dist/hud/__tests__/index.test.js.map +1 -1
  105. package/dist/hud/__tests__/reconcile.test.js +367 -1
  106. package/dist/hud/__tests__/reconcile.test.js.map +1 -1
  107. package/dist/hud/__tests__/tmux.test.js +118 -7
  108. package/dist/hud/__tests__/tmux.test.js.map +1 -1
  109. package/dist/hud/constants.d.ts +10 -0
  110. package/dist/hud/constants.d.ts.map +1 -1
  111. package/dist/hud/constants.js +19 -0
  112. package/dist/hud/constants.js.map +1 -1
  113. package/dist/hud/index.d.ts +6 -1
  114. package/dist/hud/index.d.ts.map +1 -1
  115. package/dist/hud/index.js +12 -3
  116. package/dist/hud/index.js.map +1 -1
  117. package/dist/hud/reconcile.d.ts +10 -2
  118. package/dist/hud/reconcile.d.ts.map +1 -1
  119. package/dist/hud/reconcile.js +78 -30
  120. package/dist/hud/reconcile.js.map +1 -1
  121. package/dist/hud/tmux.d.ts +14 -1
  122. package/dist/hud/tmux.d.ts.map +1 -1
  123. package/dist/hud/tmux.js +129 -15
  124. package/dist/hud/tmux.js.map +1 -1
  125. package/dist/mcp/wiki-server.d.ts +3 -3
  126. package/dist/ralplan/consensus-gate.js +9 -1
  127. package/dist/ralplan/consensus-gate.js.map +1 -1
  128. package/dist/scripts/__tests__/codex-native-hook.test.js +170 -17
  129. package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
  130. package/dist/scripts/__tests__/run-test-files.test.js +115 -1
  131. package/dist/scripts/__tests__/run-test-files.test.js.map +1 -1
  132. package/dist/scripts/codex-native-hook.d.ts.map +1 -1
  133. package/dist/scripts/codex-native-hook.js +74 -11
  134. package/dist/scripts/codex-native-hook.js.map +1 -1
  135. package/dist/scripts/notify-hook/team-worker-stop.d.ts.map +1 -1
  136. package/dist/scripts/notify-hook/team-worker-stop.js +54 -21
  137. package/dist/scripts/notify-hook/team-worker-stop.js.map +1 -1
  138. package/dist/scripts/run-test-files.js +218 -160
  139. package/dist/scripts/run-test-files.js.map +1 -1
  140. package/dist/state/__tests__/operations.test.js +463 -0
  141. package/dist/state/__tests__/operations.test.js.map +1 -1
  142. package/dist/team/__tests__/delivery-log.test.js +18 -0
  143. package/dist/team/__tests__/delivery-log.test.js.map +1 -1
  144. package/dist/team/__tests__/runtime.test.js +48 -0
  145. package/dist/team/__tests__/runtime.test.js.map +1 -1
  146. package/dist/team/__tests__/tmux-session.test.js +107 -0
  147. package/dist/team/__tests__/tmux-session.test.js.map +1 -1
  148. package/dist/team/__tests__/tmux-test-fixture.d.ts.map +1 -1
  149. package/dist/team/__tests__/tmux-test-fixture.js +14 -2
  150. package/dist/team/__tests__/tmux-test-fixture.js.map +1 -1
  151. package/dist/team/__tests__/tmux-test-fixture.test.js +1 -0
  152. package/dist/team/__tests__/tmux-test-fixture.test.js.map +1 -1
  153. package/dist/team/__tests__/worker-bootstrap.test.js +54 -1
  154. package/dist/team/__tests__/worker-bootstrap.test.js.map +1 -1
  155. package/dist/team/delivery-log.d.ts +1 -1
  156. package/dist/team/delivery-log.d.ts.map +1 -1
  157. package/dist/team/delivery-log.js.map +1 -1
  158. package/dist/team/repo-aware-decomposition.d.ts +4 -0
  159. package/dist/team/repo-aware-decomposition.d.ts.map +1 -1
  160. package/dist/team/repo-aware-decomposition.js.map +1 -1
  161. package/dist/team/runtime.d.ts.map +1 -1
  162. package/dist/team/runtime.js +78 -9
  163. package/dist/team/runtime.js.map +1 -1
  164. package/dist/team/tmux-session.d.ts +1 -0
  165. package/dist/team/tmux-session.d.ts.map +1 -1
  166. package/dist/team/tmux-session.js +16 -5
  167. package/dist/team/tmux-session.js.map +1 -1
  168. package/dist/team/ultragoal-context.d.ts +12 -0
  169. package/dist/team/ultragoal-context.d.ts.map +1 -1
  170. package/dist/team/ultragoal-context.js +32 -8
  171. package/dist/team/ultragoal-context.js.map +1 -1
  172. package/dist/utils/__tests__/paths.test.js +23 -0
  173. package/dist/utils/__tests__/paths.test.js.map +1 -1
  174. package/dist/utils/paths.d.ts.map +1 -1
  175. package/dist/utils/paths.js +4 -2
  176. package/dist/utils/paths.js.map +1 -1
  177. package/dist/utils/toml.d.ts +4 -0
  178. package/dist/utils/toml.d.ts.map +1 -0
  179. package/dist/utils/toml.js +75 -0
  180. package/dist/utils/toml.js.map +1 -0
  181. package/package.json +1 -1
  182. package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
  183. package/plugins/oh-my-codex/skills/autopilot/SKILL.md +3 -0
  184. package/plugins/oh-my-codex/skills/deep-interview/SKILL.md +34 -0
  185. package/plugins/oh-my-codex/skills/ultrawork/SKILL.md +32 -17
  186. package/skills/autopilot/SKILL.md +3 -0
  187. package/skills/deep-interview/SKILL.md +34 -0
  188. package/skills/ultrawork/SKILL.md +32 -17
  189. package/src/scripts/__tests__/codex-native-hook.test.ts +218 -28
  190. package/src/scripts/__tests__/run-test-files.test.ts +138 -2
  191. package/src/scripts/codex-native-hook.ts +80 -10
  192. package/src/scripts/notify-hook/team-worker-stop.ts +58 -18
  193. package/src/scripts/run-test-files.ts +229 -150
  194. package/templates/AGENTS.md +39 -199
  195. package/templates/catalog-manifest.json +7 -0
@@ -6,14 +6,24 @@ const DEFAULT_TEST_TIMEOUT_MS = 0;
6
6
  const DEFAULT_RUNNER_TIMEOUT_MS = 30 * 60 * 1_000;
7
7
  const DEFAULT_FORCE_EXIT_GRACE_MS = 30_000;
8
8
  const DEFAULT_CI_TEST_CONCURRENCY = 1;
9
- const RUNTIME_STATE_ENV_KEYS = [
10
- 'OMX_ROOT',
11
- 'OMX_STATE_ROOT',
12
- 'OMX_TEAM_STATE_ROOT',
13
- 'OMX_SESSION_ID',
14
- 'CODEX_SESSION_ID',
15
- 'SESSION_ID',
16
- ] as const;
9
+ const DEFAULT_LOCAL_TEST_CONCURRENCY = 1;
10
+ const PRESERVED_TEST_ENV_KEYS = new Set([
11
+ 'OMX_EXPLORE_BIN',
12
+ 'OMX_NODE_TEST_CONCURRENCY',
13
+ 'OMX_NODE_TEST_FORCE_EXIT',
14
+ 'OMX_NODE_TEST_FORCE_EXIT_GRACE_MS',
15
+ 'OMX_NODE_TEST_PRESERVE_RUNTIME_ENV',
16
+ 'OMX_NODE_TEST_RUNNER_TIMEOUT_MS',
17
+ 'OMX_NODE_TEST_TIMEOUT_MS',
18
+ ]);
19
+
20
+ type TestRunResult = {
21
+ status: number | null;
22
+ signal: NodeJS.Signals | null;
23
+ errorMessage?: string;
24
+ timedOut: boolean;
25
+ abnormal: boolean;
26
+ };
17
27
 
18
28
  function parseBooleanEnv(value: string | undefined): boolean {
19
29
  if (!value) return false;
@@ -60,7 +70,36 @@ function parseTestConcurrency(env: NodeJS.ProcessEnv): number | undefined {
60
70
  return undefined;
61
71
  }
62
72
 
63
- return env.CI === 'true' || env.GITHUB_ACTIONS === 'true' ? DEFAULT_CI_TEST_CONCURRENCY : undefined;
73
+ return env.CI === 'true' || env.GITHUB_ACTIONS === 'true'
74
+ ? DEFAULT_CI_TEST_CONCURRENCY
75
+ : DEFAULT_LOCAL_TEST_CONCURRENCY;
76
+ }
77
+
78
+ function shouldScrubRuntimeEnvKey(key: string): boolean {
79
+ if (PRESERVED_TEST_ENV_KEYS.has(key)) return false;
80
+ return (
81
+ key.startsWith('OMX_') ||
82
+ key.startsWith('OMXBOX_') ||
83
+ key.startsWith('CODEX_') ||
84
+ key === 'USE_OMX_EXPLORE_CMD' ||
85
+ key === 'SESSION_ID' ||
86
+ key === 'TMUX' ||
87
+ key === 'TMUX_PANE'
88
+ );
89
+ }
90
+
91
+ function buildChildEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
92
+ const childEnv = { ...env };
93
+ delete childEnv.NODE_TEST_CONTEXT;
94
+ if (!parseBooleanEnv(env.OMX_NODE_TEST_PRESERVE_RUNTIME_ENV)) {
95
+ for (const key of Object.keys(childEnv)) {
96
+ if (shouldScrubRuntimeEnvKey(key)) {
97
+ delete childEnv[key];
98
+ }
99
+ }
100
+ }
101
+ childEnv.OMX_TEST_RELAX_TMUX_TIMEOUT = '1';
102
+ return childEnv;
64
103
  }
65
104
 
66
105
  const roots = process.argv.slice(2);
@@ -82,41 +121,33 @@ const runnerTimeoutMs = parseTimeoutMs(process.env.OMX_NODE_TEST_RUNNER_TIMEOUT_
82
121
  const forceExitGraceMs = parseTimeoutMs(process.env.OMX_NODE_TEST_FORCE_EXIT_GRACE_MS, DEFAULT_FORCE_EXIT_GRACE_MS);
83
122
  const testConcurrency = parseTestConcurrency(process.env);
84
123
  const forceExit = parseBooleanEnv(process.env.OMX_NODE_TEST_FORCE_EXIT);
85
- const testArgs = ['--test'];
124
+ const sharedTestArgs = ['--test'];
86
125
  if (testTimeoutMs > 0) {
87
- testArgs.push(`--test-timeout=${testTimeoutMs}`);
126
+ sharedTestArgs.push(`--test-timeout=${testTimeoutMs}`);
88
127
  }
89
128
  if (testConcurrency) {
90
- testArgs.push(`--test-concurrency=${testConcurrency}`);
129
+ sharedTestArgs.push(`--test-concurrency=${testConcurrency}`);
91
130
  }
92
131
  if (forceExit) {
93
- testArgs.push('--test-force-exit', '--test-reporter=tap');
132
+ sharedTestArgs.push('--test-force-exit', '--test-reporter=tap');
94
133
  }
95
- testArgs.push(...files);
96
134
 
97
135
  console.error(
98
136
  `[run-test-files] running ${files.length} test file(s) from ${targets.join(', ')}${
99
137
  testTimeoutMs > 0 ? ` with per-test timeout ${testTimeoutMs}ms` : ' with per-test timeout disabled'
100
138
  }${testConcurrency ? `, test concurrency ${testConcurrency}` : ', default test concurrency'}${
101
139
  forceExit ? `, force exit enabled with ${forceExitGraceMs}ms completion grace` : ', force exit disabled'
102
- }${runnerTimeoutMs > 0 ? `, and runner timeout ${runnerTimeoutMs}ms` : ', and runner timeout disabled'}`,
140
+ }${runnerTimeoutMs > 0 ? `, and runner timeout ${runnerTimeoutMs}ms` : ', and runner timeout disabled'}, with per-file process isolation`,
103
141
  );
104
142
 
105
- const childEnv = { ...process.env };
106
- delete childEnv.NODE_TEST_CONTEXT;
107
- childEnv.OMX_TEST_RELAX_TMUX_TIMEOUT = '1';
108
- if (!parseBooleanEnv(process.env.OMX_NODE_TEST_PRESERVE_RUNTIME_ENV)) {
109
- for (const key of RUNTIME_STATE_ENV_KEYS) {
110
- delete childEnv[key];
111
- }
112
- }
143
+ const childEnv = buildChildEnv(process.env);
113
144
 
114
- function reportAbnormalExit(signal: NodeJS.Signals | null, errorMessage?: string): void {
145
+ function reportAbnormalExit(file: string, signal: NodeJS.Signals | null, errorMessage?: string): void {
115
146
  if (errorMessage) {
116
- console.error(`[run-test-files] node --test error: ${errorMessage}`);
147
+ console.error(`[run-test-files] node --test error for ${file}: ${errorMessage}`);
117
148
  }
118
149
  console.error(
119
- `[run-test-files] node --test did not exit normally${signal ? ` (signal: ${signal})` : ''}. `
150
+ `[run-test-files] node --test did not exit normally for ${file}${signal ? ` (signal: ${signal})` : ''}. `
120
151
  + `Roots: ${targets.join(', ')}. Test files: ${files.length}. `
121
152
  + `Per-test timeout: ${testTimeoutMs > 0 ? `${testTimeoutMs}ms` : 'disabled'}. `
122
153
  + `Test concurrency: ${testConcurrency ?? 'default'}. `
@@ -146,155 +177,203 @@ function terminateChild(child: ChildProcess): void {
146
177
  signalChild(child, 'SIGKILL');
147
178
  }
148
179
 
149
- function runWithCompletionForceExit(): void {
150
- let finished = false;
151
- let sawFailure = false;
152
- let lastTapOk = 0;
153
- let tapTests: number | undefined;
154
- let tapPass: number | undefined;
155
- let tapFail = 0;
156
- let tapCancelled = 0;
157
- let completedFromSummary = false;
158
- let completionTimer: NodeJS.Timeout | undefined;
159
- let runnerTimer: NodeJS.Timeout | undefined;
160
- let stdoutRemainder = '';
161
- let stderrRemainder = '';
162
-
163
- const child = spawn(process.execPath, testArgs, {
164
- stdio: ['ignore', 'pipe', 'pipe'],
165
- env: childEnv,
166
- detached: !isWindows(),
167
- });
168
-
169
- function finish(exitCode: number, reason: string): void {
170
- if (finished) return;
171
- finished = true;
172
- if (completionTimer) clearTimeout(completionTimer);
173
- if (runnerTimer) clearTimeout(runnerTimer);
174
- console.error(`[run-test-files] ${reason}; exiting with status ${exitCode}`);
175
- terminateChild(child);
176
- child.stdout?.destroy();
177
- child.stderr?.destroy();
178
- child.unref();
179
- process.exit(exitCode);
180
- }
180
+ function runFileWithCompletionForceExit(file: string): Promise<TestRunResult> {
181
+ return new Promise((resolveRun) => {
182
+ let finished = false;
183
+ let sawFailure = false;
184
+ let lastTapOk = 0;
185
+ let tapTests: number | undefined;
186
+ let tapPass: number | undefined;
187
+ let tapFail = 0;
188
+ let tapCancelled = 0;
189
+ let completedFromSummary = false;
190
+ let completionTimer: NodeJS.Timeout | undefined;
191
+ let runnerTimer: NodeJS.Timeout | undefined;
192
+ let stdoutRemainder = '';
193
+ let stderrRemainder = '';
194
+
195
+ const child = spawn(process.execPath, [...sharedTestArgs, file], {
196
+ stdio: ['ignore', 'pipe', 'pipe'],
197
+ env: childEnv,
198
+ detached: !isWindows(),
199
+ });
200
+
201
+ function finish(status: number | null, signal: NodeJS.Signals | null, reason: string, terminate: boolean): void {
202
+ if (finished) return;
203
+ finished = true;
204
+ if (completionTimer) clearTimeout(completionTimer);
205
+ if (runnerTimer) clearTimeout(runnerTimer);
206
+ console.error(`[run-test-files] ${file}: ${reason}; status ${status ?? 'unknown'}`);
207
+ if (terminate) {
208
+ terminateChild(child);
209
+ }
210
+ child.stdout?.destroy();
211
+ child.stderr?.destroy();
212
+ child.unref();
213
+ resolveRun({
214
+ status,
215
+ signal,
216
+ timedOut: reason.startsWith('runner timeout'),
217
+ abnormal: status === null || signal !== null || reason.startsWith('node --test failed to spawn'),
218
+ });
219
+ }
181
220
 
182
- function markFailure(): void {
183
- sawFailure = true;
184
- if (completionTimer) {
185
- clearTimeout(completionTimer);
186
- completionTimer = undefined;
221
+ function markFailure(): void {
222
+ sawFailure = true;
223
+ if (completionTimer) {
224
+ clearTimeout(completionTimer);
225
+ completionTimer = undefined;
226
+ }
187
227
  }
188
- }
189
228
 
190
- function armCompletionTimer(reason: string): void {
191
- if (sawFailure) return;
192
- if (completionTimer) clearTimeout(completionTimer);
193
- completionTimer = setTimeout(() => {
229
+ function armCompletionTimer(reason: string): void {
194
230
  if (sawFailure) return;
195
- finish(0, reason);
196
- }, forceExitGraceMs);
197
- }
198
-
199
- function sawCleanTapSummary(): boolean {
200
- if (tapTests === undefined || tapPass === undefined) return false;
201
- return tapTests === tapPass && tapFail === 0 && tapCancelled === 0;
202
- }
203
-
204
- function parseTapLine(line: string): void {
205
- if (/^(?:not ok|Bail out!)/.test(line)) {
206
- markFailure();
207
- return;
231
+ if (completionTimer) clearTimeout(completionTimer);
232
+ completionTimer = setTimeout(() => {
233
+ if (sawFailure) return;
234
+ finish(0, null, reason, true);
235
+ }, forceExitGraceMs);
208
236
  }
209
237
 
210
- const summary = line.match(/^# (tests|pass|fail|cancelled) (\d+)$/);
211
- if (summary) {
212
- const count = Number(summary[2]);
213
- if (summary[1] === 'tests') tapTests = count;
214
- if (summary[1] === 'pass') tapPass = count;
215
- if (summary[1] === 'fail') tapFail = count;
216
- if (summary[1] === 'cancelled') tapCancelled = count;
217
- if ((summary[1] === 'fail' || summary[1] === 'cancelled') && count > 0) markFailure();
218
- return;
238
+ function sawCleanTapSummary(): boolean {
239
+ if (tapTests === undefined || tapPass === undefined) return false;
240
+ return tapTests === tapPass && tapFail === 0 && tapCancelled === 0;
219
241
  }
220
242
 
221
- const ok = line.match(/^ok (\d+)\b/);
222
- if (ok) {
223
- lastTapOk = Number(ok[1]);
224
- if (lastTapOk >= files.length) {
225
- armCompletionTimer(`force-exit completion grace elapsed after TAP ok ${lastTapOk} with no later failures`);
243
+ function parseTapLine(line: string): void {
244
+ if (/^(?:not ok|Bail out!)/.test(line)) {
245
+ markFailure();
246
+ return;
226
247
  }
227
- return;
228
- }
229
248
 
230
- const plan = line.match(/^1\.\.(\d+)$/);
231
- if (plan && Number(plan[1]) === lastTapOk && !sawFailure) {
232
- armCompletionTimer(`force-exit completion grace elapsed after TAP plan ${line}`);
233
- return;
234
- }
249
+ const summary = line.match(/^# (tests|pass|fail|cancelled) (\d+)$/);
250
+ if (summary) {
251
+ const count = Number(summary[2]);
252
+ if (summary[1] === 'tests') tapTests = count;
253
+ if (summary[1] === 'pass') tapPass = count;
254
+ if (summary[1] === 'fail') tapFail = count;
255
+ if (summary[1] === 'cancelled') tapCancelled = count;
256
+ if ((summary[1] === 'fail' || summary[1] === 'cancelled') && count > 0) markFailure();
257
+ return;
258
+ }
235
259
 
236
- if (/^# duration_ms /.test(line) && sawCleanTapSummary()) {
237
- completedFromSummary = true;
238
- armCompletionTimer('force-exit completion grace elapsed after clean TAP summary');
239
- }
240
- }
260
+ const ok = line.match(/^ok (\d+)\b/);
261
+ if (ok) {
262
+ lastTapOk = Number(ok[1]);
263
+ if (lastTapOk >= files.length) {
264
+ armCompletionTimer(`force-exit completion grace elapsed after TAP ok ${lastTapOk} with no later failures`);
265
+ }
266
+ return;
267
+ }
241
268
 
242
- function handleOutput(chunk: Buffer, stream: NodeJS.WriteStream, isStdout: boolean): void {
243
- stream.write(chunk);
244
- const text = chunk.toString('utf8');
245
- let combined = (isStdout ? stdoutRemainder : stderrRemainder) + text;
246
- const lines = combined.split(/\r?\n/);
247
- combined = lines.pop() ?? '';
248
- if (isStdout) {
249
- stdoutRemainder = combined;
250
- } else {
251
- stderrRemainder = combined;
252
- }
253
- for (const line of lines) parseTapLine(line);
254
- }
269
+ const plan = line.match(/^1\.\.(\d+)$/);
270
+ if (plan && Number(plan[1]) === lastTapOk && !sawFailure) {
271
+ armCompletionTimer(`force-exit completion grace elapsed after TAP plan ${line}`);
272
+ return;
273
+ }
255
274
 
256
- child.stdout?.on('data', (chunk: Buffer) => handleOutput(chunk, process.stdout, true));
257
- child.stderr?.on('data', (chunk: Buffer) => handleOutput(chunk, process.stderr, false));
275
+ if (/^# duration_ms /.test(line) && sawCleanTapSummary()) {
276
+ completedFromSummary = true;
277
+ armCompletionTimer('force-exit completion grace elapsed after clean TAP summary');
278
+ }
279
+ }
258
280
 
259
- child.on('error', (error) => {
260
- reportAbnormalExit(null, error.message);
261
- finish(1, 'node --test failed to spawn');
262
- });
281
+ function handleOutput(chunk: Buffer, stream: NodeJS.WriteStream, isStdout: boolean): void {
282
+ stream.write(chunk);
283
+ const text = chunk.toString('utf8');
284
+ let combined = (isStdout ? stdoutRemainder : stderrRemainder) + text;
285
+ const lines = combined.split(/\r?\n/);
286
+ combined = lines.pop() ?? '';
287
+ if (isStdout) {
288
+ stdoutRemainder = combined;
289
+ } else {
290
+ stderrRemainder = combined;
291
+ }
292
+ for (const line of lines) parseTapLine(line);
293
+ }
263
294
 
264
- child.on('exit', (status, signal) => {
265
- if (finished) return;
266
- if (stdoutRemainder) parseTapLine(stdoutRemainder);
267
- if (stderrRemainder) parseTapLine(stderrRemainder);
268
- if (typeof status === 'number') {
269
- finish(status, `node --test exited normally${completedFromSummary ? ' after clean TAP summary' : ''}`);
270
- return;
295
+ child.stdout?.on('data', (chunk: Buffer) => handleOutput(chunk, process.stdout, true));
296
+ child.stderr?.on('data', (chunk: Buffer) => handleOutput(chunk, process.stderr, false));
297
+
298
+ child.on('error', (error) => {
299
+ reportAbnormalExit(file, null, error.message);
300
+ finish(null, null, 'node --test failed to spawn', true);
301
+ });
302
+
303
+ child.on('exit', (status, signal) => {
304
+ if (finished) return;
305
+ if (stdoutRemainder) parseTapLine(stdoutRemainder);
306
+ if (stderrRemainder) parseTapLine(stderrRemainder);
307
+ if (typeof status === 'number') {
308
+ finish(status, null, `node --test exited normally${completedFromSummary ? ' after clean TAP summary' : ''}`, false);
309
+ return;
310
+ }
311
+ reportAbnormalExit(file, signal);
312
+ finish(null, signal, 'node --test exited without a numeric status', true);
313
+ });
314
+
315
+ if (runnerTimeoutMs > 0) {
316
+ runnerTimer = setTimeout(() => {
317
+ reportAbnormalExit(file, null);
318
+ finish(1, null, `runner timeout ${runnerTimeoutMs}ms elapsed`, true);
319
+ }, runnerTimeoutMs);
271
320
  }
272
- reportAbnormalExit(signal);
273
- finish(1, 'node --test exited without a numeric status');
274
321
  });
275
-
276
- if (runnerTimeoutMs > 0) {
277
- runnerTimer = setTimeout(() => {
278
- reportAbnormalExit(null);
279
- finish(1, `runner timeout ${runnerTimeoutMs}ms elapsed`);
280
- }, runnerTimeoutMs);
281
- }
282
322
  }
283
323
 
284
- if (forceExit) {
285
- runWithCompletionForceExit();
286
- } else {
287
- const result = spawnSync(process.execPath, testArgs, {
324
+ function runFileSync(file: string): TestRunResult {
325
+ const result = spawnSync(process.execPath, [...sharedTestArgs, file], {
288
326
  stdio: 'inherit',
289
327
  env: childEnv,
290
328
  timeout: runnerTimeoutMs > 0 ? runnerTimeoutMs : undefined,
291
329
  killSignal: 'SIGTERM',
292
330
  });
293
331
 
294
- if (typeof result.status === 'number') {
295
- process.exit(result.status);
332
+ const timedOut = (result.error as NodeJS.ErrnoException | undefined)?.code === 'ETIMEDOUT';
333
+ if (result.status !== 0 && (result.error || typeof result.status !== 'number')) {
334
+ reportAbnormalExit(file, result.signal, result.error?.message);
296
335
  }
297
336
 
298
- reportAbnormalExit(result.signal, result.error?.message);
299
- process.exit(1);
337
+ return {
338
+ status: result.status,
339
+ signal: result.signal,
340
+ errorMessage: result.error?.message,
341
+ timedOut,
342
+ abnormal: Boolean(result.error || typeof result.status !== 'number'),
343
+ };
300
344
  }
345
+
346
+ async function main(): Promise<void> {
347
+ const failedFiles: string[] = [];
348
+ let abnormalExit = false;
349
+ let runnerTimedOut = false;
350
+
351
+ for (const file of files) {
352
+ const result = forceExit ? await runFileWithCompletionForceExit(file) : runFileSync(file);
353
+
354
+ if (result.status === 0) {
355
+ continue;
356
+ }
357
+
358
+ failedFiles.push(file);
359
+ abnormalExit = abnormalExit || result.abnormal;
360
+ runnerTimedOut = runnerTimedOut || result.timedOut;
361
+ }
362
+
363
+ if (failedFiles.length > 0 || runnerTimedOut) {
364
+ const failureSummary = failedFiles.length === 0 && runnerTimedOut
365
+ ? `runner timed out with 0 of ${files.length} test file(s) failed`
366
+ : `${failedFiles.length} of ${files.length} test file(s) failed${abnormalExit ? ' or timed out' : ''}${runnerTimedOut ? '; runner timed out' : ''}`;
367
+ console.error(
368
+ `[run-test-files] ${failureSummary}:`,
369
+ );
370
+ for (const file of failedFiles) {
371
+ console.error(`[run-test-files] ${file}`);
372
+ }
373
+ process.exit(1);
374
+ }
375
+
376
+ process.exit(0);
377
+ }
378
+
379
+ await main();