@boyingliu01/xp-gate 0.19.3 → 0.20.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/adapters/gate-4.sh +83 -18
- package/adapters/gate-8.sh +115 -29
- package/adapters/iac.sh +1 -1
- package/adapters/powershell.sh +60 -5
- package/adapters/python.sh +38 -4
- package/adapters/typescript.sh +15 -60
- package/bin/xp-gate.js +15 -16
- package/build-integrity/__tests__/import-resolver.test.ts +59 -0
- package/build-integrity/gate-10.ts +17 -2
- package/gate-10.sh +35 -2
- package/gate-4.sh +110 -21
- package/gate-8.sh +115 -29
- package/hooks/gate-10.sh +35 -2
- package/hooks/gate-4.sh +110 -21
- package/hooks/gate-8.sh +115 -29
- package/hooks/lib/jscpd-run.sh +63 -0
- package/hooks/lib/sprint-gate-report.sh +58 -0
- package/hooks/lib/test-failure.sh +152 -0
- package/hooks/lib/typecheck.sh +141 -0
- package/hooks/lib/validate-code-walkthrough.cjs +43 -5
- package/hooks/post-merge +100 -0
- package/hooks/pre-commit +514 -184
- package/hooks/pre-push +75 -19
- package/lib/__tests__/bootstrap.test.js +23 -41
- package/lib/__tests__/check-version.test.js +8 -1
- package/lib/__tests__/detect-deps.test.js +16 -0
- package/lib/__tests__/doctor-drift-round2.test.js +212 -0
- package/lib/__tests__/doctor-executed-surface.test.js +209 -0
- package/lib/__tests__/doctor-hook-drift.test.js +329 -0
- package/lib/__tests__/doctor-machine-mutation.test.js +116 -0
- package/lib/__tests__/doctor-module-drift.test.js +206 -0
- package/lib/__tests__/doctor-module-resolution-parity.test.js +314 -0
- package/lib/__tests__/doctor-path-equivalence.test.js +59 -0
- package/lib/__tests__/doctor-repair-boundary.test.js +52 -0
- package/lib/__tests__/doctor-tui-skill-freshness.test.js +105 -0
- package/lib/__tests__/doctor.test.js +155 -0
- package/lib/__tests__/gate-runner.test.js +19 -3
- package/lib/__tests__/helpers/lib-sandbox.cjs +55 -0
- package/lib/__tests__/init.test.js +44 -17
- package/lib/__tests__/install-skill-full-dir.test.js +151 -0
- package/lib/__tests__/install-skill.test.js +51 -132
- package/lib/__tests__/language-tools-detect.test.js +158 -0
- package/lib/__tests__/phase-transition.test.js +15 -1
- package/lib/__tests__/qoder-delphi-agents.test.js +285 -0
- package/lib/__tests__/shared-paths.test.js +20 -0
- package/lib/__tests__/shared-utils.test.js +69 -0
- package/lib/__tests__/skill-flags.test.js +48 -0
- package/lib/__tests__/ui-detector.test.ts +6 -2
- package/lib/__tests__/uninstall.test.js +20 -0
- package/lib/__tests__/update-hooks.test.js +12 -0
- package/lib/__tests__/update-skill.test.js +7 -4
- package/lib/detect-deps.js +3 -20
- package/lib/doctor-tui.js +22 -5
- package/lib/doctor.js +1178 -31
- package/lib/init.js +184 -33
- package/lib/install-cmd.js +7 -4
- package/lib/install-skill.js +38 -95
- package/lib/language-tools.js +83 -4
- package/lib/mattpocock-license-verdict.json +1 -1
- package/lib/shared-paths.js +5 -2
- package/lib/shared-utils.js +47 -3
- package/lib/uninstall.js +12 -0
- package/lib/update-hooks.js +4 -4
- package/lib/update-skill.js +6 -7
- package/mock-policy/AGENTS.md +4 -4
- package/mutation/AGENTS.md +4 -4
- package/mutation/__tests__/gate-m.test.ts +66 -14
- package/mutation/__tests__/mutmut-runner.test.ts +91 -42
- package/mutation/__tests__/test-filter-parity.test.ts +37 -0
- package/mutation/gate-m.ts +21 -8
- package/package.json +2 -2
- package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-code/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -1
- package/plugins/claude-code/skills/delphi-review/AGENTS.md +4 -4
- package/plugins/claude-code/skills/delphi-review/SKILL.md +98 -14
- package/plugins/claude-code/skills/delphi-review/references/code-walkthrough.md +36 -3
- package/plugins/claude-code/skills/delphi-review/references/requirements.md +6 -6
- package/plugins/claude-code/skills/sprint-flow/AGENTS.md +4 -4
- package/plugins/claude-code/skills/sprint-flow/references/phase-2-design.md +3 -3
- package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +4 -4
- package/plugins/dsh/README.md +169 -4
- package/plugins/dsh/dsh-overlay.patch.yml +67 -0
- package/plugins/dsh/package.json +1 -1
- package/plugins/dsh/scripts/prepack.cjs +24 -19
- package/plugins/dsh/setup-dsh.ps1 +160 -0
- package/plugins/dsh/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -1
- package/plugins/dsh/skills/delphi-review/AGENTS.md +4 -4
- package/plugins/dsh/skills/delphi-review/SKILL.md +98 -14
- package/plugins/dsh/skills/delphi-review/references/code-walkthrough.md +36 -3
- package/plugins/dsh/skills/delphi-review/references/requirements.md +6 -6
- package/plugins/dsh/skills/sprint-flow/AGENTS.md +4 -4
- package/plugins/dsh/skills/sprint-flow/references/phase-2-design.md +3 -3
- package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +4 -4
- package/plugins/dsh/src/command.test.ts +66 -2
- package/plugins/dsh/src/command.ts +36 -14
- package/plugins/dsh/src/contract.test.ts +31 -1
- package/plugins/dsh/src/delphi-config.test.ts +190 -0
- package/plugins/dsh/src/delphi-config.ts +223 -0
- package/plugins/dsh/src/delphi-run.test.ts +181 -0
- package/plugins/dsh/src/delphi-run.ts +129 -0
- package/plugins/dsh/src/delphi.test.ts +222 -0
- package/plugins/dsh/src/delphi.ts +247 -0
- package/plugins/dsh/src/dialect.test.ts +59 -0
- package/plugins/dsh/src/dialect.ts +40 -0
- package/plugins/dsh/src/index.ts +70 -4
- package/plugins/dsh/src/skills.test.ts +26 -19
- package/plugins/opencode/package.json +1 -1
- package/plugins/opencode/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -1
- package/plugins/opencode/skills/delphi-review/AGENTS.md +4 -4
- package/plugins/opencode/skills/delphi-review/SKILL.md +98 -14
- package/plugins/opencode/skills/delphi-review/references/code-walkthrough.md +36 -3
- package/plugins/opencode/skills/delphi-review/references/requirements.md +6 -6
- package/plugins/opencode/skills/sprint-flow/AGENTS.md +4 -4
- package/plugins/opencode/skills/sprint-flow/references/phase-2-design.md +3 -3
- package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +4 -4
- package/plugins/qoder/agents/delphi-architecture.md +1 -1
- package/plugins/qoder/agents/delphi-feasibility.md +1 -1
- package/plugins/qoder/agents/delphi-technical.md +1 -1
- package/plugins/qoder/plugin.json +1 -1
- package/plugins/qoder/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -1
- package/plugins/qoder/skills/delphi-review/AGENTS.md +4 -4
- package/plugins/qoder/skills/delphi-review/SKILL.md +98 -14
- package/plugins/qoder/skills/delphi-review/references/code-walkthrough.md +36 -3
- package/plugins/qoder/skills/delphi-review/references/requirements.md +6 -6
- package/plugins/qoder/skills/sprint-flow/AGENTS.md +4 -4
- package/plugins/qoder/skills/sprint-flow/references/phase-2-design.md +3 -3
- package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +4 -4
- package/principles/AGENTS.md +9 -8
- package/principles/__tests__/adapter-metrics.test.ts +292 -0
- package/principles/__tests__/analyzer.test.ts +29 -6
- package/principles/__tests__/baseline-prune-timeout.test.ts +71 -0
- package/principles/__tests__/baseline-prune.test.ts +338 -0
- package/principles/__tests__/boy-scout-init-baseline.test.ts +167 -0
- package/principles/__tests__/boy-scout-integration.test.ts +97 -6
- package/principles/__tests__/boy-scout.test.ts +253 -7
- package/principles/__tests__/config-enforcement.test.ts +469 -0
- package/principles/__tests__/config-git-timeout.test.ts +66 -0
- package/principles/__tests__/direct-execution.test.ts +187 -0
- package/principles/__tests__/index.test.ts +16 -3
- package/principles/__tests__/warnings-baseline-budgets.test.ts +74 -0
- package/principles/adapters/typescript.ts +613 -13
- package/principles/analyzer.ts +22 -0
- package/principles/baseline-prune.ts +217 -0
- package/principles/boy-scout.ts +516 -56
- package/principles/config.ts +362 -54
- package/principles/direct-execution.ts +60 -0
- package/principles/index.ts +40 -17
- package/principles/rules/__tests__/clean-code/many-exports.test.ts +16 -10
- package/principles/rules/clean-code/code-duplication.ts +8 -8
- package/principles/rules/clean-code/deep-nesting.ts +8 -8
- package/principles/rules/clean-code/god-class.ts +8 -8
- package/principles/rules/clean-code/large-file.ts +15 -11
- package/principles/rules/clean-code/long-function.ts +8 -8
- package/principles/rules/clean-code/magic-numbers.ts +10 -7
- package/principles/rules/clean-code/many-exports.ts +7 -7
- package/principles/rules/clean-code/missing-error-handling.ts +5 -5
- package/principles/rules/clean-code/too-many-params.ts +8 -8
- package/principles/rules/clean-code/unused-imports.ts +5 -5
- package/principles/rules/solid/dip.ts +18 -8
- package/principles/rules/solid/isp.ts +9 -8
- package/principles/rules/solid/lsp.ts +6 -6
- package/principles/rules/solid/ocp.ts +6 -5
- package/principles/rules/solid/srp.ts +7 -8
- package/principles/types.ts +15 -0
- package/scripts/__tests__/sync-package-content.test.js +28 -0
- package/scripts/delphi-external-review.cjs +193 -10
- package/scripts/sync-package-content.js +20 -10
- package/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -1
- package/skills/delphi-review/AGENTS.md +4 -4
- package/skills/delphi-review/SKILL.md +98 -14
- package/skills/delphi-review/references/code-walkthrough.md +36 -3
- package/skills/delphi-review/references/requirements.md +6 -6
- package/skills/sprint-flow/AGENTS.md +4 -4
- package/skills/sprint-flow/references/phase-2-design.md +3 -3
- package/skills/test-specification-alignment/AGENTS.md +4 -4
- package/lib/__tests__/download-skill.test.js +0 -281
- package/lib/download-skill.js +0 -96
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @test REQ-439
|
|
3
|
+
* @intent doctor 判技能新鲜度必须行尾无关:frontmatter 正则只认 LF + 退化分支做
|
|
4
|
+
* 字节比较,导致 autocrlf 工作树下 12/13 个技能被假报 Outdated,真差异
|
|
5
|
+
* 反而被噪声淹没(#439)
|
|
6
|
+
* @covers AC-439-01, AC-439-02, AC-439-03
|
|
7
|
+
*/
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const os = require('os');
|
|
11
|
+
|
|
12
|
+
describe('doctor-tui skill freshness (#439)', () => {
|
|
13
|
+
let tmpHome;
|
|
14
|
+
let originalHome;
|
|
15
|
+
// Same dir doctor-tui resolves as PKG_DIR (path.dirname of lib/).
|
|
16
|
+
const bundledSkillsDir = path.join(__dirname, '..', '..', 'skills');
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
originalHome = process.env.HOME;
|
|
20
|
+
tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'xpgate-fresh-'));
|
|
21
|
+
process.env.HOME = tmpHome;
|
|
22
|
+
vi.resetModules();
|
|
23
|
+
delete require.cache[require.resolve('../doctor-tui')];
|
|
24
|
+
delete require.cache[require.resolve('../shared-paths')];
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
afterEach(() => {
|
|
28
|
+
process.env.HOME = originalHome;
|
|
29
|
+
fs.rmSync(tmpHome, { recursive: true, force: true });
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// HOME has no platform marker, so detectPlatform() resolves to opencode.
|
|
33
|
+
function installSkill(name, content) {
|
|
34
|
+
const dir = path.join(tmpHome, '.config', 'opencode', 'skills', name);
|
|
35
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
36
|
+
fs.writeFileSync(path.join(dir, 'SKILL.md'), content);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function readBundled(name) {
|
|
40
|
+
return fs.readFileSync(path.join(bundledSkillsDir, name, 'SKILL.md'), 'utf8');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Same bytes, opposite line endings — the only difference a fresh install can have. */
|
|
44
|
+
function flipLineEndings(content) {
|
|
45
|
+
const lf = content.replace(/\r\n/g, '\n');
|
|
46
|
+
return lf === content ? lf.replace(/\n/g, '\r\n') : lf;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function runFreshness(name) {
|
|
50
|
+
const { diagnoseInstalledSkills } = require('../doctor-tui');
|
|
51
|
+
const checks = [];
|
|
52
|
+
const issues = diagnoseInstalledSkills({ installedSkills: { [name]: {} } }, checks);
|
|
53
|
+
const check = checks.find(c => c.name === `Skill: ${name}`);
|
|
54
|
+
return { issues, check };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
it('reads a version from CRLF frontmatter, not only LF', () => {
|
|
58
|
+
const { extractSkillVersion } = require('../doctor-tui');
|
|
59
|
+
expect(extractSkillVersion('---\r\nname: x\r\nversion: 2.1.0\r\n---\r\nbody\r\n')).toBe('2.1.0');
|
|
60
|
+
// LF must keep working -- the fix is a widening, not a swap.
|
|
61
|
+
expect(extractSkillVersion('---\nname: x\nversion: 2.1.0\n---\nbody\n')).toBe('2.1.0');
|
|
62
|
+
// ... and so must a lone CR, the third convention the helper's name covers.
|
|
63
|
+
expect(extractSkillVersion('---\rname: x\rversion: 2.1.0\r---\rbody\r')).toBe('2.1.0');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('treats a versioned skill as up to date when only line endings differ', () => {
|
|
67
|
+
const bundled = readBundled('sprint-flow');
|
|
68
|
+
expect(bundled).toMatch(/^---/);
|
|
69
|
+
installSkill('sprint-flow', flipLineEndings(bundled));
|
|
70
|
+
const { check } = runFreshness('sprint-flow');
|
|
71
|
+
expect(check, JSON.stringify(check)).toBeDefined();
|
|
72
|
+
expect(check.status).toBe('PASS');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('treats a versionless skill as up to date when only line endings differ', () => {
|
|
76
|
+
const bundled = readBundled('grilling');
|
|
77
|
+
// This is the branch the issue hit for 11 of 12 skills: no version field at all,
|
|
78
|
+
// so freshness can only be decided by content.
|
|
79
|
+
expect(bundled).not.toMatch(/^version:/m);
|
|
80
|
+
installSkill('grilling', flipLineEndings(bundled));
|
|
81
|
+
const { check } = runFreshness('grilling');
|
|
82
|
+
expect(check, JSON.stringify(check)).toBeDefined();
|
|
83
|
+
expect(check.status).toBe('PASS');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// AC-439-02 promises "differences beyond line endings" still report Outdated,
|
|
87
|
+
// which means every line-ending convention has to be tolerated, not just CRLF
|
|
88
|
+
// (Round 2 feasibility FC-05: the helper's name already says as much).
|
|
89
|
+
it('treats a lone-CR skill file as up to date too', () => {
|
|
90
|
+
const bundled = readBundled('grilling');
|
|
91
|
+
installSkill('grilling', bundled.replace(/\r\n/g, '\n').replace(/\n/g, '\r'));
|
|
92
|
+
const { check } = runFreshness('grilling');
|
|
93
|
+
expect(check, JSON.stringify(check)).toBeDefined();
|
|
94
|
+
expect(check.status).toBe('PASS');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('still reports Outdated for a real content difference (anti-vacuity)', () => {
|
|
98
|
+
const bundled = readBundled('grilling');
|
|
99
|
+
installSkill('grilling', `${flipLineEndings(bundled)}\n## Diverged section\n`);
|
|
100
|
+
const { issues, check } = runFreshness('grilling');
|
|
101
|
+
expect(check.status).toBe('WARN');
|
|
102
|
+
expect(check.detail).toContain('Outdated');
|
|
103
|
+
expect(issues).toBe(1);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
@@ -18,6 +18,7 @@ describe('doctor', () => {
|
|
|
18
18
|
let tmpProject;
|
|
19
19
|
let originalHome;
|
|
20
20
|
let originalXpGateCacheDir;
|
|
21
|
+
let originalRepoRoot;
|
|
21
22
|
let originalDetectDepsTools;
|
|
22
23
|
let originalExecSync;
|
|
23
24
|
let originalExec;
|
|
@@ -54,12 +55,19 @@ describe('doctor', () => {
|
|
|
54
55
|
beforeEach(() => {
|
|
55
56
|
originalHome = process.env.HOME;
|
|
56
57
|
originalXpGateCacheDir = process.env.XP_GATE_CACHE_DIR;
|
|
58
|
+
originalRepoRoot = process.env.XP_GATE_REPO_ROOT;
|
|
57
59
|
originalExecSync = cp.execSync;
|
|
58
60
|
originalExec = cp.exec;
|
|
59
61
|
// Unique per-describe + per-test prefixes to avoid parallel collisions.
|
|
60
62
|
tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), `xpgate-dr-${DESCRIBE_ID}-`));
|
|
61
63
|
tmpProject = fs.mkdtempSync(path.join(os.tmpdir(), `xpgate-dr-proj-${DESCRIBE_ID}-`));
|
|
62
64
|
process.env.HOME = tmpHome;
|
|
65
|
+
// #451: the hook-drift check reads the canonical githooks/ of *this* repo
|
|
66
|
+
// when no override is given. In a fixture whose whole point is "a healthy
|
|
67
|
+
// installation", that would compare the real checkout against synthetic
|
|
68
|
+
// temp files and always report drift. Point it at an empty dir instead:
|
|
69
|
+
// these tests assert installation health, not drift.
|
|
70
|
+
process.env.XP_GATE_REPO_ROOT = path.join(tmpProject, 'no-repo-hooks');
|
|
63
71
|
vi.resetModules();
|
|
64
72
|
// Invalidate all cached modules that load path/environment globals at require-time.
|
|
65
73
|
// Do NOT invalidate detect-deps.js — its GATE_CLI_TOOLS was already cleared in beforeAll
|
|
@@ -79,6 +87,8 @@ describe('doctor', () => {
|
|
|
79
87
|
afterEach(() => {
|
|
80
88
|
process.env.HOME = originalHome;
|
|
81
89
|
process.env.XP_GATE_CACHE_DIR = originalXpGateCacheDir;
|
|
90
|
+
if (originalRepoRoot === undefined) delete process.env.XP_GATE_REPO_ROOT;
|
|
91
|
+
else process.env.XP_GATE_REPO_ROOT = originalRepoRoot;
|
|
82
92
|
cp.execSync = originalExecSync;
|
|
83
93
|
cp.exec = originalExec;
|
|
84
94
|
if (tmpHome && fs.existsSync(tmpHome)) {
|
|
@@ -130,6 +140,14 @@ describe('doctor', () => {
|
|
|
130
140
|
);
|
|
131
141
|
}
|
|
132
142
|
|
|
143
|
+
function createXpGatePostMerge(dir) {
|
|
144
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
145
|
+
fs.writeFileSync(
|
|
146
|
+
path.join(dir, 'post-merge'),
|
|
147
|
+
'#!/usr/bin/env bash\n# post-merge - XP-Gate VERSION sync hook\n'
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
133
151
|
function createXpGateAdapterCommon(dir) {
|
|
134
152
|
fs.mkdirSync(dir, { recursive: true });
|
|
135
153
|
fs.writeFileSync(
|
|
@@ -166,6 +184,7 @@ describe('doctor', () => {
|
|
|
166
184
|
function setupLocalInstall() {
|
|
167
185
|
createXpGatePreCommit(projectHooksDir());
|
|
168
186
|
createXpGatePrePush(projectHooksDir());
|
|
187
|
+
createXpGatePostMerge(projectHooksDir());
|
|
169
188
|
createXpGateAdapterCommon(projectGithooksDir());
|
|
170
189
|
createXpGateAdapterScripts(projectGithooksDir());
|
|
171
190
|
|
|
@@ -182,6 +201,7 @@ describe('doctor', () => {
|
|
|
182
201
|
function setupGlobalInstall() {
|
|
183
202
|
createXpGatePreCommit(globalHooksDir());
|
|
184
203
|
createXpGatePrePush(globalHooksDir());
|
|
204
|
+
createXpGatePostMerge(globalHooksDir());
|
|
185
205
|
createXpGateAdapterCommon(globalAdaptersDir());
|
|
186
206
|
// For global mode, create adapters directly in globalAdaptersDir (not in a subdirectory)
|
|
187
207
|
fs.mkdirSync(globalAdaptersDir(), { recursive: true });
|
|
@@ -337,6 +357,21 @@ describe('doctor', () => {
|
|
|
337
357
|
);
|
|
338
358
|
});
|
|
339
359
|
|
|
360
|
+
it('AC-08: doctor detects a missing post-merge hook', async () => {
|
|
361
|
+
setupLocalInstall();
|
|
362
|
+
seedVersionCache();
|
|
363
|
+
fs.unlinkSync(path.join(projectHooksDir(), 'post-merge'));
|
|
364
|
+
mockExecSuccess();
|
|
365
|
+
const { doctor } = require('../doctor');
|
|
366
|
+
|
|
367
|
+
const result = await doctor([]);
|
|
368
|
+
|
|
369
|
+
expect(result).toBe(1);
|
|
370
|
+
expect(logSpy).toHaveBeenCalledWith(
|
|
371
|
+
expect.stringContaining('post-merge')
|
|
372
|
+
);
|
|
373
|
+
});
|
|
374
|
+
|
|
340
375
|
it('AC-08: doctor detects missing config file', async () => {
|
|
341
376
|
// No config at all
|
|
342
377
|
const { doctor } = require('../doctor');
|
|
@@ -463,6 +498,19 @@ describe('doctor', () => {
|
|
|
463
498
|
expect(fs.existsSync(path.join(projectHooksDir(), 'pre-push'))).toBe(true);
|
|
464
499
|
});
|
|
465
500
|
|
|
501
|
+
it('AC-10: doctor --fix restores a missing post-merge hook', async () => {
|
|
502
|
+
setupLocalInstall();
|
|
503
|
+
seedVersionCache();
|
|
504
|
+
fs.unlinkSync(path.join(projectHooksDir(), 'post-merge'));
|
|
505
|
+
mockExecSuccess();
|
|
506
|
+
const { doctor } = require('../doctor');
|
|
507
|
+
|
|
508
|
+
const result = await doctor(['--fix']);
|
|
509
|
+
|
|
510
|
+
expect(result).toBe(0);
|
|
511
|
+
expect(fs.existsSync(path.join(projectHooksDir(), 'post-merge'))).toBe(true);
|
|
512
|
+
});
|
|
513
|
+
|
|
466
514
|
it('AC-10: doctor --fix reinstall adapters when mode is active and adapters missing', async () => {
|
|
467
515
|
setupLocalInstall();
|
|
468
516
|
seedVersionCache();
|
|
@@ -551,6 +599,23 @@ describe('doctor', () => {
|
|
|
551
599
|
|
|
552
600
|
// === Edge cases ===
|
|
553
601
|
|
|
602
|
+
it('AC-488: doctor rejects unknown flags instead of silently ignoring them', async () => {
|
|
603
|
+
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
604
|
+
const { doctor } = require('../doctor');
|
|
605
|
+
const result = await doctor(['--syn-hooks', 'extra-arg']);
|
|
606
|
+
expect(result).toBe(1);
|
|
607
|
+
expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('Unknown flag'));
|
|
608
|
+
errorSpy.mockRestore();
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
it('AC-488: doctor still accepts its documented flags', async () => {
|
|
612
|
+
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
613
|
+
const { doctor } = require('../doctor');
|
|
614
|
+
await doctor(['--fix', '--sync-hooks']);
|
|
615
|
+
expect(errorSpy).not.toHaveBeenCalled();
|
|
616
|
+
errorSpy.mockRestore();
|
|
617
|
+
});
|
|
618
|
+
|
|
554
619
|
it('reports exit code 1 with unhealthy or missing install', async () => {
|
|
555
620
|
const { doctor } = require('../doctor');
|
|
556
621
|
const result = await doctor([]);
|
|
@@ -1053,4 +1118,94 @@ describe('doctor', () => {
|
|
|
1053
1118
|
expect(json.ok).toBe(true);
|
|
1054
1119
|
expect(json.issues).toBe(0);
|
|
1055
1120
|
});
|
|
1121
|
+
|
|
1122
|
+
// AC-488-08: the flag was advertised in the release notes since the version
|
|
1123
|
+
// that added doctor, and rejected as an unknown flag until now. Asserting the
|
|
1124
|
+
// FORMATTER alone would pass while `doctor --format json` still crashed, so
|
|
1125
|
+
// this runs the command and inspects what a script would actually parse.
|
|
1126
|
+
it('AC-488-08: doctor --format json emits one parseable document and nothing else', async () => {
|
|
1127
|
+
setupLocalInstall();
|
|
1128
|
+
seedVersionCache();
|
|
1129
|
+
mockExecSuccess();
|
|
1130
|
+
const { doctor } = require('../doctor');
|
|
1131
|
+
const seen = [];
|
|
1132
|
+
const logSpy = vi.spyOn(console, 'log').mockImplementation((msg) => { seen.push(String(msg)); });
|
|
1133
|
+
|
|
1134
|
+
let code;
|
|
1135
|
+
try {
|
|
1136
|
+
code = await doctor(['--format', 'json']);
|
|
1137
|
+
} finally {
|
|
1138
|
+
logSpy.mockRestore();
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
expect(seen, `expected exactly one JSON line, got:\n${seen.join('\n')}`).toHaveLength(1);
|
|
1142
|
+
const doc = JSON.parse(seen[0]);
|
|
1143
|
+
expect(doc.ok).toBe(code === 0);
|
|
1144
|
+
expect(Array.isArray(doc.checks)).toBe(true);
|
|
1145
|
+
expect(doc.checks.length).toBeGreaterThan(0);
|
|
1146
|
+
expect(seen[0]).not.toMatch(/XP-Gate Doctor|Diagnosis Report/);
|
|
1147
|
+
});
|
|
1148
|
+
|
|
1149
|
+
it('AC-488-08: a machine-readable doctor run cannot also mutate the machine', async () => {
|
|
1150
|
+
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
1151
|
+
const { doctor } = require('../doctor');
|
|
1152
|
+
|
|
1153
|
+
expect(await doctor(['--format', 'json', '--fix'])).toBe(1);
|
|
1154
|
+
expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('--fix'));
|
|
1155
|
+
expect(await doctor(['--json', '--sync-hooks'])).toBe(1);
|
|
1156
|
+
errorSpy.mockRestore();
|
|
1157
|
+
});
|
|
1158
|
+
|
|
1159
|
+
it('AC-488-08: an unknown --format value is rejected instead of silently ignored', async () => {
|
|
1160
|
+
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
1161
|
+
const { doctor } = require('../doctor');
|
|
1162
|
+
|
|
1163
|
+
expect(await doctor(['--format', 'yaml'])).toBe(1);
|
|
1164
|
+
expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('--format'));
|
|
1165
|
+
errorSpy.mockRestore();
|
|
1166
|
+
});
|
|
1167
|
+
|
|
1168
|
+
// AC-488-09 (#488 Round 4, feasibility): the same class as the original issue —
|
|
1169
|
+
// a flag that is accepted and then does nothing. `--format` with no value, and
|
|
1170
|
+
// `--force` with no mode that reads it, both looked like they ran.
|
|
1171
|
+
it('AC-488-09: a value-less --format is rejected instead of silently dropping the flag', async () => {
|
|
1172
|
+
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
1173
|
+
const { doctor } = require('../doctor');
|
|
1174
|
+
|
|
1175
|
+
expect(await doctor(['--format'])).toBe(1);
|
|
1176
|
+
expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('--format'));
|
|
1177
|
+
errorSpy.mockRestore();
|
|
1178
|
+
});
|
|
1179
|
+
|
|
1180
|
+
it('AC-488-09: --force is refused when no mode consumes it', () => {
|
|
1181
|
+
// Asserted through the pure contract rather than by running the command:
|
|
1182
|
+
// the only mode that reads --force is the one that copies files over the
|
|
1183
|
+
// installed hooks, so a run-based assertion would mutate this machine.
|
|
1184
|
+
const { doctorFlagContract } = require('../doctor');
|
|
1185
|
+
|
|
1186
|
+
const bare = doctorFlagContract(['--force']);
|
|
1187
|
+
expect(bare.errors).toHaveLength(2);
|
|
1188
|
+
expect(bare.errors[0]).toContain('--force');
|
|
1189
|
+
expect(bare.errors[0]).toContain('--sync-hooks');
|
|
1190
|
+
expect(doctorFlagContract(['--fix', '--force']).errors.length, '--fix does not read --force either').toBeGreaterThan(0);
|
|
1191
|
+
|
|
1192
|
+
const synced = doctorFlagContract(['--sync-hooks', '--force']);
|
|
1193
|
+
expect(synced.errors, synced.errors.join('\n')).toHaveLength(0);
|
|
1194
|
+
expect(synced.force).toBe(true);
|
|
1195
|
+
expect(synced.syncHooks).toBe(true);
|
|
1196
|
+
});
|
|
1197
|
+
|
|
1198
|
+
it('AC-488-09: --install-tools is refused unless --fix is the mode that reads it', () => {
|
|
1199
|
+
const { doctorFlagContract } = require('../doctor');
|
|
1200
|
+
|
|
1201
|
+
const bare = doctorFlagContract(['--install-tools']);
|
|
1202
|
+
expect(bare.errors).toHaveLength(2);
|
|
1203
|
+
expect(bare.errors[0]).toContain('--install-tools');
|
|
1204
|
+
expect(bare.errors[0]).toContain('--fix');
|
|
1205
|
+
expect(doctorFlagContract(['--sync-hooks', '--install-tools']).errors.length).toBeGreaterThan(0);
|
|
1206
|
+
|
|
1207
|
+
const fixing = doctorFlagContract(['--fix', '--install-tools']);
|
|
1208
|
+
expect(fixing.errors, fixing.errors.join('\n')).toHaveLength(0);
|
|
1209
|
+
expect(fixing.installToolsFlag).toBe(true);
|
|
1210
|
+
});
|
|
1056
1211
|
});
|
|
@@ -43,6 +43,17 @@ function shellParameter(expression) {
|
|
|
43
43
|
return ['$', '{', expression, '}'].join('');
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
// Path segment that must survive quoting untouched. Windows rejects `"` and `*`
|
|
47
|
+
// in a filename, so the hostile set there keeps the same bash hazards — word
|
|
48
|
+
// split, parameter expansion, command substitution, quoting, list separator —
|
|
49
|
+
// with legal characters instead (#428).
|
|
50
|
+
function hostilePathSegment() {
|
|
51
|
+
if (process.platform === 'win32') {
|
|
52
|
+
return "space $HOME `marker` 'quoted' & (paren)";
|
|
53
|
+
}
|
|
54
|
+
return 'space $HOME `marker` "quoted"';
|
|
55
|
+
}
|
|
56
|
+
|
|
46
57
|
function initializeGitProject() {
|
|
47
58
|
execFileSync('git', ['init', '-q'], { cwd: tmpProject });
|
|
48
59
|
execFileSync('git', ['config', 'user.email', 'test@example.com'], { cwd: tmpProject });
|
|
@@ -171,20 +182,25 @@ describe('gate-runner.js — runGateAdapter', () => {
|
|
|
171
182
|
path.join(callerProject, 'githooks', 'gates', 'gate-9-caller.sh'),
|
|
172
183
|
'echo "__CALLER_REPO__"\n'
|
|
173
184
|
);
|
|
185
|
+
// The fragment is written into the target project only, so a relative probe
|
|
186
|
+
// answers "which project did we run in?" without comparing path strings —
|
|
187
|
+
// bash reports $PWD in POSIX form, which can never equal the Windows path
|
|
188
|
+
// the test passes as an argument (#428).
|
|
174
189
|
fs.writeFileSync(
|
|
175
190
|
path.join(targetProject, 'githooks', 'gates', 'gate-9-target.sh'),
|
|
176
191
|
[
|
|
177
|
-
'if [
|
|
192
|
+
'if [ ! -f "githooks/gates/gate-9-target.sh" ]; then exit 88; fi',
|
|
193
|
+
'if [ -f ".xpgate-caller-only" ]; then exit 88; fi',
|
|
178
194
|
'echo "__TARGET_REPO__"',
|
|
179
195
|
].join('\n')
|
|
180
196
|
);
|
|
197
|
+
fs.writeFileSync(path.join(callerProject, '.xpgate-caller-only'), 'caller\n');
|
|
181
198
|
|
|
182
199
|
const result = spawnSync(
|
|
183
200
|
process.execPath,
|
|
184
201
|
[path.join(packageFixture, 'bin', 'xp-gate.js'), 'check', '9', targetProject],
|
|
185
202
|
{
|
|
186
203
|
cwd: callerProject,
|
|
187
|
-
env: { ...process.env, EXPECTED_TARGET: targetProject },
|
|
188
204
|
encoding: 'utf8',
|
|
189
205
|
timeout: 30000,
|
|
190
206
|
}
|
|
@@ -197,7 +213,7 @@ describe('gate-runner.js — runGateAdapter', () => {
|
|
|
197
213
|
|
|
198
214
|
it('sources fragment and helper paths containing shell metacharacters literally (REQ-standalone-gates-07)', () => {
|
|
199
215
|
const runnerAbsPath = path.resolve(__dirname, '../gate-runner.js');
|
|
200
|
-
const hostileRoot = path.join(tmpProject,
|
|
216
|
+
const hostileRoot = path.join(tmpProject, hostilePathSegment());
|
|
201
217
|
tmpAdapters = path.join(hostileRoot, 'githooks', 'adapters');
|
|
202
218
|
fs.mkdirSync(tmpAdapters, { recursive: true });
|
|
203
219
|
writeAdapterCommon();
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared scaffolding for `lib/__tests__` suites. Hoisted out of the #417 agent
|
|
3
|
+
* test because the same reset-and-mute sequence already lives in two other
|
|
4
|
+
* suites and a third inline copy tripped the architecture ratchet.
|
|
5
|
+
*/
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const os = require('os');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Drop the HOME-derived modules from Node's cache so the next require() sees
|
|
12
|
+
* the sandboxed `process.env.HOME`.
|
|
13
|
+
*
|
|
14
|
+
* @param {object} vi - Vitest API
|
|
15
|
+
* @param {string[]} resolvedPaths - already require.resolve()d module paths
|
|
16
|
+
*/
|
|
17
|
+
function resetLibModules(vi, resolvedPaths) {
|
|
18
|
+
vi.resetModules();
|
|
19
|
+
for (const resolved of resolvedPaths) delete require.cache[resolved];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param {object} vi - Vitest API
|
|
24
|
+
* @returns {{log: object, warn: object, error: object}} console spies
|
|
25
|
+
*/
|
|
26
|
+
function muteConsole(vi) {
|
|
27
|
+
const spies = {};
|
|
28
|
+
for (const level of ['log', 'warn', 'error']) {
|
|
29
|
+
spies[level] = vi.spyOn(console, level).mockImplementation(() => {});
|
|
30
|
+
}
|
|
31
|
+
return spies;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @param {string[]} prefixes - tmpdir name prefixes to create
|
|
36
|
+
* @returns {Object<string, string>} created temp directories by prefix
|
|
37
|
+
*/
|
|
38
|
+
function makeTempDirs(prefixes) {
|
|
39
|
+
const dirs = {};
|
|
40
|
+
for (const prefix of prefixes) {
|
|
41
|
+
dirs[prefix] = fs.mkdtempSync(path.join(os.tmpdir(), `${prefix}-`));
|
|
42
|
+
}
|
|
43
|
+
return dirs;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @param {Object<string, string>} dirs - directories returned by makeTempDirs
|
|
48
|
+
*/
|
|
49
|
+
function removeTempDirs(dirs) {
|
|
50
|
+
for (const dir of Object.values(dirs)) {
|
|
51
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
module.exports = { resetLibModules, muteConsole, makeTempDirs, removeTempDirs };
|
|
@@ -385,28 +385,35 @@ describe('init', () => {
|
|
|
385
385
|
return path.join(__dirname, '..', '..', 'plugins', 'qoder', 'agents');
|
|
386
386
|
}
|
|
387
387
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
setupQoderPlatform();
|
|
392
|
-
|
|
393
|
-
// Ensure agent templates exist in the npm package
|
|
388
|
+
// Real agent templates ship inside the git-tracked mirror
|
|
389
|
+
// (src/npm-package/plugins/qoder/agents/) — tests must never write there.
|
|
390
|
+
function assertRealTemplatePresent() {
|
|
394
391
|
const templateDir = agentTemplateDir();
|
|
395
|
-
fs.
|
|
396
|
-
|
|
392
|
+
expect(fs.existsSync(path.join(templateDir, 'delphi-architecture.md'))).toBe(true);
|
|
393
|
+
}
|
|
397
394
|
|
|
395
|
+
// Fresh module registry so init() picks up this test's tmpProject/tmpHome.
|
|
396
|
+
function reloadInit() {
|
|
398
397
|
vi.resetModules();
|
|
399
398
|
delete require.cache[require.resolve('../init')];
|
|
400
399
|
delete require.cache[require.resolve('../detect-deps.js')];
|
|
401
400
|
delete require.cache[require.resolve('../shared-paths')];
|
|
401
|
+
return require('../init');
|
|
402
|
+
}
|
|
402
403
|
|
|
403
|
-
|
|
404
|
+
it('deploys Delphi agents when platform is Qoder and agents do not exist', async () => {
|
|
405
|
+
fs.mkdirSync(path.join(tmpProject, '.git', 'hooks'), { recursive: true });
|
|
406
|
+
mockExecSuccess();
|
|
407
|
+
setupQoderPlatform();
|
|
408
|
+
assertRealTemplatePresent();
|
|
409
|
+
|
|
410
|
+
const { init } = reloadInit();
|
|
404
411
|
const result = await init(['--core-only']);
|
|
405
412
|
expect(result).toBe(0);
|
|
406
413
|
|
|
407
414
|
const deployedAgent = path.join(tmpProject, '.qoder', 'agents', 'delphi-architecture.md');
|
|
408
415
|
expect(fs.existsSync(deployedAgent)).toBe(true);
|
|
409
|
-
expect(fs.readFileSync(deployedAgent, 'utf8')).toContain('
|
|
416
|
+
expect(fs.readFileSync(deployedAgent, 'utf8')).toContain('name: delphi-architecture');
|
|
410
417
|
});
|
|
411
418
|
|
|
412
419
|
it('does not overwrite existing agent files (preserves user customizations)', async () => {
|
|
@@ -419,10 +426,18 @@ describe('init', () => {
|
|
|
419
426
|
fs.mkdirSync(agentsDir, { recursive: true });
|
|
420
427
|
fs.writeFileSync(path.join(agentsDir, 'delphi-architecture.md'), 'CUSTOM USER CONTENT');
|
|
421
428
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
429
|
+
assertRealTemplatePresent();
|
|
430
|
+
|
|
431
|
+
const { init } = reloadInit();
|
|
432
|
+
await init(['--core-only']);
|
|
433
|
+
|
|
434
|
+
// User content preserved
|
|
435
|
+
expect(fs.readFileSync(path.join(agentsDir, 'delphi-architecture.md'), 'utf8')).toBe('CUSTOM USER CONTENT');
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
it('deploys Delphi agents to the user-level agents dir in global mode', async () => {
|
|
439
|
+
mockExecSuccess();
|
|
440
|
+
setupQoderPlatform();
|
|
426
441
|
|
|
427
442
|
vi.resetModules();
|
|
428
443
|
delete require.cache[require.resolve('../init')];
|
|
@@ -430,10 +445,22 @@ describe('init', () => {
|
|
|
430
445
|
delete require.cache[require.resolve('../shared-paths')];
|
|
431
446
|
|
|
432
447
|
const { init } = require('../init');
|
|
433
|
-
await init(['--
|
|
448
|
+
const result = await init(['--global']);
|
|
449
|
+
expect(result).toBe(0);
|
|
434
450
|
|
|
435
|
-
|
|
436
|
-
expect(fs.
|
|
451
|
+
const globalAgent = path.join(tmpHome, '.qoder', 'agents', 'delphi-architecture.md');
|
|
452
|
+
expect(fs.existsSync(globalAgent)).toBe(true);
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
it('pins every expert template to a distinct built-in model in "[Name](modelId)" form', () => {
|
|
456
|
+
const templateDir = agentTemplateDir();
|
|
457
|
+
const { readQoderModelId } = require('../init');
|
|
458
|
+
const models = ['delphi-architecture', 'delphi-technical', 'delphi-feasibility'].map(name => {
|
|
459
|
+
const modelId = readQoderModelId(path.join(templateDir, `${name}.md`));
|
|
460
|
+
expect(modelId).not.toBeNull();
|
|
461
|
+
return modelId;
|
|
462
|
+
});
|
|
463
|
+
expect(new Set(models).size).toBe(3);
|
|
437
464
|
});
|
|
438
465
|
|
|
439
466
|
it('skips deployment when platform is not Qoder', async () => {
|