jules-orchestrator-kit 0.63.0 → 0.65.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.
@@ -0,0 +1,512 @@
1
+ /**
2
+ * The policy this kit claims to enforce, written by hand.
3
+ *
4
+ * Every entry here is derived from what the tool *advertises* — the stacks
5
+ * `detectStack` declares, the layouts each ecosystem actually uses — and never
6
+ * from the regexes, path lists or registries that implement the checks. That
7
+ * separation is the whole point: a contract generated from the implementation
8
+ * makes the implementation its own oracle, and an implementation that is its
9
+ * own oracle cannot be wrong.
10
+ *
11
+ * This is what the substring bug in the file classifier cost. `isTestFile`
12
+ * matched `/test/`, which does not occur in `tests/test_calc.py`, so the entire
13
+ * tamper guard was off for the standard pytest, Rust and RSpec layouts — and
14
+ * every mechanism that should have caught it (a large suite, a doc-sync gate,
15
+ * a nine-way CI matrix, two cold reviews, a blocking release) was sampling the
16
+ * same distribution the implementation was written from. Nine runs of
17
+ * `test/foo.test.js` do not explore `tests/test_calc.py`.
18
+ *
19
+ * Adding a stack to `detectStack` is not finished until it has a row here.
20
+ */
21
+
22
+ /**
23
+ * Paths the policy says are test files, and near-misses it says are not.
24
+ *
25
+ * The near-misses matter as much as the hits: a predicate that answers "yes"
26
+ * to everything also has no denominator.
27
+ */
28
+ export const TEST_PATH_CASES = [
29
+ // Node / JavaScript
30
+ { path: "test/calc.test.js", expected: true, why: "node, conventional" },
31
+ { path: "src/calc.spec.ts", expected: true, why: "co-located spec" },
32
+ { path: "src/__tests__/calc.js", expected: true, why: "jest convention" },
33
+ // Python
34
+ { path: "tests/test_calc.py", expected: true, why: "pytest, repository root — the reported gap" },
35
+ { path: "test/test_calc.py", expected: true, why: "pytest, singular directory" },
36
+ { path: "backend/tests/test_api.py", expected: true, why: "pytest, nested" },
37
+ // Go
38
+ { path: "internal/calc/calc_test.go", expected: true, why: "go, co-located with the code" },
39
+ { path: "cmd/api/main_test.go", expected: true, why: "go, command package" },
40
+ // Rust
41
+ { path: "tests/integration.rs", expected: true, why: "rust integration tests" },
42
+ // Ruby
43
+ { path: "spec/models/user_spec.rb", expected: true, why: "rspec" },
44
+ { path: "test/user_test.rb", expected: true, why: "minitest" },
45
+ // JVM / .NET / PHP
46
+ { path: "src/test/java/com/x/CalcTest.java", expected: true, why: "maven layout" },
47
+ { path: "tests/Unit/CalcTest.php", expected: true, why: "phpunit" },
48
+ // Solidity
49
+ { path: "test/Token.t.sol", expected: true, why: "foundry" },
50
+ // Monorepo position
51
+ { path: "packages/api/test/handler.test.js", expected: true, why: "monorepo package" },
52
+
53
+ // Near-misses: segment match, not substring
54
+ { path: "latest/build.js", expected: false, why: "`latest` is not `test`" },
55
+ { path: "contest/result.js", expected: false, why: "`contest` is not `test`" },
56
+ { path: "lib/myspec/render.js", expected: false, why: "`myspec` is not `spec`" },
57
+ { path: "src/attestation.js", expected: false, why: "contains 'test', is not one" },
58
+ { path: "src/index.js", expected: false, why: "ordinary source" },
59
+ { path: "docs/testing-guide.md", expected: false, why: "documentation about testing" },
60
+ ];
61
+
62
+ /**
63
+ * Known-bad diffs, one per guard rule, each with the finding it MUST produce.
64
+ *
65
+ * A canary that comes back clean is not a passing test — it is proof that the
66
+ * rule it names has stopped being reachable. `layout` is deliberately varied
67
+ * across ecosystems so that a canary set cannot be green by only ever handing
68
+ * the guard the layout the implementation was written from.
69
+ */
70
+ export const TAMPER_CANARIES = [
71
+ {
72
+ id: "skip-injection/node",
73
+ file: "test/calc.test.js",
74
+ removed: [],
75
+ added: ['it.skip("adds", () => { assert.equal(add(1,2), 3); });'],
76
+ expect: "TEST_SKIP_INJECTION",
77
+ },
78
+ {
79
+ id: "vacuous/node",
80
+ file: "test/calc.test.js",
81
+ removed: ["assert.equal(add(1, 2), 3);"],
82
+ added: ["assert.ok(true);"],
83
+ expect: "VACUOUS_ASSERTION",
84
+ },
85
+ {
86
+ id: "commented/node",
87
+ file: "test/calc.test.js",
88
+ removed: ["assert.equal(add(1, 2), 3);"],
89
+ added: ["// assert.equal(add(1, 2), 3);"],
90
+ expect: "COMMENTED_ASSERTION",
91
+ },
92
+ {
93
+ id: "removal/node",
94
+ file: "test/calc.test.js",
95
+ removed: ["assert.equal(add(1, 2), 3);", "assert.equal(add(2, 2), 4);"],
96
+ added: [],
97
+ expect: "ASSERTION_REMOVAL",
98
+ },
99
+ {
100
+ id: "weakening/node",
101
+ file: "test/calc.test.js",
102
+ removed: ["assert.strictEqual(add(1, 2), 3);"],
103
+ added: ["assert.ok(add(1, 2) !== undefined);"],
104
+ expect: "ASSERTION_WEAKENED",
105
+ },
106
+ {
107
+ id: "expectation/node",
108
+ file: "test/calc.test.js",
109
+ removed: ["assert.equal(add(1, 2), 3);"],
110
+ added: ["assert.equal(add(1, 2), -1);"],
111
+ expect: "ASSERTION_EXPECTATION_CHANGED",
112
+ },
113
+ // The same attacks, in the layouts the classifier used to be blind to.
114
+ // These are the canaries that were silent when the classifier matched a
115
+ // substring instead of a path segment.
116
+ {
117
+ id: "expectation/pytest-root",
118
+ file: "tests/test_calc.py",
119
+ removed: [" self.assertEqual(add(1, 2), 3)"],
120
+ added: [" self.assertEqual(add(1, 2), -1)"],
121
+ expect: "ASSERTION_EXPECTATION_CHANGED",
122
+ },
123
+ {
124
+ id: "skip-injection/pytest-root",
125
+ file: "tests/test_calc.py",
126
+ removed: [],
127
+ added: ["@pytest.mark.skip", "def test_add():"],
128
+ expect: "TEST_SKIP_INJECTION",
129
+ },
130
+ {
131
+ id: "expectation/rust",
132
+ file: "tests/integration.rs",
133
+ removed: [" assert_eq!(add(1, 2), 3);"],
134
+ added: [" assert_eq!(add(1, 2), -1);"],
135
+ expect: "ASSERTION_EXPECTATION_CHANGED",
136
+ },
137
+ {
138
+ id: "expectation/go-colocated",
139
+ file: "internal/calc/calc_test.go",
140
+ removed: ['\t\tt.Errorf("got %d want %d", got, 3)'],
141
+ added: ['\t\tt.Errorf("got %d want %d", got, 999)'],
142
+ expect: "ASSERTION_EXPECTATION_CHANGED",
143
+ },
144
+ {
145
+ id: "removal/monorepo",
146
+ file: "packages/api/test/handler.test.js",
147
+ removed: ["expect(handler(req)).toBe(200);", "expect(handler(bad)).toBe(400);"],
148
+ added: [],
149
+ expect: "ASSERTION_REMOVAL",
150
+ },
151
+ // Dialects the guard was measured silent on. `assertEqual` matched only
152
+ // because the pattern's optional dot and case-insensitive flag happened to
153
+ // line up; `assertEquals` — one letter longer — did not, and neither did
154
+ // RSpec's `.to eq(`, PHPUnit's `$this->assertSame`, Minitest's
155
+ // `assert_equal` or XCTest's `XCTAssertEqual`. Every one of them returned
156
+ // PASS with a non-zero denominator, which is the exact shape this file
157
+ // exists to reject, one level down inside the mechanism built to catch it.
158
+ {
159
+ id: "expectation/junit",
160
+ file: "src/test/java/com/x/CalcTest.java",
161
+ removed: [" assertEquals(3, calc.add(1, 2));"],
162
+ added: [" assertEquals(-1, calc.add(1, 2));"],
163
+ expect: "ASSERTION_EXPECTATION_CHANGED",
164
+ },
165
+ {
166
+ id: "expectation/rspec",
167
+ file: "spec/models/user_spec.rb",
168
+ removed: [" expect(user.age).to eq(30)"],
169
+ added: [" expect(user.age).to eq(-1)"],
170
+ expect: "ASSERTION_EXPECTATION_CHANGED",
171
+ },
172
+ {
173
+ id: "expectation/phpunit",
174
+ file: "tests/Unit/CalcTest.php",
175
+ removed: [" $this->assertSame(3, $c->add(1, 2));"],
176
+ added: [" $this->assertSame(-1, $c->add(1, 2));"],
177
+ expect: "ASSERTION_EXPECTATION_CHANGED",
178
+ },
179
+ {
180
+ id: "expectation/minitest",
181
+ file: "test/user_test.rb",
182
+ removed: [" assert_equal(3, add(1, 2))"],
183
+ added: [" assert_equal(-1, add(1, 2))"],
184
+ expect: "ASSERTION_EXPECTATION_CHANGED",
185
+ },
186
+ {
187
+ id: "expectation/xctest",
188
+ file: "Tests/CalcTests/CalcTests.swift",
189
+ removed: [" XCTAssertEqual(add(1, 2), 3)"],
190
+ added: [" XCTAssertEqual(add(1, 2), -1)"],
191
+ expect: "ASSERTION_EXPECTATION_CHANGED",
192
+ },
193
+ {
194
+ id: "weakening/junit",
195
+ file: "src/test/java/com/x/CalcTest.java",
196
+ removed: [" assertEquals(3, calc.add(1, 2));"],
197
+ added: [" assertTrue(calc.add(1, 2) != null);"],
198
+ expect: "ASSERTION_WEAKENED",
199
+ },
200
+ {
201
+ id: "removal/phpunit",
202
+ file: "tests/Unit/CalcTest.php",
203
+ removed: [" $this->assertSame(3, $c->add(1, 2));", " $this->assertSame(4, $c->add(2, 2));"],
204
+ added: [],
205
+ expect: "ASSERTION_REMOVAL",
206
+ },
207
+ {
208
+ id: "vacuous/xctest",
209
+ file: "Tests/CalcTests/CalcTests.swift",
210
+ removed: [" XCTAssertEqual(add(1, 2), 3)"],
211
+ added: [" XCTAssertTrue(true)"],
212
+ expect: "VACUOUS_ASSERTION",
213
+ },
214
+ // Skip injection is the same blindness in the other five ecosystems: a
215
+ // suite that never runs cannot fail, and `@Disabled` is as effective as
216
+ // `it.skip` at making that happen.
217
+ {
218
+ id: "skip-injection/junit",
219
+ file: "src/test/java/com/x/CalcTest.java",
220
+ removed: [],
221
+ added: [" @Disabled(\"flaky\")", " void addsTwoNumbers() {"],
222
+ expect: "TEST_SKIP_INJECTION",
223
+ },
224
+ {
225
+ id: "skip-injection/rspec",
226
+ file: "spec/models/user_spec.rb",
227
+ removed: [],
228
+ added: [" xit \"computes the age\" do"],
229
+ expect: "TEST_SKIP_INJECTION",
230
+ },
231
+ {
232
+ id: "skip-injection/phpunit",
233
+ file: "tests/Unit/CalcTest.php",
234
+ removed: [],
235
+ added: [" $this->markTestSkipped(\"later\");"],
236
+ expect: "TEST_SKIP_INJECTION",
237
+ },
238
+ {
239
+ id: "skip-injection/xctest",
240
+ file: "Tests/CalcTests/CalcTests.swift",
241
+ removed: [],
242
+ added: [" throw XCTSkip(\"not now\")"],
243
+ expect: "TEST_SKIP_INJECTION",
244
+ },
245
+ ];
246
+
247
+
248
+ /**
249
+ * Mutants of the applicability predicate.
250
+ *
251
+ * Each one must kill at least one canary. A mutant that survives means no
252
+ * canary ever required the guard to *activate* — the suite would stay green if
253
+ * the guard silently stopped looking, which is precisely the defect.
254
+ *
255
+ * These are hand-written rather than generated: the original bug was not an
256
+ * untested branch, it was a branch nobody wrote, and no mutation operator
257
+ * invents the case the code never handled.
258
+ */
259
+ export const PREDICATE_MUTANTS = [
260
+ { id: "alwaysFalse", fn: () => false, why: "the guard looks at nothing" },
261
+ { id: "rootBlind", fn: (p) => String(p).includes("/test/"), why: "the original substring bug" },
262
+ { id: "nodeOnly", fn: (p) => /\.(test|spec)\.[jt]sx?$/.test(String(p)), why: "only the layout the code was written from" },
263
+ { id: "caseSensitive", fn: (p) => String(p) === String(p).toLowerCase() && /(^|\/)tests?\//.test(String(p)), why: "case and separator drift" },
264
+ ];
265
+
266
+ /** Runner outputs that state zero collected tests, per ecosystem. */
267
+ export const EMPTY_RUN_CANARIES = [
268
+ { id: "pytest", output: "collected 0 items\n\nno tests ran in 0.01s" },
269
+ { id: "jest", output: "No tests found, exiting with code 0" },
270
+ { id: "vitest", output: "No test files found, exiting with code 0" },
271
+ { id: "cargo", output: "running 0 tests\ntest result: ok. 0 passed" },
272
+ { id: "mocha", output: " 0 passing (1ms)" },
273
+ { id: "go", output: "? example.com/app\t[no test files]" },
274
+ { id: "surefire", output: "Tests run: 0, Failures: 0, Errors: 0, Skipped: 0" },
275
+ { id: "gradle", output: "> Task :test NO-SOURCE" },
276
+ { id: "phpunit", output: "No tests executed!" },
277
+ { id: "rspec", output: "0 examples, 0 failures" },
278
+ { id: "dotnet", output: "Total tests: 0" },
279
+ { id: "xctest", output: "Executed 0 tests" },
280
+ { id: "ctest", output: "No tests were found!!!" },
281
+ ];
282
+
283
+ /** Paths the policy says an agent must never modify without an override. */
284
+ export const SCOPE_CANARIES = [
285
+ { path: ".github/workflows/ci.yml", rule: "deny", why: "runs with repo credentials" },
286
+ { path: ".gitlab-ci.yml", rule: "deny", why: "same, another forge" },
287
+ { path: "Jenkinsfile", rule: "deny", why: "same, another forge" },
288
+ { path: ".envrc", rule: "deny", why: "direnv executes it on cd" },
289
+ { path: "package-lock.json", rule: "protect", why: "decides which code installs" },
290
+ { path: "Cargo.lock", rule: "protect", why: "same, another ecosystem" },
291
+ { path: "conftest.py", rule: "protect", why: "runs before every pytest collection" },
292
+ { path: "jest.config.js", rule: "protect", why: "decides which tests run" },
293
+ { path: "CODEOWNERS", rule: "protect", why: "decides who must approve" },
294
+ ];
295
+
296
+ /**
297
+ * Edits that must produce no finding at all.
298
+ *
299
+ * A guard that answers "yes" to everything has no more discrimination than
300
+ * one that answers "no" to everything, and it is worse in practice: the
301
+ * operator learns to pass the override without reading it, and the day it
302
+ * reports something real, nobody looks. Every entry here is an edit an
303
+ * honest agent makes constantly.
304
+ *
305
+ * The first two are not hypothetical. `//` begins with a division sign, so a
306
+ * comment line matched the operator-continuation test and folded itself into
307
+ * the assertion above it — which meant *adding* an assertion next to a
308
+ * comment was reported as rewriting an expectation. Python was immune,
309
+ * because `#` is not an operator, so the fixtures this project was written
310
+ * from never showed it.
311
+ */
312
+ export const INNOCENT_EDITS = [
313
+ {
314
+ id: "add-assertion-beside-comment/node",
315
+ file: "test/calc.test.js",
316
+ context: "// arithmetic",
317
+ removed: [" assert.equal(add(1, 2), 3);"],
318
+ added: [" assert.equal(add(1, 2), 3);", " assert.equal(add(2, 2), 4);"],
319
+ why: "adding a test is the behaviour the gate exists to encourage",
320
+ },
321
+ {
322
+ id: "add-assertion-beside-comment/rust",
323
+ file: "tests/integration.rs",
324
+ context: "// arithmetic",
325
+ removed: [" assert!(add(1, 2) == 3);"],
326
+ added: [" assert!(add(1, 2) == 3);", " assert!(add(2, 2) == 4);"],
327
+ why: "same edit, same comment syntax, another ecosystem",
328
+ },
329
+ {
330
+ id: "reorder-assertions/pytest",
331
+ file: "tests/test_calc.py",
332
+ context: "# arithmetic",
333
+ removed: [" assert a() == 1", " assert b() == 2"],
334
+ added: [" assert b() == 2", " assert a() == 1"],
335
+ why: "moving an assertion changes nothing it checks",
336
+ },
337
+ {
338
+ id: "reindent/pytest",
339
+ file: "tests/test_calc.py",
340
+ context: "# arithmetic",
341
+ removed: [" assert add(1, 2) == 3"],
342
+ added: [" assert add(1, 2) == 3"],
343
+ why: "a formatter run must not read as tampering",
344
+ },
345
+ {
346
+ id: "reword-message/rspec",
347
+ file: "spec/models/user_spec.rb",
348
+ context: "# age",
349
+ removed: [' expect(u.age).to eq(30), "wrong"'],
350
+ added: [' expect(u.age).to eq(30), "unexpected age"'],
351
+ why: "RSpec writes the message outside the call, where an argument check cannot see it",
352
+ },
353
+ {
354
+ id: "reword-message/minitest",
355
+ file: "test/user_test.rb",
356
+ context: "# age",
357
+ removed: [' assert_equal 3, add(1, 2), "wrong"'],
358
+ added: [' assert_equal 3, add(1, 2), "unexpected sum"'],
359
+ why: "same, with the message in argument position and no parentheses",
360
+ },
361
+ {
362
+ id: "reword-message/node",
363
+ file: "test/calc.test.js",
364
+ context: "// arithmetic",
365
+ removed: [' assert.equal(add(1, 2), 3, "wrong");'],
366
+ added: [' assert.equal(add(1, 2), 3, "unexpected sum");'],
367
+ why: "rewording a failure message says nothing about what is checked",
368
+ },
369
+ {
370
+ id: "rename-test/junit",
371
+ file: "src/test/java/com/x/CalcTest.java",
372
+ context: "// arithmetic",
373
+ removed: [" void addsNumbers() {"],
374
+ added: [" void addsTwoNumbers() {"],
375
+ why: "a test name is not an expectation",
376
+ },
377
+ {
378
+ id: "change-import/node",
379
+ file: "test/calc.test.js",
380
+ context: "// setup",
381
+ removed: ['const calc = require("./calc");'],
382
+ added: ['const calc = require("../src/calc");'],
383
+ why: "`require(` is an import, not a claim — the loose net must not read it as one",
384
+ },
385
+ {
386
+ id: "rename-helper/node",
387
+ file: "test/calc.test.js",
388
+ context: "// setup",
389
+ removed: [" const shouldRetry = false;"],
390
+ added: [" const shouldRetry = true;"],
391
+ why: "`shouldRetry` and `expected` are identifiers; reading them as assertions makes the dialect warning worthless",
392
+ },
393
+ ];
394
+
395
+ /**
396
+ * Dialects the guard genuinely cannot parse, which it must say out loud.
397
+ *
398
+ * This is the case the whole denominator exists for. A JUnit diff used to
399
+ * return `PASS` with `inputsSeen: 1` while not one assertion in it had been
400
+ * recognised — a verdict indistinguishable from a clean Node suite. Coverage
401
+ * will always end somewhere; what must never happen again is that the edge
402
+ * is silent.
403
+ */
404
+ export const UNREADABLE_DIALECTS = [
405
+ {
406
+ id: "hspec",
407
+ file: "tests/CalcSpec.hs",
408
+ context: "-- arithmetic",
409
+ removed: [" calc `shouldBe` 3"],
410
+ added: [" calc `shouldBe` (-1)"],
411
+ why: "an infix assertion with no parentheses anywhere near it",
412
+ },
413
+ {
414
+ id: "googletest",
415
+ file: "tests/calc_test.cc",
416
+ context: "// arithmetic",
417
+ removed: [" EXPECT_EQ(add(1, 2), 3);"],
418
+ added: [" EXPECT_EQ(add(1, 2), -1);"],
419
+ why: "a macro dialect the pattern list does not cover",
420
+ },
421
+ ];
422
+
423
+ /**
424
+ * Import forms the package-integrity extractor must find.
425
+ *
426
+ * The first pass of that check reported "every relative import resolves" on
427
+ * a package whose newest script could not start: its matcher was written as
428
+ * `[^;\n]*?from`, and the import it needed to see spanned several lines. The
429
+ * check was confidently green about a file that threw ERR_MODULE_NOT_FOUND
430
+ * on load — the same failure the tool exists to prevent, committed by the
431
+ * tool's own integrity check.
432
+ *
433
+ * Every entry is a source fragment and the specifiers it must yield.
434
+ */
435
+ export const IMPORT_EXTRACTION_CASES = [
436
+ { id: "single-line named", src: 'import { a, b } from "./one.mjs";', expect: ["./one.mjs"] },
437
+ {
438
+ id: "multi-line named",
439
+ src: 'import {\n a,\n b,\n} from "./two.mjs";',
440
+ expect: ["./two.mjs"],
441
+ why: "the form the first version of the check could not see",
442
+ },
443
+ { id: "default", src: 'import three from "./three.mjs";', expect: ["./three.mjs"] },
444
+ { id: "namespace", src: 'import * as four from "./four.mjs";', expect: ["./four.mjs"] },
445
+ { id: "side-effect only", src: 'import "./five.mjs";', expect: ["./five.mjs"] },
446
+ { id: "re-export", src: 'export { six } from "./six.mjs";', expect: ["./six.mjs"] },
447
+ { id: "re-export all", src: 'export * from "./seven.mjs";', expect: ["./seven.mjs"] },
448
+ { id: "dynamic", src: 'const m = await import("./eight.mjs");', expect: ["./eight.mjs"] },
449
+ { id: "require", src: 'const nine = require("./nine.js");', expect: ["./nine.js"] },
450
+ { id: "single quotes", src: "import ten from './ten.mjs';", expect: ["./ten.mjs"] },
451
+ {
452
+ id: "bare specifiers are not files",
453
+ src: 'import { readFileSync } from "node:fs";\nimport x from "some-package";',
454
+ expect: ["node:fs", "some-package"],
455
+ why: "found, then ignored by the resolver — never resolved against the tarball",
456
+ },
457
+ ];
458
+
459
+ // Cases that exercise the mask rather than the matcher. Appended separately
460
+ // because each one is a fixture *about* fixtures: the check has to tell a
461
+ // module reference from a picture of one.
462
+ IMPORT_EXTRACTION_CASES.push(
463
+ {
464
+ id: "regex holding a quote, then a real import",
465
+ src: 'const q = /["\']/;\nimport x from "./after-regex.mjs";',
466
+ expect: ["./after-regex.mjs"],
467
+ why: "the phantom string this file's own subject matter opens",
468
+ },
469
+ {
470
+ id: "an import quoted inside a string",
471
+ src: 'const example = \'import a from "./not-real.mjs";\';',
472
+ expect: [],
473
+ why: "a picture of an import is not an import",
474
+ },
475
+ {
476
+ id: "an import written in a comment",
477
+ src: '// import a from "./commented.mjs";\nconst x = 1;',
478
+ expect: [],
479
+ why: "same, in the other place examples live",
480
+ }
481
+ );
482
+
483
+ /**
484
+ * Runs that stated a count, which must never be read as empty.
485
+ *
486
+ * The floor was written to be one-sided — only a *stated* zero fails — and
487
+ * then a phrase was allowed to outrank a statement. A healthy 190-test TAP
488
+ * suite whose one skipped fixture printed `# SKIP no tests found` was
489
+ * rejected as empty and attributed to Jest, in a repository that does not
490
+ * use Jest. A false red on a correct repository is how a user learns the
491
+ * gate is broken and turns it off.
492
+ */
493
+ export const COUNTED_RUN_CANARIES = [
494
+ {
495
+ id: "tap with a skip message",
496
+ output: "TAP version 13\n# Subtest: performance\n # SKIP no tests found\nok 1 - performance # SKIP\n1..191\n# tests 191\n# pass 190\n# skip 1",
497
+ atLeast: 1,
498
+ why: "`no tests found` inside a skip comment is not a statement about the run",
499
+ },
500
+ {
501
+ id: "pytest mentioning an empty module",
502
+ output: "collected 12 items\n\ntests/test_a.py ............\n\n12 passed in 0.3s",
503
+ atLeast: 1,
504
+ why: "a stated count is present and must win",
505
+ },
506
+ {
507
+ id: "go, verbose, two tests",
508
+ output: "--- PASS: TestAdd (0.00s)\n--- PASS: TestSub (0.00s)\nok \texample.com/lib\t0.004s",
509
+ atLeast: 1,
510
+ why: "Go's own `ok <package>` line, which a bare `^ok\\s` confused with TAP's `ok 1 - name`",
511
+ },
512
+ ];
@@ -71,8 +71,15 @@ const EXPLICIT_ZERO = [
71
71
 
72
72
  /** Go prints this per package that has no test files at all. */
73
73
  const GO_NO_TEST_FILES = /\[no test files\]/;
74
- /** Any sign that a Go package did run tests. */
75
- const GO_RAN_SOMETHING = /^(?:ok|FAIL|---\s+(?:PASS|FAIL|SKIP)):?\s/m;
74
+ /**
75
+ * Any sign that a Go package did run tests.
76
+ *
77
+ * The negative lookahead is what separates Go from TAP. `ok 1 - performance`
78
+ * is a TAP result line and `ok example.com/lib 0.004s` is a Go package
79
+ * summary, and a bare `^ok\s` matched both — so a 190-test TAP suite was
80
+ * classified as Go and reported as having stated no count at all.
81
+ */
82
+ const GO_RAN_SOMETHING = /^(?:(?:ok|FAIL)\s+(?!\d+\s)\S+|---\s+(?:PASS|FAIL|SKIP):?\s)/m;
76
83
 
77
84
  /**
78
85
  * Read a collected-test count out of a runner's output.
@@ -87,8 +94,20 @@ export function parseCollectedTests(stdout = "", stderr = "") {
87
94
  const text = `${stdout || ""}\n${stderr || ""}`;
88
95
  if (!text.trim()) return { count: null, runner: null };
89
96
 
90
- for (const rule of EXPLICIT_ZERO) {
91
- if (rule.re.test(text)) return { count: 0, runner: rule.name };
97
+ // A stated count wins over a phrase that merely resembles one.
98
+ //
99
+ // `EXPLICIT_ZERO` used to be consulted first, so any output containing the
100
+ // words "no tests found" was read as a zero — including a healthy TAP run
101
+ // of 190 passing tests whose one skipped fixture printed
102
+ // `# SKIP no tests found`. The gate rejected the suite as empty and named
103
+ // Jest as the runner, in a repository that does not use Jest. A phrase
104
+ // appears anywhere in a stream; a count is stated deliberately, so the
105
+ // count is the better witness and has to be asked first.
106
+ for (const rule of COUNT_PATTERNS) {
107
+ const m = rule.re.exec(text);
108
+ if (!m) continue;
109
+ const n = Number(m[1]);
110
+ if (Number.isFinite(n)) return { count: n, runner: rule.name };
92
111
  }
93
112
 
94
113
  // Go states absence per package rather than as a count, so it needs its own
@@ -104,11 +123,9 @@ export function parseCollectedTests(stdout = "", stderr = "") {
104
123
  return { count: perTest && perTest.length > 0 ? perTest.length : null, runner: "go" };
105
124
  }
106
125
 
107
- for (const rule of COUNT_PATTERNS) {
108
- const m = rule.re.exec(text);
109
- if (!m) continue;
110
- const n = Number(m[1]);
111
- if (Number.isFinite(n)) return { count: n, runner: rule.name };
126
+ // Only now: no runner stated a number, so a declared absence is all there is.
127
+ for (const rule of EXPLICIT_ZERO) {
128
+ if (rule.re.test(text)) return { count: 0, runner: rule.name };
112
129
  }
113
130
 
114
131
  return { count: null, runner: null };