@boyingliu01/xp-gate 0.8.9 → 0.8.10
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/lib/init.js +36 -0
- package/mock-policy/AGENTS.md +94 -0
- package/mock-policy/__tests__/config.test.ts +292 -0
- package/mock-policy/__tests__/integration.test.ts +600 -0
- package/mock-policy/__tests__/mock-decision-engine.test.ts +231 -0
- package/mock-policy/__tests__/scope-scanner.test.ts +535 -0
- package/mock-policy/config.ts +106 -0
- package/mock-policy/gate-m3.ts +233 -0
- package/mock-policy/mock-decision-engine.ts +155 -0
- package/mock-policy/schema.ts +22 -0
- package/mock-policy/scope-scanner.ts +288 -0
- package/mock-policy/types.ts +65 -0
- package/mutation/AGENTS.md +73 -0
- package/mutation/__tests__/detect-ai-test.test.ts +152 -0
- package/mutation/__tests__/gate-m.test.ts +194 -0
- package/mutation/detect-ai-test.ts +128 -0
- package/mutation/gate-m.ts +619 -0
- package/mutation/init-baseline.ts +192 -0
- package/mutation/stryker-types.ts +14 -0
- package/mutation/types.ts +70 -0
- package/mutation/update-baseline.ts +160 -0
- package/package.json +5 -2
- package/plugins/claude-code/.claude-plugin/plugin.json +2 -2
- package/plugins/claude-code/skills/delphi-review/AGENTS.md +7 -7
- package/plugins/claude-code/skills/delphi-review/INSTALL.md +1 -1
- package/plugins/claude-code/skills/delphi-review/SKILL.md +12 -12
- package/plugins/claude-code/skills/delphi-review/references/code-walkthrough.md +1 -1
- package/plugins/claude-code/skills/sprint-flow/AGENTS.md +7 -7
- package/plugins/claude-code/skills/sprint-flow/SKILL.md +1 -1
- package/plugins/claude-code/skills/sprint-flow/references/force-levels.md +1 -1
- package/plugins/claude-code/skills/sprint-flow/references/phase-3-review.md +2 -2
- package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/opencode/package.json +1 -1
- package/plugins/opencode/skills/delphi-review/AGENTS.md +7 -7
- package/plugins/opencode/skills/delphi-review/INSTALL.md +1 -1
- package/plugins/opencode/skills/delphi-review/SKILL.md +12 -12
- package/plugins/opencode/skills/delphi-review/references/code-walkthrough.md +1 -1
- package/plugins/opencode/skills/sprint-flow/AGENTS.md +7 -7
- package/plugins/opencode/skills/sprint-flow/SKILL.md +1 -1
- package/plugins/opencode/skills/sprint-flow/references/force-levels.md +1 -1
- package/plugins/opencode/skills/sprint-flow/references/phase-3-review.md +2 -2
- 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 +7 -7
- package/plugins/qoder/skills/delphi-review/INSTALL.md +1 -1
- package/plugins/qoder/skills/delphi-review/SKILL.md +12 -12
- package/plugins/qoder/skills/delphi-review/references/code-walkthrough.md +1 -1
- package/plugins/qoder/skills/sprint-flow/AGENTS.md +7 -7
- package/plugins/qoder/skills/sprint-flow/SKILL.md +51 -1
- package/plugins/qoder/skills/sprint-flow/references/force-levels.md +1 -1
- package/plugins/qoder/skills/sprint-flow/references/phase-3-review.md +2 -2
- package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +3 -3
- package/principles/AGENTS.md +111 -0
- package/principles/__tests__/analyzer.test.ts +343 -0
- package/principles/__tests__/baseline-storage.test.ts +187 -0
- package/principles/__tests__/baseline.test.ts +328 -0
- package/principles/__tests__/boy-scout-integration.test.ts +264 -0
- package/principles/__tests__/boy-scout.test.ts +646 -0
- package/principles/__tests__/config.test.ts +78 -0
- package/principles/__tests__/index.test.ts +81 -0
- package/principles/__tests__/lint-baseline.test.ts +314 -0
- package/principles/__tests__/reporter.test.ts +832 -0
- package/principles/__tests__/types.test.ts +101 -0
- package/principles/adapters/__tests__/base.test.ts +95 -0
- package/principles/adapters/__tests__/cpp.test.ts +290 -0
- package/principles/adapters/__tests__/dart.test.ts +84 -0
- package/principles/adapters/__tests__/go.test.ts +68 -0
- package/principles/adapters/__tests__/java.test.ts +68 -0
- package/principles/adapters/__tests__/kotlin.test.ts +76 -0
- package/principles/adapters/__tests__/objectivec.test.ts +453 -0
- package/principles/adapters/__tests__/python.test.ts +84 -0
- package/principles/adapters/__tests__/swift.test.ts +68 -0
- package/principles/adapters/__tests__/typescript.test.ts +116 -0
- package/principles/adapters/base.ts +124 -0
- package/principles/adapters/cpp.ts +115 -0
- package/principles/adapters/dart.ts +44 -0
- package/principles/adapters/go.ts +55 -0
- package/principles/adapters/java.ts +56 -0
- package/principles/adapters/kotlin.ts +54 -0
- package/principles/adapters/objectivec.ts +82 -0
- package/principles/adapters/python.ts +83 -0
- package/principles/adapters/swift.ts +40 -0
- package/principles/adapters/typescript.ts +66 -0
- package/principles/analyzer.ts +190 -0
- package/principles/baseline.ts +273 -0
- package/principles/boy-scout.ts +416 -0
- package/principles/config.ts +118 -0
- package/principles/index.ts +87 -0
- package/principles/lint-baseline.ts +385 -0
- package/principles/reporter.ts +188 -0
- package/principles/rules/__tests__/clean-code/code-duplication.test.ts +84 -0
- package/principles/rules/__tests__/clean-code/deep-nesting.test.ts +74 -0
- package/principles/rules/__tests__/clean-code/god-class.test.ts +111 -0
- package/principles/rules/__tests__/clean-code/large-file.test.ts +73 -0
- package/principles/rules/__tests__/clean-code/long-function.test.ts +83 -0
- package/principles/rules/__tests__/clean-code/magic-numbers.test.ts +68 -0
- package/principles/rules/__tests__/clean-code/many-exports.test.ts +118 -0
- package/principles/rules/__tests__/clean-code/missing-error-handling.test.ts +173 -0
- package/principles/rules/__tests__/clean-code/too-many-params.test.ts +60 -0
- package/principles/rules/__tests__/clean-code/unused-imports.test.ts +107 -0
- package/principles/rules/__tests__/index.test.ts +49 -0
- package/principles/rules/__tests__/solid/dip.test.ts +121 -0
- package/principles/rules/__tests__/solid/isp.test.ts +60 -0
- package/principles/rules/__tests__/solid/lsp.test.ts +184 -0
- package/principles/rules/__tests__/solid/ocp.test.ts +69 -0
- package/principles/rules/__tests__/solid/srp.test.ts +89 -0
- package/principles/rules/clean-code/code-duplication.ts +31 -0
- package/principles/rules/clean-code/deep-nesting.ts +38 -0
- package/principles/rules/clean-code/god-class.ts +54 -0
- package/principles/rules/clean-code/large-file.ts +34 -0
- package/principles/rules/clean-code/long-function.ts +47 -0
- package/principles/rules/clean-code/magic-numbers.ts +59 -0
- package/principles/rules/clean-code/many-exports.ts +40 -0
- package/principles/rules/clean-code/missing-error-handling.ts +33 -0
- package/principles/rules/clean-code/too-many-params.ts +35 -0
- package/principles/rules/clean-code/unused-imports.ts +33 -0
- package/principles/rules/index.ts +61 -0
- package/principles/rules/solid/dip.ts +47 -0
- package/principles/rules/solid/isp.ts +37 -0
- package/principles/rules/solid/lsp.ts +79 -0
- package/principles/rules/solid/ocp.ts +40 -0
- package/principles/rules/solid/srp.ts +44 -0
- package/principles/types.ts +27 -0
- package/skills/delphi-review/AGENTS.md +7 -7
- package/skills/delphi-review/INSTALL.md +1 -1
- package/skills/delphi-review/SKILL.md +12 -12
- package/skills/delphi-review/references/code-walkthrough.md +1 -1
- package/skills/sprint-flow/AGENTS.md +7 -7
- package/skills/sprint-flow/SKILL.md +1 -1
- package/skills/sprint-flow/references/force-levels.md +1 -1
- package/skills/sprint-flow/references/phase-3-review.md +2 -2
- package/skills/test-specification-alignment/AGENTS.md +3 -3
|
@@ -0,0 +1,646 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
vi.mock('fs/promises', async (importOriginal) => {
|
|
4
|
+
const actual = await importOriginal<typeof import('fs/promises')>();
|
|
5
|
+
return {
|
|
6
|
+
...actual,
|
|
7
|
+
readFile: vi.fn(),
|
|
8
|
+
access: vi.fn(),
|
|
9
|
+
writeFile: vi.fn(),
|
|
10
|
+
};
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
vi.mock('../analyzer', () => ({
|
|
14
|
+
analyze: vi.fn(),
|
|
15
|
+
getAdapterForFile: vi.fn(),
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
vi.mock('../index', () => ({
|
|
19
|
+
getAllRules: vi.fn(() => []),
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
import { readFile, access, writeFile } from 'fs/promises';
|
|
23
|
+
import {
|
|
24
|
+
enforceBoyScoutRule,
|
|
25
|
+
classifyFiles,
|
|
26
|
+
calculateDelta,
|
|
27
|
+
loadBaseline,
|
|
28
|
+
saveBaseline,
|
|
29
|
+
initBaseline,
|
|
30
|
+
analyzeWarningsForFiles,
|
|
31
|
+
autoInitBaseline,
|
|
32
|
+
initBaselineCommand,
|
|
33
|
+
runEnforcement,
|
|
34
|
+
} from '../boy-scout';
|
|
35
|
+
import { analyze } from '../analyzer';
|
|
36
|
+
import { getAllRules } from '../index';
|
|
37
|
+
|
|
38
|
+
const mockAnalyze = vi.mocked(analyze);
|
|
39
|
+
const _mockGetAllRules = vi.mocked(getAllRules);
|
|
40
|
+
void _mockGetAllRules;
|
|
41
|
+
const mockReadFile = vi.mocked(readFile);
|
|
42
|
+
const mockAccess = vi.mocked(access);
|
|
43
|
+
const mockWriteFile = vi.mocked(writeFile);
|
|
44
|
+
|
|
45
|
+
interface BaselineEntry {
|
|
46
|
+
eslint?: { warnings: number; errors: number };
|
|
47
|
+
principles?: { warnings: number; errors: number };
|
|
48
|
+
ccn?: { warnings: number; max: number };
|
|
49
|
+
totalWarnings: number;
|
|
50
|
+
lastAnalyzed: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface DeltaResult {
|
|
54
|
+
file: string;
|
|
55
|
+
status: 'NEW' | 'MODIFIED' | 'UNCHANGED';
|
|
56
|
+
baselineWarnings: number;
|
|
57
|
+
currentWarnings: number;
|
|
58
|
+
delta: number;
|
|
59
|
+
enforcement: 'PASS' | 'BLOCK';
|
|
60
|
+
reason: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
beforeEach(() => {
|
|
64
|
+
vi.clearAllMocks();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* @test REQ-QG-002 Boy Scout Rule Enforcement
|
|
69
|
+
* @intent Verify differential warning enforcement for historical projects
|
|
70
|
+
* @covers AC-QG-002-01, AC-QG-002-02, AC-QG-002-03, AC-QG-002-04, AC-QG-002-05, AC-QG-002-06, AC-QG-002-07, AC-QG-002-08, AC-QG-002-09, AC-QG-002-10, AC-QG-002-11
|
|
71
|
+
*/
|
|
72
|
+
describe('Boy Scout Rule Enforcement', () => {
|
|
73
|
+
/**
|
|
74
|
+
* @test REQ-QG-002
|
|
75
|
+
* @covers AC-QG-002-01
|
|
76
|
+
*/
|
|
77
|
+
describe('file classification', () => {
|
|
78
|
+
const baseGitDiff = ['A src/new-file.ts', 'M src/existing-file.ts', 'D src/deleted-file.ts'];
|
|
79
|
+
|
|
80
|
+
it('classifies new, modified, deleted files from git diff', () => {
|
|
81
|
+
const classified = classifyFiles(baseGitDiff);
|
|
82
|
+
expect(classified.new).toEqual(['src/new-file.ts']);
|
|
83
|
+
expect(classified.modified).toEqual(['src/existing-file.ts']);
|
|
84
|
+
expect(classified.deleted).toEqual(['src/deleted-file.ts']);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('handles renamed files correctly', () => {
|
|
88
|
+
const classified = classifyFiles([...baseGitDiff, 'R095 old-file.ts src/new-renamed-file.ts']);
|
|
89
|
+
expect(classified.renamed).toEqual([{ oldPath: 'old-file.ts', newPath: 'src/new-renamed-file.ts' }]);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('handles empty diff lines', () => {
|
|
93
|
+
const classified = classifyFiles(['', 'A src/new-file.ts', ' ']);
|
|
94
|
+
expect(classified.new).toEqual(['src/new-file.ts']);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* @test REQ-QG-002
|
|
100
|
+
* @covers AC-QG-002-02, AC-QG-002-03
|
|
101
|
+
*/
|
|
102
|
+
describe('baseline management', () => {
|
|
103
|
+
it('returns empty baseline when file missing', async () => {
|
|
104
|
+
const baseline = await loadBaseline('.nonexistent-baseline.json');
|
|
105
|
+
expect(baseline).toEqual({});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('saves baseline to file', async () => {
|
|
109
|
+
const testData: Record<string, BaselineEntry> = {
|
|
110
|
+
'src/test.ts': { totalWarnings: 3, lastAnalyzed: new Date().toISOString() }
|
|
111
|
+
};
|
|
112
|
+
const testPath = '/tmp/test-baseline-save.json';
|
|
113
|
+
|
|
114
|
+
vi.mocked(mockWriteFile).mockResolvedValue(undefined);
|
|
115
|
+
|
|
116
|
+
await saveBaseline(testPath, testData);
|
|
117
|
+
|
|
118
|
+
expect(mockWriteFile).toHaveBeenCalledWith(testPath, JSON.stringify(testData, null, 2));
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* @test REQ-QG-002
|
|
124
|
+
* @covers AC-QG-002-04, AC-QG-002-05, AC-QG-002-06, AC-QG-002-07, AC-QG-002-08
|
|
125
|
+
*/
|
|
126
|
+
describe('delta calculation', () => {
|
|
127
|
+
it('returns delta = 0 for new files with zero warnings', () => {
|
|
128
|
+
const result = calculateDelta(null, 0, 'NEW');
|
|
129
|
+
expect(result.delta).toBe(0);
|
|
130
|
+
expect(result.enforcement).toBe('PASS');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('blocks new files with any warnings', () => {
|
|
134
|
+
const result = calculateDelta(null, 3, 'NEW');
|
|
135
|
+
expect(result.delta).toBe(3);
|
|
136
|
+
expect(result.enforcement).toBe('BLOCK');
|
|
137
|
+
expect(result.reason).toContain('New files must have zero warnings');
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('allows modified files with decreased warnings', () => {
|
|
141
|
+
const result = calculateDelta({ totalWarnings: 10 } as BaselineEntry, 3, 'MODIFIED');
|
|
142
|
+
expect(result.delta).toBe(-7);
|
|
143
|
+
expect(result.enforcement).toBe('PASS');
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('allows modified files with same warnings (if >5)', () => {
|
|
147
|
+
const result = calculateDelta({ totalWarnings: 10 } as BaselineEntry, 10, 'MODIFIED');
|
|
148
|
+
expect(result.delta).toBe(0);
|
|
149
|
+
expect(result.enforcement).toBe('PASS');
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('blocks modified files with increased warnings', () => {
|
|
153
|
+
const result = calculateDelta({ totalWarnings: 2 } as BaselineEntry, 5, 'MODIFIED');
|
|
154
|
+
expect(result.delta).toBe(3);
|
|
155
|
+
expect(result.enforcement).toBe('BLOCK');
|
|
156
|
+
expect(result.reason).toContain('Modified files cannot increase warnings');
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('blocks files with ≤5 warnings that dont clear to zero', () => {
|
|
160
|
+
const baselineEntry = { totalWarnings: 3, lastAnalyzed: new Date().toISOString() } as BaselineEntry;
|
|
161
|
+
const result = calculateDelta(baselineEntry, 1, 'MODIFIED');
|
|
162
|
+
expect(result.enforcement).toBe('BLOCK');
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('allows files with baseline ≤5 warnings to clear to zero warnings', () => {
|
|
166
|
+
const baselineEntry = { totalWarnings: 3, lastAnalyzed: new Date().toISOString() } as BaselineEntry;
|
|
167
|
+
const result = calculateDelta(baselineEntry, 0, 'MODIFIED');
|
|
168
|
+
expect(result.enforcement).toBe('PASS');
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('passes modified file with auto-init (no baseline entry)', () => {
|
|
172
|
+
const result = calculateDelta(null, 5, 'MODIFIED');
|
|
173
|
+
expect(result.enforcement).toBe('PASS');
|
|
174
|
+
expect(result.reason).toContain('baseline');
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* @test REQ-QG-002
|
|
180
|
+
* @covers AC-QG-002-11
|
|
181
|
+
*/
|
|
182
|
+
describe('enforcement', () => {
|
|
183
|
+
it('returns PASS for all clean files', () => {
|
|
184
|
+
const deltas: DeltaResult[] = [
|
|
185
|
+
{ file: 'src/file1.ts', status: 'NEW', baselineWarnings: 0, currentWarnings: 0, delta: 0, enforcement: 'PASS', reason: 'Clean' },
|
|
186
|
+
{ file: 'src/file2.ts', status: 'MODIFIED', baselineWarnings: 5, currentWarnings: 3, delta: -2, enforcement: 'PASS', reason: 'Warnings decreased' }
|
|
187
|
+
];
|
|
188
|
+
|
|
189
|
+
const result = enforceBoyScoutRule(deltas);
|
|
190
|
+
expect(result.overallStatus).toBe('PASS');
|
|
191
|
+
expect(result.violations).toEqual([]);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('returns BLOCK when any file violates Boy Scout Rule', () => {
|
|
195
|
+
const deltas: DeltaResult[] = [
|
|
196
|
+
{ file: 'src/new.ts', status: 'NEW', baselineWarnings: 0, currentWarnings: 0, delta: 0, enforcement: 'PASS', reason: '' },
|
|
197
|
+
{ file: 'src/bad.ts', status: 'NEW', baselineWarnings: 0, currentWarnings: 2, delta: 2, enforcement: 'BLOCK', reason: 'New files must have zero warnings' }
|
|
198
|
+
];
|
|
199
|
+
|
|
200
|
+
const result = enforceBoyScoutRule(deltas);
|
|
201
|
+
expect(result.overallStatus).toBe('BLOCK');
|
|
202
|
+
expect(result.violations).toContainEqual(deltas[1]);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('generates detailed delta report', () => {
|
|
206
|
+
const deltas: DeltaResult[] = [
|
|
207
|
+
{ file: 'src/file.ts', status: 'NEW', baselineWarnings: 0, currentWarnings: 0, delta: 0, enforcement: 'PASS', reason: 'New file with zero warnings' }
|
|
208
|
+
];
|
|
209
|
+
|
|
210
|
+
const result = enforceBoyScoutRule(deltas);
|
|
211
|
+
expect(result.detailedReport).toEqual(deltas);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('returns summary statistics', () => {
|
|
215
|
+
const deltas: DeltaResult[] = [
|
|
216
|
+
{ file: 'src/good.ts', status: 'MODIFIED', baselineWarnings: 5, currentWarnings: 3, delta: -2, enforcement: 'PASS', reason: 'Warnings decreased' },
|
|
217
|
+
{ file: 'src/bad.ts', status: 'NEW', baselineWarnings: 0, currentWarnings: 2, delta: 2, enforcement: 'BLOCK', reason: 'New files must have zero warnings' }
|
|
218
|
+
];
|
|
219
|
+
|
|
220
|
+
const result = enforceBoyScoutRule(deltas);
|
|
221
|
+
expect(result.summary).toEqual({
|
|
222
|
+
totalFiles: 2,
|
|
223
|
+
passedFiles: 1,
|
|
224
|
+
blockedFiles: 1
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* @test REQ-QG-002
|
|
231
|
+
* @covers AC-QG-002-09
|
|
232
|
+
*/
|
|
233
|
+
describe('basic CLI functionality', () => {
|
|
234
|
+
it('runs init via initBaseline', () => {
|
|
235
|
+
expect(typeof initBaseline).toBe('function');
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* @test REQ-QG-002
|
|
241
|
+
* @covers AC-QG-002-07, AC-QG-002-08
|
|
242
|
+
*/
|
|
243
|
+
describe('threshold enforcement', () => {
|
|
244
|
+
const entry = (w: number) => ({ totalWarnings: w, lastAnalyzed: new Date().toISOString() }) as BaselineEntry;
|
|
245
|
+
|
|
246
|
+
it('blocks ≤5 warnings that dont clear to zero', () => {
|
|
247
|
+
expect(calculateDelta(entry(5), 1, 'MODIFIED').enforcement).toBe('BLOCK');
|
|
248
|
+
expect(calculateDelta(entry(3), 3, 'MODIFIED').enforcement).toBe('BLOCK');
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('passes ≤5 warnings cleared to zero or >5 warnings that decrease', () => {
|
|
252
|
+
expect(calculateDelta(entry(5), 0, 'MODIFIED').enforcement).toBe('PASS');
|
|
253
|
+
expect(calculateDelta(entry(6), 5, 'MODIFIED').enforcement).toBe('PASS');
|
|
254
|
+
expect(calculateDelta(entry(8), 8, 'MODIFIED').enforcement).toBe('PASS');
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* @test REQ-QG-002
|
|
260
|
+
* @covers AC-QG-002-02, AC-QG-002-03
|
|
261
|
+
*/
|
|
262
|
+
describe('error handling', () => {
|
|
263
|
+
it('handles empty file lists', () => {
|
|
264
|
+
const result = enforceBoyScoutRule([]);
|
|
265
|
+
expect(result.summary.totalFiles).toBe(0);
|
|
266
|
+
expect(result.overallStatus).toBe('PASS');
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* @test #173 boy-scout coverage - runEnforcement
|
|
272
|
+
* @covers AC-QG-002-runEnforcement-full
|
|
273
|
+
*/
|
|
274
|
+
describe('runEnforcement', () => {
|
|
275
|
+
const warn = (file: string) => ({ file, severity: 'warning' as const, ruleId: 'r1', message: 'm' });
|
|
276
|
+
const setup = () => {
|
|
277
|
+
mockAccess.mockRejectedValue(new Error('ENOENT'));
|
|
278
|
+
mockWriteFile.mockResolvedValue(undefined);
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
it('enforces new files with zero warnings', async () => {
|
|
282
|
+
setup();
|
|
283
|
+
mockAnalyze.mockResolvedValue({ violations: [] } as unknown as Awaited<ReturnType<typeof analyze>>);
|
|
284
|
+
const result = await runEnforcement(['clean.ts'], [], '/tmp/test-baseline.json');
|
|
285
|
+
expect(result.overallStatus).toBe('PASS');
|
|
286
|
+
expect(result.summary.totalFiles).toBe(1);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it('auto-initializes baseline for modified files and passes', async () => {
|
|
290
|
+
setup();
|
|
291
|
+
mockAnalyze.mockResolvedValue({ violations: [warn('mod.ts')] } as unknown as Awaited<ReturnType<typeof analyze>>);
|
|
292
|
+
const result = await runEnforcement([], ['mod.ts'], '/tmp/test-baseline.json');
|
|
293
|
+
expect(result.summary.totalFiles).toBe(1);
|
|
294
|
+
expect(mockWriteFile).toHaveBeenCalled();
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('blocks new files with warnings', async () => {
|
|
298
|
+
setup();
|
|
299
|
+
mockAnalyze.mockResolvedValue({ violations: [warn('bad.ts')] } as unknown as Awaited<ReturnType<typeof analyze>>);
|
|
300
|
+
const result = await runEnforcement(['bad.ts'], [], '/tmp/test-baseline.json');
|
|
301
|
+
expect(result.overallStatus).toBe('BLOCK');
|
|
302
|
+
expect(result.violations.length).toBe(1);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it('handles mixed new and modified files', async () => {
|
|
306
|
+
setup();
|
|
307
|
+
mockAnalyze.mockResolvedValue({ violations: [warn('bad.ts')] } as unknown as Awaited<ReturnType<typeof analyze>>);
|
|
308
|
+
const result = await runEnforcement(['bad.ts', 'clean.ts'], ['existing.ts'], '/tmp/test-baseline.json');
|
|
309
|
+
expect(result.summary.totalFiles).toBe(3);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it('decreased warnings shows reason and same high warnings shows "No new warnings"', async () => {
|
|
313
|
+
const baseline = { 'mod.ts': { totalWarnings: 3, lastAnalyzed: new Date().toISOString() } };
|
|
314
|
+
mockAccess.mockResolvedValue(undefined);
|
|
315
|
+
mockWriteFile.mockResolvedValue(undefined);
|
|
316
|
+
|
|
317
|
+
// Decreased case
|
|
318
|
+
mockAnalyze.mockResolvedValue({ violations: [] } as unknown as Awaited<ReturnType<typeof analyze>>);
|
|
319
|
+
mockReadFile.mockResolvedValue(JSON.stringify(baseline));
|
|
320
|
+
const decResult = await runEnforcement([], ['mod.ts'], '/tmp/test-baseline.json');
|
|
321
|
+
expect(decResult.detailedReport[0].reason).toBe('Warnings decreased by 3');
|
|
322
|
+
|
|
323
|
+
// Same high warnings case
|
|
324
|
+
const highBaseline = { 'mod.ts': { totalWarnings: 10, lastAnalyzed: new Date().toISOString() } };
|
|
325
|
+
mockAnalyze.mockResolvedValue({
|
|
326
|
+
violations: Array.from({ length: 10 }, () => warn('mod.ts')),
|
|
327
|
+
} as unknown as Awaited<ReturnType<typeof analyze>>);
|
|
328
|
+
mockReadFile.mockResolvedValue(JSON.stringify(highBaseline));
|
|
329
|
+
const sameResult = await runEnforcement([], ['mod.ts'], '/tmp/test-baseline.json');
|
|
330
|
+
expect(sameResult.detailedReport[0].reason).toBe('No new warnings introduced');
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* @test REQ-4
|
|
336
|
+
* @covers analyzeWarningsForFiles - lines 14-39
|
|
337
|
+
*/
|
|
338
|
+
describe('analyzeWarningsForFiles', () => {
|
|
339
|
+
it('returns empty object for empty string input', async () => {
|
|
340
|
+
const result = await analyzeWarningsForFiles('');
|
|
341
|
+
expect(result).toEqual({});
|
|
342
|
+
expect(mockAnalyze).not.toHaveBeenCalled();
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it('returns empty object for empty array input', async () => {
|
|
346
|
+
const result = await analyzeWarningsForFiles([]);
|
|
347
|
+
expect(result).toEqual({});
|
|
348
|
+
expect(mockAnalyze).not.toHaveBeenCalled();
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
it('parses comma-separated string to file list', async () => {
|
|
352
|
+
mockAnalyze.mockResolvedValue({
|
|
353
|
+
violations: [],
|
|
354
|
+
} as unknown as Awaited<ReturnType<typeof analyze>>);
|
|
355
|
+
|
|
356
|
+
const result = await analyzeWarningsForFiles('src/a.ts,src/b.ts');
|
|
357
|
+
|
|
358
|
+
expect(mockAnalyze).toHaveBeenCalled();
|
|
359
|
+
expect(result).toEqual({ 'src/a.ts': 0, 'src/b.ts': 0 });
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it('accepts array input', async () => {
|
|
363
|
+
mockAnalyze.mockResolvedValue({
|
|
364
|
+
violations: [],
|
|
365
|
+
} as unknown as Awaited<ReturnType<typeof analyze>>);
|
|
366
|
+
|
|
367
|
+
const result = await analyzeWarningsForFiles(['src/a.ts', 'src/b.ts']);
|
|
368
|
+
|
|
369
|
+
expect(mockAnalyze).toHaveBeenCalled();
|
|
370
|
+
expect(result).toEqual({ 'src/a.ts': 0, 'src/b.ts': 0 });
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it('counts warning/error violations per file and ignores info-level', async () => {
|
|
374
|
+
mockAnalyze.mockResolvedValue({
|
|
375
|
+
violations: [
|
|
376
|
+
{ file: 'src/a.ts', line: 1, severity: 'warning' as const, ruleId: 'r1', message: 'm' },
|
|
377
|
+
{ file: 'src/a.ts', line: 2, severity: 'warning' as const, ruleId: 'r2', message: 'm' },
|
|
378
|
+
{ file: 'src/b.ts', line: 1, severity: 'warning' as const, ruleId: 'r1', message: 'm' },
|
|
379
|
+
{ file: 'src/a.ts', line: 3, severity: 'error' as const, ruleId: 'r3', message: 'm' },
|
|
380
|
+
{ file: 'src/c.ts', line: 1, severity: 'info' as const, ruleId: 'r4', message: 'm' },
|
|
381
|
+
],
|
|
382
|
+
} as unknown as Awaited<ReturnType<typeof analyze>>);
|
|
383
|
+
|
|
384
|
+
const result = await analyzeWarningsForFiles('src/a.ts,src/b.ts,src/c.ts');
|
|
385
|
+
expect(result['src/a.ts']).toBe(3);
|
|
386
|
+
expect(result['src/b.ts']).toBe(1);
|
|
387
|
+
expect(result['src/c.ts']).toBe(0);
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
it('handles files not in the input list', async () => {
|
|
391
|
+
mockAnalyze.mockResolvedValue({
|
|
392
|
+
violations: [{ file: 'src/unknown.ts', line: 1, severity: 'warning' as const, ruleId: 'r1', message: 'm' }],
|
|
393
|
+
} as unknown as Awaited<ReturnType<typeof analyze>>);
|
|
394
|
+
|
|
395
|
+
const result = await analyzeWarningsForFiles('src/a.ts');
|
|
396
|
+
expect(result['src/a.ts']).toBe(0);
|
|
397
|
+
expect(result['src/unknown.ts']).toBe(1);
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
it('handles whitespace in comma-separated strings', async () => {
|
|
401
|
+
mockAnalyze.mockResolvedValue({ violations: [] } as unknown as Awaited<ReturnType<typeof analyze>>);
|
|
402
|
+
const result = await analyzeWarningsForFiles(' src/a.ts , src/b.ts ');
|
|
403
|
+
expect(result).toHaveProperty('src/a.ts');
|
|
404
|
+
expect(result).toHaveProperty('src/b.ts');
|
|
405
|
+
});
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* @test REQ-4
|
|
410
|
+
* @covers loadBaseline with existing file, saveBaseline - lines 112-124
|
|
411
|
+
*/
|
|
412
|
+
describe('loadBaseline', () => {
|
|
413
|
+
it('loads and parses baseline from existing file', async () => {
|
|
414
|
+
const mockData = { 'src/a.ts': { totalWarnings: 5, lastAnalized: '2024-01-01' } };
|
|
415
|
+
|
|
416
|
+
(mockAccess as ReturnType<typeof vi.fn>).mockResolvedValue(undefined);
|
|
417
|
+
(mockReadFile as ReturnType<typeof vi.fn>).mockResolvedValue(JSON.stringify(mockData));
|
|
418
|
+
|
|
419
|
+
const result = await loadBaseline('/tmp/test-baseline.json');
|
|
420
|
+
|
|
421
|
+
expect(result).toEqual(mockData);
|
|
422
|
+
expect(mockAccess).toHaveBeenCalledWith('/tmp/test-baseline.json');
|
|
423
|
+
expect(mockReadFile).toHaveBeenCalledWith('/tmp/test-baseline.json', 'utf-8');
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
it('returns empty object when file does not exist', async () => {
|
|
427
|
+
(mockAccess as ReturnType<typeof vi.fn>).mockRejectedValue(new Error('ENOENT'));
|
|
428
|
+
|
|
429
|
+
const result = await loadBaseline('/tmp/nonexistent.json');
|
|
430
|
+
|
|
431
|
+
expect(result).toEqual({});
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
it('returns empty object when JSON parse fails', async () => {
|
|
435
|
+
(mockAccess as ReturnType<typeof vi.fn>).mockResolvedValue(undefined);
|
|
436
|
+
(mockReadFile as ReturnType<typeof vi.fn>).mockResolvedValue('not json');
|
|
437
|
+
|
|
438
|
+
const result = await loadBaseline('/tmp/bad.json');
|
|
439
|
+
|
|
440
|
+
expect(result).toEqual({});
|
|
441
|
+
});
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* @test REQ-4
|
|
446
|
+
* @covers initBaseline, autoInitBaseline, initBaselineCommand - lines 201-244, 331-334
|
|
447
|
+
*/
|
|
448
|
+
describe('init and auto-init baseline', () => {
|
|
449
|
+
it('returns baseline with warning counts for files with violations', async () => {
|
|
450
|
+
mockAnalyze.mockResolvedValue({
|
|
451
|
+
violations: [
|
|
452
|
+
{ file: 'src/a.ts', line: 1, severity: 'warning' as const, ruleId: 'r1', message: 'm' },
|
|
453
|
+
{ file: 'src/a.ts', line: 2, severity: 'warning' as const, ruleId: 'r2', message: 'm' },
|
|
454
|
+
],
|
|
455
|
+
} as unknown as Awaited<ReturnType<typeof analyze>>);
|
|
456
|
+
|
|
457
|
+
const result = await initBaseline(['src/a.ts', 'src/b.ts']);
|
|
458
|
+
|
|
459
|
+
expect(result['src/a.ts'].totalWarnings).toBe(2);
|
|
460
|
+
expect(result).not.toHaveProperty('src/b.ts');
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
it('only includes files with warnings > 0', async () => {
|
|
464
|
+
mockAnalyze.mockResolvedValue({ violations: [] } as unknown as Awaited<ReturnType<typeof analyze>>);
|
|
465
|
+
|
|
466
|
+
const result = await initBaseline(['src/a.ts', 'src/b.ts']);
|
|
467
|
+
expect(Object.keys(result)).toEqual([]);
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
it('handles empty file list', async () => {
|
|
471
|
+
const result = await initBaseline([]);
|
|
472
|
+
expect(result).toEqual({});
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
it('sets lastAnalyzed to current date', async () => {
|
|
476
|
+
mockAnalyze.mockResolvedValue({
|
|
477
|
+
violations: [{ file: 'src/a.ts', severity: 'warning' as const, ruleId: 'r1', message: 'm' }],
|
|
478
|
+
} as unknown as Awaited<ReturnType<typeof analyze>>);
|
|
479
|
+
|
|
480
|
+
const before = new Date().toISOString();
|
|
481
|
+
const result = await initBaseline(['src/a.ts']);
|
|
482
|
+
const after = new Date().toISOString();
|
|
483
|
+
|
|
484
|
+
expect(result['src/a.ts'].lastAnalyzed >= before).toBe(true);
|
|
485
|
+
expect(result['src/a.ts'].lastAnalyzed <= after).toBe(true);
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
it('rethrows when analyzeWarningsForFiles fails globally', async () => {
|
|
489
|
+
mockAnalyze.mockRejectedValue(new Error('analyze failed'));
|
|
490
|
+
await expect(initBaseline(['src/fail.ts'])).rejects.toThrow('analyze failed');
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
it('analyzes files and saves baseline', async () => {
|
|
494
|
+
mockAnalyze.mockResolvedValue({
|
|
495
|
+
violations: [{ file: 'src/a.ts', severity: 'warning' as const, ruleId: 'r1', message: 'm' }],
|
|
496
|
+
} as unknown as Awaited<ReturnType<typeof analyze>>);
|
|
497
|
+
mockWriteFile.mockResolvedValue(undefined);
|
|
498
|
+
|
|
499
|
+
const result = await autoInitBaseline(['src/a.ts'], '/tmp/auto-baseline.json');
|
|
500
|
+
expect(result['src/a.ts'].totalWarnings).toBe(1);
|
|
501
|
+
expect(mockWriteFile).toHaveBeenCalledWith('/tmp/auto-baseline.json', expect.stringContaining('src/a.ts'));
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
it('does not include files with zero warnings', async () => {
|
|
505
|
+
mockAnalyze.mockResolvedValue({ violations: [] } as unknown as Awaited<ReturnType<typeof analyze>>);
|
|
506
|
+
mockWriteFile.mockResolvedValue(undefined);
|
|
507
|
+
|
|
508
|
+
const result = await autoInitBaseline(['src/a.ts'], '/tmp/auto-baseline.json');
|
|
509
|
+
expect(Object.keys(result)).toEqual([]);
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
it('handles files with warnings mixed with files without', async () => {
|
|
513
|
+
mockAnalyze.mockResolvedValue({
|
|
514
|
+
violations: [
|
|
515
|
+
{ file: 'src/warn.ts', severity: 'warning' as const, ruleId: 'r1', message: 'm' },
|
|
516
|
+
{ file: 'src/warn.ts', severity: 'warning' as const, ruleId: 'r2', message: 'm' },
|
|
517
|
+
{ file: 'src/warn.ts', severity: 'error' as const, ruleId: 'r3', message: 'm' },
|
|
518
|
+
],
|
|
519
|
+
} as unknown as Awaited<ReturnType<typeof analyze>>);
|
|
520
|
+
mockWriteFile.mockResolvedValue(undefined);
|
|
521
|
+
|
|
522
|
+
const result = await autoInitBaseline(['src/warn.ts', 'src/clean.ts'], '/tmp/auto.json');
|
|
523
|
+
expect(result['src/warn.ts'].totalWarnings).toBe(3);
|
|
524
|
+
expect(result).not.toHaveProperty('src/clean.ts');
|
|
525
|
+
expect(mockWriteFile).toHaveBeenCalled();
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
it('calls initBaseline and saves with default path', async () => {
|
|
529
|
+
mockAnalyze.mockResolvedValue({
|
|
530
|
+
violations: [{ file: 'src/a.ts', severity: 'warning' as const, ruleId: 'r1', message: 'm' }],
|
|
531
|
+
} as unknown as Awaited<ReturnType<typeof analyze>>);
|
|
532
|
+
mockWriteFile.mockResolvedValue(undefined);
|
|
533
|
+
|
|
534
|
+
await initBaselineCommand(['src/a.ts']);
|
|
535
|
+
expect(mockWriteFile).toHaveBeenCalledWith('.warnings-baseline.json', expect.stringContaining('src/a.ts'));
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
it('saves baseline even when there are no warnings', async () => {
|
|
539
|
+
mockAnalyze.mockResolvedValue({ violations: [] } as unknown as Awaited<ReturnType<typeof analyze>>);
|
|
540
|
+
mockWriteFile.mockResolvedValue(undefined);
|
|
541
|
+
|
|
542
|
+
await initBaselineCommand(['src/clean.ts']);
|
|
543
|
+
expect(mockWriteFile).toHaveBeenCalledWith('.warnings-baseline.json', expect.any(String));
|
|
544
|
+
const savedJson = JSON.parse(vi.mocked(mockWriteFile).mock.calls[0][1] as string);
|
|
545
|
+
expect(Object.keys(savedJson)).toEqual([]);
|
|
546
|
+
});
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* @test REQ-QG-002 CLI entrypoint coverage
|
|
551
|
+
* @intent Cover internal CLI helpers (main, parseArgs, splitCsvArg, ARG_HANDLERS, runInitBaselineCommand, runEnforcementCommand, showHelp) via module re-import with synthesized argv
|
|
552
|
+
* @covers analyzeWarningsForFiles-CLI-1, analyzeWarningsForFiles-CLI-2, analyzeWarningsForFiles-CLI-3
|
|
553
|
+
*/
|
|
554
|
+
/**
|
|
555
|
+
* @test REQ-QG-002 CLI entrypoint coverage
|
|
556
|
+
* @intent Cover internal CLI helpers (main, parseArgs, splitCsvArg, ARG_HANDLERS, runInitBaselineCommand, runEnforcementCommand, showHelp) via real subprocess execution with NODE_V8_COVERAGE so vitest's v8 reporter merges the child profiles.
|
|
557
|
+
* @covers boy-scout-cli-help, boy-scout-cli-init-baseline, boy-scout-cli-enforce
|
|
558
|
+
*/
|
|
559
|
+
describe('CLI entrypoint (main / parseArgs / handlers)', () => {
|
|
560
|
+
const { spawnSync } = require('child_process') as typeof import('child_process');
|
|
561
|
+
const path = require('path') as typeof import('path');
|
|
562
|
+
const fs = require('fs') as typeof import('fs');
|
|
563
|
+
const os = require('os') as typeof import('os');
|
|
564
|
+
|
|
565
|
+
const BOY_SCOUT_PATH = path.resolve(__dirname, '../boy-scout.ts');
|
|
566
|
+
|
|
567
|
+
function runCli(args: string[], cwd?: string): { code: number; stdout: string; stderr: string } {
|
|
568
|
+
const coverageDir = process.env.NODE_V8_COVERAGE ?? path.join(process.cwd(), 'coverage', '.tmp');
|
|
569
|
+
const result = spawnSync('npx', ['tsx', BOY_SCOUT_PATH, ...args], {
|
|
570
|
+
cwd: cwd ?? process.cwd(),
|
|
571
|
+
encoding: 'utf-8',
|
|
572
|
+
env: { ...process.env, NODE_V8_COVERAGE: coverageDir },
|
|
573
|
+
});
|
|
574
|
+
return {
|
|
575
|
+
code: result.status ?? -1,
|
|
576
|
+
stdout: result.stdout ?? '',
|
|
577
|
+
stderr: result.stderr ?? '',
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
it('shows help when --help is passed', () => {
|
|
582
|
+
const { code, stdout } = runCli(['--help']);
|
|
583
|
+
expect(code).toBe(0);
|
|
584
|
+
expect(stdout).toContain('Usage: boy-scout');
|
|
585
|
+
expect(stdout).toContain('--init-baseline');
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
it('shows help when -h is passed', () => {
|
|
589
|
+
const { code, stdout } = runCli(['-h']);
|
|
590
|
+
expect(code).toBe(0);
|
|
591
|
+
expect(stdout).toContain('Usage: boy-scout');
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
it('shows help when "help" keyword is passed', () => {
|
|
595
|
+
const { code, stdout } = runCli(['help']);
|
|
596
|
+
expect(code).toBe(0);
|
|
597
|
+
expect(stdout).toContain('Usage: boy-scout');
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
it('runs init-baseline command via --init-baseline flag in a clean tmp project', () => {
|
|
601
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'boy-scout-init-'));
|
|
602
|
+
try {
|
|
603
|
+
fs.writeFileSync(path.join(tmp, 'a.ts'), 'export const x = 1;\n');
|
|
604
|
+
const { code, stdout } = runCli(['--init-baseline', 'a.ts'], tmp);
|
|
605
|
+
expect(code).toBe(0);
|
|
606
|
+
expect(stdout).toContain('Baseline initialized successfully');
|
|
607
|
+
expect(fs.existsSync(path.join(tmp, '.warnings-baseline.json'))).toBe(true);
|
|
608
|
+
} finally {
|
|
609
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
610
|
+
}
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
it('runs enforcement command and prints JSON result with overallStatus', () => {
|
|
614
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'boy-scout-enforce-'));
|
|
615
|
+
try {
|
|
616
|
+
fs.writeFileSync(path.join(tmp, 'clean.ts'), 'export const ok = 1;\n');
|
|
617
|
+
const baselinePath = path.join(tmp, '.warnings-baseline.json');
|
|
618
|
+
const { code, stdout } = runCli([
|
|
619
|
+
'--new-files', 'clean.ts',
|
|
620
|
+
'--baseline', baselinePath,
|
|
621
|
+
], tmp);
|
|
622
|
+
expect([0, 1]).toContain(code);
|
|
623
|
+
expect(stdout).toContain('overallStatus');
|
|
624
|
+
} finally {
|
|
625
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
626
|
+
}
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
it('handles --modified-files argument and writes baseline auto-init', () => {
|
|
630
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'boy-scout-mod-'));
|
|
631
|
+
try {
|
|
632
|
+
fs.writeFileSync(path.join(tmp, 'mod.ts'), 'export const m = 1;\n');
|
|
633
|
+
const baselinePath = path.join(tmp, '.warnings-baseline.json');
|
|
634
|
+
fs.writeFileSync(baselinePath, '{}');
|
|
635
|
+
const { code, stdout } = runCli([
|
|
636
|
+
'--modified-files', 'mod.ts',
|
|
637
|
+
'--baseline', baselinePath,
|
|
638
|
+
], tmp);
|
|
639
|
+
expect([0, 1]).toContain(code);
|
|
640
|
+
expect(stdout).toContain('overallStatus');
|
|
641
|
+
} finally {
|
|
642
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
643
|
+
}
|
|
644
|
+
});
|
|
645
|
+
});
|
|
646
|
+
});
|