@boyingliu01/xp-gate 0.31.1 → 0.31.2
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 +41 -3
- package/adapters/gate-3.sh +8 -1
- package/adapters/gate-8.sh +67 -6
- package/adapters/go.sh +18 -1
- package/adapters/iac.sh +18 -1
- package/adapters/powershell.sh +37 -1
- package/adapters/python.sh +18 -4
- package/gate-3.sh +10 -1
- package/gate-8.sh +67 -6
- package/hooks/adapter-common.sh +41 -3
- package/hooks/gate-3.sh +10 -1
- package/hooks/gate-8.sh +67 -6
- package/hooks/lib/coverage-total.sh +20 -5
- package/hooks/lib/gate-5c-alignment.sh +145 -0
- package/hooks/lib/jscpd-run.sh +40 -1
- package/hooks/lib/native-path.sh +106 -0
- package/hooks/lib/prepush-score.py +143 -0
- package/hooks/lib/sprint-gate-report.sh +23 -0
- package/hooks/lib/toolchain-path.sh +370 -0
- package/hooks/lib/validate-code-walkthrough.cjs +224 -23
- package/hooks/pre-commit +371 -163
- package/hooks/pre-push +192 -74
- package/hooks/sprint-gate.sh +14 -3
- package/lib/__tests__/check-alignment-cli.test.mjs +20 -3
- package/lib/__tests__/check-alignment-reasons.test.mjs +91 -0
- package/lib/__tests__/doctor-delphi.test.js +46 -0
- package/lib/__tests__/doctor-exec-retry.test.js +20 -10
- package/lib/__tests__/doctor-hook-signatures.test.js +65 -0
- package/lib/__tests__/doctor-path-equivalence.test.js +12 -1
- package/lib/__tests__/doctor.test.js +7 -1
- package/lib/__tests__/hook-health-path.test.js +9 -1
- package/lib/__tests__/issue-566-sync-hooks-backup.test.js +221 -0
- package/lib/__tests__/issue-583-hook-deploy-log.test.js +121 -0
- package/lib/__tests__/issue-586-principles-cmdline.test.js +183 -0
- package/lib/__tests__/issue-589-check-all-gate3.test.js +190 -0
- package/lib/__tests__/issue-590-archlint-vacuous-pass.test.js +138 -0
- package/lib/__tests__/shared-paths-lazy-home.test.js +67 -0
- package/lib/arch.js +261 -44
- package/lib/check-alignment-runner.cjs +23 -0
- package/lib/check.js +4 -1
- package/lib/doctor.js +194 -49
- package/lib/gate-runner.js +265 -58
- package/lib/mattpocock-license-verdict.json +1 -1
- package/lib/principles.js +228 -6
- package/lib/shared-paths.js +53 -19
- package/mock-policy/AGENTS.md +3 -3
- package/mutation/AGENTS.md +3 -3
- package/mutation/__tests__/gremlins-runner.test.ts +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/delphi-review/SKILL.md +50 -0
- 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/dsh/package.json +1 -1
- package/plugins/dsh/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/dsh/skills/delphi-review/SKILL.md +50 -0
- package/plugins/dsh/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/dsh/src/delphi-config-env-diagnostics.test.ts +210 -0
- package/plugins/dsh/src/delphi-config.ts +151 -9
- package/plugins/opencode/package.json +1 -1
- package/plugins/opencode/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/opencode/skills/delphi-review/SKILL.md +50 -0
- package/plugins/opencode/skills/sprint-flow/AGENTS.md +3 -3
- 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 +3 -3
- package/plugins/qoder/skills/delphi-review/SKILL.md +50 -0
- 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/principles/__tests__/issue-587-java-member-count.test.ts +279 -0
- package/principles/__tests__/issue-588-lsp-java-false-positives.test.ts +213 -0
- package/principles/adapters/java.ts +127 -11
- package/principles/rules/__tests__/solid/issue-588-lsp-base-contract.test.ts +252 -0
- package/principles/rules/__tests__/solid/lsp.test.ts +26 -22
- package/principles/rules/solid/lsp.ts +348 -23
- package/scripts/__tests__/sync-package-content.test.js +32 -1
- package/scripts/sync-package-content.js +6 -1
- package/skills/delphi-review/AGENTS.md +3 -3
- package/skills/delphi-review/SKILL.md +50 -0
- package/skills/sprint-flow/AGENTS.md +3 -3
- package/skills/test-specification-alignment/AGENTS.md +3 -3
- package/sprint-gate.sh +14 -3
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @test REQ-608
|
|
3
|
+
* @intent `xp-gate check . --all` is documented as a full-tree check, but Gate 3
|
|
4
|
+
* resolved its file list from `git diff`, so on a clean tree it scanned
|
|
5
|
+
* the empty set and printed
|
|
6
|
+
*
|
|
7
|
+
* ⏭️ SKIPPED - Complexity (no source files to check)
|
|
8
|
+
*
|
|
9
|
+
* `--all` therefore checked LESS than a normal commit — and the wording
|
|
10
|
+
* ("no source files to check") describes an empty PROJECT when the real
|
|
11
|
+
* cause is an empty DIFF. Reading it as "this gate passed" is the
|
|
12
|
+
* documented failure mode: the reporter worked around it by running lizard
|
|
13
|
+
* by hand.
|
|
14
|
+
*
|
|
15
|
+
* Gate 4 does not depend on CHANGED_FILES and works fine under the same
|
|
16
|
+
* preamble, which is the proof that the file SOURCE is the defect and not
|
|
17
|
+
* the preamble.
|
|
18
|
+
*
|
|
19
|
+
* Under `--all` the set must be the repository's tracked sources; and a
|
|
20
|
+
* gate that genuinely only makes sense per-change must say so, not look
|
|
21
|
+
* like it found an empty project.
|
|
22
|
+
* @covers AC-608-01, AC-608-02, AC-608-03
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const { execFileSync } = require('child_process');
|
|
26
|
+
const fs = require('fs');
|
|
27
|
+
const os = require('os');
|
|
28
|
+
const path = require('path');
|
|
29
|
+
|
|
30
|
+
const gateRunner = require('../gate-runner.js');
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Create a throwaway git repo with one commit and a clean working tree, plus the
|
|
34
|
+
* given tracked files.
|
|
35
|
+
*
|
|
36
|
+
* A clean tree is the whole point: it is the state in which `--all` used to scan
|
|
37
|
+
* nothing.
|
|
38
|
+
*
|
|
39
|
+
* @param {string[]} files - repo-relative paths to create and commit
|
|
40
|
+
* @param {(dir: string) => void} [afterCommit] - optional mutation after the commit
|
|
41
|
+
* @returns {string} the repo directory
|
|
42
|
+
*/
|
|
43
|
+
function makeCleanRepo(files, afterCommit) {
|
|
44
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'xp-gate-589-'));
|
|
45
|
+
const git = (/** @type {string[]} */ args) =>
|
|
46
|
+
execFileSync('git', args, { cwd: dir, stdio: 'pipe' });
|
|
47
|
+
// Every fixture step is IO that can fail for environment reasons (no git, a
|
|
48
|
+
// read-only tmp, a path already taken). Failing with the operation and the path
|
|
49
|
+
// named turns "the test broke" into a diagnosable message instead of a bare
|
|
50
|
+
// exception from inside a helper.
|
|
51
|
+
try {
|
|
52
|
+
git(['init', '-q', '-b', 'main']);
|
|
53
|
+
git(['config', 'user.email', 't@t.t']);
|
|
54
|
+
git(['config', 'user.name', 't']);
|
|
55
|
+
git(['config', 'core.hooksPath', process.platform === 'win32' ? 'NUL' : '/dev/null']);
|
|
56
|
+
for (const f of files) {
|
|
57
|
+
const full = path.join(dir, f);
|
|
58
|
+
fs.mkdirSync(path.dirname(full), { recursive: true });
|
|
59
|
+
fs.writeFileSync(full, `// ${f}\nexport const x = 1;\n`);
|
|
60
|
+
}
|
|
61
|
+
git(['add', '-A']);
|
|
62
|
+
git(['commit', '-q', '-m', 'clean tree']);
|
|
63
|
+
} catch (err) {
|
|
64
|
+
throw new Error(`makeCleanRepo failed while preparing ${dir}: ${err.message}`);
|
|
65
|
+
}
|
|
66
|
+
if (afterCommit) afterCommit(dir);
|
|
67
|
+
return dir;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The `--all` file list the runner should produce, as a sorted array.
|
|
72
|
+
*
|
|
73
|
+
* @param {string} cwd
|
|
74
|
+
* @returns {string[]}
|
|
75
|
+
*/
|
|
76
|
+
function allModeFiles(cwd) {
|
|
77
|
+
const out = gateRunner.resolveAllModeChangedFiles(cwd);
|
|
78
|
+
return out.split('\n').filter(Boolean).sort();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
describe('REQ-608: check --all scans the tree, not the diff', () => {
|
|
82
|
+
/** @type {string[]} */
|
|
83
|
+
const repos = [];
|
|
84
|
+
|
|
85
|
+
afterEach(() => {
|
|
86
|
+
for (const dir of repos.splice(0)) {
|
|
87
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('AC-608-01: on a clean tree the file list is the tracked sources, not empty', () => {
|
|
92
|
+
const repo = makeCleanRepo(['src/a.ts', 'src/b.ts', 'lib/c.js']);
|
|
93
|
+
repos.push(repo);
|
|
94
|
+
|
|
95
|
+
const files = allModeFiles(repo);
|
|
96
|
+
|
|
97
|
+
expect(files).toContain('src/a.ts');
|
|
98
|
+
expect(files).toContain('src/b.ts');
|
|
99
|
+
expect(files).toContain('lib/c.js');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('AC-608-01: data and documentation files are not claimed as sources', () => {
|
|
103
|
+
// The control that stops `--all` from becoming "every tracked file": the
|
|
104
|
+
// point is to scan SOURCES, not to hand a markdown file to lizard.
|
|
105
|
+
const repo = makeCleanRepo(['src/a.ts', 'docs/readme.md', 'data/rows.csv', 'notes.txt']);
|
|
106
|
+
repos.push(repo);
|
|
107
|
+
|
|
108
|
+
const files = allModeFiles(repo);
|
|
109
|
+
|
|
110
|
+
expect(files).toContain('src/a.ts');
|
|
111
|
+
expect(files).not.toContain('docs/readme.md');
|
|
112
|
+
expect(files).not.toContain('data/rows.csv');
|
|
113
|
+
expect(files).not.toContain('notes.txt');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('AC-608-01: every supported language extension is discovered', () => {
|
|
117
|
+
const repo = makeCleanRepo([
|
|
118
|
+
'a.ts', 'b.tsx', 'c.js', 'd.py', 'e.go', 'f.java', 'g.kt', 'h.rs', 'i.sh',
|
|
119
|
+
]);
|
|
120
|
+
repos.push(repo);
|
|
121
|
+
|
|
122
|
+
const files = allModeFiles(repo);
|
|
123
|
+
|
|
124
|
+
for (const f of ['a.ts', 'b.tsx', 'c.js', 'd.py', 'e.go', 'f.java', 'g.kt', 'h.rs', 'i.sh']) {
|
|
125
|
+
expect(files, `${f} must be a source file`).toContain(f);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('AC-608-02: an empty file list explains that the DIFF is empty, not the project', () => {
|
|
130
|
+
// The exact wording defect: "no source files to check" describes an empty
|
|
131
|
+
// project. The honest message names the real cause and the action that fixes
|
|
132
|
+
// it.
|
|
133
|
+
const msg = gateRunner.describeEmptyChangedFiles('diff', 0);
|
|
134
|
+
|
|
135
|
+
expect(msg).toContain('no changed files');
|
|
136
|
+
expect(msg).toContain('--all');
|
|
137
|
+
// It must NOT claim the project has no sources.
|
|
138
|
+
expect(msg.toLowerCase()).not.toContain('no source files');
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('AC-608-02: under --all an empty list says the tree has no sources, not the diff', () => {
|
|
142
|
+
const msg = gateRunner.describeEmptyChangedFiles('all', 0);
|
|
143
|
+
|
|
144
|
+
expect(msg).toContain('tracked');
|
|
145
|
+
// `--all` already scanned everything, so pointing at `--all` again is useless.
|
|
146
|
+
expect(msg).not.toContain('run with `--all`');
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('AC-608-03: the resolved mode is reported so the scan scope is visible', () => {
|
|
150
|
+
// #589 point 1's second half: the user must be able to see which scope was
|
|
151
|
+
// used. A silent switch between diff and tree is the same defect one level up.
|
|
152
|
+
const fileCount = 3;
|
|
153
|
+
const allScope = gateRunner.describeChangedFilesScope('all', fileCount);
|
|
154
|
+
expect(allScope).toContain(String(fileCount));
|
|
155
|
+
expect(allScope.toLowerCase()).toContain('all');
|
|
156
|
+
|
|
157
|
+
const diffScope = gateRunner.describeChangedFilesScope('diff', 1);
|
|
158
|
+
expect(diffScope).toContain('1');
|
|
159
|
+
expect(diffScope).not.toContain('all');
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('AC-608-03: checkAllGates actually passes the all scope through to every gate', () => {
|
|
163
|
+
// The wire, not just the resolver. Every piece above can be correct while the
|
|
164
|
+
// scope never reaches the gates — which is exactly how the first attempt at
|
|
165
|
+
// this fix failed (an edit to check.js was reverted by a snapshot restore, and
|
|
166
|
+
// the resolver tests stayed green because none of them exercised the call).
|
|
167
|
+
const { checkAllGates } = require('../check.js');
|
|
168
|
+
const seen = [];
|
|
169
|
+
const saved = new Map();
|
|
170
|
+
for (const id of Object.keys(gateRunner.GATE_REGISTRY)) {
|
|
171
|
+
const gate = gateRunner.GATE_REGISTRY[id];
|
|
172
|
+
if (!gate.run) continue;
|
|
173
|
+
saved.set(id, gate.run);
|
|
174
|
+
gate.run = (_target, opts) => {
|
|
175
|
+
seen.push({ id, scope: opts && opts.scope });
|
|
176
|
+
return 0;
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
const log = console.log;
|
|
180
|
+
console.log = () => {};
|
|
181
|
+
return checkAllGates(undefined).then(() => {
|
|
182
|
+
console.log = log;
|
|
183
|
+
for (const [id, fn] of saved) gateRunner.GATE_REGISTRY[id].run = fn;
|
|
184
|
+
|
|
185
|
+
expect(seen.length).toBeGreaterThan(0);
|
|
186
|
+
const missing = seen.filter((s) => s.scope !== 'all').map((s) => s.id);
|
|
187
|
+
expect(missing, `these gates did not receive scope=all: ${missing.join(', ')}`).toEqual([]);
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
});
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @test REQ-616
|
|
3
|
+
* @intent Gate 6 reported a **vacuous pass** on a Java project:
|
|
4
|
+
*
|
|
5
|
+
* architecture_grade: { density: "0.00", level: "Excellent", score: "10.0" }
|
|
6
|
+
* files_analyzed: 1
|
|
7
|
+
* total_smells: 0
|
|
8
|
+
*
|
|
9
|
+
* The project had 140 `.java` files. archlint's detectors are built on an
|
|
10
|
+
* import graph over a set of JS/TS extensions; `.java` is not among them, so it
|
|
11
|
+
* analysed 1 file (probably a root `pom.xml`), found no smells in it, and the
|
|
12
|
+
* grade arithmetic turned "no smells" into "10.0 / Excellent".
|
|
13
|
+
*
|
|
14
|
+
* Every architecture problem Gate 6 exists to catch — cycles, god modules, dead
|
|
15
|
+
* code, shotgun surgery — is undetectable on Java, and the report said the
|
|
16
|
+
* architecture was excellent.
|
|
17
|
+
*
|
|
18
|
+
* This is the same shape as #589 (Gate 3 permanently SKIP) and #594 (NaN
|
|
19
|
+
* coverage as PASS): **"did not look" and "looked and found nothing" must be
|
|
20
|
+
* distinguishable**, and the tool's own confidence must not be copied through
|
|
21
|
+
* when the tool never looked.
|
|
22
|
+
*
|
|
23
|
+
* `architecture.yaml` is also never consumed — that part is out of scope here (it
|
|
24
|
+
* would be a new feature) and is documented as a gap instead of being silently
|
|
25
|
+
* repaired.
|
|
26
|
+
* @covers AC-616-01, AC-616-02, AC-616-03
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
const { __internals: arch } = require('../arch.js');
|
|
30
|
+
|
|
31
|
+
// The reporter's project: 140 .java files, of which archlint analysed exactly one.
|
|
32
|
+
const PROJECT_FILE_COUNT = 140;
|
|
33
|
+
|
|
34
|
+
// A scan that genuinely covered its project.
|
|
35
|
+
const COVERED_FILE_COUNT = 42;
|
|
36
|
+
|
|
37
|
+
// A scan that covered its project and found one high-severity cycle.
|
|
38
|
+
const CYCLE_FILE_COUNT = 30;
|
|
39
|
+
|
|
40
|
+
describe('REQ-616: a scan that analysed nothing must not report 10.0/Excellent', () => {
|
|
41
|
+
/**
|
|
42
|
+
* A scan payload shaped like archlint's: a grade on a tree it barely looked at.
|
|
43
|
+
* `files_analyzed: 1` against a project with 140 `.java` files and no JS/TS.
|
|
44
|
+
*/
|
|
45
|
+
function vacuousScanPayload() {
|
|
46
|
+
return JSON.stringify({
|
|
47
|
+
smells: [],
|
|
48
|
+
summary: {
|
|
49
|
+
files_analyzed: 1,
|
|
50
|
+
total_smells: 0,
|
|
51
|
+
architecture_grade: { density: '0.00', level: 'Excellent', score: '10.0' },
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
it('AC-616-01: a grade is withheld when the scan covered almost none of the project', () => {
|
|
57
|
+
// The context carries the project's real source-file count: without it there is
|
|
58
|
+
// nothing to compare against, and the case below pins that silence.
|
|
59
|
+
const verdict = arch.verdictFromScanJson(vacuousScanPayload(), { projectFileCount: PROJECT_FILE_COUNT });
|
|
60
|
+
// The parse itself succeeds -- the JSON is valid. What must not survive is the
|
|
61
|
+
// claim that the architecture is excellent.
|
|
62
|
+
expect(verdict.parseError).toBeNull();
|
|
63
|
+
expect(verdict.coverageGap, 'the coverage gap was not detected').toBeTruthy();
|
|
64
|
+
expect(verdict.summary, 'the false grade was copied through').toBeNull();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('AC-616-01: with no project size known, the grade is left alone rather than guessed at', () => {
|
|
68
|
+
// The cheap wrong fix is to withhold every grade whose files_analyzed looks small.
|
|
69
|
+
// That would silently change what the tool reports for every caller that has not
|
|
70
|
+
// supplied a count -- and a gate that stops reporting results is worse than one
|
|
71
|
+
// that over-reports.
|
|
72
|
+
const verdict = arch.verdictFromScanJson(vacuousScanPayload());
|
|
73
|
+
expect(verdict.coverageGap).toBeFalsy();
|
|
74
|
+
expect(verdict.summary).toBeTruthy();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('AC-616-01: a grade IS reported when the scan covered the project', () => {
|
|
78
|
+
// The control. Withholding every grade would make Gate 6 unable to report a real
|
|
79
|
+
// result, which is a worse failure than the bug: a gate that never has an opinion
|
|
80
|
+
// is a gate people delete.
|
|
81
|
+
const covered = JSON.stringify({
|
|
82
|
+
smells: [],
|
|
83
|
+
summary: {
|
|
84
|
+
files_analyzed: COVERED_FILE_COUNT,
|
|
85
|
+
total_smells: 0,
|
|
86
|
+
architecture_grade: { density: '0.00', level: 'Excellent', score: '10.0' },
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
const verdict = arch.verdictFromScanJson(covered, { projectFileCount: COVERED_FILE_COUNT });
|
|
90
|
+
expect(verdict.coverageGap).toBeFalsy();
|
|
91
|
+
expect(verdict.summary).toBeTruthy();
|
|
92
|
+
expect(verdict.summary.architecture_grade.score).toBe('10.0');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('AC-616-02: the gap says which language was not analysed, not just that something is wrong', () => {
|
|
96
|
+
// #590's request 2: list the unsupported languages so the score change is
|
|
97
|
+
// traceable. "Something is off" is what the operator can already see.
|
|
98
|
+
const verdict = arch.verdictFromScanJson(vacuousScanPayload(), {
|
|
99
|
+
projectFileCount: PROJECT_FILE_COUNT,
|
|
100
|
+
dominantLanguage: 'java',
|
|
101
|
+
});
|
|
102
|
+
expect(verdict.coverageGap).toBeTruthy();
|
|
103
|
+
expect(String(verdict.coverageGap)).toContain('java');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('AC-616-02: the withheld grade is explained, and the explanation names the numbers', () => {
|
|
107
|
+
const verdict = arch.verdictFromScanJson(vacuousScanPayload(), {
|
|
108
|
+
projectFileCount: PROJECT_FILE_COUNT,
|
|
109
|
+
dominantLanguage: 'java',
|
|
110
|
+
});
|
|
111
|
+
const text = String(verdict.coverageGap);
|
|
112
|
+
// The operator needs to see 1 versus 140 to believe it, not just "low coverage".
|
|
113
|
+
expect(text).toMatch(/1/);
|
|
114
|
+
expect(text).toContain(String(PROJECT_FILE_COUNT));
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('AC-616-03: a genuine grade on a JS/TS project is untouched', () => {
|
|
118
|
+
// archlint works on the languages it has detectors for; nothing here may change
|
|
119
|
+
// what it reports there. Verified by the shape the existing suite already covers.
|
|
120
|
+
const ts = JSON.stringify({
|
|
121
|
+
smells: [{ type: 'cycle', severity: 'high', files: ['a.ts', 'b.ts'] }],
|
|
122
|
+
summary: {
|
|
123
|
+
files_analyzed: CYCLE_FILE_COUNT,
|
|
124
|
+
total_smells: 1,
|
|
125
|
+
architecture_grade: { density: '0.03', level: 'Good', score: '7.5' },
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
const verdict = arch.verdictFromScanJson(ts, { projectFileCount: CYCLE_FILE_COUNT, dominantLanguage: 'typescript' });
|
|
129
|
+
expect(verdict.coverageGap).toBeFalsy();
|
|
130
|
+
expect(verdict.blocking).toBe(1);
|
|
131
|
+
expect(verdict.summary.architecture_grade.level).toBe('Good');
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('AC-616-03: an unparseable report still fails closed rather than becoming a grade question', () => {
|
|
135
|
+
const verdict = arch.verdictFromScanJson('not json at all');
|
|
136
|
+
expect(verdict.parseError).toBeTruthy();
|
|
137
|
+
});
|
|
138
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @test REQ-2 xp-gate doctor
|
|
3
|
+
* @intent Prove that the path constants follow process.env.HOME even when
|
|
4
|
+
* another module loaded shared-paths.js first (the doctor.test.js bug:
|
|
5
|
+
* detect-deps.js is loaded in beforeAll, before any HOME is set, which
|
|
6
|
+
* pinned CONFIG_FILE to the REAL home for the rest of the suite).
|
|
7
|
+
* @covers AC-05, AC-08, AC-10
|
|
8
|
+
*/
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const os = require('os');
|
|
12
|
+
|
|
13
|
+
describe('shared-paths resolves the home directory lazily', () => {
|
|
14
|
+
let originalHome;
|
|
15
|
+
let tmpA;
|
|
16
|
+
let tmpB;
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
originalHome = process.env.HOME;
|
|
20
|
+
tmpA = fs.mkdtempSync(path.join(os.tmpdir(), 'sp-a-'));
|
|
21
|
+
tmpB = fs.mkdtempSync(path.join(os.tmpdir(), 'sp-b-'));
|
|
22
|
+
vi.resetModules();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
process.env.HOME = originalHome;
|
|
27
|
+
for (const d of [tmpA, tmpB]) {
|
|
28
|
+
if (d && fs.existsSync(d)) fs.rmSync(d, { recursive: true, force: true });
|
|
29
|
+
}
|
|
30
|
+
vi.resetModules();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
function freshSharedPaths() {
|
|
34
|
+
const p = require.resolve('../shared-paths');
|
|
35
|
+
delete require.cache[p];
|
|
36
|
+
return require('../shared-paths');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
it('CONFIG_FILE follows a HOME change made after the module was first loaded', () => {
|
|
40
|
+
process.env.HOME = tmpA;
|
|
41
|
+
const first = freshSharedPaths();
|
|
42
|
+
expect(first.CONFIG_FILE).toBe(path.join(tmpA, '.config', 'xp-gate', 'xp-gate.json'));
|
|
43
|
+
|
|
44
|
+
// A second "test" changes HOME — the same module object must now reflect it.
|
|
45
|
+
process.env.HOME = tmpB;
|
|
46
|
+
expect(first.CONFIG_FILE).toBe(
|
|
47
|
+
path.join(tmpB, '.config', 'xp-gate', 'xp-gate.json'),
|
|
48
|
+
'CONFIG_FILE still points at the HOME value captured at require() time'
|
|
49
|
+
);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('HOME_DIR follows a HOME change made after the module was first loaded', () => {
|
|
53
|
+
process.env.HOME = tmpA;
|
|
54
|
+
const sp = freshSharedPaths();
|
|
55
|
+
expect(sp.HOME_DIR).toBe(tmpA);
|
|
56
|
+
process.env.HOME = tmpB;
|
|
57
|
+
expect(sp.HOME_DIR).toBe(tmpB, 'HOME_DIR is frozen at require() time');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('a fresh require after a HOME change also reflects it (regression guard)', () => {
|
|
61
|
+
process.env.HOME = tmpA;
|
|
62
|
+
freshSharedPaths();
|
|
63
|
+
process.env.HOME = tmpB;
|
|
64
|
+
const sp = freshSharedPaths();
|
|
65
|
+
expect(sp.CONFIG_FILE).toBe(path.join(tmpB, '.config', 'xp-gate', 'xp-gate.json'));
|
|
66
|
+
});
|
|
67
|
+
});
|