jev-agent-tools 0.1.4 → 0.2.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 (131) hide show
  1. package/CHANGELOG.md +70 -1
  2. package/CONTRIBUTING.md +40 -0
  3. package/README.md +42 -9
  4. package/SECURITY.md +27 -0
  5. package/dist/adapters/analysis-context.js +75 -0
  6. package/dist/adapters/ask-files.js +189 -0
  7. package/dist/adapters/ask-proof.js +144 -0
  8. package/dist/adapters/ask-syntax.js +385 -0
  9. package/dist/adapters/canonical-path.js +17 -0
  10. package/dist/adapters/command.js +181 -0
  11. package/dist/adapters/docs.js +172 -0
  12. package/dist/adapters/exec.js +207 -0
  13. package/dist/adapters/files.js +293 -0
  14. package/dist/adapters/find.js +122 -0
  15. package/dist/adapters/git-base.js +26 -0
  16. package/dist/adapters/git-inventory.js +71 -0
  17. package/dist/adapters/git.js +439 -0
  18. package/dist/adapters/locate-file.js +159 -0
  19. package/dist/adapters/output-lines.js +46 -0
  20. package/dist/adapters/private-storage.js +98 -0
  21. package/dist/adapters/risk-callers.js +426 -0
  22. package/dist/adapters/runner-version.js +78 -0
  23. package/dist/adapters/shell.js +76 -0
  24. package/dist/adapters/syntax.js +187 -0
  25. package/dist/adapters/test-inventory.js +131 -0
  26. package/dist/adapters/usage.js +20 -0
  27. package/dist/adapters/utf8.js +47 -0
  28. package/dist/configuration.js +257 -0
  29. package/dist/constants.js +119 -0
  30. package/dist/core/ask-closure.js +282 -0
  31. package/dist/core/ask-proof.js +1 -0
  32. package/dist/core/ask-references.js +194 -0
  33. package/dist/core/asks.js +436 -0
  34. package/dist/core/batches.js +65 -0
  35. package/dist/core/command-output.js +224 -0
  36. package/dist/core/diff.js +178 -0
  37. package/dist/core/docs.js +302 -0
  38. package/dist/core/find.js +108 -0
  39. package/dist/core/git.js +1 -0
  40. package/dist/core/imports.js +550 -0
  41. package/dist/core/integrity.js +45 -0
  42. package/dist/core/lexical.js +132 -0
  43. package/dist/core/locate.js +169 -0
  44. package/dist/core/output.js +120 -0
  45. package/dist/core/pointer.js +29 -0
  46. package/dist/core/risk-callers.js +851 -0
  47. package/dist/core/runner-version.js +45 -0
  48. package/dist/core/sections.js +230 -0
  49. package/dist/core/state.js +44 -0
  50. package/dist/core/syntax.js +1 -0
  51. package/dist/core/test-commands.js +334 -0
  52. package/dist/core/test-coverage.js +74 -0
  53. package/dist/core/test-discovery.js +1382 -0
  54. package/dist/core/test-evidence.js +527 -0
  55. package/dist/core/test-state.js +81 -0
  56. package/dist/core/truncate.js +12 -0
  57. package/dist/core/units.js +349 -0
  58. package/dist/describe.js +23 -0
  59. package/dist/guide.js +33 -0
  60. package/dist/host.js +24 -0
  61. package/dist/jev/client.js +434 -0
  62. package/dist/jev/pool.js +54 -0
  63. package/dist/jev/types.js +1 -0
  64. package/dist/mcp/main.js +124 -0
  65. package/dist/mcp/protocol.js +187 -0
  66. package/dist/mcp/tools.js +116 -0
  67. package/dist/presets/docs.js +62 -0
  68. package/dist/presets/risk.js +179 -0
  69. package/dist/presets/spec.js +81 -0
  70. package/dist/presets/witnesses.js +249 -0
  71. package/dist/render.js +42 -0
  72. package/dist/result.js +3 -0
  73. package/dist/runtime.js +1 -0
  74. package/dist/session.js +147 -0
  75. package/dist/texts/ask-files.js +1 -0
  76. package/dist/texts/ask.js +2 -0
  77. package/dist/texts/check-diff.js +17 -0
  78. package/dist/texts/configuration.js +1 -0
  79. package/dist/texts/find.js +14 -0
  80. package/dist/texts/guide.js +16 -0
  81. package/dist/texts/locate.js +10 -0
  82. package/dist/texts/select-tests.js +2 -0
  83. package/dist/tools/ask-files.js +217 -0
  84. package/dist/tools/ask-schema.js +70 -0
  85. package/dist/tools/ask.js +686 -0
  86. package/dist/tools/check-diff.js +402 -0
  87. package/dist/tools/docs-check.js +299 -0
  88. package/dist/tools/find.js +389 -0
  89. package/dist/tools/locate.js +303 -0
  90. package/dist/tools/select-tests.js +567 -0
  91. package/dist/tools/spec-check.js +166 -0
  92. package/docs/adr/0001-strict-typescript-pure-core-offline-tests.md +31 -0
  93. package/docs/adr/0002-one-http-protocol-across-hosts.md +17 -0
  94. package/docs/adr/0003-explicit-scope-conservative-automation.md +19 -0
  95. package/docs/adr/0004-compiled-typed-intents.md +19 -0
  96. package/docs/adr/0005-evidence-construction-before-judgment.md +19 -0
  97. package/docs/adr/0006-visible-uncertainty-constrained-controls.md +21 -0
  98. package/docs/adr/0007-bounded-evidence-visible-limits.md +21 -0
  99. package/docs/adr/0008-static-test-discovery-conservative-plans.md +19 -0
  100. package/docs/adr/0009-session-cache-requested-model-identity.md +17 -0
  101. package/docs/adr/0010-mcp-server-thin-host.md +23 -0
  102. package/docs/agent-instructions.md +91 -0
  103. package/docs/design.md +3 -3
  104. package/docs/mcp.md +231 -0
  105. package/package.json +19 -4
  106. package/server.json +57 -0
  107. package/src/adapters/canonical-path.ts +18 -0
  108. package/src/adapters/command.ts +7 -4
  109. package/src/adapters/exec.ts +226 -0
  110. package/src/adapters/private-storage.ts +143 -0
  111. package/src/adapters/risk-callers.ts +4 -2
  112. package/src/adapters/shell.ts +97 -0
  113. package/src/configuration.ts +39 -12
  114. package/src/constants.ts +11 -0
  115. package/src/core/command-output.ts +17 -1
  116. package/src/host.ts +11 -0
  117. package/src/jev/client.ts +12 -0
  118. package/src/jev/types.ts +6 -0
  119. package/src/mcp/main.ts +135 -0
  120. package/src/mcp/protocol.ts +282 -0
  121. package/src/mcp/tools.ts +166 -0
  122. package/src/session.ts +59 -0
  123. package/src/setup.ts +13 -5
  124. package/src/tools/ask-files.ts +5 -7
  125. package/src/tools/ask.ts +26 -22
  126. package/src/tools/check-diff.ts +8 -5
  127. package/src/tools/docs-check.ts +1 -0
  128. package/src/tools/find.ts +5 -2
  129. package/src/tools/locate.ts +5 -8
  130. package/src/tools/select-tests.ts +7 -4
  131. package/src/tools/spec-check.ts +1 -0
@@ -0,0 +1,1382 @@
1
+ import { posix } from "node:path";
2
+ import { isTestFile } from "./diff.js";
3
+ import { tokenize, } from "./lexical.js";
4
+ const unknown = Symbol("computed");
5
+ function literal(tokens, start) {
6
+ const t = tokens[start];
7
+ if (!t)
8
+ return { value: unknown, end: start, complete: false };
9
+ if (t.string)
10
+ return { value: t.value, end: start + 1, complete: true };
11
+ if (["true", "false", "null"].includes(t.value))
12
+ return {
13
+ value: t.value === "null" ? null : t.value === "true",
14
+ end: start + 1,
15
+ complete: true,
16
+ };
17
+ if (/^\d+$/.test(t.value))
18
+ return { value: Number(t.value), end: start + 1, complete: true };
19
+ if (t.value !== "{" && t.value !== "[")
20
+ return { value: unknown, end: start + 1, complete: false };
21
+ const object = t.value === "{";
22
+ const endToken = object ? "}" : "]";
23
+ const result = {};
24
+ const items = [];
25
+ let complete = true;
26
+ let i = start + 1;
27
+ while (i < tokens.length && tokens[i]?.value !== endToken) {
28
+ if (tokens[i]?.value === ",") {
29
+ i++;
30
+ continue;
31
+ }
32
+ const key = tokens[i]?.value ?? "";
33
+ if (object) {
34
+ if (tokens[i + 1]?.value !== ":") {
35
+ complete = false;
36
+ i = skipExpression(tokens, i, endToken);
37
+ continue;
38
+ }
39
+ i += 2;
40
+ }
41
+ const parsed = literal(tokens, i);
42
+ const delimiter = tokens[parsed.end]?.value;
43
+ if (parsed.value !== unknown &&
44
+ (delimiter === "," || delimiter === endToken)) {
45
+ if (object)
46
+ result[key] = parsed.value;
47
+ else
48
+ items.push(parsed.value);
49
+ complete &&= parsed.complete;
50
+ i = parsed.end;
51
+ }
52
+ else {
53
+ complete = false;
54
+ i = skipExpression(tokens, i, endToken);
55
+ }
56
+ }
57
+ return {
58
+ value: object ? result : items,
59
+ end: i + 1,
60
+ complete: complete && tokens[i]?.value === endToken,
61
+ };
62
+ }
63
+ function skipExpression(tokens, start, endToken) {
64
+ let depth = 0;
65
+ let i = start;
66
+ for (; i < tokens.length; i++) {
67
+ const v = tokens[i]?.value ?? "";
68
+ if (depth === 0 && (v === "," || v === endToken))
69
+ return v === "," ? i + 1 : i;
70
+ if (["{", "[", "("].includes(v))
71
+ depth++;
72
+ if (["}", "]", ")"].includes(v))
73
+ depth--;
74
+ }
75
+ return i;
76
+ }
77
+ function record(value) {
78
+ return value && typeof value === "object" && !Array.isArray(value)
79
+ ? value
80
+ : {};
81
+ }
82
+ function strings(value) {
83
+ return typeof value === "string"
84
+ ? [value]
85
+ : Array.isArray(value)
86
+ ? value.filter((v) => typeof v === "string")
87
+ : [];
88
+ }
89
+ function expand(pattern) {
90
+ const braces = /\{([^{}]+)\}/.exec(pattern);
91
+ if (braces)
92
+ return (braces[1] ?? "")
93
+ .split(",")
94
+ .flatMap((part) => expand(pattern.slice(0, braces.index) +
95
+ part +
96
+ pattern.slice(braces.index + braces[0].length)));
97
+ const group = /([?@+*]?)\(([^()]+)\)/.exec(pattern);
98
+ if (group) {
99
+ const choices = (group[2] ?? "").split("|");
100
+ if (group[1] === "?")
101
+ choices.push("");
102
+ return choices.flatMap((part) => expand(pattern.slice(0, group.index) +
103
+ part +
104
+ pattern.slice(group.index + group[0].length)));
105
+ }
106
+ return [pattern];
107
+ }
108
+ function matches(path, pattern) {
109
+ return expand(pattern.replace(/^\.\//, "")).some((p) => {
110
+ let expression = "^";
111
+ for (let i = 0; i < p.length; i++) {
112
+ const c = p[i] ?? "";
113
+ if (c === "*" && p[i + 1] === "*") {
114
+ i++;
115
+ if (p[i + 1] === "/") {
116
+ expression += "(?:.*/)?";
117
+ i++;
118
+ }
119
+ else
120
+ expression += ".*";
121
+ }
122
+ else if (c === "*")
123
+ expression += "[^/]*";
124
+ else if (c === "?")
125
+ expression += "[^/]";
126
+ else if (c === "[" && p.indexOf("]", i + 1) > i + 1) {
127
+ const end = p.indexOf("]", i + 1);
128
+ const content = p.slice(i + 1, end);
129
+ expression += `[${content.startsWith("!") ? `^${content.slice(1)}` : content}]`;
130
+ i = end;
131
+ }
132
+ else
133
+ expression += c.replace(/[\\^$.*+?()[\]{}|]/g, "\\$&");
134
+ }
135
+ return new RegExp(`${expression}$`).test(path);
136
+ });
137
+ }
138
+ function shellWords(text) {
139
+ const words = [];
140
+ let current = "";
141
+ let quote = "";
142
+ for (let i = 0; i < text.length; i++) {
143
+ const c = text[i] ?? "";
144
+ if (c === "\\" && quote !== "'")
145
+ current += text[++i] ?? "";
146
+ else if (quote) {
147
+ if (c === quote)
148
+ quote = "";
149
+ else
150
+ current += c;
151
+ }
152
+ else if (c === "'" || c === '"')
153
+ quote = c;
154
+ else if (/\s/.test(c)) {
155
+ if (current) {
156
+ words.push(current);
157
+ current = "";
158
+ }
159
+ }
160
+ else if (";&|<>".includes(c)) {
161
+ if (current)
162
+ words.push(current);
163
+ words.push(c);
164
+ current = "";
165
+ }
166
+ else
167
+ current += c;
168
+ }
169
+ if (current)
170
+ words.push(current);
171
+ return words;
172
+ }
173
+ const moduleFramework = {
174
+ vitest: "vitest",
175
+ "@jest/globals": "jest",
176
+ "node:test": "node",
177
+ "bun:test": "bun",
178
+ "@playwright/test": "playwright",
179
+ ava: "ava",
180
+ mocha: "mocha",
181
+ };
182
+ function imports(tokens) {
183
+ const result = {
184
+ frameworks: new Set(),
185
+ tests: new Set(),
186
+ suites: new Set(),
187
+ };
188
+ for (let i = 0; i < tokens.length; i++) {
189
+ const t = tokens[i];
190
+ if (!t)
191
+ continue;
192
+ if (!t.string &&
193
+ t.value === "import" &&
194
+ tokens[i + 1]?.value !== "(" &&
195
+ tokens[i + 1]?.value !== ".") {
196
+ let end = i + 1;
197
+ while (end < tokens.length &&
198
+ !tokens[end]?.string &&
199
+ tokens[end]?.value !== ";")
200
+ end++;
201
+ const source = tokens[end];
202
+ const framework = source?.string
203
+ ? moduleFramework[source.value]
204
+ : undefined;
205
+ if (!framework)
206
+ continue;
207
+ result.frameworks.add(framework);
208
+ for (let j = i + 1; j < end; j++) {
209
+ const name = tokens[j]?.value ?? "";
210
+ const local = tokens[j + 1]?.value === "as" ? tokens[j + 2]?.value : name;
211
+ if (["test", "it", "default"].includes(name) && local)
212
+ result.tests.add(local);
213
+ if (["describe", "suite"].includes(name) && local)
214
+ result.suites.add(local);
215
+ }
216
+ if (tokens[i + 1]?.value !== "{")
217
+ result.tests.add(tokens[i + 1]?.value ?? "test");
218
+ }
219
+ if (t.value === "require" &&
220
+ tokens[i + 1]?.value === "(" &&
221
+ tokens[i + 2]?.string &&
222
+ tokens[i + 3]?.value === ")") {
223
+ const framework = moduleFramework[tokens[i + 2]?.value ?? ""];
224
+ if (framework) {
225
+ result.frameworks.add(framework);
226
+ result.tests.add("test");
227
+ result.tests.add("it");
228
+ result.suites.add("describe");
229
+ }
230
+ }
231
+ if (t.value === "Deno" &&
232
+ tokens[i + 1]?.value === "." &&
233
+ tokens[i + 2]?.value === "test")
234
+ result.frameworks.add("deno");
235
+ }
236
+ return result;
237
+ }
238
+ function jsScenarios(tokens, framework, bindings) {
239
+ const tests = new Set([...bindings.tests, "test", "it"]);
240
+ const suites = new Set([...bindings.suites, "describe", "suite"]);
241
+ if (framework === "mocha") {
242
+ for (const name of ["specify", "xit", "xspecify"])
243
+ tests.add(name);
244
+ for (const name of ["context", "xcontext", "xdescribe"])
245
+ suites.add(name);
246
+ }
247
+ const result = [];
248
+ const scopes = [];
249
+ let countKnown = true;
250
+ for (let i = 0; i < tokens.length; i++) {
251
+ while (scopes.length && (scopes[scopes.length - 1]?.end ?? Infinity) < i)
252
+ scopes.pop();
253
+ const t = tokens[i];
254
+ if (!t)
255
+ continue;
256
+ if (t.string || (!tests.has(t.value) && !suites.has(t.value)))
257
+ continue;
258
+ if (tokens[i - 1]?.value === "." &&
259
+ !["t", "ctx", "context", "Deno"].includes(tokens[i - 2]?.value ?? ""))
260
+ continue;
261
+ let suite = suites.has(t.value);
262
+ let j = i + 1;
263
+ let parameterized = false;
264
+ let knownModifiers = true;
265
+ let pendingModifier = false;
266
+ while (tokens[j]?.value === ".") {
267
+ const modifier = tokens[j + 1]?.value ?? "";
268
+ pendingModifier ||= modifier === "todo" || modifier === "skip";
269
+ if (["describe", "suite"].includes(modifier))
270
+ suite = true;
271
+ if (["each", "for"].includes(modifier))
272
+ parameterized = true;
273
+ knownModifiers &&= [
274
+ "describe",
275
+ "suite",
276
+ "each",
277
+ "for",
278
+ "skipIf",
279
+ "runIf",
280
+ "if",
281
+ "only",
282
+ "skip",
283
+ "todo",
284
+ "concurrent",
285
+ "sequential",
286
+ "fails",
287
+ "serial",
288
+ "failing",
289
+ "cb",
290
+ "before",
291
+ "after",
292
+ "beforeEach",
293
+ "afterEach",
294
+ "always",
295
+ ].includes(modifier);
296
+ j += 2;
297
+ if (["each", "for", "skipIf", "runIf", "if"].includes(modifier) &&
298
+ tokens[j]?.value === "(") {
299
+ j = closing(tokens, j) + 1;
300
+ }
301
+ }
302
+ if (tokens[j]?.value !== "(")
303
+ continue;
304
+ const end = closing(tokens, j);
305
+ const args = [];
306
+ let start = j + 1;
307
+ for (let k = start; k < end; k++) {
308
+ const token = tokens[k];
309
+ if (token?.string)
310
+ continue;
311
+ if (["(", "[", "{"].includes(token?.value ?? "")) {
312
+ k = closing(tokens, k);
313
+ }
314
+ else if (token?.value === ",") {
315
+ args.push({ start, end: k });
316
+ start = k + 1;
317
+ }
318
+ }
319
+ if (start < end)
320
+ args.push({ start, end });
321
+ const title = tokens[j + 1];
322
+ const callback = args.find((arg) => {
323
+ const head = tokens[arg.start]?.value;
324
+ if (head === "function" ||
325
+ (head === "async" && tokens[arg.start + 1]?.value === "function"))
326
+ return true;
327
+ for (let k = arg.start; k < arg.end; k++) {
328
+ const token = tokens[k];
329
+ if (token?.string)
330
+ continue;
331
+ if (["(", "[", "{"].includes(token?.value ?? ""))
332
+ k = closing(tokens, k);
333
+ else if (token?.value === "=" && tokens[k + 1]?.value === ">")
334
+ return true;
335
+ }
336
+ return false;
337
+ });
338
+ const optionsArg = title?.string ? args[1] : args[0];
339
+ const nodeOptions = framework === "node" && tokens[optionsArg?.start ?? end]?.value === "{"
340
+ ? record(literal(tokens, optionsArg?.start ?? end).value)
341
+ : {};
342
+ const callbackFreeNode = framework === "node" &&
343
+ !suite &&
344
+ !callback &&
345
+ (args.length === 1 ||
346
+ (args.length === 2 && tokens[args[1]?.start ?? end]?.value === "{"));
347
+ if (callbackFreeNode &&
348
+ (pendingModifier || nodeOptions.todo || nodeOptions.skip))
349
+ continue;
350
+ // Unlike JS runners, Node executes bare literal declarations as passing tests.
351
+ if (!callbackFreeNode &&
352
+ !callback &&
353
+ title?.string &&
354
+ (args.length <= 1 ||
355
+ (args.length === 2 && tokens[args[1]?.start ?? end]?.value === "{")))
356
+ continue;
357
+ if (!args.length) {
358
+ countKnown = false;
359
+ continue;
360
+ }
361
+ const functionStart = callback && tokens[callback.start]?.value === "async"
362
+ ? callback.start + 1
363
+ : callback?.start;
364
+ const namedFunction = framework === "node" &&
365
+ callback?.start === j + 1 &&
366
+ functionStart !== undefined &&
367
+ tokens[functionStart]?.value === "function"
368
+ ? tokens[functionStart + (tokens[functionStart + 1]?.value === "*" ? 2 : 1)]
369
+ : undefined;
370
+ const objectTitle = framework === "node" && title?.value === "{"
371
+ ? record(literal(tokens, j + 1).value).name
372
+ : undefined;
373
+ const staticName = title?.string && args[0]?.end === j + 2
374
+ ? title.value
375
+ : typeof objectTitle === "string"
376
+ ? objectTitle
377
+ : namedFunction && /^[a-zA-Z_$][\w$]*$/.test(namedFunction.value)
378
+ ? namedFunction.value
379
+ : undefined;
380
+ const reliable = staticName !== undefined &&
381
+ (!!callback || callbackFreeNode) &&
382
+ knownModifiers &&
383
+ !parameterized &&
384
+ scopes.every((s) => s.reliable);
385
+ const name = staticName ?? `<dynamic:${t.line}>`;
386
+ if (!reliable)
387
+ countKnown = false;
388
+ const ancestors = scopes.map((s) => s.name);
389
+ if (suite)
390
+ scopes.push({ end, name, reliable });
391
+ else {
392
+ const nativeName = [...ancestors, name].join(framework === "playwright" ? " › " : " ");
393
+ const nativeAncestors = framework === "node"
394
+ ? ancestors.map((_, index) => ancestors.slice(0, index + 1).join(" "))
395
+ : ancestors;
396
+ result.push({
397
+ id: `s${result.length + 1}`,
398
+ name: nativeName,
399
+ nameParts: [...ancestors, name],
400
+ line: t.line,
401
+ reliable,
402
+ ...(tokens[end]
403
+ ? { range: { start: t.line, end: tokens[end]?.line ?? t.line } }
404
+ : {}),
405
+ ...(ancestors.length || framework === "node"
406
+ ? { ancestors: nativeAncestors }
407
+ : {}),
408
+ });
409
+ if (framework === "node")
410
+ scopes.push({ end, name, reliable });
411
+ }
412
+ }
413
+ const names = new Set();
414
+ for (const scenario of result) {
415
+ if (names.has(scenario.name)) {
416
+ countKnown = false;
417
+ for (const same of result)
418
+ if (same.name === scenario.name)
419
+ same.reliable = false;
420
+ }
421
+ names.add(scenario.name);
422
+ }
423
+ return { scenarios: result, countKnown };
424
+ }
425
+ function closing(tokens, start) {
426
+ const open = tokens[start]?.value;
427
+ const close = open === "(" ? ")" : open === "[" ? "]" : "}";
428
+ let depth = 0;
429
+ for (let i = start; i < tokens.length; i++) {
430
+ if (tokens[i]?.string)
431
+ continue;
432
+ if (tokens[i]?.value === open)
433
+ depth++;
434
+ if (tokens[i]?.value === close && --depth === 0)
435
+ return i;
436
+ }
437
+ return tokens.length;
438
+ }
439
+ function pythonScenarios(text, config) {
440
+ const result = [];
441
+ const classes = [];
442
+ let decorators = [];
443
+ let decoratorStart;
444
+ let countKnown = true;
445
+ const functionPatterns = strings(config.python_functions).flatMap(shellWords);
446
+ const classPatterns = strings(config.python_classes).flatMap(shellWords);
447
+ const source = text.split("\n");
448
+ for (let i = 0; i < source.length; i++) {
449
+ const line = source[i] ?? "";
450
+ const tokens = tokenize(line, true);
451
+ if (!tokens.length)
452
+ continue;
453
+ const indent = line.length - line.trimStart().length;
454
+ while (classes.length &&
455
+ indent <= (classes[classes.length - 1]?.indent ?? -1))
456
+ classes.pop();
457
+ if (tokens[0]?.value === "@") {
458
+ decoratorStart ??= i + 1;
459
+ let decoration = line.trim();
460
+ let balance = tokens.filter((t) => !t.string && ["(", "[", "{"].includes(t.value))
461
+ .length -
462
+ tokens.filter((t) => !t.string && [")", "]", "}"].includes(t.value))
463
+ .length;
464
+ while (balance > 0 && i + 1 < source.length) {
465
+ const next = source[++i] ?? "";
466
+ decoration += `\n${next}`;
467
+ const nextTokens = tokenize(next, true);
468
+ balance +=
469
+ nextTokens.filter((t) => !t.string && ["(", "[", "{"].includes(t.value)).length -
470
+ nextTokens.filter((t) => !t.string && [")", "]", "}"].includes(t.value)).length;
471
+ }
472
+ decorators.push(decoration);
473
+ continue;
474
+ }
475
+ if (tokens[0]?.value === "class") {
476
+ const name = tokens[1]?.value ?? "";
477
+ if ((classPatterns.length ? classPatterns : ["Test*"]).some((p) => matches(name, p)) ||
478
+ tokens.some((t) => t.value === "TestCase"))
479
+ classes.push({ name, indent });
480
+ }
481
+ const def = tokens[0]?.value === "async" ? 1 : 0;
482
+ if (tokens[def]?.value === "def") {
483
+ const name = tokens[def + 1]?.value ?? "";
484
+ const inClass = indent > 0;
485
+ if ((!inClass || classes.length) &&
486
+ (functionPatterns.length ? functionPatterns : ["test_*"]).some((p) => matches(name, p))) {
487
+ const qualified = [...classes.map((c) => c.name), name].join("::");
488
+ const parameterDecorators = decorators
489
+ .map((text) => tokenize(text, true))
490
+ .filter((d) => d.some((t) => t.value === "parametrize"));
491
+ let parameterIds;
492
+ if (parameterDecorators.length === 1) {
493
+ const decoration = parameterDecorators[0] ?? [];
494
+ const idsIndex = decoration.findIndex((t, index) => t.value === "ids" && decoration[index + 1]?.value === "=");
495
+ const ids = literal(decoration, idsIndex + 2);
496
+ const call = decoration.findIndex((t) => t.value === "parametrize") + 1;
497
+ const argument = literal(decoration, call + 1);
498
+ const values = literal(decoration, argument.end + 1);
499
+ if (idsIndex >= 0 &&
500
+ ids.complete &&
501
+ values.complete &&
502
+ Array.isArray(ids.value) &&
503
+ Array.isArray(values.value) &&
504
+ ids.value.length === values.value.length &&
505
+ ids.value.length > 0 &&
506
+ ids.value.every((id) => typeof id === "string" &&
507
+ /^[\x20-\x7e]+$/.test(id) &&
508
+ !id.includes("[") &&
509
+ !id.includes("]") &&
510
+ !id.includes("\\")) &&
511
+ new Set(ids.value).size === ids.value.length)
512
+ parameterIds = ids.value;
513
+ }
514
+ // Generated IDs, stacked decorators and collection hooks stay conservative.
515
+ const reliable = !parameterDecorators.length || !!parameterIds;
516
+ if (!reliable)
517
+ countKnown = false;
518
+ let endLine = i + 1;
519
+ for (let bodyLine = i + 1; bodyLine < source.length; bodyLine++) {
520
+ const body = source[bodyLine] ?? "";
521
+ if (!body.trim() || body.trimStart().startsWith("#"))
522
+ continue;
523
+ if (body.length - body.trimStart().length <= indent)
524
+ break;
525
+ endLine = bodyLine + 1;
526
+ }
527
+ for (const nodeName of parameterIds
528
+ ? parameterIds.map((id) => `${qualified}[${id}]`)
529
+ : [qualified]) {
530
+ result.push({
531
+ id: nodeName,
532
+ name: nodeName,
533
+ line: i + 1,
534
+ reliable,
535
+ range: { start: decoratorStart ?? i + 1, end: endLine },
536
+ ...(classes.length
537
+ ? { ancestors: classes.map((c) => c.name) }
538
+ : {}),
539
+ });
540
+ }
541
+ }
542
+ }
543
+ decorators = [];
544
+ decoratorStart = undefined;
545
+ }
546
+ return { scenarios: result, countKnown };
547
+ }
548
+ function native(path, framework) {
549
+ if (framework === "go")
550
+ return /_test\.go$/.test(path);
551
+ if (framework === "pytest")
552
+ return /(?:^|\/)(?:test_[^/]+|[^/]+_test)\.py$/.test(path);
553
+ if (framework === "bun")
554
+ return /(?:\.(?:test|spec)|_(?:test|spec))\.[cm]?[jt]sx?$/.test(path);
555
+ if (framework === "jest" && /(?:^|\/)__tests__\/.*\.[cm]?[jt]sx?$/.test(path))
556
+ return true;
557
+ if (framework === "ava")
558
+ return /(?:^|\/)test(?:s)?\/.*\.[cm]?[jt]sx?$/.test(path);
559
+ return (/\.(?:test|spec|vitest)\.[cm]?[jt]sx?$/.test(path) ||
560
+ /(?:^|\/)test\.[cm]?[jt]s$/.test(path));
561
+ }
562
+ function directory(path) {
563
+ const dir = posix.dirname(path);
564
+ return dir === "." ? "." : dir;
565
+ }
566
+ function relative(cwd, path) {
567
+ return posix.relative(cwd, path);
568
+ }
569
+ function under(cwd, path) {
570
+ const rel = relative(cwd, path);
571
+ return rel !== ".." && !rel.startsWith("../");
572
+ }
573
+ function configFramework(path) {
574
+ const name = posix.basename(path);
575
+ if (!/\.(?:setup|workspace)\./.test(name) &&
576
+ /^vitest(?:\.[\w-]+)*\.(?:[cm]?[jt]s)$/.test(name))
577
+ return "vitest";
578
+ if (/^jest\.config\./.test(name))
579
+ return "jest";
580
+ if (/^playwright\.config\./.test(name))
581
+ return "playwright";
582
+ if (["pytest.ini", "pyproject.toml", "setup.cfg", "tox.ini"].includes(name))
583
+ return "pytest";
584
+ if (name === "bunfig.toml")
585
+ return "bun";
586
+ if (name === "deno.json" || name === "deno.jsonc")
587
+ return "deno";
588
+ if (/^\.mocharc\./.test(name))
589
+ return "mocha";
590
+ if (/^ava\.config\.[cm]?js$/.test(name))
591
+ return "ava";
592
+ return undefined;
593
+ }
594
+ /** Configuration files needed before planning configured test paths. */
595
+ export function isTestConfiguration(path) {
596
+ const name = posix.basename(path);
597
+ return (configFramework(path) !== undefined ||
598
+ name === "package.json" ||
599
+ /^vitest\.workspace\.[cm]?[jt]s$/.test(name));
600
+ }
601
+ function iniSettings(text, path) {
602
+ const settings = {};
603
+ let active = posix.basename(path) === "pytest.ini";
604
+ let seen = active;
605
+ let key = "";
606
+ let complete = true;
607
+ const source = text.split("\n");
608
+ for (let lineIndex = 0; lineIndex < source.length; lineIndex++) {
609
+ const line = source[lineIndex] ?? "";
610
+ const trimmed = line.trim();
611
+ if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith(";"))
612
+ continue;
613
+ if (trimmed.startsWith("[")) {
614
+ active = [
615
+ "[pytest]",
616
+ "[tool.pytest.ini_options]",
617
+ "[tool:pytest]",
618
+ "[test]",
619
+ ].includes(trimmed);
620
+ seen ||= active;
621
+ continue;
622
+ }
623
+ if (!active)
624
+ continue;
625
+ const equal = line.indexOf("=");
626
+ if (equal >= 0) {
627
+ key = line.slice(0, equal).trim();
628
+ let value = line.slice(equal + 1).trim();
629
+ if (value.startsWith("[")) {
630
+ while (!literal(tokenize(value), 0).complete &&
631
+ lineIndex + 1 < source.length)
632
+ value += `\n${source[++lineIndex] ?? ""}`;
633
+ }
634
+ if (value.startsWith("[") ||
635
+ value.startsWith('"') ||
636
+ value.startsWith("'")) {
637
+ const parsed = literal(tokenize(value), 0);
638
+ if (parsed.value !== unknown)
639
+ settings[key] = parsed.value;
640
+ complete &&= parsed.complete;
641
+ }
642
+ else
643
+ settings[key] = value;
644
+ }
645
+ else if (/^\s/.test(line) && key)
646
+ settings[key] = `${String(settings[key] ?? "")} ${trimmed}`;
647
+ else
648
+ complete = false;
649
+ }
650
+ return seen ? { settings, complete } : undefined;
651
+ }
652
+ function readConfig(text, tokens = tokenize(text)) {
653
+ let start = -1;
654
+ for (let i = 0; i < tokens.length; i++) {
655
+ if (tokens[i]?.value === "default" && tokens[i - 1]?.value === "export") {
656
+ start = i + 1;
657
+ if (tokens[start]?.value === "defineConfig" &&
658
+ tokens[start + 1]?.value === "(")
659
+ start += 2;
660
+ break;
661
+ }
662
+ if (tokens[i]?.value === "exports" && tokens[i + 1]?.value === "=") {
663
+ start = i + 2;
664
+ break;
665
+ }
666
+ }
667
+ if (start < 0 && tokens[0]?.value === "{")
668
+ start = 0;
669
+ const parsed = literal(tokens, start);
670
+ return {
671
+ settings: record(parsed.value),
672
+ complete: parsed.complete && !!tokens[start] && tokens[start]?.value === "{",
673
+ };
674
+ }
675
+ function groupFrom(framework, cwd, config, settings, complete, project) {
676
+ const test = ["vitest", "deno"].includes(framework)
677
+ ? record(settings.test)
678
+ : settings;
679
+ const root = typeof settings.root === "string"
680
+ ? settings.root
681
+ : typeof test.root === "string"
682
+ ? test.root
683
+ : undefined;
684
+ const actualCwd = root ? posix.normalize(posix.join(cwd, root)) : cwd;
685
+ let include = strings(test.include ?? test.testMatch ?? test.files ?? test.spec);
686
+ let exclude = strings(test.exclude ?? test.testPathIgnorePatterns);
687
+ if (framework === "deno")
688
+ exclude.push(...strings(settings.exclude));
689
+ if (framework === "mocha")
690
+ exclude.push(...strings(test.ignore));
691
+ if (framework === "playwright") {
692
+ const testDir = typeof test.testDir === "string" ? test.testDir : ".";
693
+ include = strings(test.testMatch).map((p) => posix.join(testDir, p));
694
+ if (!include.length && testDir !== ".")
695
+ include = [
696
+ `${testDir}/**/*.spec.{js,ts,jsx,tsx,mjs,cjs}`,
697
+ `${testDir}/**/*.test.{js,ts,jsx,tsx,mjs,cjs}`,
698
+ ];
699
+ exclude = strings(test.testIgnore);
700
+ }
701
+ if (framework === "pytest") {
702
+ const paths = strings(test.testpaths).flatMap((s) => shellWords(s));
703
+ const filenames = strings(test.python_files).flatMap((s) => shellWords(s));
704
+ if (paths.length || filenames.length)
705
+ include = (paths.length ? paths : ["."]).flatMap((p) => (filenames.length ? filenames : ["test_*.py", "*_test.py"]).map((f) => posix.join(p, "**", f)));
706
+ exclude = strings(test.norecursedirs)
707
+ .flatMap((s) => shellWords(s))
708
+ .map((p) => `**/${p}/**`);
709
+ }
710
+ const negatives = include
711
+ .filter((p) => p.startsWith("!"))
712
+ .map((p) => p.slice(1));
713
+ exclude.push(...negatives);
714
+ include = include.filter((p) => !p.startsWith("!"));
715
+ const options = framework === "pytest"
716
+ ? typeof test.addopts === "string"
717
+ ? shellWords(test.addopts)
718
+ : strings(test.addopts)
719
+ : [];
720
+ return {
721
+ framework,
722
+ cwd: actualCwd,
723
+ config,
724
+ ...(project
725
+ ? { project }
726
+ : typeof test.name === "string"
727
+ ? { project: test.name }
728
+ : {}),
729
+ include,
730
+ exclude,
731
+ setup: strings(test.setupFiles ?? test.globalSetup),
732
+ options,
733
+ settings: test,
734
+ provenance: `config:${config}`,
735
+ incomplete: !complete,
736
+ };
737
+ }
738
+ /** Pure discovery over the caller's tracked-file snapshot; no I/O or configuration evaluation. */
739
+ export function discoverTests(files, plannedPaths, lexical, configurationPaths) {
740
+ const ordered = [...files].sort((a, b) => a.path.localeCompare(b.path));
741
+ const byPath = new Map(ordered.map((f) => [posix.normalize(f.path), f]));
742
+ const groups = [];
743
+ const invocations = [];
744
+ const typechecks = [];
745
+ const limits = [];
746
+ const addLimit = (path, kind, framework) => {
747
+ if (!limits.some((l) => l.path === path && l.kind === kind && l.framework === framework))
748
+ limits.push({ path, kind, ...(framework ? { framework } : {}) });
749
+ };
750
+ const visited = new Set();
751
+ const addConfig = (path, framework, options = [], project) => {
752
+ configurationPaths?.add(path);
753
+ const key = `${path}:${framework}:${project ?? ""}:${JSON.stringify(options)}`;
754
+ if (visited.has(key))
755
+ return;
756
+ visited.add(key);
757
+ const file = byPath.get(path);
758
+ if (!file) {
759
+ addLimit(path, "incomplete", framework);
760
+ return;
761
+ }
762
+ const parsed = framework === "pytest" || path.endsWith(".toml")
763
+ ? iniSettings(file.text, path)
764
+ : readConfig(file.text, lexical?.tokenize(file.path, file.text));
765
+ if (!parsed)
766
+ return;
767
+ const group = groupFrom(framework, directory(path), path, parsed.settings, parsed.complete, project);
768
+ if (framework === "deno") {
769
+ const task = record(parsed.settings.tasks).test;
770
+ if (typeof task === "string") {
771
+ const words = shellWords(task);
772
+ const flags = words.slice(2).filter((word) => word.startsWith("-"));
773
+ const safe = words[0] === "deno" &&
774
+ words[1] === "test" &&
775
+ flags.every((flag) => /^(?:-A|--allow-all|--allow-(?:read|write|net|env|run|sys|ffi)(?:=[\w@./,:*-]+)?|--no-check|--parallel|--fail-fast|--quiet)$/.test(flag)) &&
776
+ words
777
+ .slice(2)
778
+ .every((word) => flags.includes(word) || /^[\w./*-]+$/.test(word));
779
+ if (safe)
780
+ group.options.push(...flags);
781
+ else
782
+ group.incomplete = true;
783
+ }
784
+ }
785
+ group.options.push(...options);
786
+ const projects = parsed.settings.projects ?? record(parsed.settings.test).projects;
787
+ if (Array.isArray(projects)) {
788
+ for (const item of projects) {
789
+ if (typeof item === "string") {
790
+ const reference = posix.normalize(posix.join(directory(path), item));
791
+ const found = ordered.filter((f) => matches(f.path, reference) ||
792
+ (under(reference, f.path) &&
793
+ configFramework(f.path) === framework));
794
+ if (!found.length)
795
+ addLimit(reference, "incomplete", framework);
796
+ for (const child of found)
797
+ addConfig(child.path, framework, group.options);
798
+ }
799
+ else {
800
+ const childSettings = record(item);
801
+ const child = groupFrom(framework, group.cwd, path, {
802
+ ...parsed.settings,
803
+ ...childSettings,
804
+ ...(framework === "vitest"
805
+ ? {
806
+ test: {
807
+ ...record(parsed.settings.test),
808
+ ...record(childSettings.test),
809
+ },
810
+ }
811
+ : {}),
812
+ }, parsed.complete);
813
+ child.options.push(...options);
814
+ groups.push(child);
815
+ }
816
+ }
817
+ // A projects list defines separate execution contexts, not an extra root run.
818
+ if (group.incomplete)
819
+ addLimit(path, "incomplete", framework);
820
+ }
821
+ else
822
+ groups.push(group);
823
+ const typecheck = record(group.settings.typecheck);
824
+ if (strings(typecheck.include).length)
825
+ groups.push({
826
+ ...group,
827
+ include: strings(typecheck.include),
828
+ exclude: strings(typecheck.exclude),
829
+ types: true,
830
+ });
831
+ if (group.incomplete)
832
+ addLimit(path, "incomplete", framework);
833
+ };
834
+ // Configs must constrain implicit package scripts regardless of path ordering.
835
+ for (const file of ordered) {
836
+ const framework = configFramework(file.path);
837
+ if (framework)
838
+ addConfig(file.path, framework);
839
+ }
840
+ for (const file of ordered) {
841
+ const framework = configFramework(file.path);
842
+ if (framework)
843
+ addConfig(file.path, framework);
844
+ if (/^vitest\.workspace\.[cm]?[jt]s$/.test(posix.basename(file.path))) {
845
+ const tokens = lexical
846
+ ? lexical.tokenize(file.path, file.text)
847
+ : tokenize(file.text);
848
+ const index = tokens.findIndex((t, i) => t.value === "default" && tokens[i - 1]?.value === "export");
849
+ const parsed = literal(tokens, index + 1);
850
+ if (!parsed.complete)
851
+ addLimit(file.path, "incomplete", "vitest");
852
+ if (Array.isArray(parsed.value))
853
+ for (const item of parsed.value) {
854
+ if (typeof item === "string") {
855
+ const pattern = posix.join(directory(file.path), item);
856
+ const refs = ordered.filter((f) => matches(f.path, pattern) ||
857
+ (under(pattern, f.path) &&
858
+ configFramework(f.path) === "vitest"));
859
+ if (!refs.length)
860
+ addLimit(pattern, "incomplete", "vitest");
861
+ for (const ref of refs)
862
+ addConfig(ref.path, "vitest");
863
+ }
864
+ else
865
+ groups.push(groupFrom("vitest", directory(file.path), file.path, record(item), parsed.complete));
866
+ }
867
+ }
868
+ if (posix.basename(file.path) !== "package.json")
869
+ continue;
870
+ let manifest;
871
+ try {
872
+ manifest = record(JSON.parse(file.text));
873
+ }
874
+ catch {
875
+ addLimit(file.path, "incomplete");
876
+ continue;
877
+ }
878
+ for (const framework of ["jest", "ava", "mocha"])
879
+ if (manifest[framework])
880
+ groups.push(groupFrom(framework, directory(file.path), file.path, record(manifest[framework]), true));
881
+ const scripts = record(manifest.scripts);
882
+ const claimedScripts = new Set();
883
+ for (const [scriptName, script] of Object.entries(scripts).sort(([a], [b]) => Number(b === "test") - Number(a === "test"))) {
884
+ if (typeof script !== "string")
885
+ continue;
886
+ const allWords = shellWords(script);
887
+ const lastSeparator = allWords.lastIndexOf("&");
888
+ const tail = allWords.slice(lastSeparator + 1);
889
+ const avaLoader = tail[1] === "ava" &&
890
+ /^NODE_OPTIONS=--import=[\w@./-]+$/.test(tail[0] ?? "") &&
891
+ allWords
892
+ .slice(0, lastSeparator + 1)
893
+ .every((word) => ["xo", "tsc", "&"].includes(word));
894
+ const words = avaLoader ? tail : allWords;
895
+ const simple = !words.some((w) => [";", "&", "|", "<", ">"].includes(w) ||
896
+ w.includes("$") ||
897
+ w.includes("`"));
898
+ if (words[0] === "tsc" &&
899
+ simple &&
900
+ words.some((w) => w === "-p" || w === "--project" || w.startsWith("--project="))) {
901
+ typechecks.push({
902
+ cwd: directory(file.path),
903
+ invocation: {
904
+ executable: "tsc",
905
+ args: words.slice(1),
906
+ kind: "typecheck",
907
+ },
908
+ });
909
+ }
910
+ const commands = {
911
+ vitest: "vitest",
912
+ jest: "jest",
913
+ bun: "bun",
914
+ playwright: "playwright",
915
+ pytest: "pytest",
916
+ ava: "ava",
917
+ mocha: "mocha",
918
+ deno: "deno",
919
+ node: "node",
920
+ go: "go",
921
+ };
922
+ const index = words.findIndex((w, i) => commands[w] &&
923
+ (w !== "node" || words.slice(i + 1).includes("--test")) &&
924
+ (!["bun", "deno", "go"].includes(w) || words[i + 1] === "test"));
925
+ if (index < 0)
926
+ continue;
927
+ const framework = commands[words[index] ?? ""];
928
+ if (!framework)
929
+ continue;
930
+ const args = words.slice(index + 1);
931
+ const configIndex = args.findIndex((w) => ["--config", "-c"].includes(w));
932
+ const configFlag = args.find((w) => w.startsWith("--config="));
933
+ const reference = configIndex >= 0 ? args[configIndex + 1] : configFlag?.slice(9);
934
+ const options = [];
935
+ const includes = [];
936
+ let incomplete = words.some((w) => [";", "&", "|", "<", ">"].includes(w) ||
937
+ w.includes("$") ||
938
+ w.includes("`"));
939
+ if (["ava", "mocha", "deno"].includes(framework) &&
940
+ index > 0 &&
941
+ !avaLoader)
942
+ incomplete = true;
943
+ let scriptCwd = directory(file.path);
944
+ let unsafeOptions = false;
945
+ let project;
946
+ const interactive = args.some((arg) => [
947
+ "--watch",
948
+ "--watchAll",
949
+ "-w",
950
+ ...(framework === "mocha" ? [] : ["--ui"]),
951
+ ].includes(arg) ||
952
+ (framework === "mocha"
953
+ ? /^(?:--watch|--watchAll)=/
954
+ : /^(?:--watch|--watchAll|--ui)=/).test(arg));
955
+ for (let i = 0; i < args.length; i++) {
956
+ const arg = args[i] ?? "";
957
+ if (["run", "test", "--test"].includes(arg))
958
+ continue;
959
+ if ([
960
+ "--watch",
961
+ "--watchAll",
962
+ "-w",
963
+ ...(framework === "mocha" ? [] : ["--ui"]),
964
+ ].includes(arg) ||
965
+ (framework === "mocha"
966
+ ? /^(?:--watch|--watchAll)=/
967
+ : /^(?:--watch|--watchAll|--ui)=/).test(arg))
968
+ continue;
969
+ if (["--config", "-c"].includes(arg)) {
970
+ i++;
971
+ continue;
972
+ }
973
+ if (arg.startsWith("--config="))
974
+ continue;
975
+ if (["--project", "--selectProjects"].includes(arg)) {
976
+ project = args[++i];
977
+ continue;
978
+ }
979
+ if (arg.startsWith("--project=")) {
980
+ project = arg.slice(10);
981
+ continue;
982
+ }
983
+ if (arg === "--cwd" || arg.startsWith("--cwd=")) {
984
+ const cwd = arg === "--cwd" ? args[++i] : arg.slice(6);
985
+ if (cwd)
986
+ scriptCwd = posix.normalize(posix.join(scriptCwd, cwd));
987
+ else {
988
+ incomplete = true;
989
+ unsafeOptions = true;
990
+ }
991
+ continue;
992
+ }
993
+ if (framework === "deno" &&
994
+ /^(?:-A|--allow-all|--allow-(?:read|write|net|env|run|sys|ffi)(?:=.*)?)$/.test(arg)) {
995
+ options.push(arg);
996
+ continue;
997
+ }
998
+ if ([
999
+ "--reporter",
1000
+ "--environment",
1001
+ "--pool",
1002
+ "--timeout",
1003
+ "--testTimeout",
1004
+ "--loader",
1005
+ "--import",
1006
+ "--require",
1007
+ "--grep",
1008
+ "--fgrep",
1009
+ "--file",
1010
+ "--spec",
1011
+ "--ui",
1012
+ "--extension",
1013
+ "--maxWorkers",
1014
+ "--minWorkers",
1015
+ "-n",
1016
+ "-r",
1017
+ "-m",
1018
+ "-k",
1019
+ ].includes(arg)) {
1020
+ options.push(arg, args[++i] ?? "");
1021
+ continue;
1022
+ }
1023
+ if (arg.startsWith("-")) {
1024
+ if (!arg.includes("=") &&
1025
+ args[i + 1] &&
1026
+ !args[i + 1]?.startsWith("-") &&
1027
+ ![
1028
+ "--coverage",
1029
+ "--run",
1030
+ "--noEmit",
1031
+ "--passWithNoTests",
1032
+ "--bail",
1033
+ ].includes(arg)) {
1034
+ unsafeOptions = true;
1035
+ incomplete = true;
1036
+ i++;
1037
+ }
1038
+ else
1039
+ options.push(arg);
1040
+ }
1041
+ else if (/\.(?:[cm]?[jt]sx?|py)$/.test(arg) ||
1042
+ arg.includes("*") ||
1043
+ (framework === "deno" && arg.endsWith("/")) ||
1044
+ byPath.has(posix.join(directory(file.path), arg)))
1045
+ includes.push(arg);
1046
+ else
1047
+ incomplete = true;
1048
+ }
1049
+ if (unsafeOptions)
1050
+ options.length = 0;
1051
+ if (reference &&
1052
+ ["ava", "mocha", "deno"].includes(framework) &&
1053
+ !byPath.has(posix.normalize(posix.join(directory(file.path), reference))))
1054
+ incomplete = true;
1055
+ const context = JSON.stringify([
1056
+ framework,
1057
+ scriptCwd,
1058
+ reference ?? "",
1059
+ project ?? "",
1060
+ ]);
1061
+ if (claimedScripts.has(context))
1062
+ continue;
1063
+ claimedScripts.add(context);
1064
+ if (interactive)
1065
+ addLimit(file.path, "interactive_script_skipped", framework);
1066
+ if (reference)
1067
+ addConfig(posix.normalize(posix.join(directory(file.path), reference)), framework, options, project);
1068
+ if (reference && ["ava", "mocha", "deno"].includes(framework)) {
1069
+ const configPath = posix.normalize(posix.join(directory(file.path), reference));
1070
+ for (const group of groups.filter((group) => group.config === configPath && group.framework === framework)) {
1071
+ group.cwd = scriptCwd;
1072
+ if (!group.include.length || framework === "mocha")
1073
+ group.include.push(...includes);
1074
+ group.incomplete ||= incomplete;
1075
+ }
1076
+ }
1077
+ if (!reference) {
1078
+ const existing = groups.filter((g) => g.framework === framework && g.cwd === scriptCwd);
1079
+ if (!existing.length)
1080
+ groups.push({
1081
+ framework,
1082
+ cwd: scriptCwd,
1083
+ ...(project ? { project } : {}),
1084
+ include: includes,
1085
+ exclude: [],
1086
+ setup: [],
1087
+ options,
1088
+ settings: {},
1089
+ provenance: `script:${file.path}:${scriptName}`,
1090
+ incomplete,
1091
+ });
1092
+ else
1093
+ for (const group of existing) {
1094
+ group.options.push(...options);
1095
+ if (["ava", "mocha", "deno"].includes(framework) &&
1096
+ (!group.include.length || framework === "mocha"))
1097
+ group.include.push(...includes);
1098
+ if (["ava", "mocha", "deno"].includes(framework))
1099
+ group.incomplete ||= incomplete;
1100
+ }
1101
+ }
1102
+ if (simple && (index === 0 || avaLoader) && !incomplete && !interactive) {
1103
+ const prefix = framework === "node"
1104
+ ? ["--test"]
1105
+ : ["bun", "go", "deno", "playwright"].includes(framework)
1106
+ ? ["test"]
1107
+ : framework === "vitest" && args.includes("run")
1108
+ ? ["run"]
1109
+ : [];
1110
+ invocations.push({
1111
+ cwd: scriptCwd,
1112
+ framework,
1113
+ ...(reference
1114
+ ? {
1115
+ config: posix.normalize(posix.join(directory(file.path), reference)),
1116
+ }
1117
+ : {}),
1118
+ ...(project ? { project } : {}),
1119
+ invocation: {
1120
+ executable: index === 1 ? "env" : (words[index] ?? framework),
1121
+ args: [
1122
+ ...(index === 1
1123
+ ? [words[0] ?? "", words[index] ?? framework]
1124
+ : []),
1125
+ ...prefix,
1126
+ ...options,
1127
+ ],
1128
+ kind: "runner",
1129
+ },
1130
+ });
1131
+ }
1132
+ if (incomplete)
1133
+ addLimit(file.path, "incomplete", framework);
1134
+ }
1135
+ }
1136
+ const entries = [];
1137
+ for (const file of ordered) {
1138
+ const typeFile = /\.(?:test|spec)-d\.[cm]?tsx?$/.test(file.path);
1139
+ const candidate = typeFile ||
1140
+ ["vitest", "jest", "node", "bun", "ava", "go", "pytest"].some((framework) => native(file.path, framework)) ||
1141
+ groups.some((group) => {
1142
+ if (!under(group.cwd, file.path))
1143
+ return false;
1144
+ const local = relative(group.cwd, file.path);
1145
+ return group.include.some((pattern) => matches(local, pattern) ||
1146
+ (under(pattern, local) && !pattern.includes("*")));
1147
+ });
1148
+ if (!candidate)
1149
+ continue;
1150
+ if (plannedPaths) {
1151
+ plannedPaths.add(file.path);
1152
+ continue;
1153
+ }
1154
+ const python = file.path.endsWith(".py");
1155
+ const tokens = lexical
1156
+ ? lexical.tokenize(file.path, file.text, python)
1157
+ : tokenize(file.text, python);
1158
+ const bindings = imports(tokens);
1159
+ if (file.path.endsWith("_test.go")) {
1160
+ bindings.frameworks.add("go");
1161
+ if (/^\/\/\s*(?:go:build|\+build)\b/m.test(file.text) ||
1162
+ /_(?:linux|windows|darwin|amd64|arm64)_test\.go$/.test(file.path))
1163
+ addLimit(file.path, "incomplete", "go");
1164
+ }
1165
+ if (file.path.endsWith(".py") && native(file.path, "pytest"))
1166
+ bindings.frameworks.add("pytest");
1167
+ const applicable = groups.filter((g) => under(g.cwd, file.path));
1168
+ const matched = [];
1169
+ const blocked = new Set();
1170
+ for (const group of applicable) {
1171
+ const local = relative(group.cwd, file.path);
1172
+ if (group.exclude.some((p) => matches(local, p) || local.includes(p))) {
1173
+ blocked.add(group.framework);
1174
+ continue;
1175
+ }
1176
+ const explicit = group.include.length > 0;
1177
+ if (!explicit &&
1178
+ group.provenance.startsWith("script:") &&
1179
+ !bindings.frameworks.has(group.framework) &&
1180
+ !["pytest", "go"].includes(group.framework))
1181
+ continue;
1182
+ const included = explicit
1183
+ ? group.include.some((p) => matches(local, p) || (under(p, local) && !p.includes("*")))
1184
+ : native(local, group.framework) || typeFile;
1185
+ if (!included) {
1186
+ blocked.add(group.framework);
1187
+ continue;
1188
+ }
1189
+ if (!explicit && group.setup.some((p) => matches(local, p))) {
1190
+ blocked.add(group.framework);
1191
+ continue;
1192
+ }
1193
+ if (typeFile && !group.types)
1194
+ continue;
1195
+ matched.push(group);
1196
+ }
1197
+ // A closer package/config overrides an ancestor's default convention.
1198
+ const deepest = new Map();
1199
+ for (const group of applicable)
1200
+ deepest.set(group.framework, Math.max(deepest.get(group.framework) ?? 0, group.cwd.length));
1201
+ const candidates = matched.filter((g) => g.cwd.length === deepest.get(g.framework));
1202
+ for (const framework of bindings.frameworks) {
1203
+ if (blocked.has(framework) ||
1204
+ candidates.some((g) => g.framework === framework) ||
1205
+ applicable.some((g) => g.framework === framework))
1206
+ continue;
1207
+ if (!native(file.path, framework))
1208
+ continue;
1209
+ candidates.push({
1210
+ framework,
1211
+ cwd: framework === "go" ? directory(file.path) : ".",
1212
+ include: [],
1213
+ exclude: [],
1214
+ setup: [],
1215
+ options: [],
1216
+ settings: {},
1217
+ provenance: `declaration:${framework}`,
1218
+ incomplete: false,
1219
+ });
1220
+ }
1221
+ const frameworks = new Set([
1222
+ ...candidates.map((g) => g.framework),
1223
+ ...bindings.frameworks,
1224
+ ]);
1225
+ if (frameworks.size > 1 && candidates.length) {
1226
+ addLimit(file.path, "incomplete");
1227
+ continue;
1228
+ }
1229
+ if (typeFile && !candidates.length)
1230
+ candidates.push({
1231
+ framework: bindings.frameworks.values().next().value ?? "unknown",
1232
+ cwd: ".",
1233
+ include: [],
1234
+ exclude: [],
1235
+ setup: [],
1236
+ options: [],
1237
+ settings: {},
1238
+ provenance: "type convention",
1239
+ incomplete: false,
1240
+ types: true,
1241
+ });
1242
+ for (const group of candidates) {
1243
+ if (group.framework === "ava" &&
1244
+ !group.include.length &&
1245
+ /(?:^|\/)(?:_[^/]*|helpers|fixtures)(?:\/|$)/.test(file.path))
1246
+ continue;
1247
+ let extracted;
1248
+ const exclusiveMocha = group.framework === "mocha" &&
1249
+ tokens.some((token, index) => token.value === "only" &&
1250
+ tokens[index - 1]?.value === "." &&
1251
+ tokens[index + 1]?.value === "(");
1252
+ if (group.framework === "pytest")
1253
+ extracted = pythonScenarios(file.text, group.settings);
1254
+ else if (group.framework === "go") {
1255
+ const scenarios = [];
1256
+ for (let i = 0; i < tokens.length - 2; i++) {
1257
+ const name = tokens[i + 1]?.value ?? "";
1258
+ if (tokens[i]?.value === "func" &&
1259
+ tokens[i + 2]?.value === "(" &&
1260
+ /^(?:Test[^a-z]|Example(?:[^a-z]|$))/.test(name)) {
1261
+ // Go examples are runnable only with an Output/Unordered output directive.
1262
+ const end = closing(tokens, i + 2);
1263
+ const bodyStart = tokens.findIndex((t, j) => j > end && t.value === "{");
1264
+ const bodyEnd = bodyStart < 0 ? bodyStart : closing(tokens, bodyStart);
1265
+ const body = bodyStart < 0
1266
+ ? ""
1267
+ : file.text.slice(tokens[bodyStart]?.offset ?? 0, tokens[bodyEnd]?.offset ?? file.text.length);
1268
+ if (name.startsWith("Example") &&
1269
+ !/\/\/\s*(?:Unordered output|Output):/i.test(body))
1270
+ continue;
1271
+ const line = tokens[i]?.line ?? 1;
1272
+ scenarios.push({
1273
+ id: name,
1274
+ name,
1275
+ line,
1276
+ reliable: true,
1277
+ ...(tokens[bodyEnd]
1278
+ ? { range: { start: line, end: tokens[bodyEnd]?.line ?? line } }
1279
+ : {}),
1280
+ });
1281
+ }
1282
+ }
1283
+ extracted = { scenarios, countKnown: true };
1284
+ }
1285
+ else
1286
+ extracted = jsScenarios(tokens, group.framework, bindings);
1287
+ if (exclusiveMocha)
1288
+ extracted.countKnown = false;
1289
+ const explicit = group.include.length > 0;
1290
+ if (!explicit && !extracted.scenarios.length && !typeFile)
1291
+ continue;
1292
+ const kind = typeFile || group.types ? "types" : "runtime";
1293
+ const entry = {
1294
+ path: file.path,
1295
+ framework: group.framework,
1296
+ cwd: group.cwd,
1297
+ ...(group.config ? { config: group.config } : {}),
1298
+ ...(group.project ? { project: group.project } : {}),
1299
+ options: group.options,
1300
+ scenarios: extracted.scenarios,
1301
+ countKnown: extracted.countKnown &&
1302
+ !group.incomplete &&
1303
+ extracted.scenarios.length > 0,
1304
+ kind,
1305
+ provenance: group.provenance,
1306
+ ...(["ava", "deno"].includes(group.framework) && group.incomplete
1307
+ ? {
1308
+ commandRefusal: "runner configuration or options are not statically resolved",
1309
+ }
1310
+ : {}),
1311
+ ...(group.framework === "mocha"
1312
+ ? {
1313
+ ...(applicable.some((candidate) => candidate.framework === "mocha" &&
1314
+ (candidate.settings.spec !== undefined ||
1315
+ candidate.settings.file !== undefined ||
1316
+ candidate.options.some((option) => /^(?:--spec|--file)(?:=|$)/.test(option))))
1317
+ ? { commandRefusal: "Mocha additive spec is not neutralized" }
1318
+ : applicable.some((candidate) => candidate.framework === "mocha" &&
1319
+ (candidate.incomplete ||
1320
+ candidate.settings.extends !== undefined))
1321
+ ? {
1322
+ commandRefusal: "Mocha configuration is not statically resolved",
1323
+ }
1324
+ : {}),
1325
+ ...(exclusiveMocha
1326
+ ? {
1327
+ nameFilterReason: "Mocha .only declarations restrict project execution",
1328
+ }
1329
+ : {}),
1330
+ nameFilterBlocked: applicable.some((candidate) => candidate.framework === "mocha" &&
1331
+ (["grep", "fgrep", "invert"].some((key) => candidate.settings[key] !== undefined) ||
1332
+ (candidate.settings.ui !== undefined &&
1333
+ candidate.settings.ui !== "bdd"))) ||
1334
+ group.options.some((option, index) => /^(?:--grep|--fgrep|--invert|-g|-f|-i)(?:=|$)/.test(option) ||
1335
+ (option === "--ui" && group.options[index + 1] !== "bdd") ||
1336
+ (option.startsWith("--ui=") && option !== "--ui=bdd")),
1337
+ }
1338
+ : {}),
1339
+ };
1340
+ const proven = invocations.find((i) => i.framework === group.framework &&
1341
+ under(i.cwd, file.path) &&
1342
+ (!i.config || i.config === group.config) &&
1343
+ (!i.project || i.project === group.project));
1344
+ if (kind === "types") {
1345
+ const typecheck = typechecks
1346
+ .filter((t) => under(t.cwd, file.path))
1347
+ .sort((a, b) => b.cwd.length - a.cwd.length)[0];
1348
+ if (typecheck) {
1349
+ entry.invocation = typecheck.invocation;
1350
+ entry.cwd = typecheck.cwd;
1351
+ }
1352
+ }
1353
+ else if (proven)
1354
+ entry.invocation = proven.invocation;
1355
+ else if (!["ava", "mocha", "deno", "unknown"].includes(group.framework))
1356
+ addLimit(file.path, "local_runner_unproven", group.framework);
1357
+ if (!entries.some((e) => e.path === entry.path &&
1358
+ e.framework === entry.framework &&
1359
+ e.cwd === entry.cwd &&
1360
+ e.config === entry.config &&
1361
+ e.project === entry.project &&
1362
+ e.kind === entry.kind &&
1363
+ JSON.stringify(e.options) === JSON.stringify(entry.options)))
1364
+ entries.push(entry);
1365
+ if (["ava", "mocha", "deno"].includes(group.framework) &&
1366
+ group.incomplete)
1367
+ addLimit(file.path, "incomplete", group.framework);
1368
+ if (kind === "types")
1369
+ addLimit(file.path, "types", group.framework);
1370
+ }
1371
+ }
1372
+ const evidence = ordered
1373
+ .filter((f) => isTestFile(f.path) || entries.some((e) => e.path === f.path))
1374
+ .map((f) => f.path);
1375
+ for (const path of evidence)
1376
+ if (!entries.some((e) => e.path === path)) {
1377
+ addLimit(path, "incomplete");
1378
+ if (!limits.some((limit) => limit.path === path && limit.framework))
1379
+ addLimit(path, "unknown", "unknown");
1380
+ }
1381
+ return { entries, evidence, limits };
1382
+ }