@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
package/bin/xp-gate.js
CHANGED
|
@@ -22,7 +22,7 @@ const COMMANDS = {
|
|
|
22
22
|
'install-skill': {
|
|
23
23
|
description: 'Install a xp-gate skill from GitHub',
|
|
24
24
|
fn: installSkill,
|
|
25
|
-
usage: 'xp-gate install-skill <name>[@<version>] [--offline] [--verbose] [--force]
|
|
25
|
+
usage: 'xp-gate install-skill <name>[@<version>] [--offline] [--verbose] [--force]'
|
|
26
26
|
},
|
|
27
27
|
'update-skill': {
|
|
28
28
|
description: 'Update installed skill(s)',
|
|
@@ -103,7 +103,7 @@ function main() {
|
|
|
103
103
|
const name = subargs[0];
|
|
104
104
|
if (!name) {
|
|
105
105
|
console.error('Error: Skill name required');
|
|
106
|
-
console.error('Usage: xp-gate install-skill <name>[@<version>]
|
|
106
|
+
console.error('Usage: xp-gate install-skill <name>[@<version>]');
|
|
107
107
|
process.exit(1);
|
|
108
108
|
return;
|
|
109
109
|
}
|
|
@@ -285,18 +285,13 @@ function printStatsTable(stats) {
|
|
|
285
285
|
}
|
|
286
286
|
|
|
287
287
|
function parseOptions(args) {
|
|
288
|
-
const options = { offline: false, verbose: false, force: false, all: false, check: false
|
|
289
|
-
for (
|
|
290
|
-
const arg = args[i];
|
|
288
|
+
const options = { offline: false, verbose: false, force: false, all: false, check: false };
|
|
289
|
+
for (const arg of args) {
|
|
291
290
|
if (arg === '--offline') options.offline = true;
|
|
292
291
|
if (arg === '--verbose') options.verbose = true;
|
|
293
292
|
if (arg === '--force') options.force = true;
|
|
294
293
|
if (arg === '--all') options.all = true;
|
|
295
294
|
if (arg === '--check') options.check = true;
|
|
296
|
-
if (arg === '--platform' && i + 1 < args.length) {
|
|
297
|
-
options.platform = args[i + 1];
|
|
298
|
-
i++;
|
|
299
|
-
}
|
|
300
295
|
}
|
|
301
296
|
return options;
|
|
302
297
|
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @test audit-log
|
|
3
|
+
* @intent Verify UI gate bypass audit logging and threshold reporting
|
|
4
|
+
* @covers AUDIT-LOG-001
|
|
5
|
+
*/
|
|
6
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
7
|
+
import { existsSync, mkdirSync, rmSync, writeFileSync } from 'fs';
|
|
8
|
+
import { join } from 'path';
|
|
9
|
+
import {
|
|
10
|
+
appendAuditEntry,
|
|
11
|
+
readAllEntries,
|
|
12
|
+
countBypassesInWindow,
|
|
13
|
+
exceedsBypassThreshold,
|
|
14
|
+
formatRetroReport,
|
|
15
|
+
type AuditEntry,
|
|
16
|
+
} from '../audit-log';
|
|
17
|
+
|
|
18
|
+
const TEST_DIR = join(process.cwd(), '.audit-log-test');
|
|
19
|
+
|
|
20
|
+
function baseEntry(overrides: Partial<Omit<AuditEntry, 'gate_count'>> = {}): Omit<AuditEntry, 'gate_count'> {
|
|
21
|
+
return {
|
|
22
|
+
timestamp: new Date().toISOString(),
|
|
23
|
+
branch: 'main',
|
|
24
|
+
commit: 'abc123',
|
|
25
|
+
user: 'tester',
|
|
26
|
+
reason: 'manual approval for test',
|
|
27
|
+
bypass_type: 'ui-gates',
|
|
28
|
+
...overrides,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe('audit-log', () => {
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
if (existsSync(TEST_DIR)) {
|
|
35
|
+
rmSync(TEST_DIR, { recursive: true, force: true });
|
|
36
|
+
}
|
|
37
|
+
mkdirSync(TEST_DIR, { recursive: true });
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
afterEach(() => {
|
|
41
|
+
if (existsSync(TEST_DIR)) {
|
|
42
|
+
rmSync(TEST_DIR, { recursive: true, force: true });
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe('appendAuditEntry and readAllEntries', () => {
|
|
47
|
+
it('should append an entry and compute gate_count', () => {
|
|
48
|
+
const first = appendAuditEntry(baseEntry(), TEST_DIR);
|
|
49
|
+
const second = appendAuditEntry(baseEntry({ commit: 'def456' }), TEST_DIR);
|
|
50
|
+
|
|
51
|
+
expect(first.gate_count).toBe(0);
|
|
52
|
+
expect(second.gate_count).toBe(1);
|
|
53
|
+
|
|
54
|
+
const entries = readAllEntries(join(TEST_DIR, '.audit-log.jsonl'));
|
|
55
|
+
expect(entries).toHaveLength(2);
|
|
56
|
+
expect(entries[1].commit).toBe('def456');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should return empty array when log file is missing or empty', () => {
|
|
60
|
+
expect(readAllEntries(join(TEST_DIR, 'missing.jsonl'))).toEqual([]);
|
|
61
|
+
const emptyLog = join(TEST_DIR, '.audit-log.jsonl');
|
|
62
|
+
writeFileSync(emptyLog, '', 'utf8');
|
|
63
|
+
expect(readAllEntries(emptyLog)).toEqual([]);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe('rolling window calculations', () => {
|
|
68
|
+
it('should count only matching bypass type in rolling window', () => {
|
|
69
|
+
const now = new Date();
|
|
70
|
+
const old = new Date(now.getTime() - 40 * 24 * 60 * 60 * 1000).toISOString();
|
|
71
|
+
const entries: AuditEntry[] = [
|
|
72
|
+
{ ...baseEntry({ timestamp: now.toISOString(), bypass_type: 'ui-gates' }), gate_count: 0 },
|
|
73
|
+
{ ...baseEntry({ timestamp: now.toISOString(), bypass_type: 'other' }), gate_count: 0 },
|
|
74
|
+
{ ...baseEntry({ timestamp: old, bypass_type: 'ui-gates' }), gate_count: 0 },
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
expect(countBypassesInWindow(entries, 'ui-gates')).toBe(1);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should report threshold exceedance', () => {
|
|
81
|
+
const entries: AuditEntry[] = Array.from({ length: 4 }, (_, i) => ({
|
|
82
|
+
...baseEntry({ commit: `commit-${i}` }),
|
|
83
|
+
gate_count: i,
|
|
84
|
+
}));
|
|
85
|
+
expect(exceedsBypassThreshold(entries, 'ui-gates')).toEqual({ exceeded: true, count: 4 });
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe('formatRetroReport', () => {
|
|
90
|
+
it('should include recent bypasses and threshold warning', () => {
|
|
91
|
+
const entries: AuditEntry[] = Array.from({ length: 4 }, (_, i) => ({
|
|
92
|
+
...baseEntry({ reason: `reason-${i}`, branch: `branch-${i}` }),
|
|
93
|
+
gate_count: i,
|
|
94
|
+
}));
|
|
95
|
+
const report = formatRetroReport(entries, 'ui-gates');
|
|
96
|
+
expect(report).toContain('Last 30 days: 4 bypasses');
|
|
97
|
+
expect(report).toContain('EXCEEDS threshold');
|
|
98
|
+
expect(report).toContain('reason-3');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should omit recent section when no matching bypasses exist', () => {
|
|
102
|
+
const report = formatRetroReport([], 'ui-gates');
|
|
103
|
+
expect(report).toContain('Last 30 days: 0 bypasses');
|
|
104
|
+
expect(report).not.toContain('Recent bypasses');
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
});
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @test detect-deps
|
|
3
|
-
* @intent Verify checkDeps() correctly detects missing/present/outdated dependencies
|
|
3
|
+
* @intent Verify checkDeps() correctly detects missing/present/outdated dependencies across platforms,
|
|
4
|
+
* detectPlatform() identifies the correct AI agent platform,
|
|
5
|
+
* and autoInstallDeps() handles install scenarios
|
|
4
6
|
*/
|
|
5
7
|
const fs = require('fs');
|
|
6
8
|
const path = require('path');
|
|
@@ -16,6 +18,7 @@ describe('detect-deps', () => {
|
|
|
16
18
|
process.env.HOME = tmpHome;
|
|
17
19
|
vi.resetModules();
|
|
18
20
|
delete require.cache[require.resolve('../detect-deps')];
|
|
21
|
+
delete require.cache[require.resolve('../shared-paths')];
|
|
19
22
|
});
|
|
20
23
|
|
|
21
24
|
afterEach(() => {
|
|
@@ -24,8 +27,10 @@ describe('detect-deps', () => {
|
|
|
24
27
|
vi.restoreAllMocks();
|
|
25
28
|
});
|
|
26
29
|
|
|
27
|
-
function makeSkillDir(skillName, contents = {}) {
|
|
28
|
-
const dir =
|
|
30
|
+
function makeSkillDir(skillName, contents = {}, baseDir) {
|
|
31
|
+
const dir = baseDir
|
|
32
|
+
? path.join(baseDir, skillName)
|
|
33
|
+
: path.join(tmpHome, '.config', 'opencode', 'skills', skillName);
|
|
29
34
|
fs.mkdirSync(dir, { recursive: true });
|
|
30
35
|
if (contents.packageJson) {
|
|
31
36
|
fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify(contents.packageJson));
|
|
@@ -48,6 +53,8 @@ describe('detect-deps', () => {
|
|
|
48
53
|
return dir;
|
|
49
54
|
}
|
|
50
55
|
|
|
56
|
+
// ── checkDeps: backward-compatible tests (default platform = opencode) ──
|
|
57
|
+
|
|
51
58
|
it('returns ok:false missing:superpowers when no deps exist', async () => {
|
|
52
59
|
const { checkDeps } = require('../detect-deps');
|
|
53
60
|
const result = await checkDeps();
|
|
@@ -127,7 +134,6 @@ describe('detect-deps', () => {
|
|
|
127
134
|
});
|
|
128
135
|
|
|
129
136
|
it('returns ok:true (no version check) when package.json has no version and no SKILL.md', async () => {
|
|
130
|
-
// getSkillVersion returns null → skips version check → ok
|
|
131
137
|
makeSkillDir('superpowers', { packageJson: {} });
|
|
132
138
|
makeSkillDir('gstack', { packageJson: {} });
|
|
133
139
|
const { checkDeps } = require('../detect-deps');
|
|
@@ -147,13 +153,8 @@ describe('detect-deps', () => {
|
|
|
147
153
|
});
|
|
148
154
|
|
|
149
155
|
it('returns null version when neither package.json nor SKILL.md exist (skips version check)', async () => {
|
|
150
|
-
|
|
151
|
-
fs.mkdirSync(path.join(tmpHome, '.config', 'opencode', 'skills', '
|
|
152
|
-
recursive: true,
|
|
153
|
-
});
|
|
154
|
-
fs.mkdirSync(path.join(tmpHome, '.config', 'opencode', 'skills', 'gstack'), {
|
|
155
|
-
recursive: true,
|
|
156
|
-
});
|
|
156
|
+
fs.mkdirSync(path.join(tmpHome, '.config', 'opencode', 'skills', 'superpowers'), { recursive: true });
|
|
157
|
+
fs.mkdirSync(path.join(tmpHome, '.config', 'opencode', 'skills', 'gstack'), { recursive: true });
|
|
157
158
|
const { checkDeps } = require('../detect-deps');
|
|
158
159
|
const result = await checkDeps();
|
|
159
160
|
expect(result.ok).toBe(true);
|
|
@@ -164,17 +165,14 @@ describe('detect-deps', () => {
|
|
|
164
165
|
makeSkillDir('gstack', { packageJson: { version: '1.0.0' } });
|
|
165
166
|
const { checkDeps } = require('../detect-deps');
|
|
166
167
|
const result = await checkDeps();
|
|
167
|
-
// superpowers version=null → skips version check → ok
|
|
168
168
|
expect(result.ok).toBe(true);
|
|
169
169
|
});
|
|
170
170
|
|
|
171
|
-
it('compareVersions handles partial versions (e.g. 1
|
|
172
|
-
// version "1.0" in SKILL.md regex requires X.Y.Z so won't match; use package.json
|
|
171
|
+
it('compareVersions handles partial versions (e.g. 1 treated as 1.0.0)', async () => {
|
|
173
172
|
makeSkillDir('superpowers', { packageJson: { version: '1' } });
|
|
174
173
|
makeSkillDir('gstack', { packageJson: { version: '1.0.0' } });
|
|
175
174
|
const { checkDeps } = require('../detect-deps');
|
|
176
175
|
const result = await checkDeps();
|
|
177
|
-
// '1' parsed as [1] vs [1,0,0] → equal at index 0; index 1: 0 vs 0; index 2: 0 vs 0 → equal → passes
|
|
178
176
|
expect(result.ok).toBe(true);
|
|
179
177
|
});
|
|
180
178
|
|
|
@@ -196,48 +194,220 @@ describe('detect-deps', () => {
|
|
|
196
194
|
});
|
|
197
195
|
|
|
198
196
|
it('prefers SKILLS_DIR over OPENCODE_DIR when both exist', async () => {
|
|
199
|
-
// Put low version in SKILLS_DIR, high in OPENCODE_DIR
|
|
200
197
|
makeSkillDir('superpowers', { packageJson: { version: '0.0.1' } });
|
|
201
198
|
makeOpencodeDir('superpowers', { packageJson: { version: '2.0.0' } });
|
|
202
199
|
makeSkillDir('gstack', { packageJson: { version: '1.0.0' } });
|
|
203
200
|
const { checkDeps } = require('../detect-deps');
|
|
204
201
|
const result = await checkDeps();
|
|
205
|
-
// Should use SKILLS_DIR (first in possiblePaths) → 0.0.1 fails
|
|
206
202
|
expect(result.ok).toBe(false);
|
|
207
203
|
expect(result.versionMismatch.found).toBe('0.0.1');
|
|
208
204
|
});
|
|
209
205
|
|
|
210
|
-
//
|
|
206
|
+
// ── Platform-specific tests (Issue #128) ──
|
|
211
207
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
208
|
+
describe('checkDeps with platform parameter', () => {
|
|
209
|
+
it('qoder: checks ~/.qoder/skills/ for dependencies', async () => {
|
|
210
|
+
const qoderSkills = path.join(tmpHome, '.qoder', 'skills');
|
|
211
|
+
makeSkillDir('superpowers', { packageJson: { version: '2.0.0' } }, path.join(qoderSkills, 'superpowers'));
|
|
212
|
+
makeSkillDir('gstack', { packageJson: { version: '1.0.0' } }, path.join(qoderSkills, 'gstack'));
|
|
213
|
+
const { checkDeps } = require('../detect-deps');
|
|
214
|
+
const result = await checkDeps('qoder');
|
|
215
|
+
expect(result.ok).toBe(true);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('qoder: returns missing when qoder skills dir is empty', async () => {
|
|
219
|
+
fs.mkdirSync(path.join(tmpHome, '.qoder', 'skills'), { recursive: true });
|
|
220
|
+
const { checkDeps } = require('../detect-deps');
|
|
221
|
+
const result = await checkDeps('qoder');
|
|
222
|
+
expect(result.ok).toBe(false);
|
|
223
|
+
expect(result.missing).toBe('superpowers');
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('claude-code: checks ~/.claude/skills/ for dependencies', async () => {
|
|
227
|
+
const claudeSkills = path.join(tmpHome, '.claude', 'skills');
|
|
228
|
+
makeSkillDir('superpowers', { packageJson: { version: '2.0.0' } }, path.join(claudeSkills, 'superpowers'));
|
|
229
|
+
makeSkillDir('gstack', { packageJson: { version: '1.0.0' } }, path.join(claudeSkills, 'gstack'));
|
|
230
|
+
const { checkDeps } = require('../detect-deps');
|
|
231
|
+
const result = await checkDeps('claude-code');
|
|
232
|
+
expect(result.ok).toBe(true);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('claude-code: returns missing when claude skills dir is empty', async () => {
|
|
236
|
+
fs.mkdirSync(path.join(tmpHome, '.claude', 'skills'), { recursive: true });
|
|
237
|
+
const { checkDeps } = require('../detect-deps');
|
|
238
|
+
const result = await checkDeps('claude-code');
|
|
239
|
+
expect(result.ok).toBe(false);
|
|
240
|
+
expect(result.missing).toBe('superpowers');
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('unknown platform falls back to opencode profile', async () => {
|
|
244
|
+
makeSkillDir('superpowers', { packageJson: { version: '2.0.0' } });
|
|
245
|
+
makeSkillDir('gstack', { packageJson: { version: '1.0.0' } });
|
|
246
|
+
const { checkDeps } = require('../detect-deps');
|
|
247
|
+
const result = await checkDeps('unknown-platform');
|
|
248
|
+
expect(result.ok).toBe(true);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('qoder platform does NOT find deps in opencode dir', async () => {
|
|
252
|
+
// Put deps in opencode dir but NOT in qoder dir
|
|
253
|
+
makeSkillDir('superpowers', { packageJson: { version: '2.0.0' } });
|
|
254
|
+
makeSkillDir('gstack', { packageJson: { version: '1.0.0' } });
|
|
255
|
+
const { checkDeps } = require('../detect-deps');
|
|
256
|
+
const result = await checkDeps('qoder');
|
|
257
|
+
expect(result.ok).toBe(false);
|
|
258
|
+
expect(result.missing).toBe('superpowers');
|
|
259
|
+
});
|
|
218
260
|
});
|
|
219
261
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
262
|
+
// ── detectPlatform tests ──
|
|
263
|
+
|
|
264
|
+
describe('detectPlatform', () => {
|
|
265
|
+
it('returns "qoder" when ~/.qoder/skills/ exists', () => {
|
|
266
|
+
fs.mkdirSync(path.join(tmpHome, '.qoder', 'skills'), { recursive: true });
|
|
267
|
+
const { detectPlatform } = require('../detect-deps');
|
|
268
|
+
expect(detectPlatform()).toBe('qoder');
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it('returns "claude-code" when ~/.claude/skills/ exists', () => {
|
|
272
|
+
fs.mkdirSync(path.join(tmpHome, '.claude', 'skills'), { recursive: true });
|
|
273
|
+
const { detectPlatform } = require('../detect-deps');
|
|
274
|
+
expect(detectPlatform()).toBe('claude-code');
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('returns "opencode" as default when no platform dirs exist', () => {
|
|
278
|
+
const { detectPlatform } = require('../detect-deps');
|
|
279
|
+
expect(detectPlatform()).toBe('opencode');
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it('prefers qoder over claude-code when both exist', () => {
|
|
283
|
+
fs.mkdirSync(path.join(tmpHome, '.qoder', 'skills'), { recursive: true });
|
|
284
|
+
fs.mkdirSync(path.join(tmpHome, '.claude', 'skills'), { recursive: true });
|
|
285
|
+
const { detectPlatform } = require('../detect-deps');
|
|
286
|
+
expect(detectPlatform()).toBe('qoder');
|
|
287
|
+
});
|
|
226
288
|
});
|
|
227
289
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
290
|
+
// ── getSkillsDirs tests ──
|
|
291
|
+
|
|
292
|
+
describe('getSkillsDirs', () => {
|
|
293
|
+
it('returns opencode paths for opencode platform', () => {
|
|
294
|
+
const { getSkillsDirs } = require('../detect-deps');
|
|
295
|
+
const dirs = getSkillsDirs('opencode');
|
|
296
|
+
expect(dirs[0]).toContain('.config');
|
|
297
|
+
expect(dirs[0]).toContain('opencode');
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it('returns qoder paths for qoder platform', () => {
|
|
301
|
+
const { getSkillsDirs } = require('../detect-deps');
|
|
302
|
+
const dirs = getSkillsDirs('qoder');
|
|
303
|
+
expect(dirs[0]).toContain('.qoder');
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it('returns claude-code paths for claude-code platform', () => {
|
|
307
|
+
const { getSkillsDirs } = require('../detect-deps');
|
|
308
|
+
const dirs = getSkillsDirs('claude-code');
|
|
309
|
+
expect(dirs[0]).toContain('.claude');
|
|
310
|
+
});
|
|
234
311
|
});
|
|
235
312
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
313
|
+
// ── PLATFORM_PROFILES tests ──
|
|
314
|
+
|
|
315
|
+
describe('PLATFORM_PROFILES', () => {
|
|
316
|
+
it('all platforms have requiredDeps defined', () => {
|
|
317
|
+
const { PLATFORM_PROFILES } = require('../detect-deps');
|
|
318
|
+
expect(PLATFORM_PROFILES.opencode.requiredDeps.length).toBe(2);
|
|
319
|
+
expect(PLATFORM_PROFILES.qoder.requiredDeps.length).toBe(2);
|
|
320
|
+
expect(PLATFORM_PROFILES['claude-code'].requiredDeps.length).toBe(2);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it('all platforms have skillsDirs defined', () => {
|
|
324
|
+
const { PLATFORM_PROFILES } = require('../detect-deps');
|
|
325
|
+
expect(PLATFORM_PROFILES.opencode.skillsDirs.length).toBeGreaterThan(0);
|
|
326
|
+
expect(PLATFORM_PROFILES.qoder.skillsDirs.length).toBeGreaterThan(0);
|
|
327
|
+
expect(PLATFORM_PROFILES['claude-code'].skillsDirs.length).toBeGreaterThan(0);
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
// ── autoInstallDeps tests ──
|
|
332
|
+
|
|
333
|
+
describe('autoInstallDeps', () => {
|
|
334
|
+
it('returns ok:true with empty installed when deps already exist', async () => {
|
|
335
|
+
makeSkillDir('superpowers', { packageJson: { version: '2.0.0' } });
|
|
336
|
+
makeSkillDir('gstack', { packageJson: { version: '1.0.0' } });
|
|
337
|
+
const { autoInstallDeps } = require('../detect-deps');
|
|
338
|
+
const result = await autoInstallDeps();
|
|
339
|
+
expect(result.ok).toBe(true);
|
|
340
|
+
expect(result.installed).toEqual([]);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it('creates skills directory if it does not exist', async () => {
|
|
344
|
+
const { autoInstallDeps } = require('../detect-deps');
|
|
345
|
+
vi.spyOn(require('child_process'), 'spawnSync').mockImplementation((cmd, args) => {
|
|
346
|
+
const destPath = args[args.length - 1];
|
|
347
|
+
fs.mkdirSync(destPath, { recursive: true });
|
|
348
|
+
fs.writeFileSync(path.join(destPath, 'package.json'), JSON.stringify({ version: '2.0.0' }));
|
|
349
|
+
return { status: 0, stdout: Buffer.from(''), stderr: Buffer.from('') };
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
const result = await autoInstallDeps();
|
|
353
|
+
expect(result.ok).toBe(true);
|
|
354
|
+
expect(result.installed).toContain('superpowers');
|
|
355
|
+
expect(result.installed).toContain('gstack');
|
|
356
|
+
|
|
357
|
+
vi.restoreAllMocks();
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it('returns errors when git clone fails', async () => {
|
|
361
|
+
const { autoInstallDeps } = require('../detect-deps');
|
|
362
|
+
vi.spyOn(require('child_process'), 'spawnSync').mockImplementation(() => {
|
|
363
|
+
throw new Error('git not found');
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
const result = await autoInstallDeps();
|
|
367
|
+
expect(result.ok).toBe(false);
|
|
368
|
+
expect(result.errors.length).toBeGreaterThan(0);
|
|
369
|
+
expect(result.errors[0].message).toContain('git not found');
|
|
370
|
+
|
|
371
|
+
vi.restoreAllMocks();
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it('returns errors when git clone exits non-zero (Issue #155)', async () => {
|
|
375
|
+
const { autoInstallDeps } = require('../detect-deps');
|
|
376
|
+
vi.spyOn(require('child_process'), 'spawnSync').mockImplementation(() => ({
|
|
377
|
+
status: 128,
|
|
378
|
+
stdout: Buffer.from(''),
|
|
379
|
+
stderr: Buffer.from('fatal: repository not found'),
|
|
380
|
+
}));
|
|
381
|
+
|
|
382
|
+
const result = await autoInstallDeps();
|
|
383
|
+
expect(result.ok).toBe(false);
|
|
384
|
+
expect(result.errors.length).toBeGreaterThan(0);
|
|
385
|
+
expect(result.errors[0].message).toMatch(/repository not found|git clone failed|exit/i);
|
|
386
|
+
|
|
387
|
+
vi.restoreAllMocks();
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
it('passes repoUrl as argv[] argument, never via shell string (Issue #155)', async () => {
|
|
391
|
+
const { autoInstallDeps } = require('../detect-deps');
|
|
392
|
+
const spy = vi.spyOn(require('child_process'), 'spawnSync').mockImplementation((cmd, args) => {
|
|
393
|
+
const destPath = args[args.length - 1];
|
|
394
|
+
fs.mkdirSync(destPath, { recursive: true });
|
|
395
|
+
fs.writeFileSync(path.join(destPath, 'package.json'), JSON.stringify({ version: '2.0.0' }));
|
|
396
|
+
return { status: 0, stdout: Buffer.from(''), stderr: Buffer.from('') };
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
await autoInstallDeps();
|
|
400
|
+
|
|
401
|
+
expect(spy).toHaveBeenCalled();
|
|
402
|
+
for (const call of spy.mock.calls) {
|
|
403
|
+
const [cmd, args, opts] = call;
|
|
404
|
+
expect(cmd).toBe('git');
|
|
405
|
+
expect(Array.isArray(args)).toBe(true);
|
|
406
|
+
expect(args[0]).toBe('clone');
|
|
407
|
+
expect(opts && opts.shell).not.toBe(true);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
vi.restoreAllMocks();
|
|
411
|
+
});
|
|
242
412
|
});
|
|
243
413
|
});
|
|
@@ -27,6 +27,7 @@ describe('doctor', () => {
|
|
|
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(() => {});
|
|
@@ -11,6 +11,8 @@ import {
|
|
|
11
11
|
readTailEntries,
|
|
12
12
|
computeStats,
|
|
13
13
|
rotateIfNeeded,
|
|
14
|
+
parseCliOptions,
|
|
15
|
+
getCommitHash,
|
|
14
16
|
type GateAuditEntry,
|
|
15
17
|
} from '../gate-audit';
|
|
16
18
|
|
|
@@ -114,6 +116,28 @@ describe('gate-audit', () => {
|
|
|
114
116
|
const tail = readTailEntries(undefined, repoRoot);
|
|
115
117
|
expect(tail.length).toBe(20);
|
|
116
118
|
});
|
|
119
|
+
|
|
120
|
+
it('should skip malformed JSON lines', () => {
|
|
121
|
+
const repoRoot = join(TEST_DIR, 'malformed-tail');
|
|
122
|
+
const logDir = join(repoRoot, '.xp-gate');
|
|
123
|
+
mkdirSync(logDir, { recursive: true });
|
|
124
|
+
writeFileSync(
|
|
125
|
+
join(logDir, 'audit.jsonl'),
|
|
126
|
+
`not-json\n${JSON.stringify(makeEntry({ repo_path: repoRoot, gate_id: 'gate-valid' }))}\n`,
|
|
127
|
+
'utf8',
|
|
128
|
+
);
|
|
129
|
+
const tail = readTailEntries(10, repoRoot);
|
|
130
|
+
expect(tail.length).toBe(1);
|
|
131
|
+
expect(tail[0].gate_id).toBe('gate-valid');
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('should return empty array for empty log file', () => {
|
|
135
|
+
const repoRoot = join(TEST_DIR, 'empty-log');
|
|
136
|
+
const logDir = join(repoRoot, '.xp-gate');
|
|
137
|
+
mkdirSync(logDir, { recursive: true });
|
|
138
|
+
writeFileSync(join(logDir, 'audit.jsonl'), '', 'utf8');
|
|
139
|
+
expect(readTailEntries(10, repoRoot)).toEqual([]);
|
|
140
|
+
});
|
|
117
141
|
});
|
|
118
142
|
|
|
119
143
|
describe('computeStats', () => {
|
|
@@ -153,6 +177,41 @@ describe('gate-audit', () => {
|
|
|
153
177
|
const stats = computeStats(repoRoot);
|
|
154
178
|
expect(stats[0].avg_issues).toBe(2.67);
|
|
155
179
|
});
|
|
180
|
+
|
|
181
|
+
it('should return empty array for empty stats log', () => {
|
|
182
|
+
const repoRoot = join(TEST_DIR, 'empty-stats-log');
|
|
183
|
+
const logDir = join(repoRoot, '.xp-gate');
|
|
184
|
+
mkdirSync(logDir, { recursive: true });
|
|
185
|
+
writeFileSync(join(logDir, 'audit.jsonl'), '', 'utf8');
|
|
186
|
+
expect(computeStats(repoRoot)).toEqual([]);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('should skip malformed lines during stats aggregation', () => {
|
|
190
|
+
const repoRoot = join(TEST_DIR, 'malformed-stats');
|
|
191
|
+
const logDir = join(repoRoot, '.xp-gate');
|
|
192
|
+
mkdirSync(logDir, { recursive: true });
|
|
193
|
+
writeFileSync(
|
|
194
|
+
join(logDir, 'audit.jsonl'),
|
|
195
|
+
`${JSON.stringify(makeEntry({ repo_path: repoRoot, gate_id: 'gate-1' }))}\n{bad json}\n`,
|
|
196
|
+
'utf8',
|
|
197
|
+
);
|
|
198
|
+
const stats = computeStats(repoRoot);
|
|
199
|
+
expect(stats).toHaveLength(1);
|
|
200
|
+
expect(stats[0].gate_id).toBe('gate-1');
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
describe('CLI helpers', () => {
|
|
205
|
+
it('should parse CLI options and skip dangling flags', () => {
|
|
206
|
+
expect(parseCliOptions(['--gate-id', 'gate-1', '--passed', 'true', '--dangling'])).toEqual({
|
|
207
|
+
'gate-id': 'gate-1',
|
|
208
|
+
passed: 'true',
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('should return a commit hash string or unknown', () => {
|
|
213
|
+
expect(typeof getCommitHash()).toBe('string');
|
|
214
|
+
});
|
|
156
215
|
});
|
|
157
216
|
|
|
158
217
|
describe('rotateIfNeeded', () => {
|
|
@@ -24,9 +24,20 @@ describe('init', () => {
|
|
|
24
24
|
vi.resetModules();
|
|
25
25
|
delete require.cache[require.resolve('../init')];
|
|
26
26
|
delete require.cache[require.resolve('../detect-deps.js')];
|
|
27
|
+
delete require.cache[require.resolve('../shared-paths')];
|
|
27
28
|
logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
28
29
|
warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
29
30
|
errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
31
|
+
vi.spyOn(childProcess, 'spawnSync').mockImplementation((cmd, args) => {
|
|
32
|
+
if (cmd === 'git' && Array.isArray(args) && args[0] === 'clone') {
|
|
33
|
+
return {
|
|
34
|
+
status: 1,
|
|
35
|
+
stdout: Buffer.from(''),
|
|
36
|
+
stderr: Buffer.from('git clone disabled in test (default stub)'),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
return { status: 0, stdout: Buffer.from(''), stderr: Buffer.from('') };
|
|
40
|
+
});
|
|
30
41
|
});
|
|
31
42
|
|
|
32
43
|
afterEach(() => {
|
|
@@ -55,7 +66,21 @@ describe('init', () => {
|
|
|
55
66
|
});
|
|
56
67
|
}
|
|
57
68
|
|
|
69
|
+
function stubSpawnGitClone(status = 0, stderr = '') {
|
|
70
|
+
vi.spyOn(childProcess, 'spawnSync').mockImplementation((cmd, args) => {
|
|
71
|
+
if (cmd === 'git' && Array.isArray(args) && args[0] === 'clone') {
|
|
72
|
+
return {
|
|
73
|
+
status,
|
|
74
|
+
stdout: Buffer.from(''),
|
|
75
|
+
stderr: Buffer.from(stderr),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
return { status: 0, stdout: Buffer.from(''), stderr: Buffer.from('') };
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
58
82
|
function mockExecSuccess() {
|
|
83
|
+
stubSpawnGitClone(1, 'git clone disabled in test');
|
|
59
84
|
execSpy = vi.spyOn(childProcess, 'execSync').mockImplementation((cmd) => {
|
|
60
85
|
if (cmd === 'git rev-parse --git-dir') {
|
|
61
86
|
return path.join(tmpProject, '.git') + '\n';
|
|
@@ -68,6 +93,7 @@ describe('init', () => {
|
|
|
68
93
|
}
|
|
69
94
|
|
|
70
95
|
function mockExecGitDirOnly() {
|
|
96
|
+
stubSpawnGitClone(1, 'git clone disabled in test');
|
|
71
97
|
execSpy = vi.spyOn(childProcess, 'execSync').mockImplementation((cmd) => {
|
|
72
98
|
if (cmd === 'git rev-parse --git-dir') {
|
|
73
99
|
return path.join(tmpProject, '.git') + '\n';
|
|
@@ -80,6 +106,7 @@ describe('init', () => {
|
|
|
80
106
|
}
|
|
81
107
|
|
|
82
108
|
function mockExecFail() {
|
|
109
|
+
stubSpawnGitClone(1, 'git clone disabled in test');
|
|
83
110
|
execSpy = vi.spyOn(childProcess, 'execSync').mockImplementation(() => {
|
|
84
111
|
throw new Error('Not a git repo');
|
|
85
112
|
});
|
|
@@ -102,6 +129,16 @@ describe('init', () => {
|
|
|
102
129
|
});
|
|
103
130
|
|
|
104
131
|
it('init([]) with missing superpowers warns Missing dependencies', async () => {
|
|
132
|
+
vi.spyOn(childProcess, 'spawnSync').mockImplementation((cmd, args) => {
|
|
133
|
+
if (cmd === 'git' && Array.isArray(args) && args[0] === 'clone') {
|
|
134
|
+
return {
|
|
135
|
+
status: 1,
|
|
136
|
+
stdout: Buffer.from(''),
|
|
137
|
+
stderr: Buffer.from('git clone not available in test'),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
return { status: 0, stdout: Buffer.from(''), stderr: Buffer.from('') };
|
|
141
|
+
});
|
|
105
142
|
const { init } = require('../init');
|
|
106
143
|
const result = await init([]);
|
|
107
144
|
expect(result).toBe(0);
|
|
@@ -110,7 +147,16 @@ describe('init', () => {
|
|
|
110
147
|
});
|
|
111
148
|
|
|
112
149
|
it('init([]) with versionMismatch warns version detail', async () => {
|
|
113
|
-
|
|
150
|
+
vi.spyOn(childProcess, 'spawnSync').mockImplementation((cmd, args) => {
|
|
151
|
+
if (cmd === 'git' && Array.isArray(args) && args[0] === 'clone') {
|
|
152
|
+
return {
|
|
153
|
+
status: 1,
|
|
154
|
+
stdout: Buffer.from(''),
|
|
155
|
+
stderr: Buffer.from('git clone not available in test'),
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
return { status: 0, stdout: Buffer.from(''), stderr: Buffer.from('') };
|
|
159
|
+
});
|
|
114
160
|
const sp = path.join(skillsDir(), 'superpowers');
|
|
115
161
|
fs.mkdirSync(sp, { recursive: true });
|
|
116
162
|
fs.writeFileSync(path.join(sp, 'package.json'), JSON.stringify({ version: '0.0.1' }));
|