@boyingliu01/xp-gate 0.19.2 → 0.20.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/gate-4.sh +83 -18
- package/adapters/gate-8.sh +49 -5
- package/adapters/iac.sh +1 -1
- package/adapters/typescript.sh +15 -60
- package/bin/xp-gate.js +15 -16
- package/gate-10.sh +35 -2
- package/gate-4.sh +110 -21
- package/gate-8.sh +49 -5
- package/hooks/gate-10.sh +35 -2
- package/hooks/gate-4.sh +110 -21
- package/hooks/gate-8.sh +49 -5
- package/hooks/lib/jscpd-run.sh +63 -0
- package/hooks/lib/sprint-gate-report.sh +58 -0
- package/hooks/lib/test-failure.sh +152 -0
- package/hooks/lib/typecheck.sh +141 -0
- package/hooks/lib/validate-code-walkthrough.cjs +43 -5
- package/hooks/post-merge +100 -0
- package/hooks/pre-commit +401 -155
- package/hooks/pre-push +75 -19
- package/lib/__tests__/bootstrap.test.js +23 -41
- package/lib/__tests__/check-version.test.js +8 -1
- package/lib/__tests__/detect-deps.test.js +16 -0
- package/lib/__tests__/doctor-drift-round2.test.js +212 -0
- package/lib/__tests__/doctor-executed-surface.test.js +209 -0
- package/lib/__tests__/doctor-hook-drift.test.js +329 -0
- package/lib/__tests__/doctor-machine-mutation.test.js +116 -0
- package/lib/__tests__/doctor-module-drift.test.js +206 -0
- package/lib/__tests__/doctor-module-resolution-parity.test.js +314 -0
- package/lib/__tests__/doctor-path-equivalence.test.js +59 -0
- package/lib/__tests__/doctor-repair-boundary.test.js +52 -0
- package/lib/__tests__/doctor-tui-skill-freshness.test.js +105 -0
- package/lib/__tests__/doctor.test.js +155 -0
- package/lib/__tests__/gate-runner.test.js +19 -3
- package/lib/__tests__/helpers/lib-sandbox.cjs +55 -0
- package/lib/__tests__/init.test.js +44 -17
- package/lib/__tests__/install-skill-full-dir.test.js +151 -0
- package/lib/__tests__/install-skill.test.js +51 -132
- package/lib/__tests__/language-tools-detect.test.js +158 -0
- package/lib/__tests__/phase-transition.test.js +15 -1
- package/lib/__tests__/qoder-delphi-agents.test.js +285 -0
- package/lib/__tests__/shared-paths.test.js +20 -0
- package/lib/__tests__/shared-utils.test.js +69 -0
- package/lib/__tests__/skill-flags.test.js +48 -0
- package/lib/__tests__/ui-detector.test.ts +6 -2
- package/lib/__tests__/uninstall.test.js +20 -0
- package/lib/__tests__/update-hooks.test.js +12 -0
- package/lib/__tests__/update-skill.test.js +7 -4
- package/lib/detect-deps.js +3 -20
- package/lib/doctor-tui.js +22 -5
- package/lib/doctor.js +1178 -31
- package/lib/init.js +184 -33
- package/lib/install-cmd.js +7 -4
- package/lib/install-skill.js +38 -95
- package/lib/language-tools.js +83 -4
- package/lib/mattpocock-license-verdict.json +1 -1
- package/lib/shared-paths.js +5 -2
- package/lib/shared-utils.js +47 -3
- package/lib/uninstall.js +12 -0
- package/lib/update-hooks.js +4 -4
- package/lib/update-skill.js +6 -7
- package/mock-policy/AGENTS.md +4 -4
- package/mutation/AGENTS.md +4 -4
- package/mutation/__tests__/gate-m.test.ts +66 -14
- package/mutation/__tests__/mutmut-runner.test.ts +91 -42
- package/mutation/__tests__/test-filter-parity.test.ts +37 -0
- package/mutation/gate-m.ts +21 -8
- package/package.json +1 -1
- package/plugins/claude-code/.claude-plugin/plugin.json +2 -2
- package/plugins/claude-code/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -1
- package/plugins/claude-code/skills/delphi-review/AGENTS.md +4 -4
- package/plugins/claude-code/skills/delphi-review/SKILL.md +98 -14
- package/plugins/claude-code/skills/delphi-review/references/code-walkthrough.md +36 -3
- package/plugins/claude-code/skills/delphi-review/references/requirements.md +6 -6
- package/plugins/claude-code/skills/sprint-flow/AGENTS.md +4 -4
- package/plugins/claude-code/skills/sprint-flow/references/phase-2-design.md +3 -3
- package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +4 -4
- package/plugins/dsh/README.md +169 -4
- package/plugins/dsh/dsh-overlay.patch.yml +67 -0
- package/plugins/dsh/package.json +1 -1
- package/plugins/dsh/scripts/prepack.cjs +24 -19
- package/plugins/dsh/setup-dsh.ps1 +160 -0
- package/plugins/dsh/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -1
- package/plugins/dsh/skills/delphi-review/AGENTS.md +4 -4
- package/plugins/dsh/skills/delphi-review/SKILL.md +98 -14
- package/plugins/dsh/skills/delphi-review/references/code-walkthrough.md +36 -3
- package/plugins/dsh/skills/delphi-review/references/requirements.md +6 -6
- package/plugins/dsh/skills/sprint-flow/AGENTS.md +4 -4
- package/plugins/dsh/skills/sprint-flow/references/phase-2-design.md +3 -3
- package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +4 -4
- package/plugins/dsh/src/command.test.ts +66 -2
- package/plugins/dsh/src/command.ts +36 -14
- package/plugins/dsh/src/contract.test.ts +31 -1
- package/plugins/dsh/src/delphi-config.test.ts +190 -0
- package/plugins/dsh/src/delphi-config.ts +223 -0
- package/plugins/dsh/src/delphi-run.test.ts +181 -0
- package/plugins/dsh/src/delphi-run.ts +129 -0
- package/plugins/dsh/src/delphi.test.ts +222 -0
- package/plugins/dsh/src/delphi.ts +247 -0
- package/plugins/dsh/src/dialect.test.ts +59 -0
- package/plugins/dsh/src/dialect.ts +40 -0
- package/plugins/dsh/src/index.ts +70 -4
- package/plugins/dsh/src/skills.test.ts +26 -19
- package/plugins/opencode/package.json +1 -1
- package/plugins/opencode/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -1
- package/plugins/opencode/skills/delphi-review/AGENTS.md +4 -4
- package/plugins/opencode/skills/delphi-review/SKILL.md +98 -14
- package/plugins/opencode/skills/delphi-review/references/code-walkthrough.md +36 -3
- package/plugins/opencode/skills/delphi-review/references/requirements.md +6 -6
- package/plugins/opencode/skills/sprint-flow/AGENTS.md +4 -4
- package/plugins/opencode/skills/sprint-flow/references/phase-2-design.md +3 -3
- package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +4 -4
- package/plugins/qoder/agents/delphi-architecture.md +1 -1
- package/plugins/qoder/agents/delphi-feasibility.md +1 -1
- package/plugins/qoder/agents/delphi-technical.md +1 -1
- package/plugins/qoder/plugin.json +2 -2
- package/plugins/qoder/skills/admin-template-guidelines/SKILL.md +6 -797
- package/plugins/qoder/skills/admin-template-guidelines/references/rule-1.md +91 -0
- package/plugins/qoder/skills/admin-template-guidelines/references/rule-2.md +155 -0
- package/plugins/qoder/skills/admin-template-guidelines/references/rule-3.md +70 -0
- package/plugins/qoder/skills/admin-template-guidelines/references/rule-4.md +154 -0
- package/plugins/qoder/skills/admin-template-guidelines/references/rule-5.md +126 -0
- package/plugins/qoder/skills/admin-template-guidelines/references/rule-6.md +180 -0
- package/plugins/qoder/skills/batch-grill-me/SKILL.md +15 -0
- package/plugins/qoder/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -1
- package/plugins/qoder/skills/clipboard-vision/describe-clipboard.ps1 +115 -0
- package/plugins/qoder/skills/clipboard-vision/describe-clipboard.sh +161 -0
- package/plugins/qoder/skills/delphi-review/.delphi-config.json.example +61 -28
- package/plugins/qoder/skills/delphi-review/AGENTS.md +4 -4
- package/plugins/qoder/skills/delphi-review/INSTALL.md +97 -98
- package/plugins/qoder/skills/delphi-review/SKILL.md +170 -63
- package/plugins/qoder/skills/delphi-review/hooks.md +19 -0
- package/plugins/qoder/skills/delphi-review/opencode.json.delphi.example +6 -10
- package/plugins/qoder/skills/delphi-review/references/code-walkthrough.md +36 -3
- package/plugins/qoder/skills/delphi-review/references/orchestrator-dispatch.md +64 -0
- package/plugins/qoder/skills/delphi-review/references/requirements.md +402 -0
- package/plugins/qoder/skills/delphi-review/references/round-templates.md +134 -0
- package/plugins/qoder/skills/delphi-review/scripts/verify-consensus.sh +43 -0
- package/plugins/qoder/skills/delphi-review/tools.md +29 -0
- package/plugins/qoder/skills/domain-modeling/ADR-FORMAT.md +64 -0
- package/plugins/qoder/skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
- package/plugins/qoder/skills/domain-modeling/SKILL.md +74 -0
- package/plugins/qoder/skills/grill-with-docs/SKILL.md +7 -0
- package/plugins/qoder/skills/grilling/SKILL.md +12 -0
- package/plugins/qoder/skills/ralph-loop/SKILL.md +87 -428
- package/plugins/qoder/skills/ralph-loop/references/components.md +113 -0
- package/plugins/qoder/skills/ralph-loop/references/workflow-details.md +99 -0
- package/plugins/qoder/skills/sprint-flow/AGENTS.md +5 -5
- package/plugins/qoder/skills/sprint-flow/SKILL.md +255 -454
- package/plugins/qoder/skills/sprint-flow/__tests__/sprint-flow.test.cjs +457 -0
- package/plugins/qoder/skills/sprint-flow/hooks.md +18 -0
- package/plugins/qoder/skills/sprint-flow/references/components/memory.md +1 -1
- package/plugins/qoder/skills/sprint-flow/references/components/middleware.md +1 -1
- package/plugins/qoder/skills/sprint-flow/references/components/skill-invocations.md +21 -20
- package/plugins/qoder/skills/sprint-flow/references/components/tool-descriptions.md +22 -23
- package/plugins/qoder/skills/sprint-flow/references/force-levels.md +0 -1
- package/plugins/qoder/skills/sprint-flow/references/orchestration-rules.md +34 -27
- package/plugins/qoder/skills/sprint-flow/references/phase-1-prep.md +1 -1
- package/plugins/qoder/skills/sprint-flow/references/phase-2-design.md +160 -123
- package/plugins/qoder/skills/sprint-flow/references/phase-3-build.md +203 -2
- package/plugins/qoder/skills/sprint-flow/references/phase-4-verify.md +107 -34
- package/plugins/qoder/skills/sprint-flow/references/phase-5-ship.md +194 -12
- package/plugins/qoder/skills/sprint-flow/references/phase-6-close.md +139 -7
- package/plugins/qoder/skills/sprint-flow/references/phase-overview.md +486 -0
- package/plugins/qoder/skills/sprint-flow/scripts/verify-sprint-state.sh +38 -0
- package/plugins/qoder/skills/sprint-flow/tools.md +29 -0
- package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +4 -4
- package/plugins/qoder/skills/test-specification-alignment/SKILL.md +104 -601
- package/plugins/qoder/skills/to-issues/SKILL.md +1 -1
- package/principles/AGENTS.md +9 -8
- package/principles/__tests__/adapter-metrics.test.ts +292 -0
- package/principles/__tests__/analyzer.test.ts +29 -6
- package/principles/__tests__/baseline-prune-timeout.test.ts +71 -0
- package/principles/__tests__/baseline-prune.test.ts +338 -0
- package/principles/__tests__/boy-scout-init-baseline.test.ts +167 -0
- package/principles/__tests__/boy-scout-integration.test.ts +97 -6
- package/principles/__tests__/boy-scout.test.ts +253 -7
- package/principles/__tests__/config-enforcement.test.ts +469 -0
- package/principles/__tests__/config-git-timeout.test.ts +66 -0
- package/principles/__tests__/direct-execution.test.ts +187 -0
- package/principles/__tests__/index.test.ts +16 -3
- package/principles/__tests__/warnings-baseline-budgets.test.ts +74 -0
- package/principles/adapters/typescript.ts +613 -13
- package/principles/analyzer.ts +22 -0
- package/principles/baseline-prune.ts +217 -0
- package/principles/boy-scout.ts +481 -51
- package/principles/config.ts +362 -54
- package/principles/direct-execution.ts +60 -0
- package/principles/index.ts +40 -17
- package/principles/rules/__tests__/clean-code/many-exports.test.ts +16 -10
- package/principles/rules/clean-code/code-duplication.ts +8 -8
- package/principles/rules/clean-code/deep-nesting.ts +8 -8
- package/principles/rules/clean-code/god-class.ts +8 -8
- package/principles/rules/clean-code/large-file.ts +15 -11
- package/principles/rules/clean-code/long-function.ts +8 -8
- package/principles/rules/clean-code/magic-numbers.ts +10 -7
- package/principles/rules/clean-code/many-exports.ts +7 -7
- package/principles/rules/clean-code/missing-error-handling.ts +5 -5
- package/principles/rules/clean-code/too-many-params.ts +8 -8
- package/principles/rules/clean-code/unused-imports.ts +5 -5
- package/principles/rules/solid/dip.ts +18 -8
- package/principles/rules/solid/isp.ts +9 -8
- package/principles/rules/solid/lsp.ts +6 -6
- package/principles/rules/solid/ocp.ts +6 -5
- package/principles/rules/solid/srp.ts +7 -8
- package/principles/types.ts +15 -0
- package/scripts/__tests__/sync-package-content.test.js +28 -0
- package/scripts/delphi-external-review.cjs +193 -10
- package/scripts/sync-package-content.js +7 -4
- package/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -1
- package/skills/delphi-review/AGENTS.md +4 -4
- package/skills/delphi-review/SKILL.md +98 -14
- package/skills/delphi-review/references/code-walkthrough.md +36 -3
- package/skills/delphi-review/references/requirements.md +6 -6
- package/skills/sprint-flow/AGENTS.md +4 -4
- package/skills/sprint-flow/references/phase-2-design.md +3 -3
- package/skills/test-specification-alignment/AGENTS.md +4 -4
- package/lib/__tests__/download-skill.test.js +0 -281
- package/lib/download-skill.js +0 -96
- package/plugins/qoder/skills/ralph-loop/references/components/memory.md +0 -55
- package/plugins/qoder/skills/ralph-loop/references/components/middleware.md +0 -54
- package/plugins/qoder/skills/ralph-loop/references/components/skill-invocations.md +0 -39
- package/plugins/qoder/skills/ralph-loop/references/components/system-prompt.md +0 -24
- package/plugins/qoder/skills/ralph-loop/references/components/tool-descriptions.md +0 -32
|
@@ -62,6 +62,13 @@ interface DeltaResult {
|
|
|
62
62
|
|
|
63
63
|
beforeEach(() => {
|
|
64
64
|
vi.clearAllMocks();
|
|
65
|
+
// Default the mocked reader to "no baseline yet". A bare vi.fn() resolves to
|
|
66
|
+
// undefined, which loadBaseline now correctly rejects as unreadable content --
|
|
67
|
+
// so tests that do not arm a baseline must say ENOENT explicitly, exactly as a
|
|
68
|
+
// real first run would.
|
|
69
|
+
const enoent = Object.assign(new Error('ENOENT: no such file or directory'), { code: 'ENOENT' });
|
|
70
|
+
(mockReadFile as ReturnType<typeof vi.fn>).mockRejectedValue(enoent);
|
|
71
|
+
(mockAccess as ReturnType<typeof vi.fn>).mockRejectedValue(enoent);
|
|
65
72
|
});
|
|
66
73
|
|
|
67
74
|
/**
|
|
@@ -101,6 +108,11 @@ describe('Boy Scout Rule Enforcement', () => {
|
|
|
101
108
|
*/
|
|
102
109
|
describe('baseline management', () => {
|
|
103
110
|
it('returns empty baseline when file missing', async () => {
|
|
111
|
+
// `fs/promises` is mocked for this file, so a bare call would hit a stub
|
|
112
|
+
// `access` that resolves and a stub `readFile` returning undefined. Arm
|
|
113
|
+
// the ENOENT path explicitly so this test exercises its stated intent.
|
|
114
|
+
(mockAccess as ReturnType<typeof vi.fn>).mockRejectedValue(new Error('ENOENT'));
|
|
115
|
+
|
|
104
116
|
const baseline = await loadBaseline('.nonexistent-baseline.json');
|
|
105
117
|
expect(baseline).toEqual({});
|
|
106
118
|
});
|
|
@@ -419,7 +431,8 @@ describe('Boy Scout Rule Enforcement', () => {
|
|
|
419
431
|
const result = await loadBaseline('/tmp/test-baseline.json');
|
|
420
432
|
|
|
421
433
|
expect(result).toEqual(mockData);
|
|
422
|
-
|
|
434
|
+
// `access` is no longer called first: probing then reading is a TOCTOU race,
|
|
435
|
+
// and it made every I/O failure look like "no baseline yet" (#455 review).
|
|
423
436
|
expect(mockReadFile).toHaveBeenCalledWith('/tmp/test-baseline.json', 'utf-8');
|
|
424
437
|
});
|
|
425
438
|
|
|
@@ -431,13 +444,170 @@ describe('Boy Scout Rule Enforcement', () => {
|
|
|
431
444
|
expect(result).toEqual({});
|
|
432
445
|
});
|
|
433
446
|
|
|
434
|
-
it('
|
|
447
|
+
it('throws when the file exists but is not valid JSON', async () => {
|
|
435
448
|
(mockAccess as ReturnType<typeof vi.fn>).mockResolvedValue(undefined);
|
|
436
449
|
(mockReadFile as ReturnType<typeof vi.fn>).mockResolvedValue('not json');
|
|
437
450
|
|
|
438
|
-
|
|
451
|
+
// Behaviour changed in #455: a present-but-unreadable baseline used to
|
|
452
|
+
// degrade to `{}`, which silently gave every file an empty budget and made
|
|
453
|
+
// the gate pass everything. A corrupt file must fail loudly instead.
|
|
454
|
+
await expect(loadBaseline('/tmp/bad.json')).rejects.toThrow(/not valid JSON/i);
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* @test REQ-QG-005
|
|
459
|
+
* @intent 验证畸形基线条目(裸数字而非 {totalWarnings,...} 对象)被拒绝:
|
|
460
|
+
* 畸形条目会让 entry.totalWarnings 取到 undefined,使
|
|
461
|
+
* `currentWarnings > undefined` 与 `undefined <= 5` 同时为假 ->
|
|
462
|
+
* 该文件无论增加多少警告都永远 PASS(issue #455)
|
|
463
|
+
* @covers AC-QG-005-01
|
|
464
|
+
*/
|
|
465
|
+
describe('malformed entries (#455)', () => {
|
|
466
|
+
const armBaseline = (content: string) => {
|
|
467
|
+
(mockAccess as ReturnType<typeof vi.fn>).mockResolvedValue(undefined);
|
|
468
|
+
(mockReadFile as ReturnType<typeof vi.fn>).mockResolvedValue(content);
|
|
469
|
+
};
|
|
439
470
|
|
|
440
|
-
|
|
471
|
+
it('rejects a bare number where an entry object is required', async () => {
|
|
472
|
+
// This is the exact shape committed in 791d825 and still in HEAD:
|
|
473
|
+
// {"src/npm-package/lib/test-alignment.ts": 1}
|
|
474
|
+
armBaseline(JSON.stringify({ 'src/a.ts': 1 }));
|
|
475
|
+
|
|
476
|
+
await expect(loadBaseline('/tmp/x.json')).rejects.toThrow(/malformed/i);
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
it('rejects an entry whose totalWarnings is not a finite number', async () => {
|
|
480
|
+
armBaseline(JSON.stringify({ 'src/a.ts': { lastAnalyzed: '2024-01-01' } }));
|
|
481
|
+
|
|
482
|
+
await expect(loadBaseline('/tmp/x.json')).rejects.toThrow(/malformed/i);
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
it('rejects an entry whose totalWarnings is null', async () => {
|
|
486
|
+
armBaseline(JSON.stringify({ 'src/a.ts': { totalWarnings: null, lastAnalyzed: 'x' } }));
|
|
487
|
+
|
|
488
|
+
await expect(loadBaseline('/tmp/x.json')).rejects.toThrow(/malformed/i);
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
it('names the offending file so the broken entry can be found', async () => {
|
|
492
|
+
armBaseline(JSON.stringify({ 'src/ok.ts': { totalWarnings: 2, lastAnalyzed: 'x' }, 'src/bad.ts': 1 }));
|
|
493
|
+
|
|
494
|
+
await expect(loadBaseline('/tmp/x.json')).rejects.toThrow(/src\/bad\.ts/);
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
it('still accepts a well-formed baseline', async () => {
|
|
498
|
+
const good = { 'src/a.ts': { totalWarnings: 3, lastAnalyzed: '2024-01-01' } };
|
|
499
|
+
armBaseline(JSON.stringify(good));
|
|
500
|
+
|
|
501
|
+
await expect(loadBaseline('/tmp/x.json')).resolves.toEqual(good);
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
it('accepts an empty baseline object', async () => {
|
|
505
|
+
armBaseline('{}');
|
|
506
|
+
|
|
507
|
+
await expect(loadBaseline('/tmp/x.json')).resolves.toEqual({});
|
|
508
|
+
});
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* @test REQ-QG-005
|
|
513
|
+
* @intent 验证修复路径可用:initBaselineCommand 是修坏基线的工具,
|
|
514
|
+
* 因此不能要求基线先完好,否则畸形条目无法通过任何受支持路径修复(#455)
|
|
515
|
+
* @covers AC-QG-005-03
|
|
516
|
+
*/
|
|
517
|
+
describe('repair path tolerates corrupt entries (#455)', () => {
|
|
518
|
+
const armRepair = (content: string) => {
|
|
519
|
+
(mockReadFile as ReturnType<typeof vi.fn>).mockResolvedValue(content);
|
|
520
|
+
mockAnalyze.mockResolvedValue({
|
|
521
|
+
files: [],
|
|
522
|
+
violations: [],
|
|
523
|
+
summary: { totalFiles: 1, totalViolations: 0, errorCount: 0, warningCount: 0, infoCount: 0, bySeverity: {} },
|
|
524
|
+
} as never);
|
|
525
|
+
vi.mocked(mockWriteFile).mockResolvedValue(undefined);
|
|
526
|
+
};
|
|
527
|
+
|
|
528
|
+
it('drops a malformed entry instead of refusing to run', async () => {
|
|
529
|
+
armRepair(JSON.stringify({ 'src/bad.ts': 1 }));
|
|
530
|
+
|
|
531
|
+
const result = await initBaselineCommand(['src/new.ts'], '/tmp/x.json');
|
|
532
|
+
|
|
533
|
+
// The corrupt entry must not survive into the rewritten baseline...
|
|
534
|
+
const written = JSON.parse(vi.mocked(mockWriteFile).mock.calls[0][1] as string);
|
|
535
|
+
expect(written).not.toHaveProperty('src/bad.ts');
|
|
536
|
+
// ...and losing a budget must not be reported as success: the entry is
|
|
537
|
+
// gone unless the caller names it, so the exit code has to say so.
|
|
538
|
+
expect(result.ok).toBe(false);
|
|
539
|
+
expect(result.reason).toContain('src/bad.ts');
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
it('reports success when every dropped entry is rebuilt from the file list', async () => {
|
|
543
|
+
// Same corrupt file, but this time the caller asks for that very path, so
|
|
544
|
+
// the entry is measured afresh and nothing is lost.
|
|
545
|
+
armRepair(JSON.stringify({ 'src/bad.ts': 1 }));
|
|
546
|
+
mockAnalyze.mockResolvedValue({
|
|
547
|
+
files: [],
|
|
548
|
+
violations: [
|
|
549
|
+
{ file: 'src/bad.ts', ruleId: 'clean-code.magic-numbers', severity: 'warning', line: 1, message: 'x' },
|
|
550
|
+
],
|
|
551
|
+
summary: { totalFiles: 1, totalViolations: 1, errorCount: 0, warningCount: 1, infoCount: 0, bySeverity: {} },
|
|
552
|
+
} as never);
|
|
553
|
+
|
|
554
|
+
const result = await initBaselineCommand(['src/bad.ts'], '/tmp/x.json');
|
|
555
|
+
|
|
556
|
+
expect(result.ok).toBe(true);
|
|
557
|
+
const written = JSON.parse(vi.mocked(mockWriteFile).mock.calls[0][1] as string);
|
|
558
|
+
expect(written['src/bad.ts'].totalWarnings).toBe(1);
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
it('refuses to write when the baseline cannot be read at all', async () => {
|
|
562
|
+
// EACCES and friends must not be treated as "empty", or the save below
|
|
563
|
+
// would replace content we never managed to inspect.
|
|
564
|
+
const eacces = Object.assign(new Error('EACCES: permission denied'), { code: 'EACCES' });
|
|
565
|
+
(mockReadFile as ReturnType<typeof vi.fn>).mockRejectedValue(eacces);
|
|
566
|
+
|
|
567
|
+
const result = await initBaselineCommand(['src/new.ts'], '/tmp/x.json');
|
|
568
|
+
|
|
569
|
+
expect(result.ok).toBe(false);
|
|
570
|
+
expect(result.reason).toMatch(/Cannot read baseline/);
|
|
571
|
+
expect(mockWriteFile).not.toHaveBeenCalled();
|
|
572
|
+
});
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* @test REQ-QG-005
|
|
577
|
+
* @intent 验证 delta 为 NaN 时判定为 BLOCK:NaN 只可能来自损坏的基线,
|
|
578
|
+
* 而 `NaN > n` 与 `NaN <= n` 都是 false,会让门禁静默通过
|
|
579
|
+
* @covers AC-QG-005-02
|
|
580
|
+
*/
|
|
581
|
+
describe('non-finite delta (#455)', () => {
|
|
582
|
+
it('blocks when the baseline value is not a finite number', () => {
|
|
583
|
+
const broken = { totalWarnings: undefined, lastAnalyzed: 'x' } as unknown as BaselineEntry;
|
|
584
|
+
|
|
585
|
+
const result = calculateDelta(broken, 5, 'MODIFIED');
|
|
586
|
+
|
|
587
|
+
expect(result.enforcement).toBe('BLOCK');
|
|
588
|
+
// delta must be NaN ("cannot compute"), never a real-looking number.
|
|
589
|
+
expect(Number.isNaN(result.delta)).toBe(true);
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
it('blocks the bare-number baseline shape that shipped in 791d825', () => {
|
|
593
|
+
// {"src/a.ts": 1} -- JSON.parse accepts it, TS accepts the cast, and
|
|
594
|
+
// `entry.totalWarnings` is undefined. Before the fix both block
|
|
595
|
+
// conditions compared false against undefined and the file passed.
|
|
596
|
+
const bareNumber = 1 as unknown as BaselineEntry;
|
|
597
|
+
|
|
598
|
+
const result = calculateDelta(bareNumber, 14, 'MODIFIED');
|
|
599
|
+
|
|
600
|
+
expect(result.enforcement).toBe('BLOCK');
|
|
601
|
+
expect(result.reason).toMatch(/corrupt/i);
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
it('still enforces the normal threshold for a well-formed baseline', () => {
|
|
605
|
+
// Guard against the fix being implemented by blocking everything.
|
|
606
|
+
const good = { totalWarnings: 13, lastAnalyzed: 'x' } as BaselineEntry;
|
|
607
|
+
|
|
608
|
+
expect(calculateDelta(good, 13, 'MODIFIED').enforcement).toBe('PASS');
|
|
609
|
+
expect(calculateDelta(good, 14, 'MODIFIED').enforcement).toBe('BLOCK');
|
|
610
|
+
});
|
|
441
611
|
});
|
|
442
612
|
});
|
|
443
613
|
|
|
@@ -564,12 +734,16 @@ describe('Boy Scout Rule Enforcement', () => {
|
|
|
564
734
|
|
|
565
735
|
const BOY_SCOUT_PATH = path.resolve(__dirname, '../boy-scout.ts');
|
|
566
736
|
|
|
737
|
+
// Resolve the local tsx CLI once: spawning `npx tsx` per test re-resolves
|
|
738
|
+
// the package each call and needs shell:true on Windows; running the
|
|
739
|
+
// resolved CLI through process.execPath is far faster and shell-free.
|
|
740
|
+
const tsxCli = require.resolve('tsx/cli');
|
|
741
|
+
|
|
567
742
|
function runCli(args: string[], cwd?: string): { code: number; stdout: string; stderr: string } {
|
|
568
743
|
const coverageDir = process.env.NODE_V8_COVERAGE ?? path.join(process.cwd(), 'coverage', '.tmp');
|
|
569
|
-
const result = spawnSync(
|
|
744
|
+
const result = spawnSync(process.execPath, [tsxCli, BOY_SCOUT_PATH, ...args], {
|
|
570
745
|
cwd: cwd ?? process.cwd(),
|
|
571
746
|
encoding: 'utf-8',
|
|
572
|
-
shell: true,
|
|
573
747
|
env: { ...process.env, NODE_V8_COVERAGE: coverageDir },
|
|
574
748
|
});
|
|
575
749
|
return {
|
|
@@ -604,7 +778,9 @@ describe('Boy Scout Rule Enforcement', () => {
|
|
|
604
778
|
fs.writeFileSync(path.join(tmp, 'a.ts'), 'export const x = 1;\n');
|
|
605
779
|
const { code, stdout } = runCli(['--init-baseline', 'a.ts'], tmp);
|
|
606
780
|
expect(code).toBe(0);
|
|
607
|
-
|
|
781
|
+
// The command MERGES into any existing baseline rather than replacing it
|
|
782
|
+
// (#445), so the message reports an update, not a fresh initialization.
|
|
783
|
+
expect(stdout).toContain('Baseline updated:');
|
|
608
784
|
expect(fs.existsSync(path.join(tmp, '.warnings-baseline.json'))).toBe(true);
|
|
609
785
|
} finally {
|
|
610
786
|
fs.rmSync(tmp, { recursive: true, force: true });
|
|
@@ -643,5 +819,75 @@ describe('Boy Scout Rule Enforcement', () => {
|
|
|
643
819
|
fs.rmSync(tmp, { recursive: true, force: true });
|
|
644
820
|
}
|
|
645
821
|
});
|
|
822
|
+
|
|
823
|
+
/**
|
|
824
|
+
* @test REQ-QG-005
|
|
825
|
+
* @intent 验证 `--init-baseline` 的两种文件来源都被接受,且传了文件却没记录
|
|
826
|
+
* 任何东西时必须失败:早先它无条件消费下一个 token,于是
|
|
827
|
+
* `--init-baseline --new-files a.ts` 把 flag 当文件名、静默写空基线并 exit 0
|
|
828
|
+
* @covers AC-QG-005-04
|
|
829
|
+
*/
|
|
830
|
+
describe('--init-baseline accepts both file sources (#455)', () => {
|
|
831
|
+
// A long function is a WARNING (a bare magic number is only `info`), and
|
|
832
|
+
// `initBaseline` counts warning-severity violations only -- so the fixture
|
|
833
|
+
// has to trip a warning rule or the entry is legitimately absent.
|
|
834
|
+
const LONG_FUNCTION = 'export function f(): number {\n' + ' const a = 1;\n'.repeat(55) + ' return a;\n}\n';
|
|
835
|
+
|
|
836
|
+
it('records files named via --new-files, not just positional ones', () => {
|
|
837
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'boy-scout-init-nf-'));
|
|
838
|
+
try {
|
|
839
|
+
fs.writeFileSync(path.join(tmp, 'warn.ts'), LONG_FUNCTION);
|
|
840
|
+
const baselinePath = path.join(tmp, '.warnings-baseline.json');
|
|
841
|
+
const { code, stdout } = runCli([
|
|
842
|
+
'--init-baseline', '--new-files', 'warn.ts',
|
|
843
|
+
'--baseline', baselinePath,
|
|
844
|
+
], tmp);
|
|
845
|
+
|
|
846
|
+
expect(code).toBe(0);
|
|
847
|
+
expect(stdout).toContain('Baseline updated:');
|
|
848
|
+
const written = JSON.parse(fs.readFileSync(baselinePath, 'utf-8'));
|
|
849
|
+
// The regression: this used to be {} because the file list was read as
|
|
850
|
+
// empty, so a caller was told the file was recorded when it was not.
|
|
851
|
+
expect(written['warn.ts']?.totalWarnings).toBeGreaterThan(0);
|
|
852
|
+
} finally {
|
|
853
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
854
|
+
}
|
|
855
|
+
});
|
|
856
|
+
|
|
857
|
+
it('does not swallow the following flag as a filename', () => {
|
|
858
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'boy-scout-init-swallow-'));
|
|
859
|
+
try {
|
|
860
|
+
fs.writeFileSync(path.join(tmp, 'warn.ts'), LONG_FUNCTION);
|
|
861
|
+
const baselinePath = path.join(tmp, 'custom-baseline.json');
|
|
862
|
+
const { code } = runCli([
|
|
863
|
+
'--init-baseline', 'warn.ts',
|
|
864
|
+
'--baseline', baselinePath,
|
|
865
|
+
], tmp);
|
|
866
|
+
|
|
867
|
+
expect(code).toBe(0);
|
|
868
|
+
// The baseline must land at the path given by --baseline. If the parser
|
|
869
|
+
// ate '--baseline' as a filename, the default path gets written instead.
|
|
870
|
+
expect(fs.existsSync(baselinePath)).toBe(true);
|
|
871
|
+
expect(fs.existsSync(path.join(tmp, '.warnings-baseline.json'))).toBe(false);
|
|
872
|
+
} finally {
|
|
873
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
874
|
+
}
|
|
875
|
+
});
|
|
876
|
+
|
|
877
|
+
it('fails loudly when no files are given at all', () => {
|
|
878
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'boy-scout-init-none-'));
|
|
879
|
+
try {
|
|
880
|
+
const baselinePath = path.join(tmp, '.warnings-baseline.json');
|
|
881
|
+
const { code, stderr } = runCli(['--init-baseline', '--baseline', baselinePath], tmp);
|
|
882
|
+
|
|
883
|
+
// Writing an empty baseline and exiting 0 is how a caller concludes a
|
|
884
|
+
// file was recorded when nothing was.
|
|
885
|
+
expect(code).toBe(1);
|
|
886
|
+
expect(stderr).toMatch(/No files to record/);
|
|
887
|
+
} finally {
|
|
888
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
889
|
+
}
|
|
890
|
+
});
|
|
891
|
+
});
|
|
646
892
|
});
|
|
647
893
|
});
|