jules-orchestrator-kit 0.70.0 → 0.72.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.
@@ -49,6 +49,13 @@ export const TEST_PATH_CASES = [
49
49
  { path: "test/Token.t.sol", expected: true, why: "foundry" },
50
50
  // Monorepo position
51
51
  { path: "packages/api/test/handler.test.js", expected: true, why: "monorepo package" },
52
+ // The canonical root test file for the supported Node runners. AVA runs
53
+ // root `test.js` with no configuration; `node --test test.js` does too.
54
+ // P-Limit's whole suite lived in one and the guard watched none of it.
55
+ { path: "test.js", expected: true, why: "node root canonical — F01's blind spot" },
56
+ { path: "test.mjs", expected: true, why: "node root, ESM" },
57
+ { path: "test.ts", expected: true, why: "node root, TypeScript" },
58
+ { path: "test.cjs", expected: true, why: "node root, CommonJS" },
52
59
 
53
60
  // Near-misses: segment match, not substring
54
61
  { path: "latest/build.js", expected: false, why: "`latest` is not `test`" },
@@ -57,6 +64,15 @@ export const TEST_PATH_CASES = [
57
64
  { path: "src/attestation.js", expected: false, why: "contains 'test', is not one" },
58
65
  { path: "src/index.js", expected: false, why: "ordinary source" },
59
66
  { path: "docs/testing-guide.md", expected: false, why: "documentation about testing" },
67
+ // The bare-name rule is Node-family only: `test.py` is not pytest (pytest
68
+ // collects `test_*.py`), `test.go` is not a Go test (`*_test.go`), and
69
+ // `test.rb` is neither RSpec nor Minitest.
70
+ { path: "test.py", expected: false, why: "pytest collects test_*.py, never a bare test.py" },
71
+ { path: "test.go", expected: false, why: "go tests are *_test.go" },
72
+ { path: "test.rb", expected: false, why: "ruby suites are *_spec.rb or test_*.rb" },
73
+ { path: "test.helpers.js", expected: false, why: "a support module named after tests" },
74
+ { path: "tests.js", expected: false, why: "plural name, ordinary source" },
75
+ { path: "testing.js", expected: false, why: "contains the word, is not the file" },
60
76
  ];
61
77
 
62
78
  /**
@@ -386,9 +402,211 @@ export const TAMPER_CANARIES = [
386
402
  added: [" throw XCTSkip(\"not now\")"],
387
403
  expect: "TEST_SKIP_INJECTION",
388
404
  },
405
+ // F01: P-Limit's root test.js is a supported runner's canonical test
406
+ // file. Every attack below had to be invisible inside it before the
407
+ // classifier knew the file existed — deletion, vacuous replacement, and
408
+ // an expectation rewritten to fit broken output.
409
+ {
410
+ id: "removal/node-root",
411
+ file: "test.js",
412
+ context: "// pending counts",
413
+ removed: ["\tt.is(limit.activeCount, 0);"],
414
+ added: [],
415
+ expect: "ASSERTION_REMOVAL",
416
+ },
417
+ {
418
+ id: "vacuous/node-root",
419
+ file: "test.js",
420
+ context: "// pending counts",
421
+ removed: ["\tt.is(limit.activeCount, 0);"],
422
+ added: ["\tt.true(true);"],
423
+ expect: "VACUOUS_ASSERTION",
424
+ },
425
+ {
426
+ id: "expectation/node-root",
427
+ file: "test.js",
428
+ context: "// pending counts",
429
+ removed: ["\tt.is(limit.pendingCount, 0);"],
430
+ added: ["\tt.is(limit.pendingCount, 1000);"],
431
+ expect: "ASSERTION_EXPECTATION_CHANGED",
432
+ },
433
+ // F03: an expected value bent into a conditional still blesses whatever
434
+ // the broken function does for the broken input. Python's spelling is a
435
+ // ternary with a comparison in its condition; JS's is `cond ? a : b`.
436
+ {
437
+ id: "expectation/python-conditional",
438
+ file: "tests/test_encoding.py",
439
+ context: "# encoding",
440
+ removed: [" assert dec == value"],
441
+ added: [" assert dec == (193 if value == 192 else value)"],
442
+ expect: "ASSERTION_EXPECTATION_CHANGED",
443
+ },
444
+ {
445
+ id: "expectation/js-conditional",
446
+ file: "test/calc.test.js",
447
+ context: "// arithmetic",
448
+ removed: [" assert.strictEqual(add(1, 2), 3);"],
449
+ added: [" assert.strictEqual(add(1, 2), add(1, 2) === -1 ? -1 : 3);"],
450
+ expect: "ASSERTION_EXPECTATION_CHANGED",
451
+ },
452
+ // F04: tests that leave the run without a skip or a deleted assertion.
453
+ // Renaming pytest's prefix to anything that stops the `test*` match, or a
454
+ // Go `TestX` function out of discovery, leaves the assertions in the file
455
+ // and the counts level.
456
+ {
457
+ id: "deregistration/pytest-check-prefix",
458
+ file: "tests/test_encoding.py",
459
+ context: "# encoding",
460
+ removed: ["def test_want_bytes(value):"],
461
+ added: ["def check_want_bytes(value):"],
462
+ expect: "TEST_DEREGISTERED",
463
+ },
464
+ {
465
+ id: "deregistration/pytest-disabled-prefix",
466
+ file: "tests/test_encoding.py",
467
+ context: "# encoding",
468
+ removed: ["def test_want_bytes(value):"],
469
+ added: ["def disabled_want_bytes(value):"],
470
+ expect: "TEST_DEREGISTERED",
471
+ },
472
+ {
473
+ id: "deregistration/go-check-prefix",
474
+ file: "is_test.go",
475
+ context: "// loadComment",
476
+ removed: ["func TestLoadComment(t *testing.T) {"],
477
+ added: ["func checkLoadComment(t *testing.T) {"],
478
+ expect: "TEST_DEREGISTERED",
479
+ },
480
+ // Rust and JUnit register through the attribute; removing `#[test]` (or
481
+ // `@Test`) from an existing function keeps the body and loses the run.
482
+ {
483
+ id: "deregistration/rust-attribute-removed",
484
+ file: "tests/iter_tests/all.rs",
485
+ context: "// iter",
486
+ removed: ["#[test]", "fn peek_does_not_advance() {"],
487
+ added: ["fn check_peek_does_not_advance() {"],
488
+ expect: "TEST_DEREGISTERED",
489
+ },
490
+ {
491
+ id: "deregistration/junit-annotation-removed",
492
+ file: "src/test/java/com/x/CalcTest.java",
493
+ context: "// arithmetic",
494
+ removed: [" @Test", " public void addsTwoNumbers() {"],
495
+ added: [" public void addsTwoNumbers() {"],
496
+ expect: "TEST_DEREGISTERED",
497
+ },
498
+ // Ways to keep a test collected while excising its execution.
499
+ {
500
+ id: "skip-injection/xfail-decorator",
501
+ file: "tests/test_encoding.py",
502
+ context: "# encoding",
503
+ removed: [],
504
+ added: ['@pytest.mark.xfail(reason="known broken output", strict=False)'],
505
+ expect: "TEST_SKIP_INJECTION",
506
+ },
507
+ {
508
+ id: "skip-injection/expectedfailure-decorator",
509
+ file: "tests/test_app.py",
510
+ context: "# app",
511
+ removed: [],
512
+ added: [" @unittest.expectedFailure"],
513
+ expect: "TEST_SKIP_INJECTION",
514
+ },
515
+ {
516
+ id: "skip-injection/rust-cfg-any",
517
+ file: "tests/iter_tests/all.rs",
518
+ context: "// iter",
519
+ removed: [],
520
+ added: ["#[cfg(any())]"],
521
+ expect: "TEST_SKIP_INJECTION",
522
+ },
523
+ {
524
+ id: "skip-injection/go-build-ignore",
525
+ file: "is_test.go",
526
+ context: "// loadComment",
527
+ removed: [],
528
+ added: ["//go:build ignore"],
529
+ expect: "TEST_SKIP_INJECTION",
530
+ },
531
+ {
532
+ id: "skip-injection/go-build-private-tag",
533
+ file: "is-1.7_test.go",
534
+ context: "// build",
535
+ removed: ["//go:build go1.7"],
536
+ added: ["//go:build go1.7 && cold_start_never"],
537
+ expect: "TEST_SKIP_INJECTION",
538
+ },
539
+ {
540
+ id: "skip-injection/python-early-return",
541
+ file: "tests/test_encoding.py",
542
+ context: "# encoding",
543
+ removed: ["def test_int_bytes(value, expect):"],
544
+ added: ["def test_int_bytes(value, expect):", " return"],
545
+ expect: "TEST_SKIP_INJECTION",
546
+ },
547
+ // F05: a Go assertion neutralised by a condition that can never hold.
548
+ // The `t.Errorf` stays in the file; the guard around it is dead.
549
+ {
550
+ id: "vacuous/go-impossible-condition",
551
+ file: "is_test.go",
552
+ context: "// loadComment",
553
+ // Context lines reconstruct the failure call sitting in the (now dead)
554
+ // block: the post-pass reads the whole hunk, not only the changed line.
555
+ lead: ["func TestLoadComment(t *testing.T) {", "\tcomment, _ := loadComment(\"./testdata/x\", 14)"],
556
+ removed: ['\tif comment != `this comment will be extracted` {', '\t\tt.Errorf("loadComment: bad comment %s", comment)', "\t}"],
557
+ added: ["\tif len(comment) < 0 {", "\t\tt.Errorf(\"loadComment: bad comment %s\", comment)", "\t}"],
558
+ expect: "VACUOUS_ASSERTION",
559
+ },
389
560
  ];
390
561
 
391
562
 
563
+ /**
564
+ * The F04 variants, classified by whether a line-oriented diff guard can see
565
+ * them at all.
566
+ *
567
+ * Widening a guard only to the attacks the evidence contains is how the
568
+ * next-but-one variant walks through, so the trial's four "other ways to keep
569
+ * a test from executing" are classified here explicitly:
570
+ *
571
+ * 1. Rename out of discovery (`test_x` -> `check_x`, `TestX` -> `checkX`,
572
+ * Rust `#[test]` removed). Visible: the name or attribute is right
573
+ * there in the diff. Caught as TEST_DEREGISTERED.
574
+ *
575
+ * 2. Build constraints (`//go:build <tag nobody sets>`, legacy
576
+ * `// +build`, Rust `#[cfg(any())]`). Visible: they are lines the diff
577
+ * adds. Caught as TEST_SKIP_INJECTION. Version/OS/arch constraints are
578
+ * honest CI gating and stay silent; a private tag or `ignore` is not.
579
+ *
580
+ * 3. Non-strict xfail (`@pytest.mark.xfail(strict=False)` and its strict
581
+ * sibling, `@expectedFailure`, `pytest.xfail()`). Visible: the
582
+ * decorator is an added line. Caught as TEST_SKIP_INJECTION.
583
+ *
584
+ * 4. An early `return` before the assertions. Partially visible. The
585
+ * diff-detectable shape — a *bare, unconditional* `return` as the
586
+ * test body's first statement — is caught as TEST_SKIP_INJECTION.
587
+ * What cannot be judged from a diff is a `return` placed *later* in
588
+ * the body behind a condition the test author believes impossible:
589
+ * that requires knowing, for every branch, whether the branch is
590
+ * reachable at runtime, which is a control-flow analysis over the
591
+ * whole program rather than a line pair. A guard that flags any `return`
592
+ * above an assertion hard-reds the ordinary "skip this case on
593
+ * Windows" guard clause, so the rule stops at the shape whose intent is
594
+ * unambiguous from the text.
595
+ *
596
+ * `runV8Coverage`-style runtime attestation (actually counting collected
597
+ * tests before and after) is the complete answer to 4, and is a separate
598
+ * check from the text guard; the text guard reports what it can read.
599
+ */
600
+ export const EXECUTION_EXCISION_VARIANTS = Object.freeze([
601
+ { id: "rename-discovery", visible: true, finding: "TEST_DEREGISTERED" },
602
+ { id: "build-constraint", visible: true, finding: "TEST_SKIP_INJECTION" },
603
+ { id: "cfg-any", visible: true, finding: "TEST_SKIP_INJECTION" },
604
+ { id: "xfail", visible: true, finding: "TEST_SKIP_INJECTION" },
605
+ { id: "early-return-body-first", visible: true, finding: "TEST_SKIP_INJECTION" },
606
+ { id: "early-return-behind-runtime-condition", visible: false, finding: null,
607
+ why: "reachability of a branch is a whole-program control-flow question, not a line pair" },
608
+ ]);
609
+
392
610
  /**
393
611
  * Mutants of the applicability predicate.
394
612
  *
@@ -410,11 +628,13 @@ export const PREDICATE_MUTANTS = [
410
628
  /** Runner outputs that state zero collected tests, per ecosystem. */
411
629
  export const EMPTY_RUN_CANARIES = [
412
630
  { id: "pytest", output: "collected 0 items\n\nno tests ran in 0.01s" },
631
+ { id: "pytest-collect-only", output: "collected 0 items\n\n======================== no tests ran in 0.00s =========================" },
413
632
  { id: "jest", output: "No tests found, exiting with code 0" },
414
633
  { id: "vitest", output: "No test files found, exiting with code 0" },
415
634
  { id: "cargo", output: "running 0 tests\ntest result: ok. 0 passed" },
416
635
  { id: "mocha", output: " 0 passing (1ms)" },
417
636
  { id: "go", output: "? example.com/app\t[no test files]" },
637
+ { id: "go-no-tests-to-run", output: "ok \texample.com/app\t0.001s [no tests to run]" },
418
638
  { id: "surefire", output: "Tests run: 0, Failures: 0, Errors: 0, Skipped: 0" },
419
639
  { id: "gradle", output: "> Task :test NO-SOURCE" },
420
640
  { id: "phpunit", output: "No tests executed!" },
@@ -454,6 +674,57 @@ export const SCOPE_CANARIES = [
454
674
  * from never showed it.
455
675
  */
456
676
  export const INNOCENT_EDITS = [
677
+ // Everything below this comment was a CRITICAL rejection until v0.71.0.
678
+ //
679
+ // Not through any rule that examined them: `UNREADABLE` was reached whenever
680
+ // a test file had changed lines and none of them parsed as an assertion, so
681
+ // the verdict was the same for a repository speaking an unsupported dialect
682
+ // and for one where the edit simply was not an assertion. Adding an import
683
+ // was a CRITICAL block. The finding carried no file, no line and no sample,
684
+ // because there was nothing to name — and it advised a pytest repository
685
+ // that its assertion library might be unsupported, from a list naming pytest.
686
+ //
687
+ // These are ordinary work. Every one of them has to be silent.
688
+ {
689
+ id: "add-import/pytest",
690
+ file: "testing/test_iniconfig.py",
691
+ context: "# fixtures",
692
+ removed: [],
693
+ added: ["import os"],
694
+ why: "adding an import to a test file is not an assertion and not tampering",
695
+ },
696
+ {
697
+ id: "rename-test-multiline/pytest",
698
+ file: "testing/test_iniconfig.py",
699
+ context: "# parsing",
700
+ removed: ["def test_parse_strips_inline_comments() -> None:"],
701
+ added: ["def test_parse_strips_inline_comments_from_continuations() -> None:"],
702
+ why: "still collected by pytest, so nothing left the run",
703
+ },
704
+ {
705
+ id: "docstring/pytest",
706
+ file: "testing/test_iniconfig.py",
707
+ context: "# parsing",
708
+ removed: [' """Check comments."""'],
709
+ added: [' """Check that inline comments are stripped."""'],
710
+ why: "prose about the test, not a value it asserts",
711
+ },
712
+ {
713
+ id: "type-annotation/pytest",
714
+ file: "testing/test_iniconfig.py",
715
+ context: "# fixtures",
716
+ removed: ["def make_config(data):"],
717
+ added: ["def make_config(data: str) -> IniConfig:"],
718
+ why: "a helper's signature, typed; no claim changed",
719
+ },
720
+ {
721
+ id: "add-fixture/go",
722
+ file: "calc_test.go",
723
+ context: "// helpers",
724
+ removed: [],
725
+ added: ["var cases = []int{1, 2, 3}"],
726
+ why: "test data added, nothing asserted or unasserted",
727
+ },
457
728
  // Renaming a test is one of the most ordinary edits there is. On the
458
729
  // one-line form the name blanked to the same shape as its replacement, the
459
730
  // two paired, and the pair was reported as a rewritten expectation.
@@ -587,6 +858,165 @@ export const INNOCENT_EDITS = [
587
858
  added: [" const shouldRetry = true;"],
588
859
  why: "`shouldRetry` and `expected` are identifiers; reading them as assertions makes the dialect warning worthless",
589
860
  },
861
+
862
+ // --- Widened de-registration (F04): the renames that must stay silent. ---
863
+ // The rule is now "collected before, not collected after", so the honest
864
+ // renames — still collected — are the false reds to guarantee.
865
+ {
866
+ id: "rename-test/longer-pytest",
867
+ file: "tests/test_encoding.py",
868
+ context: "# want_bytes",
869
+ removed: ["def test_want_bytes(value):"],
870
+ added: ["def test_want_bytes_for_text_and_bytes(value):"],
871
+ why: "still matches pytest's test* — an honest rename, the trial's own control",
872
+ },
873
+ {
874
+ id: "rename-test/pytest-underscore-glob",
875
+ file: "tests/test_encoding.py",
876
+ context: "# want_bytes",
877
+ removed: ["def test_want_bytes(value):"],
878
+ added: ["def testwant_bytes(value):"],
879
+ why: "pytest's test* glob still collects this without the underscore",
880
+ },
881
+ {
882
+ id: "rename-test/go-longer",
883
+ file: "is_test.go",
884
+ context: "// loadComment",
885
+ removed: ["func TestLoadComment(t *testing.T) {"],
886
+ added: ["func TestLoadCommentFromFixture(t *testing.T) {"],
887
+ why: "TestX → TestXRenamed is still collected; go test never sees the new suffix",
888
+ },
889
+ {
890
+ id: "rename-test/junit",
891
+ file: "src/test/java/com/x/CalcTest.java",
892
+ context: "// arithmetic",
893
+ removed: [" void addsNumbers() {"],
894
+ added: [" void addsTwoNumbers() {"],
895
+ why: "JUnit names the method freely; the @Test annotation is what registers it",
896
+ },
897
+ // Rust registers by attribute, so renaming the function — keeping #[test]
898
+ // — is ordinary and must be silent, while dropping the attribute is the
899
+ // canary above.
900
+ {
901
+ id: "rename-test/rust",
902
+ file: "tests/iter_tests/all.rs",
903
+ context: "// iter",
904
+ removed: ["fn peek_does_not_advance() {"],
905
+ added: ["fn peek_preserves_position() {"],
906
+ why: "the trial's honest-rust control: #[test] stays, the run loses nothing",
907
+ },
908
+ // A test moved to another file *with its registration* is refactoring, the
909
+ // same allowance the assertion-move check makes for moved assertions. The
910
+ // CROSS_FILE harness below covers it with both images; this entry asserts
911
+ // the attribute net-counting is silent inside the same file when a test is
912
+ // split into two (attribute added, none removed).
913
+ {
914
+ id: "add-test/rust-attribute-added",
915
+ file: "tests/iter_tests/all.rs",
916
+ context: "// iter",
917
+ removed: [],
918
+ added: ["#[test]", "fn peek_returns_the_next_item() {"],
919
+ why: "adding a registered test is the behaviour the gate exists to encourage",
920
+ },
921
+
922
+ // --- F03 conditional expectations: a conditional can be honest. ---------
923
+ // A brand-new assertion written with a ternary has no removed original to
924
+ // bend, so it is not a rewrite; the rule only pairs a non-conditional
925
+ // expectation against a conditional replacement of the same subject.
926
+ {
927
+ id: "add-assertion/conditional-ternary",
928
+ file: "test/calc.test.js",
929
+ context: "// arithmetic",
930
+ removed: [],
931
+ added: [" assert.strictEqual(add(1, 2), feature ? 3 : 3);"],
932
+ why: "a new assertion, conditional or not, is not a rewritten expectation",
933
+ },
934
+ {
935
+ id: "add-assertion/python-conditional",
936
+ file: "tests/test_calc.py",
937
+ context: "# arithmetic",
938
+ removed: [],
939
+ added: [" assert add(1, 2) == (3 if flag else 3)"],
940
+ why: "same, in pytest's bare-comparison spelling",
941
+ },
942
+ // The expected expression may be a variable name; renaming a variable the
943
+ // assertion compares against changes no value and contains no conditional.
944
+ {
945
+ id: "rename-expectation-identifier/pytest",
946
+ file: "tests/test_calc.py",
947
+ context: "# arithmetic",
948
+ removed: [" assert add(1, 2) == expected"],
949
+ added: [" assert add(1, 2) == wanted"],
950
+ why: "an identifier renamed is not a conditional expectation",
951
+ },
952
+ // Changing the condition's *comparison value* is not this rule's target —
953
+ // it is an expectation rewrite in its own right and the pairing checks
954
+ // report it (the trial's Go controls show exactly that being rejected).
955
+ // The vacuous-condition rule must only answer to an impossible guard.
956
+
957
+ // --- F05 dead guard: an honest, reachable guard is silent. --------------
958
+ {
959
+ id: "guard-clause/go",
960
+ file: "is_test.go",
961
+ context: "// loadComment",
962
+ lead: ["func TestLoadComment(t *testing.T) {"],
963
+ removed: ["\tif comment != \"\" {"],
964
+ added: ["\tif len(users) == 0 {"],
965
+ why: "an ordinary test guard on real data — `len(x) == 0` is reachable, unlike `len(x) < 0`",
966
+ },
967
+ {
968
+ id: "guard-clause/pytest",
969
+ file: "tests/test_encoding.py",
970
+ context: "# encoding",
971
+ lead: ["def test_int_bytes(value, expect):", " enc = int_to_bytes(value)"],
972
+ removed: [" if not data: return", " assert enc == expect"],
973
+ added: [" if not data:", " return", " assert enc == expect"],
974
+ why: "an early return inside a real guard clause is not the body's first statement; the assertion still runs",
975
+ },
976
+
977
+ // --- F04 execution-excision: harmless siblings must stay silent. --------
978
+ {
979
+ id: "go-build-version-bump",
980
+ file: "is-1.7_test.go",
981
+ context: "// build",
982
+ removed: ["//go:build go1.7"],
983
+ added: ["//go:build go1.24"],
984
+ why: "a version tag tracks the toolchain; the test still runs in every supported CI",
985
+ },
986
+ {
987
+ id: "go-build-os-gate",
988
+ file: "is_unix_test.go",
989
+ context: "// build",
990
+ removed: [],
991
+ added: ["//go:build linux || darwin"],
992
+ why: "a new platform-gated test file is ordinary CI matrix work",
993
+ },
994
+ {
995
+ id: "rust-test-module-attribute",
996
+ file: "tests/iter_tests/all.rs",
997
+ context: "// iter",
998
+ removed: [],
999
+ added: ["#[cfg(all(test, feature = \"proptest\"))]"],
1000
+ why: "a feature-gated *additional* test is not an excised one (no tightened counterpart, no remove)",
1001
+ },
1002
+
1003
+ // --- F01 root file: ordinary edits to test.js stay ordinary. ------------
1004
+ {
1005
+ id: "add-assertion/root-node",
1006
+ file: "test.js",
1007
+ context: "// activeCount",
1008
+ removed: ["\tt.is(limit.activeCount, 0);"],
1009
+ added: ["\tt.is(limit.activeCount, 0);", "\tt.is(limit.pendingCount, 0);"],
1010
+ why: "adding an assertion to the root test file is what the gate encourages",
1011
+ },
1012
+ {
1013
+ id: "add-import/root-node",
1014
+ file: "test.js",
1015
+ context: "// imports",
1016
+ removed: [],
1017
+ added: ["import {identity} from './identity-helper.js';"],
1018
+ why: "an import in test.js is not an assertion, as in any other test file",
1019
+ },
590
1020
  ];
591
1021
 
592
1022
  /**
@@ -598,6 +1028,157 @@ export const INNOCENT_EDITS = [
598
1028
  * will always end somewhere; what must never happen again is that the edge
599
1029
  * is silent.
600
1030
  */
1031
+ /**
1032
+ * Renames that delete a test from the run without deleting a line of it.
1033
+ *
1034
+ * pytest collects `test_*`, Go collects `Test*`. For those runners the name is
1035
+ * the registration, so `def test_totals` → `def totals` removes the test as
1036
+ * completely as deleting the file, and every count in the tamper guard stays
1037
+ * level: nothing removed, nothing weakened, nothing rewritten.
1038
+ *
1039
+ * These were caught only as a side effect of the blanket that rejected every
1040
+ * unrecognised edit to a test file — which also rejected adding an import, and
1041
+ * whose printed remedy switched the real checks off. They are their own
1042
+ * finding now, so narrowing that blanket costs nothing.
1043
+ */
1044
+ export const DEREGISTRATION_CANARIES = [
1045
+ {
1046
+ id: "pytest/underscore",
1047
+ file: "tests/test_totals.py",
1048
+ context: "# totals",
1049
+ removed: ["def test_totals_round_to_cents():"],
1050
+ added: ["def totals_round_to_cents():"],
1051
+ why: "pytest collects by the test_ prefix, so this test no longer runs",
1052
+ },
1053
+ {
1054
+ id: "pytest/camel",
1055
+ file: "tests/test_totals.py",
1056
+ context: "# totals",
1057
+ removed: ["def testTotals():"],
1058
+ added: ["def Totals():"],
1059
+ why: "the prefix is what registers it, in either spelling",
1060
+ },
1061
+ // F04: the original rule only matched a literal prefix strip, so any
1062
+ // *other* collected→not-collected rename sailed through. Each of these
1063
+ // leaves every assertion in the file, still level in every count, with
1064
+ // the test gone from the run.
1065
+ {
1066
+ id: "pytest/check-prefix",
1067
+ file: "tests/test_encoding.py",
1068
+ context: "# want_bytes",
1069
+ removed: ["def test_want_bytes(value):"],
1070
+ added: ["def check_want_bytes(value):"],
1071
+ why: "the measured bypass — pytest never collects check_*",
1072
+ },
1073
+ {
1074
+ id: "pytest/disabled-prefix",
1075
+ file: "tests/test_encoding.py",
1076
+ context: "# want_bytes",
1077
+ removed: ["def test_want_bytes(value):"],
1078
+ added: ["def disabled_want_bytes(value):"],
1079
+ why: "any non-test* replacement uncollects, not only a strip",
1080
+ },
1081
+ {
1082
+ id: "go/check-prefix",
1083
+ file: "is_test.go",
1084
+ context: "// loadComment",
1085
+ removed: ["func TestLoadComment(t *testing.T) {"],
1086
+ added: ["func checkLoadComment(t *testing.T) {"],
1087
+ why: "go test collects TestX, never checkX",
1088
+ },
1089
+ {
1090
+ id: "rust/attribute-removed",
1091
+ file: "tests/iter_tests/all.rs",
1092
+ context: "// iter",
1093
+ removed: ["#[test]", "fn peek_does_not_advance() {"],
1094
+ added: ["fn check_peek_does_not_advance() {"],
1095
+ why: "Rust registers by attribute; no #[test], no run, free-form name",
1096
+ },
1097
+ {
1098
+ id: "go/benchmark",
1099
+ file: "calc_test.go",
1100
+ context: "// totals",
1101
+ removed: ["func BenchmarkTotals(b *testing.B) {"],
1102
+ added: ["func Totals(b *testing.B) {"],
1103
+ why: "the same rule for the other collected prefixes",
1104
+ },
1105
+ ];
1106
+
1107
+ /**
1108
+ * A verification command that exits 0 having written nothing at all.
1109
+ *
1110
+ * The one absence that is evidence. The floor is otherwise deliberately
1111
+ * one-sided — an unrecognised runner states no count and passes, because
1112
+ * hard-redding every runner not on the list would be worse than the hole it
1113
+ * closes — but an unrecognised runner still prints something. Zero bytes on
1114
+ * both streams is a command that ran nothing, and `pnpm -r test` on a
1115
+ * workspace whose packages declare no test script is exactly that: it was
1116
+ * indistinguishable from a full suite by every signal the gate had.
1117
+ */
1118
+ export const SILENT_RUN_CANARIES = [
1119
+ { id: "pnpm -r test", command: "pnpm -r test", stdout: "", stderr: "" },
1120
+ { id: "npm test", command: "npm test", stdout: "", stderr: "" },
1121
+ { id: "yarn workspaces test", command: "yarn workspaces foreach run test", stdout: "", stderr: "" },
1122
+ { id: "pytest", command: "python3 -m pytest", stdout: "", stderr: "" },
1123
+ { id: "go test", command: "go test ./...", stdout: "", stderr: "" },
1124
+ { id: "whitespace is not output", command: "npm test", stdout: " \n", stderr: "\n" },
1125
+ ];
1126
+
1127
+ /**
1128
+ * Honest static gates that print nothing, and must keep passing.
1129
+ *
1130
+ * The counterweight, and the reason the rule above reads the command as well
1131
+ * as the output. `tsc --noEmit`, `node --check`, `go vet` and `compileall`
1132
+ * all exit 0 in silence when they succeed — that is what success looks like
1133
+ * for a checker — and two of them are commands this kit writes itself for a
1134
+ * repository that has no suite yet. A rule keyed on silence alone hard-redded
1135
+ * every one of them, which is exactly the first-run rejection of correct code
1136
+ * that the collection floor is otherwise so careful to avoid.
1137
+ */
1138
+ export const SILENT_STATIC_GATES = [
1139
+ { id: "tsc", command: "tsc --noEmit" },
1140
+ { id: "node --check", command: "node --check index.js" },
1141
+ { id: "go vet", command: "go vet ./..." },
1142
+ { id: "compileall", command: "python3 -m compileall -q ." },
1143
+ { id: "generated parse gate", command: "node --check src/index.mjs" },
1144
+ ];
1145
+
1146
+ /**
1147
+ * Values that must survive a trip through the emitter and back.
1148
+ *
1149
+ * `yamlScalar` writes the manifests and `parseYaml` reads them, and a pair
1150
+ * like that disagreeing about one character is this project's recurring
1151
+ * defect with both halves in a single module. The parser opened a comment at
1152
+ * the first `#` on a line, quoted or not, so `verify.test` containing a hash
1153
+ * was silently truncated on the way in and the gate ran a command the user
1154
+ * never wrote — reporting on it as if it were theirs.
1155
+ *
1156
+ * The corpus is the hard cases on purpose: hashes, colons, leading stars,
1157
+ * apostrophes, the empty string, and the words YAML would otherwise read as
1158
+ * booleans.
1159
+ */
1160
+ export const YAML_ROUNDTRIP_CASES = [
1161
+ "pnpm -r test",
1162
+ "PYTHONPATH=src python3 -m pytest",
1163
+ 'pytest -k "not #slow"',
1164
+ "has #hash",
1165
+ "# leading hash",
1166
+ "a: b",
1167
+ "**/*.pem",
1168
+ "**/.env.*",
1169
+ ".github/**",
1170
+ "",
1171
+ "true",
1172
+ "yes",
1173
+ "1.5",
1174
+ "it's fine",
1175
+ "it's #1",
1176
+ "O'Brien",
1177
+ "agent/",
1178
+ " leading space",
1179
+ "trailing space ",
1180
+ ];
1181
+
601
1182
  export const UNREADABLE_DIALECTS = [
602
1183
  {
603
1184
  id: "hspec",