@mmnto/cli 1.105.0 → 1.107.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/dist/commands/doctor-qbd.d.ts +40 -0
- package/dist/commands/doctor-qbd.d.ts.map +1 -0
- package/dist/commands/doctor-qbd.js +156 -0
- package/dist/commands/doctor-qbd.js.map +1 -0
- package/dist/commands/doctor-qbd.test.d.ts +13 -0
- package/dist/commands/doctor-qbd.test.d.ts.map +1 -0
- package/dist/commands/doctor-qbd.test.js +548 -0
- package/dist/commands/doctor-qbd.test.js.map +1 -0
- package/dist/commands/mail-degraded-e4.test.d.ts +43 -0
- package/dist/commands/mail-degraded-e4.test.d.ts.map +1 -0
- package/dist/commands/mail-degraded-e4.test.js +317 -0
- package/dist/commands/mail-degraded-e4.test.js.map +1 -0
- package/dist/commands/orient.d.ts.map +1 -1
- package/dist/commands/orient.js +23 -0
- package/dist/commands/orient.js.map +1 -1
- package/dist/commands/qbd-seam.d.ts +77 -0
- package/dist/commands/qbd-seam.d.ts.map +1 -0
- package/dist/commands/qbd-seam.js +150 -0
- package/dist/commands/qbd-seam.js.map +1 -0
- package/dist/commands/qbd-seam.test.d.ts +16 -0
- package/dist/commands/qbd-seam.test.d.ts.map +1 -0
- package/dist/commands/qbd-seam.test.js +339 -0
- package/dist/commands/qbd-seam.test.js.map +1 -0
- package/dist/commands/review-fan.d.ts.map +1 -1
- package/dist/commands/review-fan.js +26 -0
- package/dist/commands/review-fan.js.map +1 -1
- package/dist/commands/run-compiled-rules.d.ts.map +1 -1
- package/dist/commands/run-compiled-rules.js +137 -5
- package/dist/commands/run-compiled-rules.js.map +1 -1
- package/dist/commands/run-compiled-rules.test.js +359 -1
- package/dist/commands/run-compiled-rules.test.js.map +1 -1
- package/dist/commands/search.d.ts.map +1 -1
- package/dist/commands/search.js +15 -0
- package/dist/commands/search.js.map +1 -1
- package/dist/commands/shield.d.ts.map +1 -1
- package/dist/commands/shield.js +34 -0
- package/dist/commands/shield.js.map +1 -1
- package/dist/commands/spec.d.ts.map +1 -1
- package/dist/commands/spec.js +16 -0
- package/dist/commands/spec.js.map +1 -1
- package/dist/index.js +8 -1
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as fs from 'node:fs';
|
|
2
2
|
import * as os from 'node:os';
|
|
3
3
|
import * as path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
4
5
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
5
6
|
import * as totem from '@mmnto/totem';
|
|
6
7
|
import { readLedgerEvents, saveCompiledRules, TotemError, TotemParseError } from '@mmnto/totem';
|
|
@@ -1120,10 +1121,18 @@ describe('--ast-parse-mode lenient', () => {
|
|
|
1120
1121
|
});
|
|
1121
1122
|
// totem-context: helper has no OrExit suffix; the rule misclassifies test-fixture builders that return literal objects
|
|
1122
1123
|
function makeAstRuleAndDiff() {
|
|
1124
|
+
// fileGlobs target a REGISTERED extension on purpose. These tests mock
|
|
1125
|
+
// `applyAstRulesToAdditions` wholesale — the fixture rule exists only to make
|
|
1126
|
+
// `astRules.length > 0` so the mocked pipeline is reached, and the rust
|
|
1127
|
+
// wording lives in the mocked error, not the rule. A `**/*.rs` glob would now
|
|
1128
|
+
// be condemned by the rule-load target-mismatch guard
|
|
1129
|
+
// (mmnto-ai/totem-strategy#971, Prop 309 Class 7) before the mock is ever
|
|
1130
|
+
// consulted, which would test the guard instead of the parse-mode routing
|
|
1131
|
+
// these cases exist for. The guard's own coverage lives in its describe block.
|
|
1123
1132
|
const astRule = makeRule('', 'rust pattern', 'No unsafe', {
|
|
1124
1133
|
engine: 'ast-grep',
|
|
1125
1134
|
astGrepPattern: 'unsafe { $$$ }',
|
|
1126
|
-
fileGlobs: ['**/*.
|
|
1135
|
+
fileGlobs: ['**/*.ts'],
|
|
1127
1136
|
});
|
|
1128
1137
|
saveCompiledRules(path.join(tmpDir, TOTEM_DIR, 'compiled-rules.json'), [astRule]);
|
|
1129
1138
|
const diff = `diff --git a/src/lib.rs b/src/lib.rs
|
|
@@ -1535,4 +1544,353 @@ describe('corpus-bearing zero-rules hard-error (mmnto-ai/totem-strategy#971)', (
|
|
|
1535
1544
|
})).rejects.toThrow(/enforcement disarmed/i);
|
|
1536
1545
|
});
|
|
1537
1546
|
});
|
|
1547
|
+
// ─── Target-mismatched rule hard-error (mmnto-ai/totem-strategy#971, Prop 309 Class 7) ──
|
|
1548
|
+
//
|
|
1549
|
+
// An AST rule whose declared `fileGlobs` ALL target extensions with no
|
|
1550
|
+
// registered Tree-sitter language can never execute. Worse, it does not fail
|
|
1551
|
+
// quietly: `rule-engine.ts` throws the moment a diff contains a file the rule's
|
|
1552
|
+
// globs claim, and that throw aborts the entire lint. Before this guard the
|
|
1553
|
+
// failure was diff-dependent — green on every run until the unlucky one. The
|
|
1554
|
+
// guard moves it to rule-LOAD time so it fires deterministically.
|
|
1555
|
+
describe('target-mismatched rule hard-error (mmnto-ai/totem-strategy#971, Prop 309 Class 7)', () => {
|
|
1556
|
+
let tmpDir;
|
|
1557
|
+
beforeEach(() => {
|
|
1558
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'totem-rcr-target-'));
|
|
1559
|
+
fs.mkdirSync(path.join(tmpDir, TOTEM_DIR), { recursive: true });
|
|
1560
|
+
});
|
|
1561
|
+
afterEach(() => {
|
|
1562
|
+
cleanTmpDir(tmpDir);
|
|
1563
|
+
});
|
|
1564
|
+
/** The observed specimen's shape: ast-grep scoped entirely to `.json` files. */
|
|
1565
|
+
function mismatchedAstGrepRule(overrides = {}) {
|
|
1566
|
+
return makeRule('', 'No inline secrets in agent config', 'No inline secrets', {
|
|
1567
|
+
lessonHash: 'target-mismatch-astgrep',
|
|
1568
|
+
engine: 'ast-grep',
|
|
1569
|
+
astGrepPattern: 'pair($KEY, $VALUE)',
|
|
1570
|
+
fileGlobs: ['**/.mcp.json', '**/.cursor/mcp.json'],
|
|
1571
|
+
...overrides,
|
|
1572
|
+
});
|
|
1573
|
+
}
|
|
1574
|
+
const cleanDiff = () => makeDiff('src/app.ts', ' const x = 1;');
|
|
1575
|
+
// ── Induced failure ─────────────────────────────────
|
|
1576
|
+
it('hard-errors at load when every positive glob of an ast-grep rule targets an unregistered extension', async () => {
|
|
1577
|
+
writeRules(tmpDir, [mismatchedAstGrepRule()]);
|
|
1578
|
+
await expect(runCompiledRules({
|
|
1579
|
+
diff: cleanDiff(),
|
|
1580
|
+
cwd: tmpDir,
|
|
1581
|
+
totemDir: TOTEM_DIR,
|
|
1582
|
+
format: 'text',
|
|
1583
|
+
tag: 'Test',
|
|
1584
|
+
})).rejects.toThrow(/enforcement disarmed/i);
|
|
1585
|
+
});
|
|
1586
|
+
it('names the lessonHash, the engine, every glob, the unregistered extension, and the archive-or-fix remedy', async () => {
|
|
1587
|
+
writeRules(tmpDir, [mismatchedAstGrepRule()]);
|
|
1588
|
+
let message = '';
|
|
1589
|
+
let hint = '';
|
|
1590
|
+
try {
|
|
1591
|
+
await runCompiledRules({
|
|
1592
|
+
diff: cleanDiff(),
|
|
1593
|
+
cwd: tmpDir,
|
|
1594
|
+
totemDir: TOTEM_DIR,
|
|
1595
|
+
format: 'text',
|
|
1596
|
+
tag: 'Test',
|
|
1597
|
+
});
|
|
1598
|
+
}
|
|
1599
|
+
catch (err) {
|
|
1600
|
+
message = err instanceof Error ? err.message : String(err);
|
|
1601
|
+
hint = err instanceof TotemError ? err.recoveryHint : '';
|
|
1602
|
+
}
|
|
1603
|
+
// Everything the operator needs to act without opening the manifest.
|
|
1604
|
+
expect(message).toContain('target-mismatch-astgrep');
|
|
1605
|
+
expect(message).toContain('No inline secrets');
|
|
1606
|
+
expect(message).toContain("engine 'ast-grep'");
|
|
1607
|
+
expect(message).toContain('**/.mcp.json');
|
|
1608
|
+
expect(message).toContain('**/.cursor/mcp.json');
|
|
1609
|
+
expect(message).toContain('.json');
|
|
1610
|
+
// The detonation mechanism is named, not just the mismatch.
|
|
1611
|
+
expect(message).toContain('abort the entire lint');
|
|
1612
|
+
// Both remedies, plus the registry snapshot that makes "fix the globs" actionable.
|
|
1613
|
+
expect(hint).toContain("status: 'archived'");
|
|
1614
|
+
expect(hint).toContain('fileGlobs');
|
|
1615
|
+
expect(hint).toContain('.tsx');
|
|
1616
|
+
});
|
|
1617
|
+
it("hard-errors for the engine:'ast' variant — the abort seam unions both AST engines", async () => {
|
|
1618
|
+
// rule-engine.ts searches `allAstRules` (Tree-sitter 'ast' ∪ 'ast-grep') for
|
|
1619
|
+
// a rule claiming the unparseable file, so an 'ast' rule arms the identical
|
|
1620
|
+
// landmine and must be validated identically.
|
|
1621
|
+
writeRules(tmpDir, [
|
|
1622
|
+
makeRule('', 'No secrets in settings', 'No secrets in settings', {
|
|
1623
|
+
lessonHash: 'target-mismatch-ast',
|
|
1624
|
+
engine: 'ast',
|
|
1625
|
+
astQuery: '(pair) @p',
|
|
1626
|
+
fileGlobs: ['**/settings.json'],
|
|
1627
|
+
}),
|
|
1628
|
+
]);
|
|
1629
|
+
let message = '';
|
|
1630
|
+
try {
|
|
1631
|
+
await runCompiledRules({
|
|
1632
|
+
diff: cleanDiff(),
|
|
1633
|
+
cwd: tmpDir,
|
|
1634
|
+
totemDir: TOTEM_DIR,
|
|
1635
|
+
format: 'text',
|
|
1636
|
+
tag: 'Test',
|
|
1637
|
+
});
|
|
1638
|
+
}
|
|
1639
|
+
catch (err) {
|
|
1640
|
+
message = err instanceof Error ? err.message : String(err);
|
|
1641
|
+
}
|
|
1642
|
+
expect(message).toMatch(/enforcement disarmed/i);
|
|
1643
|
+
expect(message).toContain('target-mismatch-ast');
|
|
1644
|
+
expect(message).toContain("engine 'ast'");
|
|
1645
|
+
});
|
|
1646
|
+
it("fires on a diff that touches NONE of the rule's globs — deterministic at load, not on first contact", async () => {
|
|
1647
|
+
// The whole point of the hardening. The diff is a .ts file; the broken rule
|
|
1648
|
+
// is scoped to .json. Pre-guard this run was green and the landmine stayed
|
|
1649
|
+
// armed for whichever future diff happened to touch a .json file.
|
|
1650
|
+
writeRules(tmpDir, [mismatchedAstGrepRule()]);
|
|
1651
|
+
await expect(runCompiledRules({
|
|
1652
|
+
diff: makeDiff('src/unrelated.ts', ' const unrelated = true;'),
|
|
1653
|
+
cwd: tmpDir,
|
|
1654
|
+
totemDir: TOTEM_DIR,
|
|
1655
|
+
format: 'json',
|
|
1656
|
+
tag: 'Test',
|
|
1657
|
+
})).rejects.toThrow(/enforcement disarmed/i);
|
|
1658
|
+
});
|
|
1659
|
+
it('reports every offender in one deterministic failure rather than stopping at the first', async () => {
|
|
1660
|
+
writeRules(tmpDir, [
|
|
1661
|
+
mismatchedAstGrepRule({ lessonHash: 'offender-one' }),
|
|
1662
|
+
mismatchedAstGrepRule({ lessonHash: 'offender-two', fileGlobs: ['config/*.yaml'] }),
|
|
1663
|
+
]);
|
|
1664
|
+
let message = '';
|
|
1665
|
+
try {
|
|
1666
|
+
await runCompiledRules({
|
|
1667
|
+
diff: cleanDiff(),
|
|
1668
|
+
cwd: tmpDir,
|
|
1669
|
+
totemDir: TOTEM_DIR,
|
|
1670
|
+
format: 'text',
|
|
1671
|
+
tag: 'Test',
|
|
1672
|
+
});
|
|
1673
|
+
}
|
|
1674
|
+
catch (err) {
|
|
1675
|
+
message = err instanceof Error ? err.message : String(err);
|
|
1676
|
+
}
|
|
1677
|
+
expect(message).toContain('2 active AST rule(s)');
|
|
1678
|
+
expect(message).toContain('offender-one');
|
|
1679
|
+
expect(message).toContain('offender-two');
|
|
1680
|
+
expect(message).toContain('.yaml');
|
|
1681
|
+
});
|
|
1682
|
+
// ── Operator escape (mmnto-ai/totem#1982) ───────────
|
|
1683
|
+
it('degrades to a warning under --ast-parse-mode lenient instead of hard-erroring', async () => {
|
|
1684
|
+
// Without this, the guard would remove the ONLY escape for a repo whose
|
|
1685
|
+
// rules target a pack language it has not installed — `.rs` here, exactly
|
|
1686
|
+
// the shape @mmnto/pack-rust-architecture provides.
|
|
1687
|
+
writeRules(tmpDir, [
|
|
1688
|
+
mismatchedAstGrepRule({ lessonHash: 'pack-language-rule', fileGlobs: ['**/*.rs'] }),
|
|
1689
|
+
]);
|
|
1690
|
+
const stderrSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
|
|
1691
|
+
try {
|
|
1692
|
+
const result = await runCompiledRules({
|
|
1693
|
+
diff: cleanDiff(),
|
|
1694
|
+
cwd: tmpDir,
|
|
1695
|
+
totemDir: TOTEM_DIR,
|
|
1696
|
+
format: 'json',
|
|
1697
|
+
tag: 'Test',
|
|
1698
|
+
astParseMode: 'lenient',
|
|
1699
|
+
});
|
|
1700
|
+
expect(result.rules).toHaveLength(1);
|
|
1701
|
+
expect(JSON.parse(result.output).pass).toBe(true);
|
|
1702
|
+
// The disclosure survives the exit-code escape: lenient covers the failure,
|
|
1703
|
+
// never the accounting.
|
|
1704
|
+
const messages = stderrSpy.mock.calls.map((c) => c.join(' '));
|
|
1705
|
+
const warning = messages.find((m) => m.includes('target-mismatched'));
|
|
1706
|
+
expect(warning).toBeDefined();
|
|
1707
|
+
expect(warning).toContain('pack-language-rule');
|
|
1708
|
+
expect(warning).toContain('.rs');
|
|
1709
|
+
expect(warning).toContain('totem sync --packs-only');
|
|
1710
|
+
}
|
|
1711
|
+
finally {
|
|
1712
|
+
stderrSpy.mockRestore();
|
|
1713
|
+
}
|
|
1714
|
+
});
|
|
1715
|
+
it('strict mode is the default — an omitted astParseMode still hard-errors', async () => {
|
|
1716
|
+
writeRules(tmpDir, [mismatchedAstGrepRule({ fileGlobs: ['**/*.rs'] })]);
|
|
1717
|
+
await expect(runCompiledRules({
|
|
1718
|
+
diff: cleanDiff(),
|
|
1719
|
+
cwd: tmpDir,
|
|
1720
|
+
totemDir: TOTEM_DIR,
|
|
1721
|
+
format: 'text',
|
|
1722
|
+
tag: 'Test',
|
|
1723
|
+
})).rejects.toThrow(/enforcement disarmed/i);
|
|
1724
|
+
});
|
|
1725
|
+
// ── Negative controls ───────────────────────────────
|
|
1726
|
+
it('control: the same rule with status:archived loads clean — archiving is the offered remedy', async () => {
|
|
1727
|
+
// Closes the loop on the offered fix: loadCompiledRules drops inert rules
|
|
1728
|
+
// before the guard ever sees them, so following the hint actually works.
|
|
1729
|
+
writeRules(tmpDir, [mismatchedAstGrepRule({ status: 'archived' })]);
|
|
1730
|
+
const stderrSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
|
|
1731
|
+
try {
|
|
1732
|
+
const result = await runCompiledRules({
|
|
1733
|
+
diff: cleanDiff(),
|
|
1734
|
+
cwd: tmpDir,
|
|
1735
|
+
totemDir: TOTEM_DIR,
|
|
1736
|
+
format: 'text',
|
|
1737
|
+
tag: 'Test',
|
|
1738
|
+
});
|
|
1739
|
+
expect(result.rules).toHaveLength(0);
|
|
1740
|
+
expect(result.violations).toHaveLength(0);
|
|
1741
|
+
}
|
|
1742
|
+
finally {
|
|
1743
|
+
stderrSpy.mockRestore();
|
|
1744
|
+
}
|
|
1745
|
+
});
|
|
1746
|
+
it('control: a globless ast-grep rule loads clean — the documented Lang.Tsx fallback stays permitted', async () => {
|
|
1747
|
+
// Unscoped pre-1.16 rules legitimately carry no fileGlobs, and
|
|
1748
|
+
// rule-engine.ts only throws for a rule with non-empty globs, so a globless
|
|
1749
|
+
// rule cannot arm the landmine.
|
|
1750
|
+
writeRules(tmpDir, [
|
|
1751
|
+
makeRule('', 'No foo log', 'No foo log', {
|
|
1752
|
+
engine: 'ast-grep',
|
|
1753
|
+
astGrepPattern: 'console.log("foo")',
|
|
1754
|
+
}),
|
|
1755
|
+
]);
|
|
1756
|
+
const result = await runCompiledRules({
|
|
1757
|
+
diff: cleanDiff(),
|
|
1758
|
+
cwd: tmpDir,
|
|
1759
|
+
totemDir: TOTEM_DIR,
|
|
1760
|
+
format: 'json',
|
|
1761
|
+
tag: 'Test',
|
|
1762
|
+
});
|
|
1763
|
+
expect(result.rules).toHaveLength(1);
|
|
1764
|
+
expect(JSON.parse(result.output).pass).toBe(true);
|
|
1765
|
+
});
|
|
1766
|
+
it('control: one resolvable glob among unresolvable ones loads clean', async () => {
|
|
1767
|
+
// The rule CAN execute — the condemnation requires that not one positive
|
|
1768
|
+
// glob resolves.
|
|
1769
|
+
writeRules(tmpDir, [mismatchedAstGrepRule({ fileGlobs: ['**/*.json', '**/*.ts'] })]);
|
|
1770
|
+
const result = await runCompiledRules({
|
|
1771
|
+
diff: cleanDiff(),
|
|
1772
|
+
cwd: tmpDir,
|
|
1773
|
+
totemDir: TOTEM_DIR,
|
|
1774
|
+
format: 'json',
|
|
1775
|
+
tag: 'Test',
|
|
1776
|
+
});
|
|
1777
|
+
expect(result.rules).toHaveLength(1);
|
|
1778
|
+
expect(JSON.parse(result.output).pass).toBe(true);
|
|
1779
|
+
});
|
|
1780
|
+
it('control: extensionless positive globs (src/**) load clean — absence of evidence is not proof of mismatch', async () => {
|
|
1781
|
+
// TRAILING_EXT_RE extracts nothing from `src/**` or `**/*.{ts,json}`, but at
|
|
1782
|
+
// run time both match .ts files and dispatch fine. Condemning them would
|
|
1783
|
+
// hard-error legitimately broad rules.
|
|
1784
|
+
writeRules(tmpDir, [
|
|
1785
|
+
mismatchedAstGrepRule({ lessonHash: 'broad-one', fileGlobs: ['src/**'] }),
|
|
1786
|
+
mismatchedAstGrepRule({ lessonHash: 'broad-two', fileGlobs: ['**/*.{ts,json}'] }),
|
|
1787
|
+
mismatchedAstGrepRule({ lessonHash: 'broad-three', fileGlobs: ['packages/**/*'] }),
|
|
1788
|
+
]);
|
|
1789
|
+
const result = await runCompiledRules({
|
|
1790
|
+
diff: cleanDiff(),
|
|
1791
|
+
cwd: tmpDir,
|
|
1792
|
+
totemDir: TOTEM_DIR,
|
|
1793
|
+
format: 'json',
|
|
1794
|
+
tag: 'Test',
|
|
1795
|
+
});
|
|
1796
|
+
expect(result.rules).toHaveLength(3);
|
|
1797
|
+
expect(JSON.parse(result.output).pass).toBe(true);
|
|
1798
|
+
});
|
|
1799
|
+
// ── .mts/.cts registration (mmnto-ai/totem#2513) ────
|
|
1800
|
+
it('control: an ast-grep rule scoped entirely to .mts/.cts loads clean — both are registered built-ins', async () => {
|
|
1801
|
+
// Before #2513 registered them, `.mts`/`.cts` were TOTAL mismatches and this
|
|
1802
|
+
// guard condemned an all-.mts rule at load. They resolve to the typescript
|
|
1803
|
+
// grammar now, so the guard must let them through.
|
|
1804
|
+
writeRules(tmpDir, [
|
|
1805
|
+
mismatchedAstGrepRule({ lessonHash: 'mts-only', fileGlobs: ['**/*.mts'] }),
|
|
1806
|
+
mismatchedAstGrepRule({ lessonHash: 'cts-only', fileGlobs: ['**/*.cts'] }),
|
|
1807
|
+
]);
|
|
1808
|
+
const result = await runCompiledRules({
|
|
1809
|
+
diff: cleanDiff(),
|
|
1810
|
+
cwd: tmpDir,
|
|
1811
|
+
totemDir: TOTEM_DIR,
|
|
1812
|
+
format: 'json',
|
|
1813
|
+
tag: 'Test',
|
|
1814
|
+
});
|
|
1815
|
+
expect(result.rules).toHaveLength(2);
|
|
1816
|
+
expect(JSON.parse(result.output).pass).toBe(true);
|
|
1817
|
+
});
|
|
1818
|
+
it('still condemns a rule scoped entirely to a dead dummy extension — registration did not soften the guard', async () => {
|
|
1819
|
+
// The partial-permit semantics this guard ships with are unchanged by
|
|
1820
|
+
// #2513: an extension NO language covers is still a load-time hard error.
|
|
1821
|
+
writeRules(tmpDir, [
|
|
1822
|
+
mismatchedAstGrepRule({ lessonHash: 'dead-ext-only', fileGlobs: ['**/*.zzznotalang'] }),
|
|
1823
|
+
]);
|
|
1824
|
+
let message = '';
|
|
1825
|
+
try {
|
|
1826
|
+
await runCompiledRules({
|
|
1827
|
+
diff: cleanDiff(),
|
|
1828
|
+
cwd: tmpDir,
|
|
1829
|
+
totemDir: TOTEM_DIR,
|
|
1830
|
+
format: 'text',
|
|
1831
|
+
tag: 'Test',
|
|
1832
|
+
});
|
|
1833
|
+
}
|
|
1834
|
+
catch (err) {
|
|
1835
|
+
message = err instanceof Error ? err.message : String(err);
|
|
1836
|
+
}
|
|
1837
|
+
expect(message).toMatch(/enforcement disarmed/i);
|
|
1838
|
+
expect(message).toContain('dead-ext-only');
|
|
1839
|
+
expect(message).toContain('.zzznotalang');
|
|
1840
|
+
});
|
|
1841
|
+
it('control: a REGEX rule scoped entirely to .json is untouched — the guard is AST-only', async () => {
|
|
1842
|
+
// Regex rules never dispatch through the Tree-sitter language registry, so
|
|
1843
|
+
// .json scoping is completely legitimate for them.
|
|
1844
|
+
writeRules(tmpDir, [
|
|
1845
|
+
makeRule('"apiKey"', 'No inline keys', 'No inline keys', {
|
|
1846
|
+
fileGlobs: ['**/.mcp.json'],
|
|
1847
|
+
}),
|
|
1848
|
+
]);
|
|
1849
|
+
const result = await runCompiledRules({
|
|
1850
|
+
diff: cleanDiff(),
|
|
1851
|
+
cwd: tmpDir,
|
|
1852
|
+
totemDir: TOTEM_DIR,
|
|
1853
|
+
format: 'json',
|
|
1854
|
+
tag: 'Test',
|
|
1855
|
+
});
|
|
1856
|
+
expect(result.rules).toHaveLength(1);
|
|
1857
|
+
expect(JSON.parse(result.output).pass).toBe(true);
|
|
1858
|
+
});
|
|
1859
|
+
it('control: an all-negation glob list loads clean — it matches every file, including resolvable ones', async () => {
|
|
1860
|
+
writeRules(tmpDir, [mismatchedAstGrepRule({ fileGlobs: ['!**/*.json'] })]);
|
|
1861
|
+
const result = await runCompiledRules({
|
|
1862
|
+
diff: cleanDiff(),
|
|
1863
|
+
cwd: tmpDir,
|
|
1864
|
+
totemDir: TOTEM_DIR,
|
|
1865
|
+
format: 'json',
|
|
1866
|
+
tag: 'Test',
|
|
1867
|
+
});
|
|
1868
|
+
expect(result.rules).toHaveLength(1);
|
|
1869
|
+
expect(JSON.parse(result.output).pass).toBe(true);
|
|
1870
|
+
});
|
|
1871
|
+
// ── The real corpus ─────────────────────────────────
|
|
1872
|
+
it("the repository's own .totem/compiled-rules.json loads clean through the guard", async () => {
|
|
1873
|
+
// Regression anchor for the corpus itself: both known specimens are archived,
|
|
1874
|
+
// so the live manifest must pass. Copied into the tmp dir so the run's metric
|
|
1875
|
+
// and ledger writes never touch the real .totem.
|
|
1876
|
+
const realManifest = fileURLToPath(new URL('../../../../.totem/compiled-rules.json', import.meta.url));
|
|
1877
|
+
// A missing fixture would make every assertion below vacuous — fail loudly
|
|
1878
|
+
// instead of silently skipping.
|
|
1879
|
+
expect(fs.existsSync(realManifest)).toBe(true);
|
|
1880
|
+
fs.copyFileSync(realManifest, path.join(tmpDir, TOTEM_DIR, 'compiled-rules.json'));
|
|
1881
|
+
// A .txt path no compiled rule scopes to, so the run exercises rule LOAD
|
|
1882
|
+
// (where the guard lives) without dispatching AST rules against fixtures
|
|
1883
|
+
// that do not exist in the tmp dir.
|
|
1884
|
+
const result = await runCompiledRules({
|
|
1885
|
+
diff: makeDiff('docs/example.txt', 'A plain note.'),
|
|
1886
|
+
cwd: tmpDir,
|
|
1887
|
+
totemDir: TOTEM_DIR,
|
|
1888
|
+
format: 'json',
|
|
1889
|
+
tag: 'Test',
|
|
1890
|
+
});
|
|
1891
|
+
// Non-vacuity: the guard actually had globbed AST rules to inspect.
|
|
1892
|
+
const globbedAstRules = result.rules.filter((r) => (r.engine === 'ast-grep' || r.engine === 'ast') && r.fileGlobs && r.fileGlobs.length > 0);
|
|
1893
|
+
expect(globbedAstRules.length).toBeGreaterThan(0);
|
|
1894
|
+
});
|
|
1895
|
+
});
|
|
1538
1896
|
//# sourceMappingURL=run-compiled-rules.test.js.map
|