@boyingliu01/xp-gate 0.19.3 → 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.
Files changed (173) hide show
  1. package/adapters/gate-4.sh +83 -18
  2. package/adapters/gate-8.sh +49 -5
  3. package/adapters/iac.sh +1 -1
  4. package/adapters/typescript.sh +15 -60
  5. package/bin/xp-gate.js +15 -16
  6. package/gate-10.sh +35 -2
  7. package/gate-4.sh +110 -21
  8. package/gate-8.sh +49 -5
  9. package/hooks/gate-10.sh +35 -2
  10. package/hooks/gate-4.sh +110 -21
  11. package/hooks/gate-8.sh +49 -5
  12. package/hooks/lib/jscpd-run.sh +63 -0
  13. package/hooks/lib/sprint-gate-report.sh +58 -0
  14. package/hooks/lib/test-failure.sh +152 -0
  15. package/hooks/lib/typecheck.sh +141 -0
  16. package/hooks/lib/validate-code-walkthrough.cjs +43 -5
  17. package/hooks/post-merge +100 -0
  18. package/hooks/pre-commit +389 -162
  19. package/hooks/pre-push +75 -19
  20. package/lib/__tests__/bootstrap.test.js +23 -41
  21. package/lib/__tests__/check-version.test.js +8 -1
  22. package/lib/__tests__/detect-deps.test.js +16 -0
  23. package/lib/__tests__/doctor-drift-round2.test.js +212 -0
  24. package/lib/__tests__/doctor-executed-surface.test.js +209 -0
  25. package/lib/__tests__/doctor-hook-drift.test.js +329 -0
  26. package/lib/__tests__/doctor-machine-mutation.test.js +116 -0
  27. package/lib/__tests__/doctor-module-drift.test.js +206 -0
  28. package/lib/__tests__/doctor-module-resolution-parity.test.js +314 -0
  29. package/lib/__tests__/doctor-path-equivalence.test.js +59 -0
  30. package/lib/__tests__/doctor-repair-boundary.test.js +52 -0
  31. package/lib/__tests__/doctor-tui-skill-freshness.test.js +105 -0
  32. package/lib/__tests__/doctor.test.js +155 -0
  33. package/lib/__tests__/gate-runner.test.js +19 -3
  34. package/lib/__tests__/helpers/lib-sandbox.cjs +55 -0
  35. package/lib/__tests__/init.test.js +44 -17
  36. package/lib/__tests__/install-skill-full-dir.test.js +151 -0
  37. package/lib/__tests__/install-skill.test.js +51 -132
  38. package/lib/__tests__/language-tools-detect.test.js +158 -0
  39. package/lib/__tests__/phase-transition.test.js +15 -1
  40. package/lib/__tests__/qoder-delphi-agents.test.js +285 -0
  41. package/lib/__tests__/shared-paths.test.js +20 -0
  42. package/lib/__tests__/shared-utils.test.js +69 -0
  43. package/lib/__tests__/skill-flags.test.js +48 -0
  44. package/lib/__tests__/ui-detector.test.ts +6 -2
  45. package/lib/__tests__/uninstall.test.js +20 -0
  46. package/lib/__tests__/update-hooks.test.js +12 -0
  47. package/lib/__tests__/update-skill.test.js +7 -4
  48. package/lib/detect-deps.js +3 -20
  49. package/lib/doctor-tui.js +22 -5
  50. package/lib/doctor.js +1178 -31
  51. package/lib/init.js +184 -33
  52. package/lib/install-cmd.js +7 -4
  53. package/lib/install-skill.js +38 -95
  54. package/lib/language-tools.js +83 -4
  55. package/lib/mattpocock-license-verdict.json +1 -1
  56. package/lib/shared-paths.js +5 -2
  57. package/lib/shared-utils.js +47 -3
  58. package/lib/uninstall.js +12 -0
  59. package/lib/update-hooks.js +4 -4
  60. package/lib/update-skill.js +6 -7
  61. package/mock-policy/AGENTS.md +4 -4
  62. package/mutation/AGENTS.md +4 -4
  63. package/mutation/__tests__/gate-m.test.ts +66 -14
  64. package/mutation/__tests__/mutmut-runner.test.ts +91 -42
  65. package/mutation/__tests__/test-filter-parity.test.ts +37 -0
  66. package/mutation/gate-m.ts +21 -8
  67. package/package.json +1 -1
  68. package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
  69. package/plugins/claude-code/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -1
  70. package/plugins/claude-code/skills/delphi-review/AGENTS.md +4 -4
  71. package/plugins/claude-code/skills/delphi-review/SKILL.md +98 -14
  72. package/plugins/claude-code/skills/delphi-review/references/code-walkthrough.md +36 -3
  73. package/plugins/claude-code/skills/delphi-review/references/requirements.md +6 -6
  74. package/plugins/claude-code/skills/sprint-flow/AGENTS.md +4 -4
  75. package/plugins/claude-code/skills/sprint-flow/references/phase-2-design.md +3 -3
  76. package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +4 -4
  77. package/plugins/dsh/README.md +169 -4
  78. package/plugins/dsh/dsh-overlay.patch.yml +67 -0
  79. package/plugins/dsh/package.json +1 -1
  80. package/plugins/dsh/scripts/prepack.cjs +24 -19
  81. package/plugins/dsh/setup-dsh.ps1 +160 -0
  82. package/plugins/dsh/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -1
  83. package/plugins/dsh/skills/delphi-review/AGENTS.md +4 -4
  84. package/plugins/dsh/skills/delphi-review/SKILL.md +98 -14
  85. package/plugins/dsh/skills/delphi-review/references/code-walkthrough.md +36 -3
  86. package/plugins/dsh/skills/delphi-review/references/requirements.md +6 -6
  87. package/plugins/dsh/skills/sprint-flow/AGENTS.md +4 -4
  88. package/plugins/dsh/skills/sprint-flow/references/phase-2-design.md +3 -3
  89. package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +4 -4
  90. package/plugins/dsh/src/command.test.ts +66 -2
  91. package/plugins/dsh/src/command.ts +36 -14
  92. package/plugins/dsh/src/contract.test.ts +31 -1
  93. package/plugins/dsh/src/delphi-config.test.ts +190 -0
  94. package/plugins/dsh/src/delphi-config.ts +223 -0
  95. package/plugins/dsh/src/delphi-run.test.ts +181 -0
  96. package/plugins/dsh/src/delphi-run.ts +129 -0
  97. package/plugins/dsh/src/delphi.test.ts +222 -0
  98. package/plugins/dsh/src/delphi.ts +247 -0
  99. package/plugins/dsh/src/dialect.test.ts +59 -0
  100. package/plugins/dsh/src/dialect.ts +40 -0
  101. package/plugins/dsh/src/index.ts +70 -4
  102. package/plugins/dsh/src/skills.test.ts +26 -19
  103. package/plugins/opencode/package.json +1 -1
  104. package/plugins/opencode/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -1
  105. package/plugins/opencode/skills/delphi-review/AGENTS.md +4 -4
  106. package/plugins/opencode/skills/delphi-review/SKILL.md +98 -14
  107. package/plugins/opencode/skills/delphi-review/references/code-walkthrough.md +36 -3
  108. package/plugins/opencode/skills/delphi-review/references/requirements.md +6 -6
  109. package/plugins/opencode/skills/sprint-flow/AGENTS.md +4 -4
  110. package/plugins/opencode/skills/sprint-flow/references/phase-2-design.md +3 -3
  111. package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +4 -4
  112. package/plugins/qoder/agents/delphi-architecture.md +1 -1
  113. package/plugins/qoder/agents/delphi-feasibility.md +1 -1
  114. package/plugins/qoder/agents/delphi-technical.md +1 -1
  115. package/plugins/qoder/plugin.json +1 -1
  116. package/plugins/qoder/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -1
  117. package/plugins/qoder/skills/delphi-review/AGENTS.md +4 -4
  118. package/plugins/qoder/skills/delphi-review/SKILL.md +98 -14
  119. package/plugins/qoder/skills/delphi-review/references/code-walkthrough.md +36 -3
  120. package/plugins/qoder/skills/delphi-review/references/requirements.md +6 -6
  121. package/plugins/qoder/skills/sprint-flow/AGENTS.md +4 -4
  122. package/plugins/qoder/skills/sprint-flow/references/phase-2-design.md +3 -3
  123. package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +4 -4
  124. package/principles/AGENTS.md +9 -8
  125. package/principles/__tests__/adapter-metrics.test.ts +292 -0
  126. package/principles/__tests__/analyzer.test.ts +29 -6
  127. package/principles/__tests__/baseline-prune-timeout.test.ts +71 -0
  128. package/principles/__tests__/baseline-prune.test.ts +338 -0
  129. package/principles/__tests__/boy-scout-init-baseline.test.ts +167 -0
  130. package/principles/__tests__/boy-scout-integration.test.ts +97 -6
  131. package/principles/__tests__/boy-scout.test.ts +253 -7
  132. package/principles/__tests__/config-enforcement.test.ts +469 -0
  133. package/principles/__tests__/config-git-timeout.test.ts +66 -0
  134. package/principles/__tests__/direct-execution.test.ts +187 -0
  135. package/principles/__tests__/index.test.ts +16 -3
  136. package/principles/__tests__/warnings-baseline-budgets.test.ts +74 -0
  137. package/principles/adapters/typescript.ts +613 -13
  138. package/principles/analyzer.ts +22 -0
  139. package/principles/baseline-prune.ts +217 -0
  140. package/principles/boy-scout.ts +481 -51
  141. package/principles/config.ts +362 -54
  142. package/principles/direct-execution.ts +60 -0
  143. package/principles/index.ts +40 -17
  144. package/principles/rules/__tests__/clean-code/many-exports.test.ts +16 -10
  145. package/principles/rules/clean-code/code-duplication.ts +8 -8
  146. package/principles/rules/clean-code/deep-nesting.ts +8 -8
  147. package/principles/rules/clean-code/god-class.ts +8 -8
  148. package/principles/rules/clean-code/large-file.ts +15 -11
  149. package/principles/rules/clean-code/long-function.ts +8 -8
  150. package/principles/rules/clean-code/magic-numbers.ts +10 -7
  151. package/principles/rules/clean-code/many-exports.ts +7 -7
  152. package/principles/rules/clean-code/missing-error-handling.ts +5 -5
  153. package/principles/rules/clean-code/too-many-params.ts +8 -8
  154. package/principles/rules/clean-code/unused-imports.ts +5 -5
  155. package/principles/rules/solid/dip.ts +18 -8
  156. package/principles/rules/solid/isp.ts +9 -8
  157. package/principles/rules/solid/lsp.ts +6 -6
  158. package/principles/rules/solid/ocp.ts +6 -5
  159. package/principles/rules/solid/srp.ts +7 -8
  160. package/principles/types.ts +15 -0
  161. package/scripts/__tests__/sync-package-content.test.js +28 -0
  162. package/scripts/delphi-external-review.cjs +193 -10
  163. package/scripts/sync-package-content.js +7 -4
  164. package/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -1
  165. package/skills/delphi-review/AGENTS.md +4 -4
  166. package/skills/delphi-review/SKILL.md +98 -14
  167. package/skills/delphi-review/references/code-walkthrough.md +36 -3
  168. package/skills/delphi-review/references/requirements.md +6 -6
  169. package/skills/sprint-flow/AGENTS.md +4 -4
  170. package/skills/sprint-flow/references/phase-2-design.md +3 -3
  171. package/skills/test-specification-alignment/AGENTS.md +4 -4
  172. package/lib/__tests__/download-skill.test.js +0 -281
  173. package/lib/download-skill.js +0 -96
@@ -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
- expect(mockAccess).toHaveBeenCalledWith('/tmp/test-baseline.json');
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('returns empty object when JSON parse fails', async () => {
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
- const result = await loadBaseline('/tmp/bad.json');
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
- expect(result).toEqual({});
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('npx', ['tsx', BOY_SCOUT_PATH, ...args], {
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
- expect(stdout).toContain('Baseline initialized successfully');
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
  });