xp-gate 0.5.1
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/adapter-common.sh +192 -0
- package/adapters/cpp.sh +76 -0
- package/adapters/dart.sh +41 -0
- package/adapters/flutter.sh +41 -0
- package/adapters/go.sh +59 -0
- package/adapters/iac.sh +189 -0
- package/adapters/java.sh +191 -0
- package/adapters/kotlin.sh +77 -0
- package/adapters/objectivec.sh +38 -0
- package/adapters/powershell.sh +138 -0
- package/adapters/python.sh +104 -0
- package/adapters/shell.sh +55 -0
- package/adapters/swift.sh +44 -0
- package/adapters/typescript.sh +61 -0
- package/bin/xp-gate.js +157 -0
- package/hooks/adapter-common.sh +192 -0
- package/hooks/pre-commit +1667 -0
- package/hooks/pre-push +395 -0
- package/lib/__tests__/detect-deps.test.js +209 -0
- package/lib/__tests__/doctor.test.js +448 -0
- package/lib/__tests__/download-skill.test.js +281 -0
- package/lib/__tests__/init.test.js +327 -0
- package/lib/__tests__/install-skill.test.js +326 -0
- package/lib/__tests__/migrate.test.js +212 -0
- package/lib/__tests__/rollback.test.js +183 -0
- package/lib/__tests__/ui-detector.test.ts +200 -0
- package/lib/__tests__/uninstall-skill.test.js +189 -0
- package/lib/__tests__/uninstall.test.js +589 -0
- package/lib/__tests__/update-skill.test.js +276 -0
- package/lib/detect-deps.js +157 -0
- package/lib/doctor.js +370 -0
- package/lib/download-skill.js +96 -0
- package/lib/init.js +367 -0
- package/lib/install-skill.js +184 -0
- package/lib/migrate.js +120 -0
- package/lib/rollback.js +78 -0
- package/lib/ui-detector.ts +99 -0
- package/lib/uninstall-skill.js +69 -0
- package/lib/uninstall.js +401 -0
- package/lib/update-skill.js +90 -0
- package/package.json +39 -0
- package/plugins/claude-code/.claude-plugin/plugin.json +21 -0
- package/plugins/claude-code/bin/delphi-review-guard.sh +68 -0
- package/plugins/claude-code/bin/xp-gate-check +47 -0
- package/plugins/claude-code/hooks/hooks.json +37 -0
- package/skills/delphi-review/.delphi-config.json.example +45 -0
- package/skills/delphi-review/AGENTS.md +54 -0
- package/skills/delphi-review/INSTALL.md +152 -0
- package/skills/delphi-review/SKILL.md +371 -0
- package/skills/delphi-review/evals/evals.json +82 -0
- package/skills/delphi-review/opencode.json.delphi.example +56 -0
- package/skills/delphi-review/references/code-walkthrough.md +486 -0
- package/skills/ralph-loop/SKILL.md +330 -0
- package/skills/ralph-loop/evals/evals.json +311 -0
- package/skills/ralph-loop/evolution-history.json +59 -0
- package/skills/ralph-loop/evolution-log.md +16 -0
- package/skills/ralph-loop/references/components/memory.md +55 -0
- package/skills/ralph-loop/references/components/middleware.md +54 -0
- package/skills/ralph-loop/references/components/skill-invocations.md +39 -0
- package/skills/ralph-loop/references/components/system-prompt.md +24 -0
- package/skills/ralph-loop/references/components/tool-descriptions.md +32 -0
- package/skills/ralph-loop/references/phase-2-build-ralph.md +89 -0
- package/skills/ralph-loop/templates/progress-log.md +36 -0
- package/skills/sprint-flow/SKILL.md +600 -0
- package/skills/sprint-flow/evals/evals.json +78 -0
- package/skills/sprint-flow/evolution-history.json +39 -0
- package/skills/sprint-flow/evolution-log.md +23 -0
- package/skills/sprint-flow/references/components/memory.md +87 -0
- package/skills/sprint-flow/references/components/middleware.md +72 -0
- package/skills/sprint-flow/references/components/skill-invocations.md +104 -0
- package/skills/sprint-flow/references/components/system-prompt.md +27 -0
- package/skills/sprint-flow/references/components/tool-descriptions.md +96 -0
- package/skills/sprint-flow/references/phase-0-think.md +115 -0
- package/skills/sprint-flow/references/phase-1-plan.md +178 -0
- package/skills/sprint-flow/references/phase-2-build.md +198 -0
- package/skills/sprint-flow/references/phase-3-review.md +213 -0
- package/skills/sprint-flow/references/phase-4-uat.md +125 -0
- package/skills/sprint-flow/references/phase-5-feedback.md +100 -0
- package/skills/sprint-flow/references/phase-6-ship.md +193 -0
- package/skills/sprint-flow/references/phase-7-land.md +140 -0
- package/skills/sprint-flow/references/phase-8-cleanup.md +192 -0
- package/skills/sprint-flow/templates/emergent-issues-template.md +120 -0
- package/skills/sprint-flow/templates/pain-document-template.md +115 -0
- package/skills/sprint-flow/templates/sprint-summary-template.md +120 -0
- package/skills/test-specification-alignment/AGENTS.md +59 -0
- package/skills/test-specification-alignment/SKILL.md +605 -0
- package/skills/test-specification-alignment/evals/evals.json +75 -0
- package/skills/test-specification-alignment/references/alignment-verification-algorithm.md +493 -0
- package/skills/test-specification-alignment/references/phase2-constraint-enforcement.md +431 -0
- package/skills/test-specification-alignment/references/specification-format.md +348 -0
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @test REQ-2 xp-gate doctor
|
|
3
|
+
* @intent Verify doctor correctly diagnoses installation health, handles --fix, and detects partial uninstall
|
|
4
|
+
* @covers AC-05, AC-08, AC-10
|
|
5
|
+
*/
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const os = require('os');
|
|
9
|
+
const childProcess = require('child_process');
|
|
10
|
+
|
|
11
|
+
describe('doctor', () => {
|
|
12
|
+
let tmpHome;
|
|
13
|
+
let tmpProject;
|
|
14
|
+
let originalHome;
|
|
15
|
+
let logSpy;
|
|
16
|
+
let warnSpy;
|
|
17
|
+
let errorSpy;
|
|
18
|
+
let execSpy;
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
originalHome = process.env.HOME;
|
|
22
|
+
tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'xpgate-dr-'));
|
|
23
|
+
tmpProject = fs.mkdtempSync(path.join(os.tmpdir(), 'xpgate-dr-proj-'));
|
|
24
|
+
process.env.HOME = tmpHome;
|
|
25
|
+
vi.resetModules();
|
|
26
|
+
delete require.cache[require.resolve('../doctor')];
|
|
27
|
+
delete require.cache[require.resolve('../uninstall')];
|
|
28
|
+
delete require.cache[require.resolve('../init')];
|
|
29
|
+
delete require.cache[require.resolve('../detect-deps.js')];
|
|
30
|
+
logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
31
|
+
warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
32
|
+
errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
afterEach(() => {
|
|
36
|
+
process.env.HOME = originalHome;
|
|
37
|
+
if (tmpHome && fs.existsSync(tmpHome)) {
|
|
38
|
+
fs.rmSync(tmpHome, { recursive: true, force: true });
|
|
39
|
+
}
|
|
40
|
+
if (tmpProject && fs.existsSync(tmpProject)) {
|
|
41
|
+
fs.rmSync(tmpProject, { recursive: true, force: true });
|
|
42
|
+
}
|
|
43
|
+
vi.restoreAllMocks();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
function configFile() {
|
|
47
|
+
return path.join(tmpHome, '.config', 'xp-gate', 'xp-gate.json');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function globalHooksDir() {
|
|
51
|
+
return path.join(tmpHome, '.config', 'xp-gate', 'hooks');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function globalAdaptersDir() {
|
|
55
|
+
return path.join(tmpHome, '.config', 'xp-gate', 'adapters');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function projectGitDir() {
|
|
59
|
+
return path.join(tmpProject, '.git');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function projectHooksDir() {
|
|
63
|
+
return path.join(tmpProject, '.git', 'hooks');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function projectGithooksDir() {
|
|
67
|
+
return path.join(tmpProject, 'githooks');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function projectAdaptersDir() {
|
|
71
|
+
return path.join(tmpProject, 'githooks', 'adapters');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function createXpGatePreCommit(dir) {
|
|
75
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
76
|
+
fs.writeFileSync(
|
|
77
|
+
path.join(dir, 'pre-commit'),
|
|
78
|
+
'#!/bin/bash\n# OpenCode Quality Gates - Pre-Commit Hook - Test\n'
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function createXpGatePrePush(dir) {
|
|
83
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
84
|
+
fs.writeFileSync(
|
|
85
|
+
path.join(dir, 'pre-push'),
|
|
86
|
+
'#!/bin/bash\n# Pre-push Hook - Code Walkthrough Result Validator\n'
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function createXpGateAdapterCommon(dir) {
|
|
91
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
92
|
+
fs.writeFileSync(
|
|
93
|
+
path.join(dir, 'adapter-common.sh'),
|
|
94
|
+
'#!/usr/bin/env bash\n\n# Common adapter functions\ndetect_project_lang() {\n echo "typescript"\n}\n'
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function createXpGateAdapterScripts(dir) {
|
|
99
|
+
fs.mkdirSync(path.join(dir, 'adapters'), { recursive: true });
|
|
100
|
+
fs.writeFileSync(
|
|
101
|
+
path.join(dir, 'adapters', 'typescript.sh'),
|
|
102
|
+
'#!/usr/bin/env bash\necho "ts adapter"\n'
|
|
103
|
+
);
|
|
104
|
+
fs.writeFileSync(
|
|
105
|
+
path.join(dir, 'adapters', 'python.sh'),
|
|
106
|
+
'#!/usr/bin/env bash\necho "py adapter"\n'
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function setupLocalInstall() {
|
|
111
|
+
createXpGatePreCommit(projectHooksDir());
|
|
112
|
+
createXpGatePrePush(projectHooksDir());
|
|
113
|
+
createXpGateAdapterCommon(projectGithooksDir());
|
|
114
|
+
createXpGateAdapterScripts(projectGithooksDir());
|
|
115
|
+
|
|
116
|
+
fs.mkdirSync(path.dirname(configFile()), { recursive: true });
|
|
117
|
+
fs.writeFileSync(
|
|
118
|
+
configFile(),
|
|
119
|
+
JSON.stringify({
|
|
120
|
+
mode: 'local',
|
|
121
|
+
lastInit: new Date().toISOString()
|
|
122
|
+
}, null, 2)
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function setupGlobalInstall() {
|
|
127
|
+
createXpGatePreCommit(globalHooksDir());
|
|
128
|
+
createXpGatePrePush(globalHooksDir());
|
|
129
|
+
createXpGateAdapterCommon(globalAdaptersDir());
|
|
130
|
+
createXpGateAdapterScripts(globalAdaptersDir());
|
|
131
|
+
createXpGatePreCommit(projectHooksDir());
|
|
132
|
+
createXpGatePrePush(projectHooksDir());
|
|
133
|
+
createXpGateAdapterCommon(projectGithooksDir());
|
|
134
|
+
createXpGateAdapterScripts(projectGithooksDir());
|
|
135
|
+
|
|
136
|
+
fs.mkdirSync(path.dirname(configFile()), { recursive: true });
|
|
137
|
+
fs.writeFileSync(
|
|
138
|
+
configFile(),
|
|
139
|
+
JSON.stringify({
|
|
140
|
+
mode: 'global',
|
|
141
|
+
lastInit: new Date().toISOString()
|
|
142
|
+
}, null, 2)
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function mockExecSuccess() {
|
|
147
|
+
execSpy = vi.spyOn(childProcess, 'execSync').mockImplementation((cmd) => {
|
|
148
|
+
if (cmd === 'git rev-parse --git-dir') {
|
|
149
|
+
return path.join(tmpProject, '.git') + '\n';
|
|
150
|
+
}
|
|
151
|
+
if (cmd.includes('git config --global core.hooksPath')) {
|
|
152
|
+
if (cmd.includes('--unset')) {
|
|
153
|
+
return '';
|
|
154
|
+
}
|
|
155
|
+
return globalHooksDir() + '\n';
|
|
156
|
+
}
|
|
157
|
+
if (cmd === 'node --version') {
|
|
158
|
+
return 'v20.0.0\n';
|
|
159
|
+
}
|
|
160
|
+
if (cmd === 'git --version') {
|
|
161
|
+
return 'git version 2.39.0\n';
|
|
162
|
+
}
|
|
163
|
+
if (cmd === 'bash --version') {
|
|
164
|
+
return 'GNU bash, version 5.1.16\n';
|
|
165
|
+
}
|
|
166
|
+
return '';
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function mockExecFail() {
|
|
171
|
+
execSpy = vi.spyOn(childProcess, 'execSync').mockImplementation(() => {
|
|
172
|
+
throw new Error('Command failed');
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// === AC-05: healthy diagnosis ===
|
|
177
|
+
|
|
178
|
+
it('AC-05: doctor reports all checks passed for healthy local install', async () => {
|
|
179
|
+
setupLocalInstall();
|
|
180
|
+
mockExecSuccess();
|
|
181
|
+
const { doctor } = require('../doctor');
|
|
182
|
+
|
|
183
|
+
const result = await doctor([]);
|
|
184
|
+
|
|
185
|
+
expect(result).toBe(0);
|
|
186
|
+
|
|
187
|
+
// Should report Config file check
|
|
188
|
+
expect(logSpy).toHaveBeenCalledWith(
|
|
189
|
+
expect.stringContaining('Config file')
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
// Should report hooks check
|
|
193
|
+
expect(logSpy).toHaveBeenCalledWith(
|
|
194
|
+
expect.stringContaining('Hooks')
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
// Should report Adapters directory check
|
|
198
|
+
expect(logSpy).toHaveBeenCalledWith(
|
|
199
|
+
expect.stringContaining('Adapters directory')
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
// Should report all checks passed
|
|
203
|
+
expect(logSpy).toHaveBeenCalledWith(
|
|
204
|
+
expect.stringContaining('All checks passed')
|
|
205
|
+
);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('AC-05: doctor reports all checks passed for healthy global install', async () => {
|
|
209
|
+
setupGlobalInstall();
|
|
210
|
+
mockExecSuccess();
|
|
211
|
+
const { doctor } = require('../doctor');
|
|
212
|
+
|
|
213
|
+
const result = await doctor([]);
|
|
214
|
+
|
|
215
|
+
expect(result).toBe(0);
|
|
216
|
+
expect(logSpy).toHaveBeenCalledWith(
|
|
217
|
+
expect.stringContaining('All checks passed')
|
|
218
|
+
);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// === AC-08: partial uninstall detection ===
|
|
222
|
+
|
|
223
|
+
it('AC-08: doctor detects missing hooks in partial install', async () => {
|
|
224
|
+
setupLocalInstall();
|
|
225
|
+
// Remove hooks to simulate partial state
|
|
226
|
+
fs.unlinkSync(path.join(projectHooksDir(), 'pre-commit'));
|
|
227
|
+
fs.unlinkSync(path.join(projectHooksDir(), 'pre-push'));
|
|
228
|
+
mockExecSuccess();
|
|
229
|
+
const { doctor } = require('../doctor');
|
|
230
|
+
|
|
231
|
+
const result = await doctor([]);
|
|
232
|
+
|
|
233
|
+
expect(result).toBe(1);
|
|
234
|
+
expect(logSpy).toHaveBeenCalledWith(
|
|
235
|
+
expect.stringContaining('Missing')
|
|
236
|
+
);
|
|
237
|
+
expect(logSpy).toHaveBeenCalledWith(
|
|
238
|
+
expect.stringContaining('pre-commit')
|
|
239
|
+
);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('AC-08: doctor detects missing config file', async () => {
|
|
243
|
+
// No config at all
|
|
244
|
+
const { doctor } = require('../doctor');
|
|
245
|
+
|
|
246
|
+
const result = await doctor([]);
|
|
247
|
+
|
|
248
|
+
expect(result).toBe(1);
|
|
249
|
+
expect(logSpy).toHaveBeenCalledWith(
|
|
250
|
+
expect.stringContaining('Config file: Not found')
|
|
251
|
+
);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('AC-08: doctor detects corrupt config JSON', async () => {
|
|
255
|
+
fs.mkdirSync(path.dirname(configFile()), { recursive: true });
|
|
256
|
+
fs.writeFileSync(configFile(), 'this is not json');
|
|
257
|
+
|
|
258
|
+
const { doctor } = require('../doctor');
|
|
259
|
+
|
|
260
|
+
const result = await doctor([]);
|
|
261
|
+
|
|
262
|
+
expect(result).toBe(1);
|
|
263
|
+
expect(logSpy).toHaveBeenCalledWith(
|
|
264
|
+
expect.stringContaining('Corrupt JSON')
|
|
265
|
+
);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('AC-08: doctor detects missing adapters', async () => {
|
|
269
|
+
setupLocalInstall();
|
|
270
|
+
// Remove adapters dir
|
|
271
|
+
fs.rmSync(projectAdaptersDir(), { recursive: true, force: true });
|
|
272
|
+
mockExecSuccess();
|
|
273
|
+
const { doctor } = require('../doctor');
|
|
274
|
+
|
|
275
|
+
const result = await doctor([]);
|
|
276
|
+
|
|
277
|
+
expect(result).toBe(1);
|
|
278
|
+
expect(logSpy).toHaveBeenCalledWith(
|
|
279
|
+
expect.stringContaining('Missing')
|
|
280
|
+
);
|
|
281
|
+
expect(logSpy).toHaveBeenCalledWith(
|
|
282
|
+
expect.stringContaining('Adapters directory')
|
|
283
|
+
);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it('AC-08: doctor detects wrong core.hooksPath in global mode', async () => {
|
|
287
|
+
setupGlobalInstall();
|
|
288
|
+
// Mock hooksPath pointing somewhere else
|
|
289
|
+
execSpy = vi.spyOn(childProcess, 'execSync').mockImplementation((cmd) => {
|
|
290
|
+
if (cmd.includes('git config --global core.hooksPath')) {
|
|
291
|
+
if (cmd.includes('--unset')) {
|
|
292
|
+
return '';
|
|
293
|
+
}
|
|
294
|
+
return '/wrong/path\n';
|
|
295
|
+
}
|
|
296
|
+
if (cmd === 'git rev-parse --git-dir') {
|
|
297
|
+
return path.join(tmpProject, '.git') + '\n';
|
|
298
|
+
}
|
|
299
|
+
if (cmd === 'node --version') {
|
|
300
|
+
return 'v20.0.0\n';
|
|
301
|
+
}
|
|
302
|
+
if (cmd === 'git --version') {
|
|
303
|
+
return 'git version 2.39.0\n';
|
|
304
|
+
}
|
|
305
|
+
if (cmd === 'bash --version') {
|
|
306
|
+
return 'GNU bash, version 5.1.16\n';
|
|
307
|
+
}
|
|
308
|
+
return '';
|
|
309
|
+
});
|
|
310
|
+
const { doctor } = require('../doctor');
|
|
311
|
+
|
|
312
|
+
const result = await doctor([]);
|
|
313
|
+
|
|
314
|
+
expect(result).toBe(1);
|
|
315
|
+
expect(logSpy).toHaveBeenCalledWith(
|
|
316
|
+
expect.stringContaining('Expected ')
|
|
317
|
+
);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
// === AC-10: --fix only when mode === "active" ===
|
|
321
|
+
|
|
322
|
+
it('AC-10: doctor --fix does nothing when mode is uninstalled', async () => {
|
|
323
|
+
fs.mkdirSync(path.dirname(configFile()), { recursive: true });
|
|
324
|
+
fs.writeFileSync(
|
|
325
|
+
configFile(),
|
|
326
|
+
JSON.stringify({ mode: 'uninstalled', uninstalled: '2025-01-01' }, null, 2)
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
const { doctor } = require('../doctor');
|
|
330
|
+
|
|
331
|
+
const result = await doctor(['--fix']);
|
|
332
|
+
|
|
333
|
+
expect(result).toBe(0);
|
|
334
|
+
// Should NOT attempt fix operations
|
|
335
|
+
expect(logSpy).toHaveBeenCalledWith(
|
|
336
|
+
expect.stringContaining('xp-gate is not installed')
|
|
337
|
+
);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it('AC-10: doctor --fix reinstall hooks when mode is active and hooks missing', async () => {
|
|
341
|
+
setupLocalInstall();
|
|
342
|
+
// Remove hooks to create a fixable issue
|
|
343
|
+
fs.unlinkSync(path.join(projectHooksDir(), 'pre-commit'));
|
|
344
|
+
fs.unlinkSync(path.join(projectHooksDir(), 'pre-push'));
|
|
345
|
+
mockExecSuccess();
|
|
346
|
+
const { doctor } = require('../doctor');
|
|
347
|
+
|
|
348
|
+
const result = await doctor(['--fix']);
|
|
349
|
+
|
|
350
|
+
expect(result).toBe(0);
|
|
351
|
+
// Should have reinstalled hooks
|
|
352
|
+
expect(fs.existsSync(path.join(projectHooksDir(), 'pre-commit'))).toBe(true);
|
|
353
|
+
expect(fs.existsSync(path.join(projectHooksDir(), 'pre-push'))).toBe(true);
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it('AC-10: doctor --fix reinstall adapters when mode is active and adapters missing', async () => {
|
|
357
|
+
setupLocalInstall();
|
|
358
|
+
// Remove adapters
|
|
359
|
+
fs.rmSync(projectAdaptersDir(), { recursive: true, force: true });
|
|
360
|
+
mockExecSuccess();
|
|
361
|
+
const { doctor } = require('../doctor');
|
|
362
|
+
|
|
363
|
+
const result = await doctor(['--fix']);
|
|
364
|
+
|
|
365
|
+
expect(result).toBe(0);
|
|
366
|
+
// Should have reinstalled adapters
|
|
367
|
+
expect(fs.existsSync(projectAdaptersDir())).toBe(true);
|
|
368
|
+
expect(fs.existsSync(path.join(projectAdaptersDir(), 'typescript.sh'))).toBe(true);
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it('AC-10: doctor --fix corrects core.hooksPath in global mode', async () => {
|
|
372
|
+
setupGlobalInstall();
|
|
373
|
+
// Mock hooksPath pointing somewhere else
|
|
374
|
+
execSpy = vi.spyOn(childProcess, 'execSync').mockImplementation((cmd) => {
|
|
375
|
+
if (cmd.includes('git config --global core.hooksPath')) {
|
|
376
|
+
if (cmd.includes('--unset')) {
|
|
377
|
+
return '';
|
|
378
|
+
}
|
|
379
|
+
// Return WRONG path to trigger fix
|
|
380
|
+
return '/wrong/path\n';
|
|
381
|
+
}
|
|
382
|
+
if (cmd === 'git rev-parse --git-dir') {
|
|
383
|
+
return path.join(tmpProject, '.git') + '\n';
|
|
384
|
+
}
|
|
385
|
+
if (cmd === 'node --version') {
|
|
386
|
+
return 'v20.0.0\n';
|
|
387
|
+
}
|
|
388
|
+
if (cmd === 'git --version') {
|
|
389
|
+
return 'git version 2.39.0\n';
|
|
390
|
+
}
|
|
391
|
+
if (cmd === 'bash --version') {
|
|
392
|
+
return 'GNU bash, version 5.1.16\n';
|
|
393
|
+
}
|
|
394
|
+
return '';
|
|
395
|
+
});
|
|
396
|
+
const { doctor } = require('../doctor');
|
|
397
|
+
|
|
398
|
+
const result = await doctor(['--fix']);
|
|
399
|
+
|
|
400
|
+
// Exit 1 because post-fix diagnosis still sees wrong path (mock returns wrong path)
|
|
401
|
+
// But fix was attempted — verify it called git config --global core.hooksPath with correct path
|
|
402
|
+
expect(result).toBe(1);
|
|
403
|
+
const setCalls = execSpy.mock.calls.filter(
|
|
404
|
+
c => c[0].includes('git config --global core.hooksPath') && !c[0].includes('--unset')
|
|
405
|
+
);
|
|
406
|
+
// Should have at least the fix call: read (wrong path found) + set
|
|
407
|
+
const fixCall = setCalls.find(
|
|
408
|
+
c => c[0].includes('git config --global core.hooksPath') && c[0].includes(globalHooksDir())
|
|
409
|
+
);
|
|
410
|
+
expect(fixCall).toBeTruthy();
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
it('AC-10: doctor --fix does NOT fix corrupt config', async () => {
|
|
414
|
+
fs.mkdirSync(path.dirname(configFile()), { recursive: true });
|
|
415
|
+
fs.writeFileSync(configFile(), 'not valid json');
|
|
416
|
+
|
|
417
|
+
const { doctor } = require('../doctor');
|
|
418
|
+
|
|
419
|
+
const result = await doctor(['--fix']);
|
|
420
|
+
|
|
421
|
+
expect(result).toBe(1);
|
|
422
|
+
// Corrupt config cannot be auto-fixed
|
|
423
|
+
expect(logSpy).toHaveBeenCalledWith(
|
|
424
|
+
expect.stringContaining('Corrupt JSON')
|
|
425
|
+
);
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
// === Edge cases ===
|
|
429
|
+
|
|
430
|
+
it('reports exit code 1 with unhealthy or missing install', async () => {
|
|
431
|
+
const { doctor } = require('../doctor');
|
|
432
|
+
const result = await doctor([]);
|
|
433
|
+
expect(result).toBe(1);
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
it('detects missing environment dependencies', async () => {
|
|
437
|
+
setupLocalInstall();
|
|
438
|
+
mockExecFail();
|
|
439
|
+
const { doctor } = require('../doctor');
|
|
440
|
+
|
|
441
|
+
const result = await doctor([]);
|
|
442
|
+
|
|
443
|
+
expect(result).toBe(1);
|
|
444
|
+
expect(logSpy).toHaveBeenCalledWith(
|
|
445
|
+
expect.stringContaining('Not found')
|
|
446
|
+
);
|
|
447
|
+
});
|
|
448
|
+
});
|