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.
- package/.agent/rules/jules-protocol.md +0 -1
- package/CHANGELOG.md +66 -0
- package/JULES_RULES_TEMPLATE.md +0 -1
- package/README.md +56 -1
- package/ROADMAP_V1.md +31 -6
- package/bin/agentctl.mjs +101 -8
- package/package.json +1 -1
- package/scripts/guard-reach-check.mjs +74 -8
- package/src/config.mjs +434 -5
- package/src/coverage.mjs +85 -13
- package/src/engine.mjs +106 -130
- package/src/git.mjs +104 -1
- package/src/guard-policy.mjs +581 -0
- package/src/ops/command-registry.mjs +109 -0
- package/src/ops/test-collection.mjs +103 -3
- package/src/security.mjs +554 -4
- package/src/stack-detector.mjs +73 -14
- package/src/test-paths.mjs +18 -1
- package/src/wizard-init.mjs +54 -27
- package/src/wizard-oracle.mjs +1 -1
- package/src/wizard-task.mjs +15 -1
package/src/guard-policy.mjs
CHANGED
|
@@ -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",
|