@boyingliu01/xp-gate 0.7.0 → 0.8.0
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/bin/xp-gate.js +4 -9
- package/lib/__tests__/audit-log.test.ts +107 -0
- package/lib/__tests__/detect-deps.test.js +212 -42
- package/lib/__tests__/doctor.test.js +1 -0
- package/lib/__tests__/gate-audit.test.ts +59 -0
- package/lib/__tests__/init.test.js +47 -1
- package/lib/__tests__/install-skill.test.js +1 -45
- package/lib/__tests__/ui-detector.test.ts +186 -26
- package/lib/__tests__/ui-review.test.ts +119 -0
- package/lib/__tests__/uninstall.test.js +1 -0
- package/lib/detect-deps.js +180 -37
- package/lib/doctor.js +7 -9
- package/lib/gate-audit.ts +26 -42
- package/lib/init.js +42 -13
- package/lib/install-skill.js +20 -48
- package/lib/rollback.js +1 -16
- package/lib/shared-paths.js +30 -0
- package/lib/shared-utils.js +25 -0
- package/lib/ui-detector.ts +20 -14
- package/lib/ui-review.ts +58 -53
- package/lib/uninstall.js +8 -9
- package/package.json +1 -1
- package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-code/skills/admin-template-guidelines/SKILL.md +904 -0
- package/plugins/claude-code/skills/delphi-review/.delphi-config.json.example +45 -0
- package/plugins/claude-code/skills/delphi-review/AGENTS.md +66 -0
- package/plugins/claude-code/skills/delphi-review/INSTALL.md +152 -0
- package/plugins/claude-code/skills/delphi-review/SKILL.md +454 -0
- package/plugins/claude-code/skills/delphi-review/opencode.json.delphi.example +56 -0
- package/plugins/claude-code/skills/delphi-review/references/code-walkthrough.md +486 -0
- package/plugins/claude-code/skills/improve-codebase-architecture/SKILL.md +156 -0
- package/plugins/claude-code/skills/ralph-loop/SKILL.md +501 -0
- package/plugins/claude-code/skills/ralph-loop/references/components/memory.md +55 -0
- package/plugins/claude-code/skills/ralph-loop/references/components/middleware.md +54 -0
- package/plugins/claude-code/skills/ralph-loop/references/components/skill-invocations.md +39 -0
- package/plugins/claude-code/skills/ralph-loop/references/components/system-prompt.md +24 -0
- package/plugins/claude-code/skills/ralph-loop/references/components/tool-descriptions.md +32 -0
- package/plugins/claude-code/skills/ralph-loop/references/phase-2-build-ralph.md +89 -0
- package/plugins/claude-code/skills/ralph-loop/templates/progress-log.md +36 -0
- package/plugins/claude-code/skills/sprint-flow/AGENTS.md +68 -0
- package/plugins/claude-code/skills/sprint-flow/SKILL.md +1248 -0
- package/plugins/claude-code/skills/sprint-flow/references/components/memory.md +87 -0
- package/plugins/claude-code/skills/sprint-flow/references/components/middleware.md +72 -0
- package/plugins/claude-code/skills/sprint-flow/references/components/skill-invocations.md +104 -0
- package/plugins/claude-code/skills/sprint-flow/references/components/system-prompt.md +27 -0
- package/plugins/claude-code/skills/sprint-flow/references/components/tool-descriptions.md +96 -0
- package/plugins/claude-code/skills/sprint-flow/references/force-levels.md +203 -0
- package/plugins/claude-code/skills/sprint-flow/references/phase-0-think.md +115 -0
- package/plugins/claude-code/skills/sprint-flow/references/phase-1-plan.md +178 -0
- package/plugins/claude-code/skills/sprint-flow/references/phase-2-build.md +198 -0
- package/plugins/claude-code/skills/sprint-flow/references/phase-3-review.md +213 -0
- package/plugins/claude-code/skills/sprint-flow/references/phase-4-uat.md +125 -0
- package/plugins/claude-code/skills/sprint-flow/references/phase-5-feedback.md +100 -0
- package/plugins/claude-code/skills/sprint-flow/references/phase-6-ship.md +193 -0
- package/plugins/claude-code/skills/sprint-flow/references/phase-7-land.md +140 -0
- package/plugins/claude-code/skills/sprint-flow/references/phase-8-cleanup.md +192 -0
- package/plugins/claude-code/skills/sprint-flow/references/phase-minus-0-5-auto-estimate.md +257 -0
- package/plugins/claude-code/skills/sprint-flow/templates/auto-estimate-learning-log.md +136 -0
- package/plugins/claude-code/skills/sprint-flow/templates/auto-estimate-output-template.md +132 -0
- package/plugins/claude-code/skills/sprint-flow/templates/emergent-issues-template.md +120 -0
- package/plugins/claude-code/skills/sprint-flow/templates/pain-document-template.md +115 -0
- package/plugins/claude-code/skills/sprint-flow/templates/sprint-progress-template.md +151 -0
- package/plugins/claude-code/skills/sprint-flow/templates/sprint-summary-template.md +120 -0
- package/plugins/claude-code/skills/test-driven-development/SKILL.md +71 -0
- package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +56 -0
- package/plugins/claude-code/skills/test-specification-alignment/SKILL.md +702 -0
- package/plugins/claude-code/skills/test-specification-alignment/references/alignment-verification-algorithm.md +493 -0
- package/plugins/claude-code/skills/test-specification-alignment/references/phase2-constraint-enforcement.md +431 -0
- package/plugins/claude-code/skills/test-specification-alignment/references/specification-format.md +348 -0
- package/plugins/claude-code/skills/to-issues/SKILL.md +277 -0
- package/plugins/opencode/README.md +38 -0
- package/plugins/opencode/index.ts +85 -0
- package/plugins/opencode/package.json +18 -0
- package/plugins/opencode/skills/admin-template-guidelines/SKILL.md +904 -0
- package/plugins/opencode/skills/delphi-review/.delphi-config.json.example +45 -0
- package/plugins/opencode/skills/delphi-review/AGENTS.md +66 -0
- package/plugins/opencode/skills/delphi-review/INSTALL.md +152 -0
- package/plugins/opencode/skills/delphi-review/SKILL.md +454 -0
- package/plugins/opencode/skills/delphi-review/opencode.json.delphi.example +56 -0
- package/plugins/opencode/skills/delphi-review/references/code-walkthrough.md +486 -0
- package/plugins/opencode/skills/improve-codebase-architecture/SKILL.md +156 -0
- package/plugins/opencode/skills/ralph-loop/SKILL.md +501 -0
- package/plugins/opencode/skills/ralph-loop/references/components/memory.md +55 -0
- package/plugins/opencode/skills/ralph-loop/references/components/middleware.md +54 -0
- package/plugins/opencode/skills/ralph-loop/references/components/skill-invocations.md +39 -0
- package/plugins/opencode/skills/ralph-loop/references/components/system-prompt.md +24 -0
- package/plugins/opencode/skills/ralph-loop/references/components/tool-descriptions.md +32 -0
- package/plugins/opencode/skills/ralph-loop/references/phase-2-build-ralph.md +89 -0
- package/plugins/opencode/skills/ralph-loop/templates/progress-log.md +36 -0
- package/plugins/opencode/skills/sprint-flow/AGENTS.md +68 -0
- package/plugins/opencode/skills/sprint-flow/SKILL.md +1248 -0
- package/plugins/opencode/skills/sprint-flow/references/components/memory.md +87 -0
- package/plugins/opencode/skills/sprint-flow/references/components/middleware.md +72 -0
- package/plugins/opencode/skills/sprint-flow/references/components/skill-invocations.md +104 -0
- package/plugins/opencode/skills/sprint-flow/references/components/system-prompt.md +27 -0
- package/plugins/opencode/skills/sprint-flow/references/components/tool-descriptions.md +96 -0
- package/plugins/opencode/skills/sprint-flow/references/force-levels.md +203 -0
- package/plugins/opencode/skills/sprint-flow/references/phase-0-think.md +115 -0
- package/plugins/opencode/skills/sprint-flow/references/phase-1-plan.md +178 -0
- package/plugins/opencode/skills/sprint-flow/references/phase-2-build.md +198 -0
- package/plugins/opencode/skills/sprint-flow/references/phase-3-review.md +213 -0
- package/plugins/opencode/skills/sprint-flow/references/phase-4-uat.md +125 -0
- package/plugins/opencode/skills/sprint-flow/references/phase-5-feedback.md +100 -0
- package/plugins/opencode/skills/sprint-flow/references/phase-6-ship.md +193 -0
- package/plugins/opencode/skills/sprint-flow/references/phase-7-land.md +140 -0
- package/plugins/opencode/skills/sprint-flow/references/phase-8-cleanup.md +192 -0
- package/plugins/opencode/skills/sprint-flow/references/phase-minus-0-5-auto-estimate.md +257 -0
- package/plugins/opencode/skills/sprint-flow/templates/auto-estimate-learning-log.md +136 -0
- package/plugins/opencode/skills/sprint-flow/templates/auto-estimate-output-template.md +132 -0
- package/plugins/opencode/skills/sprint-flow/templates/emergent-issues-template.md +120 -0
- package/plugins/opencode/skills/sprint-flow/templates/pain-document-template.md +115 -0
- package/plugins/opencode/skills/sprint-flow/templates/sprint-progress-template.md +151 -0
- package/plugins/opencode/skills/sprint-flow/templates/sprint-summary-template.md +120 -0
- package/plugins/opencode/skills/test-driven-development/SKILL.md +71 -0
- package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +56 -0
- package/plugins/opencode/skills/test-specification-alignment/SKILL.md +702 -0
- package/plugins/opencode/skills/test-specification-alignment/references/alignment-verification-algorithm.md +493 -0
- package/plugins/opencode/skills/test-specification-alignment/references/phase2-constraint-enforcement.md +431 -0
- package/plugins/opencode/skills/test-specification-alignment/references/specification-format.md +348 -0
- package/plugins/opencode/skills/to-issues/SKILL.md +277 -0
- package/plugins/opencode/tsconfig.json +15 -0
- package/plugins/qoder/skills/admin-template-guidelines/SKILL.md +904 -0
- package/plugins/qoder/skills/delphi-review/.delphi-config.json.example +45 -0
- package/plugins/qoder/skills/delphi-review/AGENTS.md +66 -0
- package/plugins/qoder/skills/delphi-review/INSTALL.md +152 -0
- package/plugins/qoder/skills/delphi-review/SKILL.md +454 -0
- package/plugins/qoder/skills/delphi-review/opencode.json.delphi.example +56 -0
- package/plugins/qoder/skills/delphi-review/references/code-walkthrough.md +486 -0
- package/plugins/qoder/skills/improve-codebase-architecture/SKILL.md +156 -0
- package/plugins/qoder/skills/ralph-loop/SKILL.md +501 -0
- package/plugins/qoder/skills/ralph-loop/references/components/memory.md +55 -0
- package/plugins/qoder/skills/ralph-loop/references/components/middleware.md +54 -0
- package/plugins/qoder/skills/ralph-loop/references/components/skill-invocations.md +39 -0
- package/plugins/qoder/skills/ralph-loop/references/components/system-prompt.md +24 -0
- package/plugins/qoder/skills/ralph-loop/references/components/tool-descriptions.md +32 -0
- package/plugins/qoder/skills/ralph-loop/references/phase-2-build-ralph.md +89 -0
- package/plugins/qoder/skills/ralph-loop/templates/progress-log.md +36 -0
- package/plugins/qoder/skills/sprint-flow/AGENTS.md +68 -0
- package/plugins/qoder/skills/sprint-flow/SKILL.md +1248 -0
- package/plugins/qoder/skills/sprint-flow/references/components/memory.md +87 -0
- package/plugins/qoder/skills/sprint-flow/references/components/middleware.md +72 -0
- package/plugins/qoder/skills/sprint-flow/references/components/skill-invocations.md +104 -0
- package/plugins/qoder/skills/sprint-flow/references/components/system-prompt.md +27 -0
- package/plugins/qoder/skills/sprint-flow/references/components/tool-descriptions.md +96 -0
- package/plugins/qoder/skills/sprint-flow/references/force-levels.md +203 -0
- package/plugins/qoder/skills/sprint-flow/references/phase-0-think.md +115 -0
- package/plugins/qoder/skills/sprint-flow/references/phase-1-plan.md +178 -0
- package/plugins/qoder/skills/sprint-flow/references/phase-2-build.md +198 -0
- package/plugins/qoder/skills/sprint-flow/references/phase-3-review.md +213 -0
- package/plugins/qoder/skills/sprint-flow/references/phase-4-uat.md +125 -0
- package/plugins/qoder/skills/sprint-flow/references/phase-5-feedback.md +100 -0
- package/plugins/qoder/skills/sprint-flow/references/phase-6-ship.md +193 -0
- package/plugins/qoder/skills/sprint-flow/references/phase-7-land.md +140 -0
- package/plugins/qoder/skills/sprint-flow/references/phase-8-cleanup.md +192 -0
- package/plugins/qoder/skills/sprint-flow/references/phase-minus-0-5-auto-estimate.md +257 -0
- package/plugins/qoder/skills/sprint-flow/templates/auto-estimate-learning-log.md +136 -0
- package/plugins/qoder/skills/sprint-flow/templates/auto-estimate-output-template.md +132 -0
- package/plugins/qoder/skills/sprint-flow/templates/emergent-issues-template.md +120 -0
- package/plugins/qoder/skills/sprint-flow/templates/pain-document-template.md +115 -0
- package/plugins/qoder/skills/sprint-flow/templates/sprint-progress-template.md +151 -0
- package/plugins/qoder/skills/sprint-flow/templates/sprint-summary-template.md +120 -0
- package/plugins/qoder/skills/test-driven-development/SKILL.md +71 -0
- package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +56 -0
- package/plugins/qoder/skills/test-specification-alignment/SKILL.md +702 -0
- package/plugins/qoder/skills/test-specification-alignment/references/alignment-verification-algorithm.md +493 -0
- package/plugins/qoder/skills/test-specification-alignment/references/phase2-constraint-enforcement.md +431 -0
- package/plugins/qoder/skills/test-specification-alignment/references/specification-format.md +348 -0
- package/plugins/qoder/skills/to-issues/SKILL.md +277 -0
- package/skills/admin-template-guidelines/SKILL.md +904 -0
- package/skills/delphi-review/SKILL.md +112 -77
- package/skills/improve-codebase-architecture/SKILL.md +156 -0
- package/skills/ralph-loop/SKILL.md +165 -34
- package/skills/sprint-flow/SKILL.md +448 -107
- package/skills/sprint-flow/references/components/skill-invocations.md +1 -1
- package/skills/sprint-flow/references/components/tool-descriptions.md +1 -1
- package/skills/sprint-flow/references/force-levels.md +203 -0
- package/skills/sprint-flow/references/phase-1-plan.md +4 -4
- package/skills/sprint-flow/references/phase-minus-0-5-auto-estimate.md +20 -1
- package/skills/sprint-flow/templates/auto-estimate-output-template.md +7 -5
- package/skills/sprint-flow/templates/sprint-progress-template.md +151 -0
- package/skills/test-driven-development/SKILL.md +71 -0
- package/skills/test-specification-alignment/SKILL.md +98 -24
- package/skills/to-issues/SKILL.md +277 -0
- package/plugins/qoder/AGENTS.md +0 -93
- package/plugins/qoder/README.md +0 -87
- package/plugins/qoder/widgets/quality-report.html +0 -163
- package/plugins/qoder/widgets/sprint-dashboard.html +0 -172
- package/skills/delphi-review/evals/evals.json +0 -82
- package/skills/delphi-review/references/qoder-multi-model.md +0 -191
- package/skills/ralph-loop/evals/evals.json +0 -311
- package/skills/ralph-loop/evolution-history.json +0 -59
- package/skills/ralph-loop/evolution-log.md +0 -16
- package/skills/sprint-flow/evals/evals.json +0 -130
- package/skills/sprint-flow/evolution-history.json +0 -39
- package/skills/sprint-flow/evolution-log.md +0 -23
- package/skills/sprint-flow/references/qoder-adaptation.md +0 -173
- package/skills/test-specification-alignment/evals/evals.json +0 -75
|
@@ -69,6 +69,7 @@ describe('install-skill', () => {
|
|
|
69
69
|
vi.resetModules();
|
|
70
70
|
delete require.cache[require.resolve('../install-skill')];
|
|
71
71
|
delete require.cache[require.resolve('../detect-deps')];
|
|
72
|
+
delete require.cache[require.resolve('../shared-paths')];
|
|
72
73
|
delete require.cache[require.resolve('../download-skill')];
|
|
73
74
|
delete require.cache[require.resolve('../rollback')];
|
|
74
75
|
vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
@@ -323,49 +324,4 @@ describe('install-skill', () => {
|
|
|
323
324
|
expect(console.warn).not.toHaveBeenCalled();
|
|
324
325
|
expect(console.error).toHaveBeenCalledWith('Error: Failed to download test-spec');
|
|
325
326
|
});
|
|
326
|
-
|
|
327
|
-
// --- Qoder platform tests ---
|
|
328
|
-
|
|
329
|
-
function qoderSkillsDir() {
|
|
330
|
-
return path.join(tmpHome, '.qoder', 'skills');
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
it('qoder platform skips dep check (installs without superpowers/gstack)', async () => {
|
|
334
|
-
// Do NOT call setupValidDeps() — no superpowers/gstack present
|
|
335
|
-
mockHttpsGet({ statusCode: 200, body: '# Sprint Flow Qoder' });
|
|
336
|
-
|
|
337
|
-
const { installSkill } = require('../install-skill');
|
|
338
|
-
const result = await installSkill('sprint-flow', { platform: 'qoder' });
|
|
339
|
-
|
|
340
|
-
expect(result).toBe(0);
|
|
341
|
-
expect(console.log).toHaveBeenCalledWith('✓ sprint-flow installed');
|
|
342
|
-
});
|
|
343
|
-
|
|
344
|
-
it('qoder platform installs to ~/.qoder/skills/ instead of ~/.config/opencode/skills/', async () => {
|
|
345
|
-
mockHttpsGet({ statusCode: 200, body: '# Qoder Skill' });
|
|
346
|
-
|
|
347
|
-
const { installSkill } = require('../install-skill');
|
|
348
|
-
const result = await installSkill('sprint-flow', { platform: 'qoder' });
|
|
349
|
-
|
|
350
|
-
expect(result).toBe(0);
|
|
351
|
-
// Verify installed in Qoder directory
|
|
352
|
-
const qoderFile = path.join(qoderSkillsDir(), 'sprint-flow', 'SKILL.md');
|
|
353
|
-
expect(fs.existsSync(qoderFile)).toBe(true);
|
|
354
|
-
expect(fs.readFileSync(qoderFile, 'utf8')).toContain('Qoder Skill');
|
|
355
|
-
|
|
356
|
-
// Verify NOT installed in opencode directory
|
|
357
|
-
const opencodeFile = path.join(skillsDir(), 'sprint-flow', 'SKILL.md');
|
|
358
|
-
expect(fs.existsSync(opencodeFile)).toBe(false);
|
|
359
|
-
});
|
|
360
|
-
|
|
361
|
-
it('default platform still requires deps and installs to opencode dir', async () => {
|
|
362
|
-
// Without setupValidDeps, default platform should fail on dep check
|
|
363
|
-
const { installSkill } = require('../install-skill');
|
|
364
|
-
const result = await installSkill('sprint-flow');
|
|
365
|
-
|
|
366
|
-
expect(result).toBe(1);
|
|
367
|
-
expect(console.error).toHaveBeenCalledWith(
|
|
368
|
-
expect.stringContaining('superpowers is required')
|
|
369
|
-
);
|
|
370
|
-
});
|
|
371
327
|
});
|
|
@@ -1,15 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @test ui-detector
|
|
3
3
|
* @intent Verify detectUiSprint correctly identifies UI file changes
|
|
4
|
+
* @covers Issue #155 — getChangedFiles must use spawnSync (array args) to prevent
|
|
5
|
+
* shell-meta injection via baseBranch parameter
|
|
4
6
|
*/
|
|
5
7
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
6
|
-
import {
|
|
8
|
+
import { spawnSync } from 'child_process';
|
|
7
9
|
|
|
8
10
|
vi.mock('child_process', () => ({
|
|
9
|
-
|
|
11
|
+
spawnSync: vi.fn(),
|
|
12
|
+
// Keep execSync as a vi.fn so any unintentional callers fail loudly
|
|
13
|
+
execSync: vi.fn(() => {
|
|
14
|
+
throw new Error('execSync must not be used: command injection risk (Issue #155)');
|
|
15
|
+
}),
|
|
10
16
|
}));
|
|
11
17
|
|
|
12
|
-
const
|
|
18
|
+
const mockSpawnSync = vi.mocked(spawnSync);
|
|
19
|
+
|
|
20
|
+
function mockSpawnReturn(stdout: string) {
|
|
21
|
+
mockSpawnSync.mockReturnValue({
|
|
22
|
+
pid: 1,
|
|
23
|
+
stdout: Buffer.from(stdout),
|
|
24
|
+
stderr: Buffer.from(''),
|
|
25
|
+
status: 0,
|
|
26
|
+
signal: null,
|
|
27
|
+
output: [null, stdout, ''],
|
|
28
|
+
} as unknown as ReturnType<typeof spawnSync>);
|
|
29
|
+
}
|
|
13
30
|
|
|
14
31
|
describe('ui-detector', () => {
|
|
15
32
|
beforeEach(() => {
|
|
@@ -18,7 +35,7 @@ describe('ui-detector', () => {
|
|
|
18
35
|
|
|
19
36
|
describe('detectUiSprint', () => {
|
|
20
37
|
it('should return false for empty diff', async () => {
|
|
21
|
-
|
|
38
|
+
mockSpawnReturn('');
|
|
22
39
|
const { detectUiSprint } = await import('../ui-detector');
|
|
23
40
|
const result = detectUiSprint();
|
|
24
41
|
expect(result.isUiSprint).toBe(false);
|
|
@@ -27,14 +44,14 @@ describe('ui-detector', () => {
|
|
|
27
44
|
});
|
|
28
45
|
|
|
29
46
|
it('should return false for pure backend changes', async () => {
|
|
30
|
-
|
|
47
|
+
mockSpawnReturn('src/auth.ts\nsrc/db.ts\n');
|
|
31
48
|
const { detectUiSprint } = await import('../ui-detector');
|
|
32
49
|
const result = detectUiSprint('main');
|
|
33
50
|
expect(result.isUiSprint).toBe(false);
|
|
34
51
|
});
|
|
35
52
|
|
|
36
53
|
it('should return true for template files in view directories', async () => {
|
|
37
|
-
|
|
54
|
+
mockSpawnReturn('views/index.njk\n');
|
|
38
55
|
const { detectUiSprint } = await import('../ui-detector');
|
|
39
56
|
const result = detectUiSprint();
|
|
40
57
|
expect(result.isUiSprint).toBe(true);
|
|
@@ -43,7 +60,7 @@ describe('ui-detector', () => {
|
|
|
43
60
|
});
|
|
44
61
|
|
|
45
62
|
it('should return true for component files in view directories', async () => {
|
|
46
|
-
|
|
63
|
+
mockSpawnReturn('src/components/Button.tsx\n');
|
|
47
64
|
const { detectUiSprint } = await import('../ui-detector');
|
|
48
65
|
const result = detectUiSprint();
|
|
49
66
|
expect(result.isUiSprint).toBe(true);
|
|
@@ -51,43 +68,36 @@ describe('ui-detector', () => {
|
|
|
51
68
|
});
|
|
52
69
|
|
|
53
70
|
it('should return false for component files NOT in view directories', async () => {
|
|
54
|
-
|
|
71
|
+
mockSpawnReturn('src/hooks/useAuth.tsx\n');
|
|
55
72
|
const { detectUiSprint } = await import('../ui-detector');
|
|
56
73
|
const result = detectUiSprint();
|
|
57
74
|
expect(result.isUiSprint).toBe(false);
|
|
58
75
|
});
|
|
59
76
|
|
|
60
77
|
it('should return true for style files in view directories', async () => {
|
|
61
|
-
|
|
78
|
+
mockSpawnReturn('views/styles/main.css\n');
|
|
62
79
|
const { detectUiSprint } = await import('../ui-detector');
|
|
63
80
|
const result = detectUiSprint();
|
|
64
81
|
expect(result.isUiSprint).toBe(true);
|
|
65
82
|
});
|
|
66
83
|
|
|
67
84
|
it('should return false for style files NOT in view directories', async () => {
|
|
68
|
-
|
|
85
|
+
mockSpawnReturn('src/index.css\n');
|
|
69
86
|
const { detectUiSprint } = await import('../ui-detector');
|
|
70
87
|
const result = detectUiSprint();
|
|
71
88
|
expect(result.isUiSprint).toBe(false);
|
|
72
89
|
});
|
|
73
90
|
|
|
74
91
|
it('should return true for mixed changes (backend + UI)', async () => {
|
|
75
|
-
|
|
92
|
+
mockSpawnReturn('src/auth.ts\nviews/login.html\n');
|
|
76
93
|
const { detectUiSprint } = await import('../ui-detector');
|
|
77
94
|
const result = detectUiSprint();
|
|
78
95
|
expect(result.isUiSprint).toBe(true);
|
|
79
96
|
expect(result.matchedFiles).toContain('views/login.html');
|
|
80
97
|
});
|
|
81
98
|
|
|
82
|
-
it('should return true for
|
|
83
|
-
|
|
84
|
-
const { detectUiSprint } = await import('../ui-detector');
|
|
85
|
-
const result = detectUiSprint();
|
|
86
|
-
expect(result.isUiSprint).toBe(true);
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it('should handle renamed files correctly', async () => {
|
|
90
|
-
mockExecSync.mockReturnValue('views/a.html → views/b.html\n');
|
|
99
|
+
it('should return true for renamed UI files', async () => {
|
|
100
|
+
mockSpawnReturn('views/a.html → views/b.html\n');
|
|
91
101
|
const { detectUiSprint } = await import('../ui-detector');
|
|
92
102
|
const result = detectUiSprint();
|
|
93
103
|
expect(result.isUiSprint).toBe(true);
|
|
@@ -95,24 +105,25 @@ describe('ui-detector', () => {
|
|
|
95
105
|
});
|
|
96
106
|
|
|
97
107
|
it('should return false for pure documentation changes', async () => {
|
|
98
|
-
|
|
108
|
+
mockSpawnReturn('docs/README.md\n');
|
|
99
109
|
const { detectUiSprint } = await import('../ui-detector');
|
|
100
110
|
const result = detectUiSprint();
|
|
101
111
|
expect(result.isUiSprint).toBe(false);
|
|
102
112
|
});
|
|
103
113
|
|
|
104
114
|
it('should use main as default base branch', async () => {
|
|
105
|
-
|
|
115
|
+
mockSpawnReturn('');
|
|
106
116
|
const { detectUiSprint } = await import('../ui-detector');
|
|
107
117
|
detectUiSprint();
|
|
108
|
-
expect(
|
|
109
|
-
|
|
118
|
+
expect(mockSpawnSync).toHaveBeenCalledWith(
|
|
119
|
+
'git',
|
|
120
|
+
['diff', '--name-only', 'main..HEAD'],
|
|
110
121
|
expect.any(Object)
|
|
111
122
|
);
|
|
112
123
|
});
|
|
113
124
|
|
|
114
125
|
it('should handle git command failure gracefully', async () => {
|
|
115
|
-
|
|
126
|
+
mockSpawnSync.mockImplementation(() => {
|
|
116
127
|
throw new Error('git not available');
|
|
117
128
|
});
|
|
118
129
|
const { detectUiSprint } = await import('../ui-detector');
|
|
@@ -120,8 +131,36 @@ describe('ui-detector', () => {
|
|
|
120
131
|
expect(result.isUiSprint).toBe(false);
|
|
121
132
|
});
|
|
122
133
|
|
|
134
|
+
it('should treat non-zero git exit status as failure (Issue #155)', async () => {
|
|
135
|
+
mockSpawnSync.mockReturnValue({
|
|
136
|
+
pid: 1,
|
|
137
|
+
stdout: Buffer.from(''),
|
|
138
|
+
stderr: Buffer.from('fatal: bad revision'),
|
|
139
|
+
status: 128,
|
|
140
|
+
signal: null,
|
|
141
|
+
output: [null, '', 'fatal: bad revision'],
|
|
142
|
+
} as unknown as ReturnType<typeof spawnSync>);
|
|
143
|
+
const { detectUiSprint } = await import('../ui-detector');
|
|
144
|
+
const result = detectUiSprint('main');
|
|
145
|
+
expect(result.isUiSprint).toBe(false);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('should not interpret shell metacharacters in baseBranch (Issue #155)', async () => {
|
|
149
|
+
mockSpawnReturn('');
|
|
150
|
+
const { getChangedFiles } = await import('../ui-detector');
|
|
151
|
+
const malicious = 'main; touch /tmp/xp-gate-pwned';
|
|
152
|
+
getChangedFiles(malicious);
|
|
153
|
+
expect(mockSpawnSync).toHaveBeenCalledWith(
|
|
154
|
+
'git',
|
|
155
|
+
['diff', '--name-only', `${malicious}..HEAD`],
|
|
156
|
+
expect.objectContaining({ shell: false })
|
|
157
|
+
);
|
|
158
|
+
const callArgs = mockSpawnSync.mock.calls[0];
|
|
159
|
+
expect(Array.isArray(callArgs[1])).toBe(true);
|
|
160
|
+
});
|
|
161
|
+
|
|
123
162
|
it('should handle multiple UI files across different types', async () => {
|
|
124
|
-
|
|
163
|
+
mockSpawnReturn('views/index.njk\nsrc/components/Button.tsx\nsrc/auth.ts\n');
|
|
125
164
|
const { detectUiSprint } = await import('../ui-detector');
|
|
126
165
|
const result = detectUiSprint();
|
|
127
166
|
expect(result.isUiSprint).toBe(true);
|
|
@@ -129,6 +168,29 @@ describe('ui-detector', () => {
|
|
|
129
168
|
});
|
|
130
169
|
});
|
|
131
170
|
|
|
171
|
+
describe('getChangedFiles', () => {
|
|
172
|
+
it('parses git diff output into file array', async () => {
|
|
173
|
+
mockSpawnReturn('src/a.ts\nsrc/b.ts\n');
|
|
174
|
+
const { getChangedFiles } = await import('../ui-detector');
|
|
175
|
+
const files = getChangedFiles('main');
|
|
176
|
+
expect(files).toEqual(['src/a.ts', 'src/b.ts']);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('returns empty array for empty diff', async () => {
|
|
180
|
+
mockSpawnReturn('');
|
|
181
|
+
const { getChangedFiles } = await import('../ui-detector');
|
|
182
|
+
const files = getChangedFiles('main');
|
|
183
|
+
expect(files).toEqual([]);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('filters out empty lines', async () => {
|
|
187
|
+
mockSpawnReturn('src/a.ts\n\nsrc/b.ts\n');
|
|
188
|
+
const { getChangedFiles } = await import('../ui-detector');
|
|
189
|
+
const files = getChangedFiles('main');
|
|
190
|
+
expect(files).toEqual(['src/a.ts', 'src/b.ts']);
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
132
194
|
describe('parseRenamedFile', () => {
|
|
133
195
|
it('should extract new path from renamed file', async () => {
|
|
134
196
|
const { parseRenamedFile } = await import('../ui-detector');
|
|
@@ -197,4 +259,102 @@ describe('ui-detector', () => {
|
|
|
197
259
|
expect(rules).toContain('style-.css');
|
|
198
260
|
});
|
|
199
261
|
});
|
|
262
|
+
|
|
263
|
+
describe('collectUiMatches', () => {
|
|
264
|
+
it('should detect UI files in components directory', async () => {
|
|
265
|
+
const { collectUiMatches } = await import('../ui-detector');
|
|
266
|
+
const result = collectUiMatches(['src/components/Button.tsx', 'src/utils/helper.ts']);
|
|
267
|
+
expect(result.isUiSprint).toBe(true);
|
|
268
|
+
expect(result.matchedFiles).toEqual(['src/components/Button.tsx']);
|
|
269
|
+
expect(result.matchedRules.length).toBeGreaterThan(0);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it('should exclude __tests__ directory files', async () => {
|
|
273
|
+
const { collectUiMatches } = await import('../ui-detector');
|
|
274
|
+
const result = collectUiMatches(['src/__tests__/Button.test.tsx']);
|
|
275
|
+
expect(result.isUiSprint).toBe(false);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('excludes files under src/coverage/', async () => {
|
|
279
|
+
const { collectUiMatches } = await import('../ui-detector');
|
|
280
|
+
const result = collectUiMatches(['src/coverage/components/Coverage.tsx']);
|
|
281
|
+
expect(result.isUiSprint).toBe(false);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it('should handle empty file list', async () => {
|
|
285
|
+
const { collectUiMatches } = await import('../ui-detector');
|
|
286
|
+
const result = collectUiMatches([]);
|
|
287
|
+
expect(result.isUiSprint).toBe(false);
|
|
288
|
+
expect(result.matchedFiles).toEqual([]);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it('should collect multiple UI files', async () => {
|
|
292
|
+
const { collectUiMatches } = await import('../ui-detector');
|
|
293
|
+
const result = collectUiMatches([
|
|
294
|
+
'src/components/Header.tsx',
|
|
295
|
+
'src/utils/api.ts',
|
|
296
|
+
'views/styles/app.css',
|
|
297
|
+
]);
|
|
298
|
+
expect(result.isUiSprint).toBe(true);
|
|
299
|
+
expect(result.matchedFiles).toHaveLength(2);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it('should aggregate unique rules across files', async () => {
|
|
303
|
+
const { collectUiMatches } = await import('../ui-detector');
|
|
304
|
+
const result = collectUiMatches([
|
|
305
|
+
'src/components/Button.tsx',
|
|
306
|
+
'src/components/Card.tsx',
|
|
307
|
+
]);
|
|
308
|
+
expect(result.isUiSprint).toBe(true);
|
|
309
|
+
expect(result.matchedFiles.length).toBeGreaterThan(0);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it('should respect .ui-gate-ignore patterns', async () => {
|
|
313
|
+
const { collectUiMatches } = await import('../ui-detector');
|
|
314
|
+
const withoutIgnore = collectUiMatches(['legacy/components/Old.tsx']);
|
|
315
|
+
expect(withoutIgnore.isUiSprint).toBe(true);
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
describe('isExcluded and loadUiGateIgnore', () => {
|
|
320
|
+
it('excludes paths matching **/coverage/** with leading directory', async () => {
|
|
321
|
+
const { isExcluded } = await import('../ui-detector');
|
|
322
|
+
expect(isExcluded('src/coverage/report.html', ['**/coverage/**'])).toBe(true);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it('does not match non-excluded paths', async () => {
|
|
326
|
+
const { isExcluded } = await import('../ui-detector');
|
|
327
|
+
expect(isExcluded('src/app.ts', ['**/coverage/**'])).toBe(false);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it('returns empty array when .ui-gate-ignore does not exist', async () => {
|
|
331
|
+
const { loadUiGateIgnore } = await import('../ui-detector');
|
|
332
|
+
expect(loadUiGateIgnore('/tmp')).toEqual([]);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it('excludes paths matching **/node_modules/** with leading directory', async () => {
|
|
336
|
+
const { isExcluded } = await import('../ui-detector');
|
|
337
|
+
expect(isExcluded('lib/node_modules/pkg/index.js', ['**/node_modules/**'])).toBe(true);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it('excludes paths matching **/dist/** with leading directory', async () => {
|
|
341
|
+
const { isExcluded } = await import('../ui-detector');
|
|
342
|
+
expect(isExcluded('src/dist/bundle.js', ['**/dist/**'])).toBe(true);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it('excludes paths matching **/build/** with leading directory', async () => {
|
|
346
|
+
const { isExcluded } = await import('../ui-detector');
|
|
347
|
+
expect(isExcluded('src/build/output.js', ['**/build/**'])).toBe(true);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it('excludes paths matching **/__tests__/**', async () => {
|
|
351
|
+
const { isExcluded } = await import('../ui-detector');
|
|
352
|
+
expect(isExcluded('src/__tests__/Button.test.tsx', ['**/__tests__/**'])).toBe(true);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it('excludes paths matching **/*.test.*', async () => {
|
|
356
|
+
const { isExcluded } = await import('../ui-detector');
|
|
357
|
+
expect(isExcluded('src/Button.test.tsx', ['**/*.test.*'])).toBe(true);
|
|
358
|
+
});
|
|
359
|
+
});
|
|
200
360
|
});
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @test ui-review
|
|
3
|
+
* @intent Verify UI review file generation helpers
|
|
4
|
+
* @covers UI-REVIEW-001
|
|
5
|
+
*/
|
|
6
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
7
|
+
import { execSync } from 'child_process';
|
|
8
|
+
import { existsSync, mkdirSync, readFileSync, rmSync } from 'fs';
|
|
9
|
+
import { join } from 'path';
|
|
10
|
+
|
|
11
|
+
vi.mock('child_process', () => ({
|
|
12
|
+
execSync: vi.fn(),
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
const mockExecSync = vi.mocked(execSync);
|
|
16
|
+
const TEST_DIR = join(process.cwd(), '.ui-review-test');
|
|
17
|
+
|
|
18
|
+
describe('ui-review', () => {
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
vi.clearAllMocks();
|
|
21
|
+
if (existsSync(TEST_DIR)) {
|
|
22
|
+
rmSync(TEST_DIR, { recursive: true, force: true });
|
|
23
|
+
}
|
|
24
|
+
mkdirSync(TEST_DIR, { recursive: true });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
afterEach(() => {
|
|
28
|
+
if (existsSync(TEST_DIR)) {
|
|
29
|
+
rmSync(TEST_DIR, { recursive: true, force: true });
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should parse file list and renamed paths', async () => {
|
|
34
|
+
const { parseFileList } = await import('../ui-review');
|
|
35
|
+
expect(parseFileList('views/a.html → views/b.html\n\nsrc/auth.ts\n')).toEqual([
|
|
36
|
+
'views/b.html',
|
|
37
|
+
'src/auth.ts',
|
|
38
|
+
]);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should return changed files from staged and modified diff', async () => {
|
|
42
|
+
mockExecSync.mockReturnValue('src/components/Button.tsx\nviews/index.html\n');
|
|
43
|
+
const { getChangedFilesForReview } = await import('../ui-review');
|
|
44
|
+
expect(getChangedFilesForReview()).toEqual(['src/components/Button.tsx', 'views/index.html']);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should fall back to tracked files when no changes are present', async () => {
|
|
48
|
+
mockExecSync
|
|
49
|
+
.mockReturnValueOnce('')
|
|
50
|
+
.mockReturnValueOnce('src/components/Fallback.tsx\n');
|
|
51
|
+
const { getChangedFilesForReview } = await import('../ui-review');
|
|
52
|
+
expect(getChangedFilesForReview()).toEqual(['src/components/Fallback.tsx']);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should fall back to find command when git ls-files fails', async () => {
|
|
56
|
+
mockExecSync
|
|
57
|
+
.mockReturnValueOnce('')
|
|
58
|
+
.mockImplementationOnce(() => { throw new Error('not git'); })
|
|
59
|
+
.mockReturnValueOnce('views/index.html\n');
|
|
60
|
+
const { getChangedFilesForReview } = await import('../ui-review');
|
|
61
|
+
expect(getChangedFilesForReview()).toEqual(['views/index.html']);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should build an approved review result with 24h expiry', async () => {
|
|
65
|
+
mockExecSync.mockReturnValue('abc123\n');
|
|
66
|
+
const { buildUiReviewResult } = await import('../ui-review');
|
|
67
|
+
const now = new Date('2026-06-02T00:00:00.000Z');
|
|
68
|
+
const result = buildUiReviewResult(['views/index.html'], now);
|
|
69
|
+
expect(result).toMatchObject({
|
|
70
|
+
commit: 'abc123',
|
|
71
|
+
verdict: 'APPROVED',
|
|
72
|
+
design_review: 'APPROVED',
|
|
73
|
+
browser_qa: 'APPROVED',
|
|
74
|
+
ui_changes_detected: ['views/index.html'],
|
|
75
|
+
});
|
|
76
|
+
expect(result.expires).toBe('2026-06-03T00:00:00.000Z');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should run main and exit 0 when no UI changes are detected', async () => {
|
|
80
|
+
mockExecSync.mockReturnValue('src/auth.ts\n');
|
|
81
|
+
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as never);
|
|
82
|
+
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined);
|
|
83
|
+
const { main } = await import('../ui-review');
|
|
84
|
+
main();
|
|
85
|
+
expect(exitSpy).toHaveBeenCalledWith(0);
|
|
86
|
+
expect(logSpy).toHaveBeenCalledWith('ℹ️ No UI changes detected in staged/modified files.');
|
|
87
|
+
exitSpy.mockRestore();
|
|
88
|
+
logSpy.mockRestore();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('should run main and write result when UI changes are detected', async () => {
|
|
92
|
+
const cwdSpy = vi.spyOn(process, 'cwd').mockReturnValue(TEST_DIR);
|
|
93
|
+
mockExecSync
|
|
94
|
+
.mockReturnValueOnce('views/index.html\n')
|
|
95
|
+
.mockReturnValueOnce('abc123\n');
|
|
96
|
+
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined);
|
|
97
|
+
const { main } = await import('../ui-review');
|
|
98
|
+
main();
|
|
99
|
+
expect(existsSync(join(TEST_DIR, '.ui-gate-result.json'))).toBe(true);
|
|
100
|
+
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('Generated .ui-gate-result.json'));
|
|
101
|
+
cwdSpy.mockRestore();
|
|
102
|
+
logSpy.mockRestore();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should write .ui-gate-result.json', async () => {
|
|
106
|
+
const { writeUiReviewResult } = await import('../ui-review');
|
|
107
|
+
const result = {
|
|
108
|
+
commit: 'abc123',
|
|
109
|
+
verdict: 'APPROVED',
|
|
110
|
+
expires: '2026-06-03T00:00:00.000Z',
|
|
111
|
+
design_review: 'APPROVED',
|
|
112
|
+
browser_qa: 'APPROVED',
|
|
113
|
+
ui_changes_detected: ['views/index.html'],
|
|
114
|
+
};
|
|
115
|
+
writeUiReviewResult(result, TEST_DIR);
|
|
116
|
+
const parsed = JSON.parse(readFileSync(join(TEST_DIR, '.ui-gate-result.json'), 'utf8'));
|
|
117
|
+
expect(parsed.ui_changes_detected).toEqual(['views/index.html']);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
@@ -27,6 +27,7 @@ describe('uninstall', () => {
|
|
|
27
27
|
delete require.cache[require.resolve('../uninstall')];
|
|
28
28
|
delete require.cache[require.resolve('../init')];
|
|
29
29
|
delete require.cache[require.resolve('../detect-deps.js')];
|
|
30
|
+
delete require.cache[require.resolve('../shared-paths')];
|
|
30
31
|
logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
31
32
|
warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
32
33
|
errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|