@boyingliu01/xp-gate 0.31.3 → 0.32.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/adapters/java.sh +256 -4
- package/adapters/powershell.sh +45 -6
- package/bin/xp-gate.js +8 -2
- package/build-integrity/__tests__/gate-10.test.ts +88 -1
- package/build-integrity/gate-10.ts +51 -16
- package/dsh.bundle.patch.yml +15 -0
- package/gate-13-compiler-strictness.sh +1031 -0
- package/gate-3.sh +48 -13
- package/hooks/gate-13-compiler-strictness.sh +1031 -0
- package/hooks/gate-3.sh +48 -13
- package/hooks/lib/coverage-total.sh +18 -2
- package/hooks/lib/java-test-selection.sh +251 -0
- package/hooks/lib/jscpd-run.sh +9 -0
- package/hooks/lib/validate-code-walkthrough.cjs +186 -29
- package/hooks/pre-commit +269 -13
- package/lib/__tests__/detect-deps-dsh-profile.test.js +106 -0
- package/lib/__tests__/doctor-tui-skill-freshness.test.js +140 -0
- package/lib/__tests__/gate-runner.test.js +29 -2
- package/lib/__tests__/issue-586-principles-cmdline.test.js +35 -0
- package/lib/__tests__/issue-618-gate-not-run.test.js +100 -0
- package/lib/__tests__/shared-utils.test.js +21 -0
- package/lib/check.js +6 -2
- package/lib/detect-deps.js +42 -10
- package/lib/doctor-tui.js +111 -21
- package/lib/gate-runner.js +111 -37
- package/lib/mattpocock-license-verdict.json +1 -1
- package/lib/principles.js +48 -6
- package/lib/shared-utils.js +8 -0
- package/mock-policy/AGENTS.md +3 -3
- package/mock-policy/gate-m3.ts +57 -23
- package/mutation/AGENTS.md +3 -3
- package/mutation/__tests__/pitest-gradle-availability.test.ts +74 -0
- package/mutation/__tests__/test-filter-parity.test.ts +5 -2
- package/mutation/gate-m.ts +30 -24
- package/mutation/runners/mutmut-runner.ts +80 -30
- package/mutation/runners/pitest-runner.ts +60 -42
- package/package.json +14 -2
- package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-code/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/claude-code/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/dsh/lib/command.d.ts +38 -0
- package/plugins/dsh/lib/command.d.ts.map +1 -0
- package/plugins/dsh/lib/command.js +87 -0
- package/plugins/dsh/lib/credentials.d.ts +39 -0
- package/plugins/dsh/lib/credentials.d.ts.map +1 -0
- package/plugins/dsh/lib/credentials.js +57 -0
- package/plugins/dsh/lib/delphi-config.d.ts +129 -0
- package/plugins/dsh/lib/delphi-config.d.ts.map +1 -0
- package/plugins/dsh/lib/delphi-config.js +369 -0
- package/plugins/dsh/lib/delphi-run.d.ts +24 -0
- package/plugins/dsh/lib/delphi-run.d.ts.map +1 -0
- package/plugins/dsh/lib/delphi-run.js +113 -0
- package/plugins/dsh/lib/delphi.d.ts +94 -0
- package/plugins/dsh/lib/delphi.d.ts.map +1 -0
- package/plugins/dsh/lib/delphi.js +207 -0
- package/plugins/dsh/lib/dialect.d.ts +24 -0
- package/plugins/dsh/lib/dialect.d.ts.map +1 -0
- package/plugins/dsh/lib/dialect.js +36 -0
- package/plugins/dsh/lib/evidence.d.ts +70 -0
- package/plugins/dsh/lib/evidence.d.ts.map +1 -0
- package/plugins/dsh/lib/evidence.js +92 -0
- package/plugins/dsh/lib/gate-runner.d.ts +49 -0
- package/plugins/dsh/lib/gate-runner.d.ts.map +1 -0
- package/plugins/dsh/lib/gate-runner.js +95 -0
- package/plugins/dsh/lib/git-context.d.ts +26 -0
- package/plugins/dsh/lib/git-context.d.ts.map +1 -0
- package/plugins/dsh/lib/git-context.js +30 -0
- package/plugins/dsh/lib/hooks.d.ts +42 -0
- package/plugins/dsh/lib/hooks.d.ts.map +1 -0
- package/plugins/dsh/lib/hooks.js +113 -0
- package/plugins/dsh/lib/index.d.ts +20 -0
- package/plugins/dsh/lib/index.d.ts.map +1 -0
- package/plugins/dsh/lib/index.js +430 -0
- package/plugins/dsh/lib/skill-provider.d.ts +15 -0
- package/plugins/dsh/lib/skill-provider.d.ts.map +1 -0
- package/plugins/dsh/lib/skill-provider.js +205 -0
- package/plugins/dsh/lib/sprint.d.ts +60 -0
- package/plugins/dsh/lib/sprint.d.ts.map +1 -0
- package/plugins/dsh/lib/sprint.js +92 -0
- package/plugins/dsh/package.json +14 -8
- package/plugins/dsh/scripts/verify-skill-provider.mjs +113 -0
- package/plugins/dsh/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/dsh/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/dsh/src/command-single-form.test.ts +67 -0
- package/plugins/dsh/src/command.test.ts +21 -17
- package/plugins/dsh/src/command.ts +19 -21
- package/plugins/dsh/src/compat.test.ts +200 -0
- package/plugins/dsh/src/contract.test.ts +7 -1
- package/plugins/dsh/src/credentials.test.ts +93 -0
- package/plugins/dsh/src/credentials.ts +71 -0
- package/plugins/dsh/src/delphi-cancellation.test.ts +71 -0
- package/plugins/dsh/src/delphi-config-apikey-override.test.ts +94 -0
- package/plugins/dsh/src/delphi-config-credential-refs.test.ts +75 -0
- package/plugins/dsh/src/delphi-config.ts +146 -53
- package/plugins/dsh/src/delphi-run.ts +27 -4
- package/plugins/dsh/src/delphi.ts +133 -65
- package/plugins/dsh/src/evidence.test.ts +74 -0
- package/plugins/dsh/src/evidence.ts +141 -0
- package/plugins/dsh/src/gate-runner.test.ts +14 -6
- package/plugins/dsh/src/gate-runner.ts +68 -5
- package/plugins/dsh/src/git-context.test.ts +39 -0
- package/plugins/dsh/src/git-context.ts +38 -0
- package/plugins/dsh/src/hooks.test.ts +87 -0
- package/plugins/dsh/src/hooks.ts +126 -0
- package/plugins/dsh/src/index.test.ts +63 -6
- package/plugins/dsh/src/index.ts +340 -13
- package/plugins/dsh/src/skill-provider.test.ts +194 -0
- package/plugins/dsh/src/skill-provider.ts +231 -0
- package/plugins/dsh/src/sprint.test.ts +64 -0
- package/plugins/dsh/src/sprint.ts +122 -0
- package/plugins/dsh/src/tri-state.test.ts +123 -0
- package/plugins/opencode/index.ts +192 -58
- package/plugins/opencode/package.json +1 -1
- package/plugins/opencode/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/opencode/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/qoder/plugin.json +1 -1
- package/plugins/qoder/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/qoder/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +3 -3
- package/principles/AGENTS.md +3 -3
- package/principles/__tests__/analyze-read-failure.test.ts +70 -0
- package/principles/__tests__/baseline-entry-total-warnings.test.ts +76 -0
- package/principles/__tests__/baseline-prune.test.ts +61 -0
- package/principles/__tests__/cli-files-separators.test.ts +69 -0
- package/principles/__tests__/config-merge-warnings.test.ts +148 -0
- package/principles/adapters/__tests__/cpp-code-block.test.ts +123 -0
- package/principles/adapters/__tests__/cpp-scanner-equivalence.test.ts +67 -0
- package/principles/adapters/__tests__/typescript-block-comment-lines.test.ts +53 -0
- package/principles/adapters/__tests__/typescript-masking.test.ts +96 -0
- package/principles/adapters/cpp.ts +102 -50
- package/principles/adapters/java.ts +22 -3
- package/principles/adapters/typescript.ts +548 -178
- package/principles/analyzer.ts +36 -3
- package/principles/baseline-prune.ts +19 -7
- package/principles/baseline.ts +58 -70
- package/principles/config.ts +56 -35
- package/principles/index.ts +122 -40
- package/principles/rules/clean-code/deep-nesting.ts +36 -22
- package/principles/rules/clean-code/unused-imports.ts +30 -19
- package/principles/rules/solid/dip.ts +30 -20
- package/principles/rules/solid/lsp.ts +17 -2
- package/principles/rules/solid/ocp.ts +33 -23
- package/principles/rules/solid/srp.ts +65 -23
- package/scripts/delphi-external-review.cjs +25 -2
- package/scripts/sync-package-content.js +28 -1
- package/skills/delphi-review/AGENTS.md +3 -3
- package/skills/sprint-flow/AGENTS.md +3 -3
- package/skills/test-specification-alignment/AGENTS.md +3 -3
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @test REQ-620
|
|
3
|
+
* @intent Pin the single behavioural difference introduced when the two copies
|
|
4
|
+
* of the per-tool accumulator were merged into buildEntry:
|
|
5
|
+
* `createFromFiles` tested `totalWarnings` for TRUTHINESS, so a caller
|
|
6
|
+
* passing an explicit `0` had it replaced by the sum of the per-tool
|
|
7
|
+
* counts. That is exactly the case where the caller is overriding a
|
|
8
|
+
* stale per-tool number, so the override was silently discarded.
|
|
9
|
+
*
|
|
10
|
+
* `initializeWithAnalyzer` already used `!== undefined`, so the two
|
|
11
|
+
* entry points disagreed about the same input. The merged version uses
|
|
12
|
+
* the `!== undefined` reading for both.
|
|
13
|
+
* @covers AC-620-04
|
|
14
|
+
*/
|
|
15
|
+
import { describe, it, expect } from 'vitest';
|
|
16
|
+
import { BaselineStorage } from '../baseline';
|
|
17
|
+
|
|
18
|
+
function storage(): InstanceType<typeof BaselineStorage> {
|
|
19
|
+
// Explicit config: a timeout far above any test's runtime.
|
|
20
|
+
return new BaselineStorage({ maxSize: 100, timeoutMs: 10000, batchSize: 5 });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('AC-620-04: an explicit totalWarnings wins, including zero', () => {
|
|
24
|
+
it('uses an explicit zero instead of the summed per-tool count', async () => {
|
|
25
|
+
const baseline = await storage().createFromFiles([
|
|
26
|
+
{ file: 'a.ts', counts: { eslint: { warnings: 3, errors: 0 }, totalWarnings: 0 } },
|
|
27
|
+
]);
|
|
28
|
+
expect(baseline['a.ts'].totalWarnings).toBe(0);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('uses an explicit nonzero value instead of the summed count', async () => {
|
|
32
|
+
const baseline = await storage().createFromFiles([
|
|
33
|
+
{ file: 'a.ts', counts: { eslint: { warnings: 3, errors: 0 }, totalWarnings: 42 } },
|
|
34
|
+
]);
|
|
35
|
+
expect(baseline['a.ts'].totalWarnings).toBe(42);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('sums the per-tool counts when no explicit total is given', async () => {
|
|
39
|
+
const baseline = await storage().createFromFiles([
|
|
40
|
+
{
|
|
41
|
+
file: 'a.ts',
|
|
42
|
+
counts: {
|
|
43
|
+
eslint: { warnings: 3, errors: 0 },
|
|
44
|
+
ruff: { warnings: 2, errors: 1 },
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
]);
|
|
48
|
+
expect(baseline['a.ts'].totalWarnings).toBe(5);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('keeps every per-tool count it was given', async () => {
|
|
52
|
+
const baseline = await storage().createFromFiles([
|
|
53
|
+
{
|
|
54
|
+
file: 'a.ts',
|
|
55
|
+
counts: {
|
|
56
|
+
eslint: { warnings: 1, errors: 2 },
|
|
57
|
+
ccn: { warnings: 3, max: 9 },
|
|
58
|
+
shellcheck: { warnings: 4, errors: 5 },
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
]);
|
|
62
|
+
const entry = baseline['a.ts'];
|
|
63
|
+
expect(entry.eslint).toEqual({ warnings: 1, errors: 2 });
|
|
64
|
+
expect(entry.ccn).toEqual({ warnings: 3, max: 9 });
|
|
65
|
+
expect(entry.shellcheck).toEqual({ warnings: 4, errors: 5 });
|
|
66
|
+
// Tools that were not supplied must not appear at all.
|
|
67
|
+
expect(entry.ruff).toBeUndefined();
|
|
68
|
+
expect(entry.golangci).toBeUndefined();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('reports zero warnings for a file with no tool counts', async () => {
|
|
72
|
+
const baseline = await storage().createFromFiles([{ file: 'a.ts', counts: {} }]);
|
|
73
|
+
expect(baseline['a.ts'].totalWarnings).toBe(0);
|
|
74
|
+
expect(baseline['a.ts'].lastAnalyzed).toBeTypeOf('string');
|
|
75
|
+
});
|
|
76
|
+
});
|
|
@@ -336,3 +336,64 @@ describe('#452 REQ-3 stale baseline entries are pruned', () => {
|
|
|
336
336
|
}
|
|
337
337
|
});
|
|
338
338
|
});
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* @test REQ-628
|
|
342
|
+
* @intent `parseBaselineFiles` accepts two shapes: the FLAT map that
|
|
343
|
+
* `saveBaseline` writes, and a legacy `{files: {...}}` wrapper kept for
|
|
344
|
+
* forwards compatibility. The wrapper branch had no test, so deleting it
|
|
345
|
+
* left the whole suite green — a silent regression that would make every
|
|
346
|
+
* real baseline look empty (all entries filtered out as non-objects,
|
|
347
|
+
* since the only top-level key is `files`). These cases pin both shapes
|
|
348
|
+
* plus the scalar-filtering rule that follows from them.
|
|
349
|
+
* @covers AC-628-01, AC-628-02, AC-628-03
|
|
350
|
+
*/
|
|
351
|
+
describe('#628 REQ-3 parseBaselineFiles shape handling', () => {
|
|
352
|
+
it('AC-628-01: a flat map is read entry-for-entry', () => {
|
|
353
|
+
const { root } = makeRepoWithCommit();
|
|
354
|
+
const flat = JSON.stringify({
|
|
355
|
+
'src/a.ts': { totalWarnings: 2, lastAnalyzed: '2026-01-01T00:00:00Z' },
|
|
356
|
+
'src/gone.ts': { totalWarnings: 1, lastAnalyzed: '2026-01-01T00:00:00Z' },
|
|
357
|
+
});
|
|
358
|
+
stageIndexOnly(root, ['src/a.ts']);
|
|
359
|
+
|
|
360
|
+
const result = pruneBaselineEntries(flat, root);
|
|
361
|
+
expect(result.kept).toContain('src/a.ts');
|
|
362
|
+
expect(result.removed).toContain('src/gone.ts');
|
|
363
|
+
expect(result.error).toBeUndefined();
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
it('AC-628-02: a {files:{...}} wrapper is unwrapped, not treated as one entry', () => {
|
|
367
|
+
const { root } = makeRepoWithCommit();
|
|
368
|
+
// Without the unwrap, the sole top-level key `files` is an object, so the
|
|
369
|
+
// flat path would yield ONE entry called "files" and silently lose every
|
|
370
|
+
// real path. Assert the real paths surface.
|
|
371
|
+
const wrapped = JSON.stringify({
|
|
372
|
+
files: {
|
|
373
|
+
'src/a.ts': { totalWarnings: 2, lastAnalyzed: '2026-01-01T00:00:00Z' },
|
|
374
|
+
'src/gone.ts': { totalWarnings: 1, lastAnalyzed: '2026-01-01T00:00:00Z' },
|
|
375
|
+
},
|
|
376
|
+
});
|
|
377
|
+
stageIndexOnly(root, ['src/a.ts']);
|
|
378
|
+
|
|
379
|
+
const result = pruneBaselineEntries(wrapped, root);
|
|
380
|
+
expect(result.kept).toContain('src/a.ts');
|
|
381
|
+
expect(result.removed).toContain('src/gone.ts');
|
|
382
|
+
expect(result.kept).not.toContain('files');
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it('AC-628-03: scalar top-level keys are metadata and are dropped, not kept as entries', () => {
|
|
386
|
+
const { root } = makeRepoWithCommit();
|
|
387
|
+
const withMeta = JSON.stringify({
|
|
388
|
+
version: 2,
|
|
389
|
+
generatedAt: '2026-01-01T00:00:00Z',
|
|
390
|
+
'src/a.ts': { totalWarnings: 2, lastAnalyzed: '2026-01-01T00:00:00Z' },
|
|
391
|
+
});
|
|
392
|
+
stageIndexOnly(root, ['src/a.ts']);
|
|
393
|
+
|
|
394
|
+
const result = pruneBaselineEntries(withMeta, root);
|
|
395
|
+
expect(result.kept).toEqual(['src/a.ts']);
|
|
396
|
+
expect(result.kept).not.toContain('version');
|
|
397
|
+
expect(result.kept).not.toContain('generatedAt');
|
|
398
|
+
});
|
|
399
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @test REQ-623
|
|
3
|
+
* @intent `--files` must accept the shapes a shell actually produces. CI passed
|
|
4
|
+
* `--files "$SRC_FILES"` -- one argument containing newline-separated
|
|
5
|
+
* paths -- and parseArgs split on spaces only, so the whole blob became
|
|
6
|
+
* a single impossible path. The run then had nothing to analyse and fell
|
|
7
|
+
* through to the console/empty path, writing "✓ No violations found"
|
|
8
|
+
* into a file named principles.sarif. The step exited 0, so for months
|
|
9
|
+
* the GitHub Security tab received a text file where SARIF was expected.
|
|
10
|
+
*
|
|
11
|
+
* These tests pin the separator handling, not one call site: a fix that
|
|
12
|
+
* only changes the workflow leaves the next caller to hit the same trap.
|
|
13
|
+
* @covers AC-623-01, AC-623-02, AC-623-03
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { describe, it, expect } from 'vitest';
|
|
17
|
+
import { parseArgs } from '../index';
|
|
18
|
+
|
|
19
|
+
describe('AC-623-01: --files separates on any whitespace, not just spaces', () => {
|
|
20
|
+
it('splits a newline-separated list (the CI shape)', () => {
|
|
21
|
+
const opts = parseArgs(['--files', 'a.ts\nb.ts\nc.ts']);
|
|
22
|
+
expect(opts.files).toEqual(['a.ts', 'b.ts', 'c.ts']);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('splits a space-separated list', () => {
|
|
26
|
+
const opts = parseArgs(['--files', 'a.ts b.ts']);
|
|
27
|
+
expect(opts.files).toEqual(['a.ts', 'b.ts']);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('splits a tab-separated list', () => {
|
|
31
|
+
const opts = parseArgs(['--files', 'a.ts\tb.ts']);
|
|
32
|
+
expect(opts.files).toEqual(['a.ts', 'b.ts']);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('drops the blank entries a trailing separators leave behind', () => {
|
|
36
|
+
const opts = parseArgs(['--files', 'a.ts\n\nb.ts\n']);
|
|
37
|
+
expect(opts.files).toEqual(['a.ts', 'b.ts']);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('keeps a single path intact', () => {
|
|
41
|
+
const opts = parseArgs(['--files', 'src/one.ts']);
|
|
42
|
+
expect(opts.files).toEqual(['src/one.ts']);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe('AC-623-02: a flag after --files is still parsed', () => {
|
|
47
|
+
it('reads --format sarif that follows a multi-line --files value', () => {
|
|
48
|
+
const opts = parseArgs(['--files', 'a.ts\nb.ts', '--format', 'sarif']);
|
|
49
|
+
expect(opts.format).toBe('sarif');
|
|
50
|
+
expect(opts.files).toEqual(['a.ts', 'b.ts']);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('reads --format when it comes before --files', () => {
|
|
54
|
+
const opts = parseArgs(['--format', 'sarif', '--files', 'a.ts\nb.ts']);
|
|
55
|
+
expect(opts.format).toBe('sarif');
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('AC-623-03: positional paths still accumulate', () => {
|
|
60
|
+
it('collects bare paths given without --files', () => {
|
|
61
|
+
const opts = parseArgs(['a.ts', 'b.ts']);
|
|
62
|
+
expect(opts.files).toEqual(['a.ts', 'b.ts']);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('accepts --files alongside positional paths', () => {
|
|
66
|
+
const opts = parseArgs(['--files', 'a.ts\nb.ts', 'c.ts']);
|
|
67
|
+
expect(opts.files).toEqual(['a.ts', 'b.ts', 'c.ts']);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @test REQ-620
|
|
3
|
+
* @intent Pin mergeRule's mistyped-override handling, which had NO coverage
|
|
4
|
+
* before this change: nothing asserted the warning text or the
|
|
5
|
+
* keep-the-default outcome for a bad .principlesrc value. That gap
|
|
6
|
+
* meant the four per-key loops could be consolidated into one guarded
|
|
7
|
+
* loop without any test noticing, so these tests are what makes the
|
|
8
|
+
* consolidation verifiable rather than merely plausible.
|
|
9
|
+
*
|
|
10
|
+
* Two properties per key category:
|
|
11
|
+
* 1. a correctly-typed override is applied;
|
|
12
|
+
* 2. a mistyped one is REJECTED and the default survives, with a
|
|
13
|
+
* warning naming the rule and the key.
|
|
14
|
+
* Property 2 is the one with teeth: silently accepting a bad value
|
|
15
|
+
* would let a project believe it tightened a gate.
|
|
16
|
+
*
|
|
17
|
+
* .principlesrc is JSON (config.ts parses it with JSON.parse), so the
|
|
18
|
+
* fixtures are JSON objects rather than YAML text.
|
|
19
|
+
* @covers AC-620-05
|
|
20
|
+
*/
|
|
21
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
22
|
+
import fs from 'node:fs';
|
|
23
|
+
import os from 'node:os';
|
|
24
|
+
import path from 'node:path';
|
|
25
|
+
import { loadConfig, getDefaultConfig } from '../config';
|
|
26
|
+
|
|
27
|
+
let tmpDirs: string[] = [];
|
|
28
|
+
let warnings: string[];
|
|
29
|
+
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
tmpDirs = [];
|
|
32
|
+
warnings = [];
|
|
33
|
+
// config.ts reports through process.stderr.write, not console.warn, so the
|
|
34
|
+
// spy has to sit on the stream. (Mocking console.warn instead silently
|
|
35
|
+
// captured nothing and made every "must warn" assertion look like a bug.)
|
|
36
|
+
vi.spyOn(process.stderr, 'write').mockImplementation((chunk: unknown) => {
|
|
37
|
+
warnings.push(String(chunk));
|
|
38
|
+
return true;
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
afterEach(() => {
|
|
43
|
+
vi.restoreAllMocks();
|
|
44
|
+
for (const dir of tmpDirs) fs.rmSync(dir, { recursive: true, force: true });
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
function warnText(): string {
|
|
48
|
+
return warnings.join('\n');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Default value of one key on `clean-code.long-function`. */
|
|
52
|
+
function defaultFor(key: string): unknown {
|
|
53
|
+
const rule = getDefaultConfig().rules['clean-code']['long-function'] as unknown as Record<string, unknown>;
|
|
54
|
+
return rule[key];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Write a .principlesrc from RAW JSON text and load it unchanged. */
|
|
58
|
+
async function loadRaw(jsonText: string) {
|
|
59
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'xp-gate-cfg-'));
|
|
60
|
+
tmpDirs.push(dir);
|
|
61
|
+
const cfgPath = path.join(dir, '.principlesrc');
|
|
62
|
+
fs.writeFileSync(cfgPath, jsonText, 'utf8');
|
|
63
|
+
return loadConfig(cfgPath);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Write a .principlesrc overriding `clean-code.long-function` and load it. */
|
|
67
|
+
async function loadWithRule(overrides: Record<string, unknown>) {
|
|
68
|
+
return loadRaw(JSON.stringify({ rules: { 'clean-code': { 'long-function': overrides } } }));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
describe('AC-620-05: a mistyped rule override keeps the default and warns', () => {
|
|
72
|
+
it('accepts a well-typed numeric override (threshold)', async () => {
|
|
73
|
+
const merged = await loadWithRule({ threshold: 12 });
|
|
74
|
+
expect(merged.rules['clean-code']['long-function'].threshold).toBe(12);
|
|
75
|
+
expect(warnText()).not.toMatch(/threshold/);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('rejects a non-numeric threshold and keeps the default', async () => {
|
|
79
|
+
const merged = await loadWithRule({ threshold: 'not-a-number' });
|
|
80
|
+
expect(merged.rules['clean-code']['long-function'].threshold).toBe(defaultFor('threshold'));
|
|
81
|
+
expect(warnText()).toMatch(/threshold/);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('rejects a non-finite threshold and keeps the default', async () => {
|
|
85
|
+
// A guard that only checks `typeof value === 'number'` would accept these
|
|
86
|
+
// and compare every function against NaN (or Infinity, which nothing ever
|
|
87
|
+
// exceeds -- silently disabling the rule).
|
|
88
|
+
//
|
|
89
|
+
// These literals have to be written as raw JSON TEXT: JSON.stringify turns
|
|
90
|
+
// NaN and Infinity into `null`, so handing them to the object-based helper
|
|
91
|
+
// would inject null and the test would pass against a broken guard. That
|
|
92
|
+
// exact mistake made the first version of this test vacuous.
|
|
93
|
+
const fallback = defaultFor('threshold');
|
|
94
|
+
for (const literal of ['1e999', '-1e999']) {
|
|
95
|
+
warnings = [];
|
|
96
|
+
const merged = await loadRaw(
|
|
97
|
+
`{"rules":{"clean-code":{"long-function":{"threshold":${literal}}}}}`,
|
|
98
|
+
);
|
|
99
|
+
expect(merged.rules['clean-code']['long-function'].threshold).toBe(fallback);
|
|
100
|
+
expect(warnText()).toMatch(/threshold/);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('accepts a well-typed array override (exclude)', async () => {
|
|
105
|
+
const merged = await loadWithRule({ exclude: ['**/skip.ts'] });
|
|
106
|
+
expect(merged.rules['clean-code']['long-function'].exclude).toEqual(['**/skip.ts']);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('rejects a scalar where an array is expected and keeps the default', async () => {
|
|
110
|
+
const merged = await loadWithRule({ exclude: '**/skip.ts' });
|
|
111
|
+
expect(merged.rules['clean-code']['long-function'].exclude).toEqual(defaultFor('exclude'));
|
|
112
|
+
expect(warnText()).toMatch(/exclude/);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('copies the accepted array instead of aliasing the parsed one', async () => {
|
|
116
|
+
// mergeRule spreads array candidates; sharing the reference would let a
|
|
117
|
+
// later mutation of the caller's array leak into the active config.
|
|
118
|
+
const source = ['**/skip.ts'];
|
|
119
|
+
const merged = await loadWithRule({ exclude: source });
|
|
120
|
+
expect(merged.rules['clean-code']['long-function'].exclude).not.toBe(source);
|
|
121
|
+
expect(merged.rules['clean-code']['long-function'].exclude).toEqual(source);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('accepts a well-typed enum override (severity)', async () => {
|
|
125
|
+
const merged = await loadWithRule({ severity: 'error' });
|
|
126
|
+
expect(merged.rules['clean-code']['long-function'].severity).toBe('error');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('rejects an out-of-vocabulary severity and keeps the default', async () => {
|
|
130
|
+
// The dangerous case: a project writes a severity it believes is blocking
|
|
131
|
+
// but misspells it. Silently keeping the default would let that belief stand.
|
|
132
|
+
const merged = await loadWithRule({ severity: 'fatal' });
|
|
133
|
+
expect(merged.rules['clean-code']['long-function'].severity).toBe(defaultFor('severity'));
|
|
134
|
+
expect(warnText()).toMatch(/severity/);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('rejects a non-boolean enabled flag and keeps the default', async () => {
|
|
138
|
+
const merged = await loadWithRule({ enabled: 'yes' });
|
|
139
|
+
expect(typeof merged.rules['clean-code']['long-function'].enabled).toBe('boolean');
|
|
140
|
+
expect(warnText()).toMatch(/enabled/);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('names the offending rule and key in the warning', async () => {
|
|
144
|
+
await loadWithRule({ severity: 'fatal' });
|
|
145
|
+
expect(warnText()).toContain('long-function');
|
|
146
|
+
expect(warnText()).toContain('severity');
|
|
147
|
+
});
|
|
148
|
+
});
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @test REQ-620
|
|
3
|
+
* @intent Pin the C/C++ code-block scanner's treatment of comments and string
|
|
4
|
+
* literals before its CCN-28 body is split.
|
|
5
|
+
*
|
|
6
|
+
* Every case is built to DEFEAT the fallback path. `extractCodeBlock`
|
|
7
|
+
* falls back to `substring(startPos)` capped at 200 characters, so a
|
|
8
|
+
* short fixture returns "the whole thing" even when the scanner never
|
|
9
|
+
* found a closing brace -- the assertions then pass on a broken scanner.
|
|
10
|
+
* Each source below therefore pads past 200 characters and asserts the
|
|
11
|
+
* exact end position, so a truncated scan cannot look correct.
|
|
12
|
+
*
|
|
13
|
+
* Verified by injecting three real defects (brace tracker seeing into
|
|
14
|
+
* strings, block comments untracked, backslash parity ignored): each one
|
|
15
|
+
* reddens at least one case here.
|
|
16
|
+
* @covers AC-620-03
|
|
17
|
+
*/
|
|
18
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
19
|
+
import { CppAdapter } from '../cpp';
|
|
20
|
+
|
|
21
|
+
vi.mock('fs', () => ({
|
|
22
|
+
readFileSync: vi.fn(),
|
|
23
|
+
existsSync: vi.fn(() => true),
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
import { readFileSync } from 'fs';
|
|
27
|
+
|
|
28
|
+
const MARKER = 'END_OF_FUNCTION';
|
|
29
|
+
/** Padding that keeps the source well past the 200-char fallback cap. */
|
|
30
|
+
const PAD = ' '.repeat(240);
|
|
31
|
+
|
|
32
|
+
function block(src: string, start = 0): string {
|
|
33
|
+
(readFileSync as unknown as { mockReturnValue: (v: string) => void }).mockReturnValue(src);
|
|
34
|
+
const adapter = new CppAdapter('probe.cpp');
|
|
35
|
+
return adapter.extractCodeBlock(start);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Build `void f() { <body> }` followed by `<MARKER>`, padded past the fallback
|
|
40
|
+
* cap. The padding sits INSIDE the function body so the fallback (which runs to
|
|
41
|
+
* the 200-char cap) would include the marker, while a correct scan stops at the
|
|
42
|
+
* real closing brace and does not.
|
|
43
|
+
*/
|
|
44
|
+
function wrap(body: string): { src: string; open: number } {
|
|
45
|
+
const src = `void f() {\n${body}\n${PAD}\n}${MARKER}`;
|
|
46
|
+
return { src, open: src.indexOf('{') };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
describe('AC-620-03: the C/C++ block scanner ignores braces in comments and strings', () => {
|
|
50
|
+
it('returns exactly the balanced body of a plain function', () => {
|
|
51
|
+
const { src, open } = wrap(' int a = 1;');
|
|
52
|
+
const out = block(src, open);
|
|
53
|
+
expect(out, `tail=${JSON.stringify(out.slice(-60))} len=${out.length}`).toMatch(/}$/);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('does not close on a brace inside a line comment', () => {
|
|
57
|
+
const { src, open } = wrap(' // } not the end\n int a = 1;');
|
|
58
|
+
const out = block(src, open);
|
|
59
|
+
expect(out, `extracted: ${JSON.stringify(out.slice(0, 80))}`).toContain('int a = 1;');
|
|
60
|
+
expect(out).not.toContain(MARKER);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('does not close on a brace inside a block comment', () => {
|
|
64
|
+
const { src, open } = wrap(' /* } not the end */\n int a = 1;');
|
|
65
|
+
const out = block(src, open);
|
|
66
|
+
expect(out).toContain('int a = 1;');
|
|
67
|
+
expect(out).not.toContain(MARKER);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('does not close on a brace inside a string literal', () => {
|
|
71
|
+
const { src, open } = wrap(' const char* s = "}";\n int a = 1;');
|
|
72
|
+
const out = block(src, open);
|
|
73
|
+
expect(out).toContain('int a = 1;');
|
|
74
|
+
expect(out).not.toContain(MARKER);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('keeps scanning past a string that CONTAINS the only early close brace', () => {
|
|
78
|
+
// The string's `}` is the first `}` after the opener. A scanner that counts
|
|
79
|
+
// it ends the block immediately and never reaches the statements below.
|
|
80
|
+
const { src, open } = wrap(' const char* s = "}";\n int markerStatement = 1;\n int tail = 2;');
|
|
81
|
+
const out = block(src, open);
|
|
82
|
+
expect(out, `extracted: ${JSON.stringify(out.slice(0, 120))}`)
|
|
83
|
+
.toContain('int tail = 2;');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('keeps scanning past a char literal that contains a close brace', () => {
|
|
87
|
+
const { src, open } = wrap(" char c = '}';\n int tail = 2;");
|
|
88
|
+
const out = block(src, open);
|
|
89
|
+
expect(out).toContain('int tail = 2;');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('does not close on a brace inside a char literal', () => {
|
|
93
|
+
const { src, open } = wrap(" char c = '}';\n int a = 1;");
|
|
94
|
+
const out = block(src, open);
|
|
95
|
+
expect(out).toContain('int a = 1;');
|
|
96
|
+
expect(out).not.toContain(MARKER);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('treats an escaped quote inside a string as data, not a terminator', () => {
|
|
100
|
+
// `\"` does not close the string, so the `}` after it is still string data.
|
|
101
|
+
const { src, open } = wrap(' const char* s = "a\\"}b";\n int a = 1;');
|
|
102
|
+
const out = block(src, open);
|
|
103
|
+
expect(out).toContain('int a = 1;');
|
|
104
|
+
expect(out).not.toContain(MARKER);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('treats an even run of backslashes as NOT escaping the quote', () => {
|
|
108
|
+
// `\\` is a literal backslash, so the following `"` DOES close the string,
|
|
109
|
+
// and the `}` that ends the line is real structure.
|
|
110
|
+
const { src, open } = wrap(' const char* s = "a\\\\";\n int a = 1;');
|
|
111
|
+
const out = block(src, open);
|
|
112
|
+
expect(out.trimEnd().endsWith('}')).toBe(true);
|
|
113
|
+
expect(out).toContain('int a = 1;');
|
|
114
|
+
expect(out).not.toContain(MARKER);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('falls back when no balanced block is ever found', () => {
|
|
118
|
+
const src = 'void f() {\n int a = 1;' + PAD;
|
|
119
|
+
const out = block(src, 9);
|
|
120
|
+
expect(out.length).toBe(200);
|
|
121
|
+
expect(out.length).toBeLessThan(src.length);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @test REQ-620
|
|
3
|
+
* @intent Prove the C/C++ block scanner split is behaviour-preserving on real
|
|
4
|
+
* source, not just on the handful of fixtures above. The scanner feeds
|
|
5
|
+
* every C/C++ metric (length, nesting, io operations), so an off-by-one
|
|
6
|
+
* in the refactored loop would silently shift every reported span.
|
|
7
|
+
* @covers AC-620-03
|
|
8
|
+
*/
|
|
9
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
10
|
+
import { CppAdapter } from '../cpp';
|
|
11
|
+
|
|
12
|
+
vi.mock('fs', () => ({
|
|
13
|
+
readFileSync: vi.fn(),
|
|
14
|
+
existsSync: vi.fn(() => true),
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
import { readFileSync } from 'fs';
|
|
18
|
+
|
|
19
|
+
const SOURCE = [
|
|
20
|
+
'#include <stdio.h>',
|
|
21
|
+
'/* a header comment with a } brace */',
|
|
22
|
+
'int helper(int x) {',
|
|
23
|
+
' // a line comment with a } brace',
|
|
24
|
+
' return x * 2;',
|
|
25
|
+
'}',
|
|
26
|
+
'',
|
|
27
|
+
'int main(void) {',
|
|
28
|
+
' const char* msg = "a } inside a string";',
|
|
29
|
+
" char c = '}';",
|
|
30
|
+
' /* block } comment */',
|
|
31
|
+
' if (helper(1) > 0) {',
|
|
32
|
+
' printf("%s", msg);',
|
|
33
|
+
' }',
|
|
34
|
+
' return 0;',
|
|
35
|
+
'}',
|
|
36
|
+
].join('\n');
|
|
37
|
+
|
|
38
|
+
describe('AC-620-03: the split scanner finds the same spans as the original', () => {
|
|
39
|
+
it('extracts main() up to its real closing brace', () => {
|
|
40
|
+
(readFileSync as unknown as { mockReturnValue: (v: string) => void }).mockReturnValue(SOURCE);
|
|
41
|
+
const adapter = new CppAdapter('probe.c');
|
|
42
|
+
const start = SOURCE.indexOf('int main(void)');
|
|
43
|
+
const open = SOURCE.indexOf('{', start);
|
|
44
|
+
const out = adapter.extractCodeBlock(open);
|
|
45
|
+
|
|
46
|
+
// The body must run to the LAST brace of main, not to the `}` in a comment
|
|
47
|
+
// or a string. Line count is the cheap proxy for "did not truncate".
|
|
48
|
+
const bodyLines = out.split('\n').length;
|
|
49
|
+
expect(bodyLines, `extracted: ${JSON.stringify(out)}`).toBe(
|
|
50
|
+
SOURCE.slice(open).split('\n').length,
|
|
51
|
+
);
|
|
52
|
+
expect(out).toContain('return 0;');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('every declaration in the file yields a span that ends on a balanced brace', () => {
|
|
56
|
+
(readFileSync as unknown as { mockReturnValue: (v: string) => void }).mockReturnValue(SOURCE);
|
|
57
|
+
const adapter = new CppAdapter('probe.c');
|
|
58
|
+
const fns = adapter.extractFunctions() as Array<{ name: string; code?: string }>;
|
|
59
|
+
expect(fns.length).toBeGreaterThanOrEqual(2);
|
|
60
|
+
for (const fn of fns) {
|
|
61
|
+
if (!fn.code) continue;
|
|
62
|
+
// Balanced braces modulo the literals/comments the scanner masks.
|
|
63
|
+
expect(fn.code.length, `${fn.name} produced an empty or unbounded span`)
|
|
64
|
+
.toBeLessThanOrEqual(SOURCE.length);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @test REQ-620
|
|
3
|
+
* @intent Prove the block-comment masking defect before fixing it. The masker's
|
|
4
|
+
* contract is to preserve offsets and newlines so that `lineOf()` and
|
|
5
|
+
* every line-numbered violation stays correct. A block comment
|
|
6
|
+
* containing a newline is currently collapsed, shifting every later
|
|
7
|
+
* line number up -- which silently mislabels every violation that
|
|
8
|
+
* follows a multi-line comment in the file.
|
|
9
|
+
* @covers AC-620-02
|
|
10
|
+
*/
|
|
11
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
12
|
+
import { TypeScriptAdapter } from '../typescript';
|
|
13
|
+
|
|
14
|
+
vi.mock('fs', () => ({
|
|
15
|
+
readFileSync: vi.fn(),
|
|
16
|
+
existsSync: vi.fn(() => true),
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
import { readFileSync } from 'fs';
|
|
20
|
+
|
|
21
|
+
function mask(src: string): string {
|
|
22
|
+
(readFileSync as unknown as { mockReturnValue: (v: string) => void }).mockReturnValue(src);
|
|
23
|
+
const adapter = new TypeScriptAdapter('probe.ts');
|
|
24
|
+
return (adapter as unknown as { maskedSource: string }).maskedSource;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe('AC-620-02: a multi-line block comment must not shift later line numbers', () => {
|
|
28
|
+
it('keeps the newline inside a block comment', () => {
|
|
29
|
+
const src = '/* a\nb */\nconst keep = 1;';
|
|
30
|
+
const out = mask(src);
|
|
31
|
+
expect(out.split('\n').length).toBe(src.split('\n').length);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('keeps the line count when the block comment spans three lines', () => {
|
|
35
|
+
const src = '/* one\ntwo\nthree */\nconst keep = 1;\nconst also = 2;';
|
|
36
|
+
const out = mask(src);
|
|
37
|
+
expect(out.split('\n').length).toBe(src.split('\n').length);
|
|
38
|
+
expect(out).toContain('const keep = 1;');
|
|
39
|
+
expect(out).toContain('const also = 2;');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('reports a function line number that matches the physical source', () => {
|
|
43
|
+
// The function starts on physical line 4; a collapsed block comment would
|
|
44
|
+
// make the adapter report line 2.
|
|
45
|
+
const src = ['/* a', 'b', 'c */', 'function target() {', ' return 1;', '}'].join('\n');
|
|
46
|
+
(readFileSync as unknown as { mockReturnValue: (v: string) => void }).mockReturnValue(src);
|
|
47
|
+
const adapter = new TypeScriptAdapter('probe.ts');
|
|
48
|
+
const fns = adapter.extractFunctions() as Array<{ name: string; line: number }>;
|
|
49
|
+
const target = fns.find((f) => f.name === 'target');
|
|
50
|
+
expect(target).toBeDefined();
|
|
51
|
+
expect(target!.line).toBe(4);
|
|
52
|
+
});
|
|
53
|
+
});
|