codex-multi-auth 2.1.2 → 2.1.4

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.
package/scripts/codex.js CHANGED
@@ -89,6 +89,7 @@ const shadowHomeCleanupRetryMarkerDir =
89
89
  (process.env.CODEX_MULTI_AUTH_TEST_SHADOW_RETRY_MARKER_DIR ?? "").trim();
90
90
  let warnedInvalidRuntimeRotationProxyEnv = false;
91
91
  let warnedPendingAccountReadIdOverflow = false;
92
+ let warnedShadowHomeSqliteLinkFailure = false;
92
93
 
93
94
  async function loadRuntimeConstants() {
94
95
  const fallback = {
@@ -579,6 +580,9 @@ function shouldCaptureForwardedCodexOutput(env = process.env) {
579
580
  if (override === "0") {
580
581
  return false;
581
582
  }
583
+ if ((env.CODEX_CI ?? "").trim() === "1") {
584
+ return true;
585
+ }
582
586
  // Windows child processes can report undefined isTTY; treat that as non-TTY so retry capture remains available.
583
587
  return process.stdout.isTTY !== true || process.stderr.isTTY !== true;
584
588
  }
@@ -764,6 +768,57 @@ function rewriteAppServerAccountReadResponseLine(line, pendingAuthRequestMethods
764
768
  })}${lineEnding}`;
765
769
  }
766
770
 
771
+ function filterKnownForwardedCodexStderr(text) {
772
+ if (typeof text !== "string" || text.length === 0) {
773
+ return "";
774
+ }
775
+ const lines = text.split(/\r?\n/);
776
+ const filtered = [];
777
+ for (let index = 0; index < lines.length; index += 1) {
778
+ const line = lines[index];
779
+ if (
780
+ /ERROR codex_core::session: failed to record rollout items: thread\s*$/.test(
781
+ line,
782
+ ) &&
783
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12} not found$/i.test(
784
+ lines[index + 1] ?? "",
785
+ )
786
+ ) {
787
+ index += 1;
788
+ continue;
789
+ }
790
+ if (
791
+ /ERROR rmcp::transport::streamable_http_client: fail to delete session:\s*$/.test(
792
+ line,
793
+ ) &&
794
+ /^unexpected server response: DELETE returned HTTP 404 session_id="/.test(
795
+ lines[index + 1] ?? "",
796
+ )
797
+ ) {
798
+ index += 1;
799
+ while (index + 1 < lines.length && !/"\s*$/.test(lines[index])) {
800
+ index += 1;
801
+ }
802
+ continue;
803
+ }
804
+ filtered.push(line);
805
+ }
806
+ return filtered.join("\n");
807
+ }
808
+
809
+ function withCiKnownForwardedCodexLogSilencers(env) {
810
+ if ((env.CODEX_CI ?? "").trim() !== "1") {
811
+ return env;
812
+ }
813
+ if ((env.RUST_LOG ?? "").trim().length > 0) {
814
+ return env;
815
+ }
816
+ return {
817
+ ...env,
818
+ RUST_LOG: "codex_core::session=off",
819
+ };
820
+ }
821
+
767
822
  function forwardToRealCodexOnce(
768
823
  codexBin,
769
824
  args,
@@ -794,6 +849,21 @@ function forwardToRealCodexOnce(
794
849
  } catch {
795
850
  // Best-effort cleanup only.
796
851
  }
852
+ if (captureOutput && stdout.length > 0) {
853
+ const filteredStdout = filterKnownForwardedCodexStderr(stdout);
854
+ if (filteredStdout.length > 0) {
855
+ process.stdout.write(filteredStdout);
856
+ }
857
+ }
858
+ if (captureOutput && stderr.length > 0) {
859
+ const filteredStderr = filterKnownForwardedCodexStderr(stderr);
860
+ if (filteredStderr.trim().length > 0) {
861
+ process.stderr.write(filteredStderr);
862
+ if (!filteredStderr.endsWith("\n")) {
863
+ process.stderr.write("\n");
864
+ }
865
+ }
866
+ }
797
867
  resolve({
798
868
  exitCode,
799
869
  // No-capture output stays empty by design so retry parsing cannot
@@ -812,13 +882,16 @@ function forwardToRealCodexOnce(
812
882
  finalize(1);
813
883
  };
814
884
  try {
885
+ const childEnv = captureOutput
886
+ ? withCiKnownForwardedCodexLogSilencers(env)
887
+ : env;
815
888
  child = spawn(command, commandArgs, {
816
889
  stdio: proxyAppServerAccountRead
817
890
  ? ["pipe", "pipe", "pipe"]
818
891
  : captureOutput
819
892
  ? ["inherit", "pipe", "pipe"]
820
893
  : "inherit",
821
- env,
894
+ env: childEnv,
822
895
  });
823
896
  } catch (error) {
824
897
  failLaunch(error);
@@ -883,12 +956,10 @@ function forwardToRealCodexOnce(
883
956
  child.stdout?.on("data", (chunk) => {
884
957
  const text = typeof chunk === "string" ? chunk : chunk.toString("utf8");
885
958
  stdout += text;
886
- process.stdout.write(chunk);
887
959
  });
888
960
  child.stderr?.on("data", (chunk) => {
889
961
  const text = typeof chunk === "string" ? chunk : chunk.toString("utf8");
890
962
  stderr += text;
891
- process.stderr.write(chunk);
892
963
  });
893
964
  }
894
965
 
@@ -948,6 +1019,7 @@ async function forwardToRealCodex(codexBin, rawArgs, baseEnv = process.env) {
948
1019
  );
949
1020
  lastExitCode = result.exitCode;
950
1021
  if (result.exitCode === 0) {
1022
+ repairCodexSessionIndex(resolveCodexHomeDir(baseEnv));
951
1023
  return result.exitCode;
952
1024
  }
953
1025
 
@@ -1831,6 +1903,25 @@ function mirrorFileIntoShadowHome(sourcePath, destinationPath, tightenFile) {
1831
1903
  tightenFile(destinationPath);
1832
1904
  }
1833
1905
 
1906
+ function shouldMaterializeFileIntoShadowHome(name) {
1907
+ return /\.sqlite(?:-(?:shm|wal))?$/i.test(name);
1908
+ }
1909
+
1910
+ function materializeFileIntoShadowHome(sourcePath, destinationPath) {
1911
+ try {
1912
+ linkSync(sourcePath, destinationPath);
1913
+ return true;
1914
+ } catch {
1915
+ if (!warnedShadowHomeSqliteLinkFailure) {
1916
+ warnedShadowHomeSqliteLinkFailure = true;
1917
+ console.error(
1918
+ "codex-multi-auth: skipped SQLite shadow-home materialization because hard-linking failed; refusing to copy active SQLite state.",
1919
+ );
1920
+ }
1921
+ }
1922
+ return false;
1923
+ }
1924
+
1834
1925
  function collectShadowHomeSyncFileNames(shadowCodexHome, syncFileNames) {
1835
1926
  try {
1836
1927
  for (const entry of readdirSync(shadowCodexHome, { withFileTypes: true })) {
@@ -2004,6 +2095,7 @@ function createShadowHomeMirror(
2004
2095
  continue;
2005
2096
  }
2006
2097
  const isKnownStateFile = SHADOW_HOME_STATE_FILE_SET.has(name);
2098
+ const shouldMaterializeFile = shouldMaterializeFileIntoShadowHome(name);
2007
2099
  const sourcePath = join(originalCodexHome, name);
2008
2100
  const destinationPath = join(shadowCodexHome, name);
2009
2101
  if (existsSync(destinationPath)) {
@@ -2032,6 +2124,8 @@ function createShadowHomeMirror(
2032
2124
  if (isKnownStateFile) {
2033
2125
  copyFileSync(sourcePath, destinationPath);
2034
2126
  tightenFile(destinationPath);
2127
+ } else if (shouldMaterializeFile) {
2128
+ materializeFileIntoShadowHome(sourcePath, destinationPath);
2035
2129
  } else {
2036
2130
  mirrorFileIntoShadowHome(sourcePath, destinationPath, tightenFile);
2037
2131
  }
@@ -2195,6 +2289,16 @@ function resolveRuntimeRotationProxyOriginalCodexHome(baseEnv) {
2195
2289
  return override || resolveCodexHomeDir(baseEnv);
2196
2290
  }
2197
2291
 
2292
+ function createRuntimeRotationShadowHome(originalCodexHome) {
2293
+ const shadowRoot = join(
2294
+ originalCodexHome,
2295
+ "multi-auth",
2296
+ "runtime-shadow-homes",
2297
+ );
2298
+ mkdirSync(shadowRoot, { recursive: true });
2299
+ return mkdtempSync(join(shadowRoot, "codex-multi-auth-runtime-home-"));
2300
+ }
2301
+
2198
2302
  function createRuntimeRotationProxyCodexHome(
2199
2303
  baseEnv,
2200
2304
  proxyBaseUrl,
@@ -2202,7 +2306,7 @@ function createRuntimeRotationProxyCodexHome(
2202
2306
  configTomlModule,
2203
2307
  ) {
2204
2308
  const originalCodexHome = resolveRuntimeRotationProxyOriginalCodexHome(baseEnv);
2205
- const shadowCodexHome = mkdtempSync(join(tmpdir(), "codex-multi-auth-runtime-home-"));
2309
+ const shadowCodexHome = createRuntimeRotationShadowHome(originalCodexHome);
2206
2310
  let syncShadowHomeStateBack = () => {};
2207
2311
  const cleanup = () => {
2208
2312
  try {
@@ -2770,7 +2874,11 @@ function startRuntimeRotationAppHelper(baseContext) {
2770
2874
  });
2771
2875
  }
2772
2876
 
2773
- async function createRuntimeRotationAppHelperContext(baseContext, configTomlModule) {
2877
+ async function createRuntimeRotationAppHelperContext(
2878
+ baseContext,
2879
+ configTomlModule,
2880
+ options = {},
2881
+ ) {
2774
2882
  const startedAt = Date.now();
2775
2883
  const { helper, message } = await startRuntimeRotationAppHelper(baseContext);
2776
2884
  const helperEnv = message.env ?? {};
@@ -2778,7 +2886,7 @@ async function createRuntimeRotationAppHelperContext(baseContext, configTomlModu
2778
2886
 
2779
2887
  const cleanup = async ({ exitCode } = {}) => {
2780
2888
  const livedMs = Date.now() - startedAt;
2781
- if (exitCode === 0 && livedMs < detachGraceMs) {
2889
+ if (exitCode === 0 && (options.detachOnExit === true || livedMs < detachGraceMs)) {
2782
2890
  helper.stdout?.destroy();
2783
2891
  helper.stderr?.destroy();
2784
2892
  helper.unref();
@@ -2827,6 +2935,11 @@ async function createRuntimeRotationProxyContextIfEnabled(
2827
2935
  if (isCodexAppCommand(rawArgs)) {
2828
2936
  return createRuntimeRotationAppHelperContext(baseContext, configTomlModule);
2829
2937
  }
2938
+ if (isCodexInteractiveTuiCommand(rawArgs)) {
2939
+ return createRuntimeRotationAppHelperContext(baseContext, configTomlModule, {
2940
+ detachOnExit: true,
2941
+ });
2942
+ }
2830
2943
 
2831
2944
  const proxyModule = await loadRuntimeRotationProxyModule();
2832
2945
  if (!proxyModule) {
@@ -3021,6 +3134,10 @@ function isCodexAppServerCommand(rawArgs) {
3021
3134
  return findForwardedCommand(rawArgs)?.command === "app-server";
3022
3135
  }
3023
3136
 
3137
+ function isCodexInteractiveTuiCommand(rawArgs) {
3138
+ return findForwardedCommand(rawArgs) === null;
3139
+ }
3140
+
3024
3141
  function shouldUseRuntimeRoutingForForwardedArgs(rawArgs) {
3025
3142
  if (!Array.isArray(rawArgs) || rawArgs.length === 0) {
3026
3143
  return true;
@@ -3273,6 +3390,175 @@ function normalizeExitCode(value) {
3273
3390
  return 1;
3274
3391
  }
3275
3392
 
3393
+ function extractRolloutIdFromFilename(fileName) {
3394
+ const match = fileName.match(
3395
+ /^rollout-\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/i,
3396
+ );
3397
+ return match?.[1] ?? null;
3398
+ }
3399
+
3400
+ function collectRolloutFiles(rootDir, results = []) {
3401
+ let entries = [];
3402
+ try {
3403
+ entries = readdirSync(rootDir, { withFileTypes: true });
3404
+ } catch {
3405
+ return results;
3406
+ }
3407
+ for (const entry of entries) {
3408
+ const entryPath = join(rootDir, entry.name);
3409
+ if (entry.isDirectory()) {
3410
+ collectRolloutFiles(entryPath, results);
3411
+ continue;
3412
+ }
3413
+ if (entry.isFile() && extractRolloutIdFromFilename(entry.name)) {
3414
+ results.push(entryPath);
3415
+ }
3416
+ }
3417
+ return results;
3418
+ }
3419
+
3420
+ function parseRolloutIndexEntry(rolloutPath) {
3421
+ const fileName = basename(rolloutPath);
3422
+ const idFromName = extractRolloutIdFromFilename(fileName);
3423
+ if (!idFromName) return null;
3424
+ let content = "";
3425
+ try {
3426
+ content = readFileSync(rolloutPath, "utf8");
3427
+ } catch {
3428
+ return null;
3429
+ }
3430
+ const lines = content.split(/\r?\n/).filter(Boolean);
3431
+ let id = idFromName;
3432
+ let threadName = "";
3433
+ let updatedAt = null;
3434
+ let hasSessionMeta = false;
3435
+ for (const line of lines) {
3436
+ let record;
3437
+ try {
3438
+ record = JSON.parse(line);
3439
+ } catch {
3440
+ continue;
3441
+ }
3442
+ if (typeof record?.timestamp === "string") {
3443
+ updatedAt = record.timestamp;
3444
+ }
3445
+ if (record?.type === "session_meta" && typeof record.payload?.id === "string") {
3446
+ id = record.payload.id;
3447
+ hasSessionMeta = true;
3448
+ }
3449
+ if (!threadName && record?.type === "event_msg") {
3450
+ const message = record.payload?.message;
3451
+ if (typeof message === "string" && message.trim().length > 0) {
3452
+ threadName = message.trim();
3453
+ }
3454
+ }
3455
+ }
3456
+ if (!updatedAt) {
3457
+ try {
3458
+ updatedAt = statSync(rolloutPath).mtime.toISOString();
3459
+ } catch {
3460
+ updatedAt = new Date().toISOString();
3461
+ }
3462
+ }
3463
+ if (!threadName) {
3464
+ threadName = "Codex session";
3465
+ }
3466
+ if (!hasSessionMeta) {
3467
+ return null;
3468
+ }
3469
+ if (threadName.length > 80) {
3470
+ threadName = `${threadName.slice(0, 77)}...`;
3471
+ }
3472
+ return { id, thread_name: threadName, updated_at: updatedAt };
3473
+ }
3474
+
3475
+ function writeSessionIndexAtomicSync(indexPath, lines) {
3476
+ const indexDir = dirname(indexPath);
3477
+ mkdirSync(indexDir, { recursive: true });
3478
+ for (let attempt = 0; attempt <= SHADOW_HOME_CLEANUP_BACKOFF_MS.length; attempt += 1) {
3479
+ const tempPath = join(
3480
+ indexDir,
3481
+ [
3482
+ `.${basename(indexPath)}`,
3483
+ String(process.pid),
3484
+ String(Date.now()),
3485
+ randomBytes(4).toString("hex"),
3486
+ "tmp",
3487
+ ].join("."),
3488
+ );
3489
+ try {
3490
+ writeFileSync(tempPath, `${lines.join("\n")}\n`, {
3491
+ encoding: "utf8",
3492
+ mode: 0o600,
3493
+ });
3494
+ chmodSync(tempPath, 0o600);
3495
+ renameSync(tempPath, indexPath);
3496
+ chmodSync(indexPath, 0o600);
3497
+ return;
3498
+ } catch (error) {
3499
+ try {
3500
+ rmSync(tempPath, { force: true });
3501
+ } catch {
3502
+ // Preserve the original write failure.
3503
+ }
3504
+ if (
3505
+ isRetryableShadowHomeCleanupError(error) &&
3506
+ attempt < SHADOW_HOME_CLEANUP_BACKOFF_MS.length
3507
+ ) {
3508
+ sleepSync(SHADOW_HOME_CLEANUP_BACKOFF_MS[attempt]);
3509
+ continue;
3510
+ }
3511
+ throw error;
3512
+ }
3513
+ }
3514
+ }
3515
+
3516
+ function repairCodexSessionIndex(codexHome) {
3517
+ if (!codexHome || typeof codexHome !== "string") return;
3518
+ const sessionsDir = join(codexHome, "sessions");
3519
+ if (!existsSync(sessionsDir)) return;
3520
+ const indexPath = join(codexHome, "session_index.jsonl");
3521
+ let releaseLock = null;
3522
+ try {
3523
+ releaseLock = acquireShadowHomeSyncLock(codexHome);
3524
+ const seen = new Set();
3525
+ let existingLines = [];
3526
+ if (existsSync(indexPath)) {
3527
+ existingLines = readFileSync(indexPath, "utf8")
3528
+ .split(/\r?\n/)
3529
+ .filter(Boolean);
3530
+ for (const line of existingLines) {
3531
+ try {
3532
+ const entry = JSON.parse(line);
3533
+ if (typeof entry?.id === "string") {
3534
+ seen.add(entry.id);
3535
+ }
3536
+ } catch {
3537
+ // Preserve unparsable existing lines.
3538
+ }
3539
+ }
3540
+ }
3541
+
3542
+ const additions = [];
3543
+ for (const rolloutPath of collectRolloutFiles(sessionsDir)) {
3544
+ const entry = parseRolloutIndexEntry(rolloutPath);
3545
+ if (!entry || seen.has(entry.id)) continue;
3546
+ seen.add(entry.id);
3547
+ additions.push(entry);
3548
+ }
3549
+ if (additions.length === 0) return;
3550
+ additions.sort((a, b) => a.updated_at.localeCompare(b.updated_at));
3551
+ writeSessionIndexAtomicSync(indexPath, [
3552
+ ...existingLines,
3553
+ ...additions.map((entry) => JSON.stringify(entry)),
3554
+ ]);
3555
+ } catch {
3556
+ // Best-effort repair only; forwarding must not fail because indexing did.
3557
+ } finally {
3558
+ releaseLock?.();
3559
+ }
3560
+ }
3561
+
3276
3562
  const WINDOWS_SHIM_MARKER = "codex-multi-auth windows shim guardian v1";
3277
3563
  const POWERSHELL_PROFILE_MARKER_START = "# >>> codex-multi-auth shell guard >>>";
3278
3564
  const POWERSHELL_PROFILE_MARKER_END = "# <<< codex-multi-auth shell guard <<<";
@@ -110,6 +110,7 @@ function printUsage() {
110
110
  " --plugin=dist|package Load plugin from local dist URI or package name (default: dist)",
111
111
  " --max-cases=N Hard cap number of cases per scenario",
112
112
  " --report-json=PATH Write JSON report to PATH (relative to repo root)",
113
+ " --strict-capabilities Fail unsupported account/model capabilities instead of skipping them",
113
114
  " --no-restore Keep generated local config files after run",
114
115
  " -h, --help Show help",
115
116
  ].join("\n"),
@@ -148,6 +149,15 @@ function runQuiet(command, commandArgs) {
148
149
  }
149
150
  }
150
151
 
152
+ function runQuietWindowsTaskkill(pid) {
153
+ runQuiet("cmd.exe", [
154
+ "/d",
155
+ "/s",
156
+ "/c",
157
+ `taskkill /F /T /PID ${pid}`,
158
+ ]);
159
+ }
160
+
151
161
  export function registerSpawnedCodex(pid) {
152
162
  if (!Number.isInteger(pid) || pid <= 0) return;
153
163
  spawnedCodexPids.add(pid);
@@ -171,18 +181,67 @@ export function resolveMatrixTimeoutMs(smoke = false) {
171
181
  return parsedTimeout;
172
182
  }
173
183
 
174
- function hasCompletedSuccessfully(output, token) {
184
+ function parseNdjsonEvents(output) {
185
+ const events = [];
186
+ for (const line of output.split(/\r?\n/)) {
187
+ const trimmed = line.trim();
188
+ if (!trimmed.startsWith("{")) {
189
+ continue;
190
+ }
191
+ try {
192
+ events.push(JSON.parse(trimmed));
193
+ } catch {
194
+ // Ignore wrapper noise and partial lines.
195
+ }
196
+ }
197
+ return events;
198
+ }
199
+
200
+ function findLastIndex(items, predicate) {
201
+ for (let index = items.length - 1; index >= 0; index -= 1) {
202
+ if (predicate(items[index], index)) {
203
+ return index;
204
+ }
205
+ }
206
+ return -1;
207
+ }
208
+
209
+ function hasTerminalFailure(events) {
210
+ const lastCompletedIndex = findLastIndex(
211
+ events,
212
+ (event) =>
213
+ event?.type === "turn.completed" || event?.type === "response.completed",
214
+ );
175
215
  return (
176
- output.includes(token) ||
177
- output.includes('"type":"turn.completed"') ||
178
- output.includes('"type":"response.completed"')
216
+ findLastIndex(
217
+ events,
218
+ (event, index) =>
219
+ index > lastCompletedIndex &&
220
+ (event?.type === "error" ||
221
+ event?.type === "turn.failed" ||
222
+ event?.type === "response.failed" ||
223
+ event?.type === "response.error" ||
224
+ event?.type === "response.incomplete"),
225
+ ) >= 0
179
226
  );
180
227
  }
181
228
 
182
- function getSmokeSkipReason(exitCode, output) {
183
- if (exitCode === 124) {
184
- return "timed-out";
229
+ function hasCompletedSuccessfully(output, token) {
230
+ const events = parseNdjsonEvents(output);
231
+ if (events.length > 0) {
232
+ if (hasTerminalFailure(events)) {
233
+ return false;
234
+ }
235
+ return events.some(
236
+ (event) =>
237
+ event?.type === "turn.completed" ||
238
+ event?.type === "response.completed",
239
+ );
185
240
  }
241
+ return output.includes(token);
242
+ }
243
+
244
+ function getCapabilitySkipReason(exitCode, output, smoke) {
186
245
  if (/not supported when using codex with a chatgpt account/i.test(output)) {
187
246
  return "unsupported-model";
188
247
  }
@@ -192,14 +251,26 @@ function getSmokeSkipReason(exitCode, output) {
192
251
  ) {
193
252
  return "unsupported-reasoning";
194
253
  }
254
+ if (smoke && exitCode === 124) {
255
+ return "timed-out";
256
+ }
195
257
  return null;
196
258
  }
197
259
 
198
- function finalizeModelCaseResult(caseInfo, exitCode, output, token, smoke) {
260
+ function finalizeModelCaseResult(
261
+ caseInfo,
262
+ exitCode,
263
+ output,
264
+ token,
265
+ { smoke, strictCapabilities } = {},
266
+ ) {
199
267
  const hasToken = output.includes(token);
200
268
  const completed = hasCompletedSuccessfully(output, token);
201
269
  const ok = exitCode === 0 && completed;
202
- const skipReason = !ok && smoke ? getSmokeSkipReason(exitCode, output) : null;
270
+ const skipReason =
271
+ !ok && strictCapabilities !== true
272
+ ? getCapabilitySkipReason(exitCode, output, smoke === true)
273
+ : null;
203
274
 
204
275
  return {
205
276
  ...caseInfo,
@@ -219,8 +290,12 @@ export function __finalizeModelCaseResultForTests(
219
290
  output,
220
291
  token,
221
292
  smoke = false,
293
+ strictCapabilities = false,
222
294
  ) {
223
- return finalizeModelCaseResult(caseInfo, exitCode, output, token, smoke);
295
+ return finalizeModelCaseResult(caseInfo, exitCode, output, token, {
296
+ smoke,
297
+ strictCapabilities,
298
+ });
224
299
  }
225
300
 
226
301
  function stopCodexServersInternal() {
@@ -228,7 +303,7 @@ function stopCodexServersInternal() {
228
303
  spawnedCodexPids.clear();
229
304
  for (const pid of tracked) {
230
305
  if (process.platform === "win32") {
231
- runQuiet("taskkill", ["/F", "/T", "/PID", String(pid)]);
306
+ runQuietWindowsTaskkill(pid);
232
307
  continue;
233
308
  }
234
309
  runQuiet("kill", ["-9", String(pid)]);
@@ -352,7 +427,10 @@ function executeModelCase(caseInfo, index) {
352
427
  124,
353
428
  `Timed out after ${timeoutMs}ms`,
354
429
  token,
355
- caseInfo.smoke === true,
430
+ {
431
+ smoke: caseInfo.smoke === true,
432
+ strictCapabilities: caseInfo.strictCapabilities === true,
433
+ },
356
434
  );
357
435
  }
358
436
 
@@ -364,7 +442,10 @@ function executeModelCase(caseInfo, index) {
364
442
  exitCode,
365
443
  combinedOutput,
366
444
  token,
367
- caseInfo.smoke === true,
445
+ {
446
+ smoke: caseInfo.smoke === true,
447
+ strictCapabilities: caseInfo.strictCapabilities === true,
448
+ },
368
449
  );
369
450
  }
370
451
 
@@ -442,6 +523,7 @@ async function runScenario(scenario, options) {
442
523
  (caseInfo) => ({
443
524
  ...caseInfo,
444
525
  smoke: options.smoke,
526
+ strictCapabilities: options.strictCapabilities,
445
527
  }),
446
528
  );
447
529
  console.log(`\n=== ${scenario.toUpperCase()} (${cases.length} cases) ===`);
@@ -483,6 +565,7 @@ async function main() {
483
565
  const scenarioValue =
484
566
  parseArgValue(args, "--scenario") ?? (smoke ? "modern" : "all");
485
567
  const pluginMode = parseArgValue(args, "--plugin") ?? "dist";
568
+ const strictCapabilities = args.includes("--strict-capabilities");
486
569
  const noRestore = args.includes("--no-restore");
487
570
  const maxCasesRaw = parseArgValue(args, "--max-cases");
488
571
  const maxCases = maxCasesRaw ? Number.parseInt(maxCasesRaw, 10) : 0;
@@ -530,6 +613,7 @@ async function main() {
530
613
  smoke,
531
614
  maxCases,
532
615
  pluginRef,
616
+ strictCapabilities,
533
617
  });
534
618
  allResults.push(
535
619
  ...scenarioResults.map((item) => ({ ...item, scenario })),