@boyingliu01/xp-gate 0.9.3 → 0.9.4
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/__tests__/baseline.test.js +89 -0
- package/lib/__tests__/doctor-helpers.test.js +139 -0
- package/lib/__tests__/upgrade-exec.test.js +42 -33
- package/lib/baseline.js +75 -48
- package/lib/doctor.js +180 -93
- package/lib/install-skill.js +81 -62
- package/lib/shared-phase-constants.d.ts +17 -0
- package/lib/shared-phase-constants.js +63 -0
- package/lib/shared-utils.js +14 -3
- package/lib/sprint-status.js +75 -83
- package/lib/upgrade.js +128 -85
- package/mock-policy/AGENTS.md +3 -3
- package/mutation/AGENTS.md +3 -3
- package/package.json +1 -1
- package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-code/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/claude-code/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/opencode/__tests__/xp-gate-update.test.ts +133 -4
- package/plugins/opencode/index.ts +28 -3
- package/plugins/opencode/package.json +1 -1
- package/plugins/opencode/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/opencode/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/opencode/tui-plugin.ts +44 -75
- package/plugins/qoder/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/qoder/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +3 -3
- package/principles/AGENTS.md +3 -3
- package/skills/delphi-review/AGENTS.md +3 -3
- package/skills/sprint-flow/AGENTS.md +3 -3
- package/skills/test-specification-alignment/AGENTS.md +3 -3
|
@@ -66,6 +66,95 @@ describe('baseline.js - CLI handler', () => {
|
|
|
66
66
|
});
|
|
67
67
|
});
|
|
68
68
|
|
|
69
|
+
// === TECH-005/006/007: Extracted helper unit tests (CCN reduction) ===
|
|
70
|
+
|
|
71
|
+
describe('countBaselineByTool', () => {
|
|
72
|
+
it('counts files by tool type from baseline data', () => {
|
|
73
|
+
const doc = require('../baseline.js');
|
|
74
|
+
const data = {
|
|
75
|
+
'a.ts': { eslint: { warnings: 3, errors: 0 }, totalWarnings: 3, lastAnalyzed: '2026-01-01' },
|
|
76
|
+
'b.py': { ruff: { warnings: 2, errors: 0 }, totalWarnings: 2, lastAnalyzed: '2026-01-01' },
|
|
77
|
+
'c.go': { golangci: { warnings: 1, errors: 1 }, totalWarnings: 1, lastAnalyzed: '2026-01-01' },
|
|
78
|
+
'd.sh': { shellcheck: { warnings: 4, errors: 0 }, totalWarnings: 4, lastAnalyzed: '2026-01-01' },
|
|
79
|
+
};
|
|
80
|
+
const result = doc.countBaselineByTool(data);
|
|
81
|
+
expect(result.eslintCount).toBe(1);
|
|
82
|
+
expect(result.ruffCount).toBe(1);
|
|
83
|
+
expect(result.golangciCount).toBe(1);
|
|
84
|
+
expect(result.shellcheckCount).toBe(1);
|
|
85
|
+
expect(result.totalFiles).toBe(4);
|
|
86
|
+
expect(result.totalWarnings).toBe(10);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('returns zeroes for empty baseline', () => {
|
|
90
|
+
const doc = require('../baseline.js');
|
|
91
|
+
const result = doc.countBaselineByTool({});
|
|
92
|
+
expect(result.eslintCount).toBe(0);
|
|
93
|
+
expect(result.ruffCount).toBe(0);
|
|
94
|
+
expect(result.totalFiles).toBe(0);
|
|
95
|
+
expect(result.totalWarnings).toBe(0);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('handles mixed entries with some missing tool fields', () => {
|
|
99
|
+
const doc = require('../baseline.js');
|
|
100
|
+
const data = {
|
|
101
|
+
'a.ts': { eslint: { warnings: 3, errors: 0 }, totalWarnings: 3, lastAnalyzed: '2026-01-01' },
|
|
102
|
+
'b.ts': { totalWarnings: 0, lastAnalyzed: '2026-01-01' },
|
|
103
|
+
};
|
|
104
|
+
const result = doc.countBaselineByTool(data);
|
|
105
|
+
expect(result.eslintCount).toBe(1);
|
|
106
|
+
expect(result.totalFiles).toBe(2);
|
|
107
|
+
expect(result.totalWarnings).toBe(3);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe('compareBaselines', () => {
|
|
112
|
+
it('detects added, removed, increased, and decreased files', () => {
|
|
113
|
+
const doc = require('../baseline.js');
|
|
114
|
+
const oldB = {
|
|
115
|
+
'stable.ts': { totalWarnings: 3 },
|
|
116
|
+
'increased.ts': { totalWarnings: 2 },
|
|
117
|
+
'decreased.ts': { totalWarnings: 5 },
|
|
118
|
+
'removed.ts': { totalWarnings: 1 },
|
|
119
|
+
};
|
|
120
|
+
const newB = {
|
|
121
|
+
'stable.ts': { totalWarnings: 3 },
|
|
122
|
+
'increased.ts': { totalWarnings: 5 },
|
|
123
|
+
'decreased.ts': { totalWarnings: 1 },
|
|
124
|
+
'added.ts': { totalWarnings: 4 },
|
|
125
|
+
};
|
|
126
|
+
const result = doc.compareBaselines(oldB, newB);
|
|
127
|
+
expect(result.totalWarningsDelta).toBe(2);
|
|
128
|
+
expect(result.increased.length).toBe(1);
|
|
129
|
+
expect(result.increased[0]).toContain('increased.ts');
|
|
130
|
+
expect(result.decreased.length).toBe(1);
|
|
131
|
+
expect(result.decreased[0]).toContain('decreased.ts');
|
|
132
|
+
expect(result.added.length).toBe(1);
|
|
133
|
+
expect(result.added[0]).toContain('added.ts');
|
|
134
|
+
expect(result.removed.length).toBe(1);
|
|
135
|
+
expect(result.removed[0]).toContain('removed.ts');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('returns empty arrays when baselines are identical', () => {
|
|
139
|
+
const doc = require('../baseline.js');
|
|
140
|
+
const baseline = { 'file.ts': { totalWarnings: 3 } };
|
|
141
|
+
const result = doc.compareBaselines(baseline, baseline);
|
|
142
|
+
expect(result.totalWarningsDelta).toBe(0);
|
|
143
|
+
expect(result.increased).toEqual([]);
|
|
144
|
+
expect(result.decreased).toEqual([]);
|
|
145
|
+
expect(result.added).toEqual([]);
|
|
146
|
+
expect(result.removed).toEqual([]);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('handles empty old or new baseline', () => {
|
|
150
|
+
const doc = require('../baseline.js');
|
|
151
|
+
const baseline = { 'file.ts': { totalWarnings: 3 } };
|
|
152
|
+
const result = doc.compareBaselines({}, baseline);
|
|
153
|
+
expect(result.added.length).toBe(1);
|
|
154
|
+
expect(result.totalWarningsDelta).toBe(3);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
69
158
|
describe('showBaseline - no baseline file', () => {
|
|
70
159
|
it('shows message when no baseline exists', () => {
|
|
71
160
|
const logs = [];
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @test TECH-001 Extracted doctor.js fix helpers (CCN reduction)
|
|
3
|
+
* @intent Unit-test each extracted helper in isolation
|
|
4
|
+
* @covers TECH-001
|
|
5
|
+
*/
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const os = require('os');
|
|
9
|
+
|
|
10
|
+
describe('fixVersionMismatch', () => {
|
|
11
|
+
let tmpHome;
|
|
12
|
+
let originalHome;
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
originalHome = process.env.HOME;
|
|
16
|
+
tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'xpgate-dh-'));
|
|
17
|
+
process.env.HOME = tmpHome;
|
|
18
|
+
vi.resetModules();
|
|
19
|
+
delete require.cache[require.resolve('../doctor')];
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
process.env.HOME = originalHome;
|
|
24
|
+
if (tmpHome && fs.existsSync(tmpHome)) {
|
|
25
|
+
fs.rmSync(tmpHome, { recursive: true, force: true });
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
function ensureConfigDir() {
|
|
30
|
+
const cfgDir = path.join(tmpHome, '.config', 'xp-gate');
|
|
31
|
+
fs.mkdirSync(cfgDir, { recursive: true });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
it('updates config version when package version differs', () => {
|
|
35
|
+
ensureConfigDir();
|
|
36
|
+
delete require.cache[require.resolve('../doctor')];
|
|
37
|
+
const doc = require('../doctor');
|
|
38
|
+
const config = { version: '0.3.1.1', mode: 'local' };
|
|
39
|
+
const result = doc.fixVersionMismatch(config, '0.9.3.0');
|
|
40
|
+
expect(result).toBe(true);
|
|
41
|
+
expect(config.version).toBe('0.9.3.0');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('skips when package version is null', () => {
|
|
45
|
+
delete require.cache[require.resolve('../doctor')];
|
|
46
|
+
const doc = require('../doctor');
|
|
47
|
+
const config = { version: '0.3.1.1', mode: 'local' };
|
|
48
|
+
const result = doc.fixVersionMismatch(config, null);
|
|
49
|
+
expect(result).toBe(false);
|
|
50
|
+
expect(config.version).toBe('0.3.1.1');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('skips when versions already match', () => {
|
|
54
|
+
delete require.cache[require.resolve('../doctor')];
|
|
55
|
+
const doc = require('../doctor');
|
|
56
|
+
const config = { version: '0.9.3.0', mode: 'local' };
|
|
57
|
+
const result = doc.fixVersionMismatch(config, '0.9.3.0');
|
|
58
|
+
expect(result).toBe(false);
|
|
59
|
+
expect(config.version).toBe('0.9.3.0');
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe('fixMissingHooks', () => {
|
|
64
|
+
let tmpHome;
|
|
65
|
+
let tmpProject;
|
|
66
|
+
let originalHome;
|
|
67
|
+
|
|
68
|
+
beforeEach(() => {
|
|
69
|
+
originalHome = process.env.HOME;
|
|
70
|
+
tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'xpgate-dh-'));
|
|
71
|
+
tmpProject = fs.mkdtempSync(path.join(os.tmpdir(), 'xpgate-dh-proj-'));
|
|
72
|
+
process.env.HOME = tmpHome;
|
|
73
|
+
vi.resetModules();
|
|
74
|
+
delete require.cache[require.resolve('../doctor')];
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
afterEach(() => {
|
|
78
|
+
process.env.HOME = originalHome;
|
|
79
|
+
if (tmpHome && fs.existsSync(tmpHome)) {
|
|
80
|
+
fs.rmSync(tmpHome, { recursive: true, force: true });
|
|
81
|
+
}
|
|
82
|
+
if (tmpProject && fs.existsSync(tmpProject)) {
|
|
83
|
+
fs.rmSync(tmpProject, { recursive: true, force: true });
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('restores pre-commit and pre-push hooks for local mode', () => {
|
|
88
|
+
delete require.cache[require.resolve('../doctor')];
|
|
89
|
+
const srcDir = path.join(__dirname, '..', '..');
|
|
90
|
+
const hooksDir = path.join(tmpProject, '.git', 'hooks');
|
|
91
|
+
fs.mkdirSync(hooksDir, { recursive: true });
|
|
92
|
+
const preCommit = path.join(hooksDir, 'pre-commit');
|
|
93
|
+
if (fs.existsSync(preCommit)) fs.unlinkSync(preCommit);
|
|
94
|
+
const doc = require('../doctor');
|
|
95
|
+
const result = doc.fixMissingHooks('local', srcDir, hooksDir);
|
|
96
|
+
expect(result).toBe(true);
|
|
97
|
+
expect(fs.existsSync(preCommit)).toBe(true);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe('fixMissingAdapters', () => {
|
|
102
|
+
let tmpHome;
|
|
103
|
+
let tmpProject;
|
|
104
|
+
let originalHome;
|
|
105
|
+
|
|
106
|
+
beforeEach(() => {
|
|
107
|
+
originalHome = process.env.HOME;
|
|
108
|
+
tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'xpgate-dh-'));
|
|
109
|
+
tmpProject = fs.mkdtempSync(path.join(os.tmpdir(), 'xpgate-dh-proj-'));
|
|
110
|
+
process.env.HOME = tmpHome;
|
|
111
|
+
vi.resetModules();
|
|
112
|
+
delete require.cache[require.resolve('../doctor')];
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
afterEach(() => {
|
|
116
|
+
process.env.HOME = originalHome;
|
|
117
|
+
if (tmpHome && fs.existsSync(tmpHome)) {
|
|
118
|
+
fs.rmSync(tmpHome, { recursive: true, force: true });
|
|
119
|
+
}
|
|
120
|
+
if (tmpProject && fs.existsSync(tmpProject)) {
|
|
121
|
+
fs.rmSync(tmpProject, { recursive: true, force: true });
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('copies adapters when directory is missing', () => {
|
|
126
|
+
delete require.cache[require.resolve('../doctor')];
|
|
127
|
+
const srcDir = path.join(__dirname, '..', '..');
|
|
128
|
+
const adaptersDir = path.join(tmpProject, 'githooks', 'adapters');
|
|
129
|
+
if (fs.existsSync(adaptersDir)) {
|
|
130
|
+
fs.rmSync(path.join(tmpProject, 'githooks'), { recursive: true, force: true });
|
|
131
|
+
}
|
|
132
|
+
const doc = require('../doctor');
|
|
133
|
+
const result = doc.fixMissingAdapters('local', srcDir, adaptersDir);
|
|
134
|
+
expect(result).toBe(true);
|
|
135
|
+
expect(fs.existsSync(adaptersDir)).toBe(true);
|
|
136
|
+
const files = fs.readdirSync(adaptersDir).filter(f => f.endsWith('.sh'));
|
|
137
|
+
expect(files.length).toBeGreaterThan(0);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
* standalone test verification.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
+
const { EventEmitter } = require('events');
|
|
16
|
+
|
|
15
17
|
describe('upgrade.js --apply execSync path', () => {
|
|
16
18
|
function evictCache() {
|
|
17
19
|
const resolved = require.resolve('../upgrade');
|
|
@@ -24,7 +26,19 @@ describe('upgrade.js --apply execSync path', () => {
|
|
|
24
26
|
delete require.cache[cvResolved];
|
|
25
27
|
}
|
|
26
28
|
|
|
27
|
-
function
|
|
29
|
+
function spawnEE(closeCode) {
|
|
30
|
+
const ee = new EventEmitter();
|
|
31
|
+
process.nextTick(() => ee.emit('close', closeCode));
|
|
32
|
+
return ee;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function spawnErr(err) {
|
|
36
|
+
const ee = new EventEmitter();
|
|
37
|
+
process.nextTick(() => ee.emit('error', err));
|
|
38
|
+
return ee;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function withMockedEnv(latestVersion, spawnImpl, fn) {
|
|
28
42
|
const fs = require('fs');
|
|
29
43
|
const os = require('os');
|
|
30
44
|
const cpPath = require('path').join(os.homedir(), '.xp-gate', 'version-cache.json');
|
|
@@ -34,8 +48,8 @@ describe('upgrade.js --apply execSync path', () => {
|
|
|
34
48
|
evictCache();
|
|
35
49
|
const cp = require('child_process');
|
|
36
50
|
const https = require('https');
|
|
37
|
-
const saved = {
|
|
38
|
-
cp.
|
|
51
|
+
const saved = { spawn: cp.spawn, httpsGet: https.get };
|
|
52
|
+
cp.spawn = spawnImpl;
|
|
39
53
|
const body = JSON.stringify({ latest: latestVersion });
|
|
40
54
|
https.get = (_url, _opts, cb) => {
|
|
41
55
|
const callback = typeof _opts === 'function' ? _opts : cb;
|
|
@@ -55,61 +69,56 @@ describe('upgrade.js --apply execSync path', () => {
|
|
|
55
69
|
const m = require('../upgrade');
|
|
56
70
|
return fn(m);
|
|
57
71
|
} finally {
|
|
58
|
-
cp.
|
|
72
|
+
cp.spawn = saved.spawn;
|
|
59
73
|
https.get = saved.httpsGet;
|
|
60
74
|
}
|
|
61
75
|
}
|
|
62
76
|
|
|
63
|
-
it('returns 0 when
|
|
64
|
-
const code = await withMockedEnv('99.99.99', vi.fn()
|
|
77
|
+
it('returns 0 when spawn succeeds', async () => {
|
|
78
|
+
const code = await withMockedEnv('99.99.99', vi.fn(() => spawnEE(0)), async (m) => {
|
|
65
79
|
return m.upgrade(['--apply']);
|
|
66
80
|
});
|
|
67
81
|
expect(code).toBe(0);
|
|
68
82
|
}, 10000);
|
|
69
83
|
|
|
70
|
-
it('returns 1 when
|
|
71
|
-
const err = new Error('EACCES');
|
|
72
|
-
|
|
73
|
-
const code = await withMockedEnv('99.99.99', vi.fn(() => { throw err; }), async (m) => {
|
|
84
|
+
it('returns 1 when spawn errors with EACCES', async () => {
|
|
85
|
+
const err = new Error('EACCES: permission denied');
|
|
86
|
+
const code = await withMockedEnv('99.99.99', vi.fn(() => spawnErr(err)), async (m) => {
|
|
74
87
|
return m.upgrade(['--apply']);
|
|
75
88
|
});
|
|
76
89
|
expect(code).toBe(1);
|
|
77
90
|
}, 10000);
|
|
78
91
|
|
|
79
|
-
it('returns 1 when
|
|
92
|
+
it('returns 1 when spawn errors with ETIMEDOUT', async () => {
|
|
80
93
|
const err = new Error('ETIMEDOUT');
|
|
81
|
-
|
|
82
|
-
err.stderr = Buffer.from('');
|
|
83
|
-
const code = await withMockedEnv('99.99.99', vi.fn(() => { throw err; }), async (m) => {
|
|
94
|
+
const code = await withMockedEnv('99.99.99', vi.fn(() => spawnErr(err)), async (m) => {
|
|
84
95
|
return m.upgrade(['--apply']);
|
|
85
96
|
});
|
|
86
97
|
expect(code).toBe(1);
|
|
87
98
|
}, 10000);
|
|
88
99
|
|
|
89
|
-
it('returns 1 when
|
|
90
|
-
const
|
|
91
|
-
err.stderr = Buffer.from('');
|
|
92
|
-
const code = await withMockedEnv('99.99.99', vi.fn(() => { throw err; }), async (m) => {
|
|
100
|
+
it('returns 1 when spawn exits with non-zero code', async () => {
|
|
101
|
+
const code = await withMockedEnv('99.99.99', vi.fn(() => spawnEE(1)), async (m) => {
|
|
93
102
|
return m.upgrade(['--apply']);
|
|
94
103
|
});
|
|
95
104
|
expect(code).toBe(1);
|
|
96
105
|
}, 10000);
|
|
97
106
|
|
|
98
|
-
it('calls
|
|
99
|
-
const
|
|
100
|
-
await withMockedEnv('99.99.99',
|
|
107
|
+
it('calls spawn with correct args', async () => {
|
|
108
|
+
const mockSpawn = vi.fn(() => spawnEE(0));
|
|
109
|
+
await withMockedEnv('99.99.99', mockSpawn, async (m) => {
|
|
101
110
|
await m.upgrade(['--apply']);
|
|
102
|
-
expect(
|
|
103
|
-
|
|
111
|
+
expect(mockSpawn).toHaveBeenCalledWith(
|
|
112
|
+
'npm',
|
|
113
|
+
['install', '-g', expect.stringMatching(/@99\.99\.99/)],
|
|
104
114
|
expect.objectContaining({ stdio: 'inherit', timeout: 120000 }),
|
|
105
115
|
);
|
|
106
116
|
});
|
|
107
117
|
}, 10000);
|
|
108
118
|
|
|
109
119
|
// ── default mode (no --apply/--preview) outdated path ──
|
|
110
|
-
// Covers upgrade.js L110-112: formatUpgradeMsg + console.log
|
|
111
120
|
it('default mode: shows upgrade msg when outdated', async () => {
|
|
112
|
-
await withMockedEnv('99.99.99', vi.fn()
|
|
121
|
+
await withMockedEnv('99.99.99', vi.fn(() => spawnEE(0)), async (m) => {
|
|
113
122
|
const out = [];
|
|
114
123
|
const origLog = console.log;
|
|
115
124
|
console.log = (...a) => { out.push(a.join(' ')); };
|
|
@@ -125,7 +134,7 @@ describe('upgrade.js --apply execSync path', () => {
|
|
|
125
134
|
}, 10000);
|
|
126
135
|
|
|
127
136
|
// ── withMockedEnv variant: also mocks getLocalVersion() to return null ──
|
|
128
|
-
function withMockedEnvNoLocal(latestVersion,
|
|
137
|
+
function withMockedEnvNoLocal(latestVersion, spawnImpl, fn) {
|
|
129
138
|
const fs = require('fs');
|
|
130
139
|
const os = require('os');
|
|
131
140
|
const cpPath = require('path').join(os.homedir(), '.xp-gate', 'version-cache.json');
|
|
@@ -135,8 +144,8 @@ describe('upgrade.js --apply execSync path', () => {
|
|
|
135
144
|
evictCache();
|
|
136
145
|
const cp = require('child_process');
|
|
137
146
|
const https = require('https');
|
|
138
|
-
const saved = {
|
|
139
|
-
cp.
|
|
147
|
+
const saved = { spawn: cp.spawn, httpsGet: https.get, fsReadFileSync: fs.readFileSync };
|
|
148
|
+
cp.spawn = spawnImpl;
|
|
140
149
|
// Make getLocalVersion() return null by making fs.readFileSync throw for package.json
|
|
141
150
|
fs.readFileSync = (filePath, encoding) => {
|
|
142
151
|
if (typeof filePath === 'string' && filePath.includes('package.json')) {
|
|
@@ -165,7 +174,7 @@ describe('upgrade.js --apply execSync path', () => {
|
|
|
165
174
|
const m = require('../upgrade');
|
|
166
175
|
return fn(m);
|
|
167
176
|
} finally {
|
|
168
|
-
cp.
|
|
177
|
+
cp.spawn = saved.spawn;
|
|
169
178
|
https.get = saved.httpsGet;
|
|
170
179
|
fs.readFileSync = saved.fsReadFileSync;
|
|
171
180
|
}
|
|
@@ -178,7 +187,7 @@ describe('upgrade.js --apply execSync path', () => {
|
|
|
178
187
|
const origLog = console.log;
|
|
179
188
|
console.log = (...a) => { out.push(a.join(' ')); };
|
|
180
189
|
try {
|
|
181
|
-
const code = await withMockedEnvNoLocal('0.8.12', vi.fn()
|
|
190
|
+
const code = await withMockedEnvNoLocal('0.8.12', vi.fn(() => spawnEE(0)), async (m) => m.upgrade(['--apply']));
|
|
182
191
|
expect(code).toBe(0);
|
|
183
192
|
expect(out.join(' ')).toBe('xp-gate is up to date.');
|
|
184
193
|
} finally {
|
|
@@ -192,7 +201,7 @@ describe('upgrade.js --apply execSync path', () => {
|
|
|
192
201
|
const origLog = console.log;
|
|
193
202
|
console.log = (...a) => { out.push(a.join(' ')); };
|
|
194
203
|
try {
|
|
195
|
-
const code = await withMockedEnvNoLocal('0.8.12', vi.fn()
|
|
204
|
+
const code = await withMockedEnvNoLocal('0.8.12', vi.fn(() => spawnEE(0)), async (m) => m.upgrade([]));
|
|
196
205
|
expect(code).toBe(0);
|
|
197
206
|
expect(out.join(' ')).toBe('xp-gate is up to date.');
|
|
198
207
|
} finally {
|
|
@@ -206,7 +215,7 @@ describe('upgrade.js --apply execSync path', () => {
|
|
|
206
215
|
const origLog = console.log;
|
|
207
216
|
console.log = (...a) => { out.push(a.join(' ')); };
|
|
208
217
|
try {
|
|
209
|
-
const code = await withMockedEnv('99.99.99', vi.fn()
|
|
218
|
+
const code = await withMockedEnv('99.99.99', vi.fn(() => spawnEE(0)), async (m) => m.upgrade([]));
|
|
210
219
|
expect(code).toBe(0);
|
|
211
220
|
expect(out.length + (out.join(' ').length > 0 ? 1 : 0)).toBeGreaterThan(0);
|
|
212
221
|
} finally {
|
|
@@ -220,7 +229,7 @@ describe('upgrade.js --apply execSync path', () => {
|
|
|
220
229
|
const origLog = console.log;
|
|
221
230
|
console.log = (...a) => { out.push(a.join(' ')); };
|
|
222
231
|
try {
|
|
223
|
-
const code = await withMockedEnv('1.0.0', vi.fn()
|
|
232
|
+
const code = await withMockedEnv('1.0.0', vi.fn(() => spawnEE(0)), async (m) => m.upgrade(['--preview']));
|
|
224
233
|
expect(code).toBe(0);
|
|
225
234
|
expect(out.length).toBe(1);
|
|
226
235
|
const parsed = JSON.parse(out[0]);
|
package/lib/baseline.js
CHANGED
|
@@ -220,6 +220,29 @@ async function createBaseline() {
|
|
|
220
220
|
return baseline;
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
+
/**
|
|
224
|
+
* Count baseline entries grouped by tool type.
|
|
225
|
+
* @param {object} data - Parsed baseline object (filePath → entry)
|
|
226
|
+
* @returns {{ totalFiles: number, totalWarnings: number, eslintCount: number, ruffCount: number, golangciCount: number, shellcheckCount: number }}
|
|
227
|
+
*/
|
|
228
|
+
function countBaselineByTool(data) {
|
|
229
|
+
const files = Object.keys(data);
|
|
230
|
+
const totalWarnings = files.reduce((s, f) => s + data[f].totalWarnings, 0);
|
|
231
|
+
let eslintCount = 0;
|
|
232
|
+
let ruffCount = 0;
|
|
233
|
+
let golangciCount = 0;
|
|
234
|
+
let shellcheckCount = 0;
|
|
235
|
+
|
|
236
|
+
for (const entry of Object.values(data)) {
|
|
237
|
+
if (entry.eslint) eslintCount++;
|
|
238
|
+
if (entry.ruff) ruffCount++;
|
|
239
|
+
if (entry.golangci) golangciCount++;
|
|
240
|
+
if (entry.shellcheck) shellcheckCount++;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return { totalFiles: files.length, totalWarnings, eslintCount, ruffCount, golangciCount, shellcheckCount };
|
|
244
|
+
}
|
|
245
|
+
|
|
223
246
|
/**
|
|
224
247
|
* Show current baseline.
|
|
225
248
|
*/
|
|
@@ -237,27 +260,16 @@ function showBaseline() {
|
|
|
237
260
|
return 0;
|
|
238
261
|
}
|
|
239
262
|
|
|
240
|
-
const
|
|
241
|
-
let eslintCount = 0;
|
|
242
|
-
let ruffCount = 0;
|
|
243
|
-
let golangciCount = 0;
|
|
244
|
-
let shellcheckCount = 0;
|
|
245
|
-
|
|
246
|
-
for (const entry of Object.values(data)) {
|
|
247
|
-
if (entry.eslint) eslintCount++;
|
|
248
|
-
if (entry.ruff) ruffCount++;
|
|
249
|
-
if (entry.golangci) golangciCount++;
|
|
250
|
-
if (entry.shellcheck) shellcheckCount++;
|
|
251
|
-
}
|
|
263
|
+
const counts = countBaselineByTool(data);
|
|
252
264
|
|
|
253
265
|
console.log('Lint Baseline:');
|
|
254
266
|
console.log(` Created: ${data[files[0]]?.lastAnalyzed?.slice(0, 10) || 'N/A'}`);
|
|
255
|
-
console.log(` Files tracked: ${
|
|
256
|
-
console.log(` Total warnings: ${totalWarnings}`);
|
|
257
|
-
if (eslintCount > 0) console.log(` ESLint: ${eslintCount} files`);
|
|
258
|
-
if (ruffCount > 0) console.log(` Ruff: ${ruffCount} files`);
|
|
259
|
-
if (golangciCount > 0) console.log(` golangci-lint: ${golangciCount} files`);
|
|
260
|
-
if (shellcheckCount > 0) console.log(` ShellCheck: ${shellcheckCount} files`);
|
|
267
|
+
console.log(` Files tracked: ${counts.totalFiles}`);
|
|
268
|
+
console.log(` Total warnings: ${counts.totalWarnings}`);
|
|
269
|
+
if (counts.eslintCount > 0) console.log(` ESLint: ${counts.eslintCount} files`);
|
|
270
|
+
if (counts.ruffCount > 0) console.log(` Ruff: ${counts.ruffCount} files`);
|
|
271
|
+
if (counts.golangciCount > 0) console.log(` golangci-lint: ${counts.golangciCount} files`);
|
|
272
|
+
if (counts.shellcheckCount > 0) console.log(` ShellCheck: ${counts.shellcheckCount} files`);
|
|
261
273
|
|
|
262
274
|
return 0;
|
|
263
275
|
}
|
|
@@ -272,21 +284,13 @@ async function resetBaseline() {
|
|
|
272
284
|
}
|
|
273
285
|
|
|
274
286
|
/**
|
|
275
|
-
*
|
|
287
|
+
* Compare two baselines and produce diff data.
|
|
288
|
+
* @param {object} oldBaseline
|
|
289
|
+
* @param {object} currentBaseline
|
|
290
|
+
* @returns {{ totalWarningsDelta: number, increased: string[], decreased: string[], added: string[], removed: string[] }}
|
|
276
291
|
*/
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
console.log('No baseline found. Run `xp-gate baseline create` first.');
|
|
280
|
-
return 1;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
const oldBaseline = JSON.parse(fs.readFileSync(getBaselineFile(), 'utf8'));
|
|
284
|
-
const currentBaseline = await createBaseline();
|
|
285
|
-
|
|
286
|
-
const allFiles = new Set([
|
|
287
|
-
...Object.keys(oldBaseline),
|
|
288
|
-
...Object.keys(currentBaseline),
|
|
289
|
-
]);
|
|
292
|
+
function compareBaselines(oldBaseline, currentBaseline) {
|
|
293
|
+
const allFiles = new Set([...Object.keys(oldBaseline), ...Object.keys(currentBaseline)]);
|
|
290
294
|
|
|
291
295
|
let totalWarningsDelta = 0;
|
|
292
296
|
const increased = [];
|
|
@@ -312,31 +316,54 @@ async function diffBaseline() {
|
|
|
312
316
|
}
|
|
313
317
|
}
|
|
314
318
|
|
|
319
|
+
return { totalWarningsDelta, increased, decreased, added, removed };
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Print a diff result report to console.
|
|
324
|
+
*/
|
|
325
|
+
function printDiffReport(result) {
|
|
315
326
|
console.log('Lint Baseline Diff:');
|
|
316
|
-
console.log(` Total warnings delta: ${totalWarningsDelta >= 0 ? '+' : ''}${totalWarningsDelta}`);
|
|
327
|
+
console.log(` Total warnings delta: ${result.totalWarningsDelta >= 0 ? '+' : ''}${result.totalWarningsDelta}`);
|
|
317
328
|
|
|
318
|
-
if (added.length > 0) {
|
|
319
|
-
console.log(`\nFiles added (${added.length}):`);
|
|
320
|
-
added.forEach(l => console.log(l));
|
|
329
|
+
if (result.added.length > 0) {
|
|
330
|
+
console.log(`\nFiles added (${result.added.length}):`);
|
|
331
|
+
result.added.forEach(l => console.log(l));
|
|
321
332
|
}
|
|
322
|
-
if (removed.length > 0) {
|
|
323
|
-
console.log(`\nFiles removed (${removed.length}):`);
|
|
324
|
-
removed.forEach(l => console.log(l));
|
|
333
|
+
if (result.removed.length > 0) {
|
|
334
|
+
console.log(`\nFiles removed (${result.removed.length}):`);
|
|
335
|
+
result.removed.forEach(l => console.log(l));
|
|
325
336
|
}
|
|
326
|
-
if (increased.length > 0) {
|
|
327
|
-
console.log(`\nWarnings increased (${increased.length}):`);
|
|
328
|
-
increased.forEach(l => console.log(l));
|
|
337
|
+
if (result.increased.length > 0) {
|
|
338
|
+
console.log(`\nWarnings increased (${result.increased.length}):`);
|
|
339
|
+
result.increased.forEach(l => console.log(l));
|
|
329
340
|
}
|
|
330
|
-
if (decreased.length > 0) {
|
|
331
|
-
console.log(`\nWarnings decreased (${decreased.length}):`);
|
|
332
|
-
decreased.forEach(l => console.log(l));
|
|
341
|
+
if (result.decreased.length > 0) {
|
|
342
|
+
console.log(`\nWarnings decreased (${result.decreased.length}):`);
|
|
343
|
+
result.decreased.forEach(l => console.log(l));
|
|
333
344
|
}
|
|
334
345
|
|
|
335
|
-
if (added.length === 0 && removed.length === 0 && increased.length === 0 && decreased.length === 0) {
|
|
346
|
+
if (result.added.length === 0 && result.removed.length === 0 && result.increased.length === 0 && result.decreased.length === 0) {
|
|
336
347
|
console.log(' No change from baseline.');
|
|
337
348
|
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Diff current lint state against stored baseline.
|
|
353
|
+
*/
|
|
354
|
+
async function diffBaseline() {
|
|
355
|
+
if (!fs.existsSync(getBaselineFile())) {
|
|
356
|
+
console.log('No baseline found. Run `xp-gate baseline create` first.');
|
|
357
|
+
return 1;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const oldBaseline = JSON.parse(fs.readFileSync(getBaselineFile(), 'utf8'));
|
|
361
|
+
const currentBaseline = await createBaseline();
|
|
362
|
+
|
|
363
|
+
const result = compareBaselines(oldBaseline, currentBaseline);
|
|
364
|
+
printDiffReport(result);
|
|
338
365
|
|
|
339
|
-
return totalWarningsDelta > 0 ? 1 : 0;
|
|
366
|
+
return result.totalWarningsDelta > 0 ? 1 : 0;
|
|
340
367
|
}
|
|
341
368
|
|
|
342
369
|
/**
|
|
@@ -389,4 +416,4 @@ Examples:
|
|
|
389
416
|
}
|
|
390
417
|
}
|
|
391
418
|
|
|
392
|
-
module.exports = { handleBaseline, createBaseline, showBaseline, resetBaseline, diffBaseline };
|
|
419
|
+
module.exports = { handleBaseline, createBaseline, showBaseline, resetBaseline, diffBaseline, countBaselineByTool, compareBaselines };
|