jules-orchestrator-kit 0.59.0 → 0.60.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/README.md +2 -2
- package/bin/agentctl.mjs +6 -0
- package/package.json +1 -1
- package/src/engine.mjs +5 -1
- package/src/security.mjs +237 -17
package/README.md
CHANGED
|
@@ -204,7 +204,7 @@ To maximize PR merge rates, dispatch tasks according to deterministic boundaries
|
|
|
204
204
|
* **Fail-Closed Security & Secret Redaction:** Evaluates explicit Deny rules before Allow rules against canonicalized, case-folded paths. Redacts high-entropy keys and base64-encoded credentials (such as Kubernetes `Secret` manifests).
|
|
205
205
|
* **Complexity & Cost Router:** Zero-dependency heuristic classifier (`src/router.mjs`) routing mechanical tasks to lightweight models while reserving primary models for complex refactors, with a `node --check` syntax-verification gate that transparently escalates a FAST-tier result to the primary provider if it left broken JS on disk.
|
|
206
206
|
* **Terminal UI & Diagnostic Matrix (`agentctl doctor`):** Interactive terminal dashboard, task sidecar manager, and automated transactional self-repair.
|
|
207
|
-
* **Verified Test Suite:** Tested with **
|
|
207
|
+
* **Verified Test Suite:** Tested with **957 unit tests across 134 suites passing in < 15.0s**.
|
|
208
208
|
|
|
209
209
|
<br/>
|
|
210
210
|
|
|
@@ -237,7 +237,7 @@ To maximize PR merge rates, dispatch tasks according to deterministic boundaries
|
|
|
237
237
|
| `doctor` | `agentctl doctor [--probe] [--json]` | Diagnostic check runner. `--probe` additionally starts the configured provider's CLI to confirm it answers, rather than only finding it on `PATH`. | `0` (Healthy), `1` (Failures) |
|
|
238
238
|
| `queue` | `agentctl queue [--dag] [--concurrency <n>] [--dry-run] [--json]` | Consumes and executes task envelopes in `.agent/jules-queue/` with Kahn's DAG dependency resolution. Non-task files (manifests, `README.md`) are skipped, and `--dry-run` previews without moving anything. | `0` (Complete) |
|
|
239
239
|
| `swarm` | `agentctl swarm [--json]` | Runs parallel multi-agent swarm across worker slots with PID liveness detection. | `0` (Complete) |
|
|
240
|
-
| `check` / `gate` / `audit`| `agentctl check [--mode working-tree] [--fix] [--allow-protected] [--allow-test-
|
|
240
|
+
| `check` / `gate` / `audit`| `agentctl check [--mode working-tree] [--fix] [--allow-protected] [--allow-test-change <kind>] [--json] [--json-report <path>]` | Runs security, secret scanning, rules budget audit, and tiered verification gates (with declarative assertion support) against working tree or branch. | `0` (Approved), `1` (Budget/Arg), `3` (Scope), `4` (Verify), `5` (Diff >75K), `6` (Secret), `8` (Flaky) |
|
|
241
241
|
| `mutate` / `mutation` | `agentctl mutate [--min-score <n>] [--max-mutants <n>] [--cmd <testCmd>] [--json]` | Runs zero-dependency diff mutation testing harness on changed hunks with operator inversion and safety rollback. | `0` (Passed), `1` (Score Low) |
|
|
242
242
|
| `coverage` | `agentctl coverage [--min <pct>] [--cmd <testCmd>] [--base <ref>] [--json]` | Runs native zero-dependency V8 diff coverage check against added diff lines. | `0` (Passed), `1` (Low Coverage) |
|
|
243
243
|
| `probe` / `stability` | `agentctl probe [--repeat <n>] [--min <passRate>] [--cmd <testCmd>] [--json]` | Probes test suite flakiness across N consecutive iterations with oscillation detection. | `0` (Passed), `1` (Flaky) |
|
package/bin/agentctl.mjs
CHANGED
|
@@ -388,7 +388,12 @@ async function main() {
|
|
|
388
388
|
// of spec, which necessarily rewrites what a test expects, hit a
|
|
389
389
|
// CRITICAL finding at exit 6 with no documented way past it. A guard
|
|
390
390
|
// with no override is not a guard, it is an outage.
|
|
391
|
+
//
|
|
392
|
+
// This one is the blunt form and turns off all six checks. Prefer
|
|
393
|
+
// `--allow-test-change <kind>`: answering one finding should not
|
|
394
|
+
// silence five other checks nobody looked at.
|
|
391
395
|
"allow-test-modifications": { type: "boolean" },
|
|
396
|
+
"allow-test-change": { type: "string", multiple: true },
|
|
392
397
|
json: { type: "boolean", short: "j" },
|
|
393
398
|
"json-report": { type: "string" },
|
|
394
399
|
"dry-run": { type: "boolean", short: "d" },
|
|
@@ -409,6 +414,7 @@ async function main() {
|
|
|
409
414
|
fix: values.fix,
|
|
410
415
|
allowProtected: values["allow-protected"],
|
|
411
416
|
allowTestModifications: values["allow-test-modifications"],
|
|
417
|
+
allowTestChanges: values["allow-test-change"],
|
|
412
418
|
jsonReport: values["json-report"],
|
|
413
419
|
});
|
|
414
420
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jules-orchestrator-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.60.0",
|
|
4
4
|
"description": "Zero-dependency safety gatekeeper, test oracle generator, and multi-agent coordination protocol for autonomous coding agents — Google Jules, Claude Code, Codex and Gemini CLI.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
package/src/engine.mjs
CHANGED
|
@@ -275,7 +275,11 @@ export async function gate(opts = {}) {
|
|
|
275
275
|
}
|
|
276
276
|
|
|
277
277
|
// Phase 3: Diff Secret Scanner & Security Checks
|
|
278
|
-
const secretResult = scanDiff(diffStr, {
|
|
278
|
+
const secretResult = scanDiff(diffStr, {
|
|
279
|
+
root,
|
|
280
|
+
allowTestModifications: opts.allowTestModifications === true,
|
|
281
|
+
allowTestChanges: opts.allowTestChanges,
|
|
282
|
+
});
|
|
279
283
|
// A binary file reaches the scanner as one summary line, so its contents were
|
|
280
284
|
// never looked at — a NUL byte in front of a token was enough to hide it.
|
|
281
285
|
// Inspect those files directly and fold the verdict in.
|
package/src/security.mjs
CHANGED
|
@@ -1138,8 +1138,11 @@ function locateFindingLine(lines, type, file = null) {
|
|
|
1138
1138
|
// assertion — identical once every literal is blanked out — with different
|
|
1139
1139
|
// values. That does not distinguish an attack from a deliberate change of
|
|
1140
1140
|
// spec; nothing can, from a diff alone. This reports rather than decides,
|
|
1141
|
-
// and `--allow-test-
|
|
1142
|
-
// the correct one.
|
|
1141
|
+
// and `--allow-test-change expectation` is the answer when the new
|
|
1142
|
+
// expectation is the correct one. Narrow on purpose: the blunt
|
|
1143
|
+
// `--allow-test-modifications` turns off the other five checks too, and a
|
|
1144
|
+
// check that can only be answered by disabling its neighbours ends up
|
|
1145
|
+
// disabling its neighbours.
|
|
1143
1146
|
|
|
1144
1147
|
// An assertion that states a *specific* expected value. Counting assertions
|
|
1145
1148
|
// alone let a test be gutted while looking untouched: swapping
|
|
@@ -1627,6 +1630,135 @@ const hasLiteralPlaceholder = (shape) =>
|
|
|
1627
1630
|
const collapseWhitespace = (s) => s.replace(/\s+/g, " ").trim();
|
|
1628
1631
|
const shorten = (s) => (s.length > 160 ? `${s.slice(0, 157)}…` : s);
|
|
1629
1632
|
|
|
1633
|
+
/**
|
|
1634
|
+
* Split the argument list of the outermost assertion call in `clean`.
|
|
1635
|
+
*
|
|
1636
|
+
* Comments are already stripped by the caller, so only string state has to be
|
|
1637
|
+
* tracked. Returns null whenever the shape is not confidently understood — a
|
|
1638
|
+
* truncated fragment, an unbalanced hunk, a quoting form not handled here —
|
|
1639
|
+
* because every caller uses this to *suppress* a finding, and failing to
|
|
1640
|
+
* understand a statement must never become a reason to stay quiet about it.
|
|
1641
|
+
*
|
|
1642
|
+
* @param {string} clean - comment-stripped statement text
|
|
1643
|
+
* @param {string} lang
|
|
1644
|
+
* @returns {string[] | null} top-level arguments, trimmed
|
|
1645
|
+
*/
|
|
1646
|
+
function splitAssertionArgs(clean, lang) {
|
|
1647
|
+
SPECIFIC_ASSERTION.lastIndex = 0;
|
|
1648
|
+
const m = SPECIFIC_ASSERTION.exec(clean);
|
|
1649
|
+
if (!m) return null;
|
|
1650
|
+
|
|
1651
|
+
let i = m.index + m[0].length; // just past the opening paren
|
|
1652
|
+
let depth = 1;
|
|
1653
|
+
let quote = null;
|
|
1654
|
+
let triple = false;
|
|
1655
|
+
const args = [];
|
|
1656
|
+
let start = i;
|
|
1657
|
+
|
|
1658
|
+
while (i < clean.length) {
|
|
1659
|
+
const c = clean[i];
|
|
1660
|
+
|
|
1661
|
+
if (quote !== null) {
|
|
1662
|
+
if (c === "\\") { i += 2; continue; }
|
|
1663
|
+
if (triple && c === quote && clean[i + 1] === quote && clean[i + 2] === quote) {
|
|
1664
|
+
quote = null; triple = false; i += 3; continue;
|
|
1665
|
+
}
|
|
1666
|
+
if (!triple && c === quote) { quote = null; i += 1; continue; }
|
|
1667
|
+
i += 1;
|
|
1668
|
+
continue;
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
if (c === '"' || c === "'" || c === "`") {
|
|
1672
|
+
if (lang === "python" && clean[i + 1] === c && clean[i + 2] === c) {
|
|
1673
|
+
quote = c; triple = true; i += 3; continue;
|
|
1674
|
+
}
|
|
1675
|
+
quote = c; i += 1; continue;
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
if (c === "(" || c === "[" || c === "{") { depth += 1; i += 1; continue; }
|
|
1679
|
+
if (c === ")" || c === "]" || c === "}") {
|
|
1680
|
+
depth -= 1;
|
|
1681
|
+
if (depth === 0) {
|
|
1682
|
+
args.push(clean.slice(start, i).trim());
|
|
1683
|
+
return args;
|
|
1684
|
+
}
|
|
1685
|
+
i += 1;
|
|
1686
|
+
continue;
|
|
1687
|
+
}
|
|
1688
|
+
if (c === "," && depth === 1) {
|
|
1689
|
+
args.push(clean.slice(start, i).trim());
|
|
1690
|
+
start = i + 1;
|
|
1691
|
+
i += 1;
|
|
1692
|
+
continue;
|
|
1693
|
+
}
|
|
1694
|
+
i += 1;
|
|
1695
|
+
}
|
|
1696
|
+
return null; // never closed: an unbalanced fragment, so no suppression
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
/** One plain string literal and nothing else. */
|
|
1700
|
+
const PURE_STRING_LITERAL = new RegExp(
|
|
1701
|
+
[
|
|
1702
|
+
"^'(?:\\\\.|[^'\\\\])*'$",
|
|
1703
|
+
'^"(?:\\\\.|[^"\\\\])*"$',
|
|
1704
|
+
"^`(?:\\\\.|[^`\\\\])*`$",
|
|
1705
|
+
'^"""[\\s\\S]*"""$',
|
|
1706
|
+
"^'''[\\s\\S]*'''$",
|
|
1707
|
+
].join("|")
|
|
1708
|
+
);
|
|
1709
|
+
|
|
1710
|
+
function isPureStringLiteral(arg) {
|
|
1711
|
+
if (!arg) return false;
|
|
1712
|
+
return PURE_STRING_LITERAL.test(arg.trim());
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
/**
|
|
1716
|
+
* Argument positions that carry a message for a human rather than an expected
|
|
1717
|
+
* value.
|
|
1718
|
+
*
|
|
1719
|
+
* Trailing, for `assert.equal(got, want, "message")` and
|
|
1720
|
+
* `assert_eq!(a, b, "message")`; leading, for Go's
|
|
1721
|
+
* `t.Errorf("got %d want %d", got, want)`. Two arguments is the classic
|
|
1722
|
+
* `(actual, expected)` shape, so a string in last position *there* is the
|
|
1723
|
+
* expected value: `assert.equal(name, "Alice")` must still be judged when
|
|
1724
|
+
* "Alice" becomes "Bob".
|
|
1725
|
+
*/
|
|
1726
|
+
function messageArgIndices(args) {
|
|
1727
|
+
const idx = new Set();
|
|
1728
|
+
if (args.length >= 3 && isPureStringLiteral(args[args.length - 1])) idx.add(args.length - 1);
|
|
1729
|
+
if (args.length >= 2 && isPureStringLiteral(args[0])) idx.add(0);
|
|
1730
|
+
return idx;
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
/**
|
|
1734
|
+
* True when two assertions differ only in text written to be read by a person.
|
|
1735
|
+
*
|
|
1736
|
+
* Rewording the message on a failing assertion is among the most common edits
|
|
1737
|
+
* any test file receives, and it says nothing whatsoever about what the suite
|
|
1738
|
+
* checks. But a message is a literal, so blanking literals made the two
|
|
1739
|
+
* statements the same shape and the pairing reported a rewritten expectation
|
|
1740
|
+
* every time somebody improved the wording of a failure. Firing on that is
|
|
1741
|
+
* how an operator learns to pass the override without reading it.
|
|
1742
|
+
*/
|
|
1743
|
+
function differsOnlyInMessage(cleanRemoved, cleanAdded, lang) {
|
|
1744
|
+
const a = splitAssertionArgs(cleanRemoved, lang);
|
|
1745
|
+
const b = splitAssertionArgs(cleanAdded, lang);
|
|
1746
|
+
if (!a || !b || a.length !== b.length || a.length === 0) return false;
|
|
1747
|
+
|
|
1748
|
+
const msgIdx = messageArgIndices(a);
|
|
1749
|
+
if (msgIdx.size === 0) return false;
|
|
1750
|
+
|
|
1751
|
+
let sawDifference = false;
|
|
1752
|
+
for (let i = 0; i < a.length; i++) {
|
|
1753
|
+
if (a[i].replace(/\s+/g, "") === b[i].replace(/\s+/g, "")) continue;
|
|
1754
|
+
// A difference outside a message position, or in a position that stopped
|
|
1755
|
+
// being a plain string, is a real change.
|
|
1756
|
+
if (!msgIdx.has(i) || !isPureStringLiteral(b[i])) return false;
|
|
1757
|
+
sawDifference = true;
|
|
1758
|
+
}
|
|
1759
|
+
return sawDifference;
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1630
1762
|
/**
|
|
1631
1763
|
* Pair rewritten expectations across the removed and added images of every
|
|
1632
1764
|
* hunk of one file, and report each pair.
|
|
@@ -1664,14 +1796,14 @@ function detectExpectationRewrites(file, hunks, stats, violations) {
|
|
|
1664
1796
|
if (s.removedLines.length === 0) continue;
|
|
1665
1797
|
const clean = stripComments(s.text, lang);
|
|
1666
1798
|
if (!isSpecificAssertion(clean)) continue;
|
|
1667
|
-
oldCands.push({ s, shape: blankLiterals(clean), canon: clean.replace(/\s+/g, "") });
|
|
1799
|
+
oldCands.push({ s, clean, shape: blankLiterals(clean), canon: clean.replace(/\s+/g, "") });
|
|
1668
1800
|
}
|
|
1669
1801
|
const newCands = [];
|
|
1670
1802
|
for (const s of newStmts) {
|
|
1671
1803
|
if (s.addedLines.length === 0) continue;
|
|
1672
1804
|
const clean = stripComments(s.text, lang);
|
|
1673
1805
|
if (!isSpecificAssertion(clean)) continue;
|
|
1674
|
-
newCands.push({ s, shape: blankLiterals(clean), canon: clean.replace(/\s+/g, "") });
|
|
1806
|
+
newCands.push({ s, clean, shape: blankLiterals(clean), canon: clean.replace(/\s+/g, "") });
|
|
1675
1807
|
}
|
|
1676
1808
|
|
|
1677
1809
|
// The t-th removed candidate of a shape pairs with the t-th added
|
|
@@ -1698,11 +1830,32 @@ function detectExpectationRewrites(file, hunks, stats, violations) {
|
|
|
1698
1830
|
const pairs = [];
|
|
1699
1831
|
for (const [shape, olds] of oldByShape) {
|
|
1700
1832
|
const news = newByShape.get(shape) || [];
|
|
1701
|
-
|
|
1833
|
+
|
|
1834
|
+
// Cancel the assertions that are byte-identical on both sides before
|
|
1835
|
+
// aligning anything.
|
|
1836
|
+
//
|
|
1837
|
+
// Reordering two assertions removes both and adds both back unchanged.
|
|
1838
|
+
// Positional alignment then matched the first removed against the first
|
|
1839
|
+
// added — a different assertion — and reported two rewritten
|
|
1840
|
+
// expectations for an edit that changed no expected value at all. The
|
|
1841
|
+
// same happened to an assertion that simply moved within its block.
|
|
1842
|
+
// What is present unchanged on both sides did not change; only the
|
|
1843
|
+
// residue can have been rewritten.
|
|
1844
|
+
const survivingNew = news.slice();
|
|
1845
|
+
const survivingOld = [];
|
|
1846
|
+
for (const o of olds) {
|
|
1847
|
+
const twin = survivingNew.findIndex((n) => n.canon === o.canon);
|
|
1848
|
+
if (twin === -1) survivingOld.push(o);
|
|
1849
|
+
else survivingNew.splice(twin, 1);
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
const k = Math.min(survivingOld.length, survivingNew.length);
|
|
1702
1853
|
for (let t = 0; t < k; t++) {
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1854
|
+
const r = survivingOld[t];
|
|
1855
|
+
const a = survivingNew[t];
|
|
1856
|
+
if (r.canon === a.canon) continue;
|
|
1857
|
+
if (differsOnlyInMessage(r.clean, a.clean, lang)) continue;
|
|
1858
|
+
pairs.push({ r: r.s, a: a.s });
|
|
1706
1859
|
}
|
|
1707
1860
|
}
|
|
1708
1861
|
|
|
@@ -1717,9 +1870,11 @@ function detectExpectationRewrites(file, hunks, stats, violations) {
|
|
|
1717
1870
|
const sr = blankLiterals(stripComments(r.text, lang));
|
|
1718
1871
|
const sa = blankLiterals(stripComments(a.text, lang));
|
|
1719
1872
|
if (sr === sa && hasLiteralPlaceholder(sr)) {
|
|
1720
|
-
const
|
|
1721
|
-
const
|
|
1722
|
-
if (
|
|
1873
|
+
const clr = stripComments(r.text, lang);
|
|
1874
|
+
const cla = stripComments(a.text, lang);
|
|
1875
|
+
if (clr.replace(/\s+/g, "") !== cla.replace(/\s+/g, "") && !differsOnlyInMessage(clr, cla, lang)) {
|
|
1876
|
+
pairs.push({ r, a });
|
|
1877
|
+
}
|
|
1723
1878
|
}
|
|
1724
1879
|
}
|
|
1725
1880
|
}
|
|
@@ -1736,9 +1891,10 @@ function detectExpectationRewrites(file, hunks, stats, violations) {
|
|
|
1736
1891
|
`"${shorten(collapseWhitespace(p.r.text))}" became "${shorten(collapseWhitespace(p.a.text))}". ` +
|
|
1737
1892
|
`A deliberately changed spec looks identical to a test bent to match broken ` +
|
|
1738
1893
|
`output, and a diff alone cannot tell the two apart, so this is flagged for ` +
|
|
1739
|
-
`review rather than assumed. If the new expectation is correct,
|
|
1740
|
-
|
|
1741
|
-
`and
|
|
1894
|
+
`review rather than assumed. If the new expectation is the correct one, ` +
|
|
1895
|
+
`re-run with --allow-test-change expectation — which allows exactly this ` +
|
|
1896
|
+
`check and leaves the skip, vacuous, commented, removal and weakening ` +
|
|
1897
|
+
`checks doing their job.`,
|
|
1742
1898
|
});
|
|
1743
1899
|
|
|
1744
1900
|
// Both sides are accounted for here, so they must not also feed the
|
|
@@ -1771,6 +1927,61 @@ function detectExpectationRewrites(file, hunks, stats, violations) {
|
|
|
1771
1927
|
return allPairs;
|
|
1772
1928
|
}
|
|
1773
1929
|
|
|
1930
|
+
/**
|
|
1931
|
+
* The tamper checks, by the name an operator uses to allow one of them.
|
|
1932
|
+
*
|
|
1933
|
+
* There was one override for all six, and it was a switch marked "off". A
|
|
1934
|
+
* deliberate change of spec rewrites what a test expects, which is
|
|
1935
|
+
* indistinguishable from bending a test to match broken output — so the honest
|
|
1936
|
+
* answer to that finding is sometimes an override. But reaching for it also
|
|
1937
|
+
* silenced injected `.skip()`, `expect(true).toBe(true)`, commented-out
|
|
1938
|
+
* assertions and outright deletions, none of which the operator had looked at.
|
|
1939
|
+
* The check with the highest firing rate therefore set the ceiling for every
|
|
1940
|
+
* other check in the bundle: the more useful this one became, the more often
|
|
1941
|
+
* it would be used to turn the others off.
|
|
1942
|
+
*/
|
|
1943
|
+
export const TAMPER_KINDS = new Map([
|
|
1944
|
+
["TEST_SKIP_INJECTION", "skip"],
|
|
1945
|
+
["VACUOUS_ASSERTION", "vacuous"],
|
|
1946
|
+
["COMMENTED_ASSERTION", "commented"],
|
|
1947
|
+
["ASSERTION_REMOVAL", "removal"],
|
|
1948
|
+
["ASSERTION_WEAKENED", "weakening"],
|
|
1949
|
+
["ASSERTION_EXPECTATION_CHANGED", "expectation"],
|
|
1950
|
+
]);
|
|
1951
|
+
|
|
1952
|
+
/** Every kind name, for CLI validation and help text. */
|
|
1953
|
+
export const TAMPER_KIND_NAMES = Object.freeze([...new Set(TAMPER_KINDS.values())].sort());
|
|
1954
|
+
|
|
1955
|
+
/**
|
|
1956
|
+
* Which tamper checks this run is allowed to stay quiet about.
|
|
1957
|
+
*
|
|
1958
|
+
* @param {object} options
|
|
1959
|
+
* @param {boolean} [options.allowTestModifications] - the blunt form: all of them.
|
|
1960
|
+
* @param {string|string[]} [options.allowTestChanges] - kind names, comma-separated or an array.
|
|
1961
|
+
* @returns {{ all: boolean, kinds: Set<string>, unknown: string[] }}
|
|
1962
|
+
*/
|
|
1963
|
+
export function resolveAllowedTamperKinds(options = {}) {
|
|
1964
|
+
if (options.allowTestModifications === true) {
|
|
1965
|
+
return { all: true, kinds: new Set(TAMPER_KIND_NAMES), unknown: [] };
|
|
1966
|
+
}
|
|
1967
|
+
const raw = options.allowTestChanges;
|
|
1968
|
+
const list = (Array.isArray(raw) ? raw : [raw])
|
|
1969
|
+
.flatMap((v) => String(v == null ? "" : v).split(","))
|
|
1970
|
+
.map((v) => v.trim().toLowerCase())
|
|
1971
|
+
.filter(Boolean);
|
|
1972
|
+
|
|
1973
|
+
if (list.includes("all")) {
|
|
1974
|
+
return { all: true, kinds: new Set(TAMPER_KIND_NAMES), unknown: [] };
|
|
1975
|
+
}
|
|
1976
|
+
const kinds = new Set();
|
|
1977
|
+
const unknown = [];
|
|
1978
|
+
for (const name of list) {
|
|
1979
|
+
if (TAMPER_KIND_NAMES.includes(name)) kinds.add(name);
|
|
1980
|
+
else unknown.push(name);
|
|
1981
|
+
}
|
|
1982
|
+
return { all: false, kinds, unknown };
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1774
1985
|
/**
|
|
1775
1986
|
* Detects test file assertion tampering, weakening, or test skips.
|
|
1776
1987
|
*
|
|
@@ -1781,7 +1992,8 @@ function detectExpectationRewrites(file, hunks, stats, violations) {
|
|
|
1781
1992
|
*/
|
|
1782
1993
|
export function checkTestTampering(diffOrText = "", options = {}) {
|
|
1783
1994
|
if (!diffOrText || typeof diffOrText !== "string") return { ok: true, violations: [] };
|
|
1784
|
-
|
|
1995
|
+
const allowed = resolveAllowedTamperKinds(options);
|
|
1996
|
+
if (allowed.all) return { ok: true, violations: [] };
|
|
1785
1997
|
|
|
1786
1998
|
const violations = [];
|
|
1787
1999
|
const lines = diffOrText.split("\n");
|
|
@@ -1987,9 +2199,17 @@ export function checkTestTampering(diffOrText = "", options = {}) {
|
|
|
1987
2199
|
}
|
|
1988
2200
|
}
|
|
1989
2201
|
|
|
2202
|
+
// A kind the operator has already looked at and accepted is dropped here
|
|
2203
|
+
// rather than never being computed, so the reasoning above stays one code
|
|
2204
|
+
// path regardless of what any given run allows.
|
|
2205
|
+
const reported =
|
|
2206
|
+
allowed.kinds.size === 0
|
|
2207
|
+
? violations
|
|
2208
|
+
: violations.filter((v) => !allowed.kinds.has(TAMPER_KINDS.get(v.type)));
|
|
2209
|
+
|
|
1990
2210
|
return {
|
|
1991
|
-
ok:
|
|
1992
|
-
violations,
|
|
2211
|
+
ok: reported.length === 0,
|
|
2212
|
+
violations: reported,
|
|
1993
2213
|
};
|
|
1994
2214
|
}
|
|
1995
2215
|
|