popeye-cli 2.1.0 → 2.2.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/dist/cli/interactive.d.ts.map +1 -1
- package/dist/cli/interactive.js +4 -1
- package/dist/cli/interactive.js.map +1 -1
- package/dist/generators/all.d.ts.map +1 -1
- package/dist/generators/all.js +23 -1
- package/dist/generators/all.js.map +1 -1
- package/dist/pipeline/artifact-manager.d.ts.map +1 -1
- package/dist/pipeline/artifact-manager.js +3 -0
- package/dist/pipeline/artifact-manager.js.map +1 -1
- package/dist/pipeline/gate-engine.js +1 -1
- package/dist/pipeline/gate-engine.js.map +1 -1
- package/dist/pipeline/migration.d.ts.map +1 -1
- package/dist/pipeline/migration.js +3 -26
- package/dist/pipeline/migration.js.map +1 -1
- package/dist/pipeline/orchestrator.d.ts.map +1 -1
- package/dist/pipeline/orchestrator.js +5 -0
- package/dist/pipeline/orchestrator.js.map +1 -1
- package/dist/pipeline/phases/intake.d.ts +1 -0
- package/dist/pipeline/phases/intake.d.ts.map +1 -1
- package/dist/pipeline/phases/intake.js +49 -10
- package/dist/pipeline/phases/intake.js.map +1 -1
- package/dist/pipeline/phases/role-planning.d.ts.map +1 -1
- package/dist/pipeline/phases/role-planning.js +2 -3
- package/dist/pipeline/phases/role-planning.js.map +1 -1
- package/dist/pipeline/skills/constitution-generator.d.ts +51 -0
- package/dist/pipeline/skills/constitution-generator.d.ts.map +1 -0
- package/dist/pipeline/skills/constitution-generator.js +210 -0
- package/dist/pipeline/skills/constitution-generator.js.map +1 -0
- package/dist/pipeline/skills/generator.d.ts +65 -0
- package/dist/pipeline/skills/generator.d.ts.map +1 -0
- package/dist/pipeline/skills/generator.js +221 -0
- package/dist/pipeline/skills/generator.js.map +1 -0
- package/dist/pipeline/skills/role-map.d.ts +38 -0
- package/dist/pipeline/skills/role-map.d.ts.map +1 -0
- package/dist/pipeline/skills/role-map.js +234 -0
- package/dist/pipeline/skills/role-map.js.map +1 -0
- package/dist/pipeline/skills/types.d.ts +47 -0
- package/dist/pipeline/skills/types.d.ts.map +1 -0
- package/dist/pipeline/skills/types.js +5 -0
- package/dist/pipeline/skills/types.js.map +1 -0
- package/dist/pipeline/type-defs/artifacts.d.ts +5 -0
- package/dist/pipeline/type-defs/artifacts.d.ts.map +1 -1
- package/dist/pipeline/type-defs/artifacts.js +1 -0
- package/dist/pipeline/type-defs/artifacts.js.map +1 -1
- package/dist/pipeline/type-defs/audit.d.ts +3 -0
- package/dist/pipeline/type-defs/audit.d.ts.map +1 -1
- package/dist/pipeline/type-defs/checks.d.ts +1 -0
- package/dist/pipeline/type-defs/checks.d.ts.map +1 -1
- package/dist/pipeline/type-defs/packets.d.ts +15 -0
- package/dist/pipeline/type-defs/packets.d.ts.map +1 -1
- package/dist/pipeline/type-defs/state.d.ts +5 -0
- package/dist/pipeline/type-defs/state.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/cli/interactive.ts +4 -1
- package/src/generators/all.ts +23 -1
- package/src/pipeline/artifact-manager.ts +3 -0
- package/src/pipeline/gate-engine.ts +1 -1
- package/src/pipeline/migration.ts +5 -30
- package/src/pipeline/orchestrator.ts +6 -0
- package/src/pipeline/phases/intake.ts +60 -11
- package/src/pipeline/phases/role-planning.ts +2 -3
- package/src/pipeline/skills/constitution-generator.ts +236 -0
- package/src/pipeline/skills/generator.ts +287 -0
- package/src/pipeline/skills/role-map.ts +248 -0
- package/src/pipeline/skills/types.ts +53 -0
- package/src/pipeline/type-defs/artifacts.ts +1 -0
- package/tests/pipeline/migration.test.ts +4 -3
- package/tests/pipeline/skills/constitution-generator.test.ts +201 -0
- package/tests/pipeline/skills/generator.test.ts +213 -0
- package/tests/pipeline/skills/role-map.test.ts +198 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Constitution generator tests — deterministic template generation, skip logic.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
6
|
+
import { mkdirSync, rmSync, existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import {
|
|
9
|
+
generateConstitution,
|
|
10
|
+
shouldSkipConstitution,
|
|
11
|
+
getTechStackSection,
|
|
12
|
+
getArchitectureRules,
|
|
13
|
+
getCodeQualityRules,
|
|
14
|
+
getConstraintsSection,
|
|
15
|
+
} from '../../../src/pipeline/skills/constitution-generator.js';
|
|
16
|
+
import type { ConstitutionContext } from '../../../src/pipeline/skills/types.js';
|
|
17
|
+
|
|
18
|
+
const TEST_DIR = join(process.cwd(), '.test-constitution-gen');
|
|
19
|
+
const SKILLS_DIR = join(TEST_DIR, 'skills');
|
|
20
|
+
|
|
21
|
+
function makeContext(overrides: Partial<ConstitutionContext> = {}): ConstitutionContext {
|
|
22
|
+
return {
|
|
23
|
+
language: 'python',
|
|
24
|
+
projectName: 'TestProject',
|
|
25
|
+
techStack: {
|
|
26
|
+
language: 'Python 3.11+',
|
|
27
|
+
backend: 'FastAPI',
|
|
28
|
+
database: 'PostgreSQL',
|
|
29
|
+
orm: 'SQLAlchemy',
|
|
30
|
+
testing: 'Pytest',
|
|
31
|
+
},
|
|
32
|
+
expandedSpec: 'Build a REST API',
|
|
33
|
+
skillsDir: SKILLS_DIR,
|
|
34
|
+
...overrides,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe('constitution-generator', () => {
|
|
39
|
+
beforeEach(() => {
|
|
40
|
+
if (existsSync(TEST_DIR)) rmSync(TEST_DIR, { recursive: true });
|
|
41
|
+
mkdirSync(SKILLS_DIR, { recursive: true });
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
afterEach(() => {
|
|
45
|
+
if (existsSync(TEST_DIR)) rmSync(TEST_DIR, { recursive: true });
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe('shouldSkipConstitution', () => {
|
|
49
|
+
it('should return false when constitution does not exist', () => {
|
|
50
|
+
expect(shouldSkipConstitution(SKILLS_DIR)).toBe(false);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should return true when constitution already exists', () => {
|
|
54
|
+
writeFileSync(join(SKILLS_DIR, 'POPEYE_CONSTITUTION.md'), 'existing');
|
|
55
|
+
expect(shouldSkipConstitution(SKILLS_DIR)).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('generateConstitution', () => {
|
|
60
|
+
it('should create POPEYE_CONSTITUTION.md', () => {
|
|
61
|
+
generateConstitution(makeContext());
|
|
62
|
+
const path = join(SKILLS_DIR, 'POPEYE_CONSTITUTION.md');
|
|
63
|
+
expect(existsSync(path)).toBe(true);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should include project name in header', () => {
|
|
67
|
+
generateConstitution(makeContext());
|
|
68
|
+
const content = readFileSync(join(SKILLS_DIR, 'POPEYE_CONSTITUTION.md'), 'utf-8');
|
|
69
|
+
expect(content).toContain('# Project Constitution: TestProject');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should include tech stack section', () => {
|
|
73
|
+
generateConstitution(makeContext());
|
|
74
|
+
const content = readFileSync(join(SKILLS_DIR, 'POPEYE_CONSTITUTION.md'), 'utf-8');
|
|
75
|
+
expect(content).toContain('## Tech Stack');
|
|
76
|
+
expect(content).toContain('FastAPI');
|
|
77
|
+
expect(content).toContain('PostgreSQL');
|
|
78
|
+
expect(content).toContain('SQLAlchemy');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should include governance rules', () => {
|
|
82
|
+
generateConstitution(makeContext());
|
|
83
|
+
const content = readFileSync(join(SKILLS_DIR, 'POPEYE_CONSTITUTION.md'), 'utf-8');
|
|
84
|
+
expect(content).toContain('## Governance Rules');
|
|
85
|
+
expect(content).toContain('Consensus threshold: 0.95');
|
|
86
|
+
expect(content).toContain('immutable once stored');
|
|
87
|
+
expect(content).toContain('No placeholder content');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should include immutability notice', () => {
|
|
91
|
+
generateConstitution(makeContext());
|
|
92
|
+
const content = readFileSync(join(SKILLS_DIR, 'POPEYE_CONSTITUTION.md'), 'utf-8');
|
|
93
|
+
expect(content).toContain('## Immutability');
|
|
94
|
+
expect(content).toContain('MUST NOT be modified during pipeline execution');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should not overwrite existing constitution', () => {
|
|
98
|
+
writeFileSync(join(SKILLS_DIR, 'POPEYE_CONSTITUTION.md'), 'hand-written content');
|
|
99
|
+
generateConstitution(makeContext());
|
|
100
|
+
const content = readFileSync(join(SKILLS_DIR, 'POPEYE_CONSTITUTION.md'), 'utf-8');
|
|
101
|
+
expect(content).toBe('hand-written content');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('should include session guidance when provided', () => {
|
|
105
|
+
generateConstitution(makeContext({ sessionGuidance: 'Focus on security' }));
|
|
106
|
+
const content = readFileSync(join(SKILLS_DIR, 'POPEYE_CONSTITUTION.md'), 'utf-8');
|
|
107
|
+
expect(content).toContain('Focus on security');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('should create skills dir if it does not exist', () => {
|
|
111
|
+
rmSync(SKILLS_DIR, { recursive: true });
|
|
112
|
+
expect(existsSync(SKILLS_DIR)).toBe(false);
|
|
113
|
+
generateConstitution(makeContext());
|
|
114
|
+
expect(existsSync(SKILLS_DIR)).toBe(true);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe('getTechStackSection', () => {
|
|
119
|
+
it('should list all available tech stack fields', () => {
|
|
120
|
+
const section = getTechStackSection({
|
|
121
|
+
language: 'Python 3.11+',
|
|
122
|
+
backend: 'FastAPI',
|
|
123
|
+
database: 'PostgreSQL',
|
|
124
|
+
orm: 'SQLAlchemy',
|
|
125
|
+
testing: 'Pytest',
|
|
126
|
+
});
|
|
127
|
+
expect(section).toContain('- Language: Python 3.11+');
|
|
128
|
+
expect(section).toContain('- Framework: FastAPI');
|
|
129
|
+
expect(section).toContain('- Database: PostgreSQL');
|
|
130
|
+
expect(section).toContain('- ORM: SQLAlchemy');
|
|
131
|
+
expect(section).toContain('- Testing: Pytest');
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('should skip undefined fields', () => {
|
|
135
|
+
const section = getTechStackSection({ language: 'Python 3.11+' });
|
|
136
|
+
expect(section).toContain('- Language: Python 3.11+');
|
|
137
|
+
expect(section).not.toContain('Framework');
|
|
138
|
+
expect(section).not.toContain('Database');
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe('getArchitectureRules', () => {
|
|
143
|
+
it('should include FastAPI async rule for FastAPI projects', () => {
|
|
144
|
+
const rules = getArchitectureRules({ backend: 'FastAPI' });
|
|
145
|
+
expect(rules).toContain('async/await');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('should include SQLAlchemy rule for SQLAlchemy projects', () => {
|
|
149
|
+
const rules = getArchitectureRules({ orm: 'SQLAlchemy' });
|
|
150
|
+
expect(rules).toContain('SQLAlchemy ORM');
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('should include Python-specific rules', () => {
|
|
154
|
+
const rules = getArchitectureRules({ language: 'Python 3.11+' });
|
|
155
|
+
expect(rules).toContain('PEP8');
|
|
156
|
+
expect(rules).toContain('python-dotenv');
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('should include TypeScript-specific rules', () => {
|
|
160
|
+
const rules = getArchitectureRules({ language: 'TypeScript 5.x' });
|
|
161
|
+
expect(rules).toContain('strict mode');
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('should provide generic rules when no tech matches', () => {
|
|
165
|
+
const rules = getArchitectureRules({});
|
|
166
|
+
expect(rules).toContain('Environment variables');
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
describe('getCodeQualityRules', () => {
|
|
171
|
+
it('should include file size limit', () => {
|
|
172
|
+
const rules = getCodeQualityRules();
|
|
173
|
+
expect(rules).toContain('500 lines');
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('should include test requirements', () => {
|
|
177
|
+
const rules = getCodeQualityRules();
|
|
178
|
+
expect(rules).toContain('Unit tests');
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
describe('getConstraintsSection', () => {
|
|
183
|
+
it('should include Python constraints for python language', () => {
|
|
184
|
+
const section = getConstraintsSection('python');
|
|
185
|
+
expect(section).toContain('Python 3.11+');
|
|
186
|
+
expect(section).toContain('venv');
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('should include TypeScript constraints for typescript', () => {
|
|
190
|
+
const section = getConstraintsSection('typescript');
|
|
191
|
+
expect(section).toContain('Node.js 18+');
|
|
192
|
+
expect(section).toContain('ESM');
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('should include session guidance when provided', () => {
|
|
196
|
+
const section = getConstraintsSection('python', 'Focus on security');
|
|
197
|
+
expect(section).toContain('Session-Specific Guidance');
|
|
198
|
+
expect(section).toContain('Focus on security');
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
});
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill generator tests — prompt building, parsing, rendering, skip logic.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
6
|
+
import { mkdirSync, rmSync, existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import {
|
|
9
|
+
shouldGenerateSkill,
|
|
10
|
+
buildSkillGenPrompt,
|
|
11
|
+
parseSkillPrompts,
|
|
12
|
+
renderSkillMarkdown,
|
|
13
|
+
writeGenerationMarker,
|
|
14
|
+
} from '../../../src/pipeline/skills/generator.js';
|
|
15
|
+
import type { SkillGenerationContext } from '../../../src/pipeline/skills/types.js';
|
|
16
|
+
import type { RepoSnapshot } from '../../../src/pipeline/types.js';
|
|
17
|
+
|
|
18
|
+
const TEST_DIR = join(process.cwd(), '.test-skill-generator');
|
|
19
|
+
const SKILLS_DIR = join(TEST_DIR, 'skills');
|
|
20
|
+
|
|
21
|
+
function makeSnapshot(): RepoSnapshot {
|
|
22
|
+
return {
|
|
23
|
+
snapshot_id: 'test-snap',
|
|
24
|
+
timestamp: new Date().toISOString(),
|
|
25
|
+
tree_summary: '',
|
|
26
|
+
config_files: [],
|
|
27
|
+
languages_detected: [],
|
|
28
|
+
scripts: {},
|
|
29
|
+
env_files: [],
|
|
30
|
+
migrations_present: false,
|
|
31
|
+
ports_entrypoints: [],
|
|
32
|
+
total_files: 0,
|
|
33
|
+
total_lines: 0,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function makeContext(overrides: Partial<SkillGenerationContext> = {}): SkillGenerationContext {
|
|
38
|
+
return {
|
|
39
|
+
language: 'python',
|
|
40
|
+
expandedSpec: 'Build a REST API for task management',
|
|
41
|
+
snapshot: makeSnapshot(),
|
|
42
|
+
activeRoles: ['DISPATCHER', 'ARCHITECT', 'BACKEND_PROGRAMMER', 'DB_EXPERT'],
|
|
43
|
+
skillsDir: SKILLS_DIR,
|
|
44
|
+
projectName: 'TestProject',
|
|
45
|
+
...overrides,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
describe('generator', () => {
|
|
50
|
+
beforeEach(() => {
|
|
51
|
+
if (existsSync(TEST_DIR)) rmSync(TEST_DIR, { recursive: true });
|
|
52
|
+
mkdirSync(SKILLS_DIR, { recursive: true });
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
afterEach(() => {
|
|
56
|
+
if (existsSync(TEST_DIR)) rmSync(TEST_DIR, { recursive: true });
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('shouldGenerateSkill', () => {
|
|
60
|
+
it('should return true when no .md file exists', () => {
|
|
61
|
+
expect(shouldGenerateSkill(SKILLS_DIR, 'BACKEND_PROGRAMMER')).toBe(true);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should return false when .md file already exists', () => {
|
|
65
|
+
writeFileSync(join(SKILLS_DIR, 'BACKEND_PROGRAMMER.md'), 'existing content');
|
|
66
|
+
expect(shouldGenerateSkill(SKILLS_DIR, 'BACKEND_PROGRAMMER')).toBe(false);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should check per-role independently', () => {
|
|
70
|
+
writeFileSync(join(SKILLS_DIR, 'ARCHITECT.md'), 'existing');
|
|
71
|
+
expect(shouldGenerateSkill(SKILLS_DIR, 'ARCHITECT')).toBe(false);
|
|
72
|
+
expect(shouldGenerateSkill(SKILLS_DIR, 'BACKEND_PROGRAMMER')).toBe(true);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe('buildSkillGenPrompt', () => {
|
|
77
|
+
it('should include project name and tech stack', () => {
|
|
78
|
+
const prompt = buildSkillGenPrompt(
|
|
79
|
+
makeContext(),
|
|
80
|
+
['BACKEND_PROGRAMMER'],
|
|
81
|
+
{ backend: 'FastAPI', language: 'Python 3.11+' },
|
|
82
|
+
);
|
|
83
|
+
expect(prompt).toContain('TestProject');
|
|
84
|
+
expect(prompt).toContain('FastAPI');
|
|
85
|
+
expect(prompt).toContain('Python 3.11+');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should include role descriptions', () => {
|
|
89
|
+
const prompt = buildSkillGenPrompt(
|
|
90
|
+
makeContext(),
|
|
91
|
+
['BACKEND_PROGRAMMER', 'DB_EXPERT'],
|
|
92
|
+
{ backend: 'FastAPI' },
|
|
93
|
+
);
|
|
94
|
+
expect(prompt).toContain('BACKEND_PROGRAMMER');
|
|
95
|
+
expect(prompt).toContain('DB_EXPERT');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should include session guidance when present', () => {
|
|
99
|
+
const prompt = buildSkillGenPrompt(
|
|
100
|
+
makeContext({ sessionGuidance: 'Focus on security' }),
|
|
101
|
+
['BACKEND_PROGRAMMER'],
|
|
102
|
+
{ backend: 'FastAPI' },
|
|
103
|
+
);
|
|
104
|
+
expect(prompt).toContain('Focus on security');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('should include expanded spec', () => {
|
|
108
|
+
const prompt = buildSkillGenPrompt(
|
|
109
|
+
makeContext({ expandedSpec: 'Build a REST API with auth' }),
|
|
110
|
+
['BACKEND_PROGRAMMER'],
|
|
111
|
+
{ backend: 'FastAPI' },
|
|
112
|
+
);
|
|
113
|
+
expect(prompt).toContain('Build a REST API with auth');
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe('parseSkillPrompts', () => {
|
|
118
|
+
it('should parse valid JSON response', () => {
|
|
119
|
+
const response = JSON.stringify({
|
|
120
|
+
BACKEND_PROGRAMMER: 'You are the Backend Programmer for MyProject.',
|
|
121
|
+
DB_EXPERT: 'You are the DB Expert for MyProject.',
|
|
122
|
+
});
|
|
123
|
+
const result = parseSkillPrompts(response, ['BACKEND_PROGRAMMER', 'DB_EXPERT']);
|
|
124
|
+
expect(result.BACKEND_PROGRAMMER).toContain('Backend Programmer');
|
|
125
|
+
expect(result.DB_EXPERT).toContain('DB Expert');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('should extract JSON from markdown code fences', () => {
|
|
129
|
+
const response = '```json\n{"BACKEND_PROGRAMMER": "You are the Backend Programmer."}\n```';
|
|
130
|
+
const result = parseSkillPrompts(response, ['BACKEND_PROGRAMMER']);
|
|
131
|
+
expect(result.BACKEND_PROGRAMMER).toContain('Backend Programmer');
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('should return empty for malformed JSON', () => {
|
|
135
|
+
const result = parseSkillPrompts('not json at all', ['BACKEND_PROGRAMMER']);
|
|
136
|
+
expect(Object.keys(result)).toHaveLength(0);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('should skip roles not in expectedRoles', () => {
|
|
140
|
+
const response = JSON.stringify({
|
|
141
|
+
BACKEND_PROGRAMMER: 'Valid prompt for backend.',
|
|
142
|
+
FRONTEND_PROGRAMMER: 'Should be ignored.',
|
|
143
|
+
});
|
|
144
|
+
const result = parseSkillPrompts(response, ['BACKEND_PROGRAMMER']);
|
|
145
|
+
expect(result.BACKEND_PROGRAMMER).toBeDefined();
|
|
146
|
+
expect(result.FRONTEND_PROGRAMMER).toBeUndefined();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('should skip prompts shorter than 10 chars', () => {
|
|
150
|
+
const response = JSON.stringify({
|
|
151
|
+
BACKEND_PROGRAMMER: 'Short',
|
|
152
|
+
});
|
|
153
|
+
const result = parseSkillPrompts(response, ['BACKEND_PROGRAMMER']);
|
|
154
|
+
expect(result.BACKEND_PROGRAMMER).toBeUndefined();
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
describe('renderSkillMarkdown', () => {
|
|
159
|
+
it('should produce valid YAML frontmatter format', () => {
|
|
160
|
+
const md = renderSkillMarkdown(
|
|
161
|
+
'BACKEND_PROGRAMMER',
|
|
162
|
+
'You are the Backend Programmer.',
|
|
163
|
+
['follow_architecture', 'must_follow_master_plan'],
|
|
164
|
+
['endpoints', 'services'],
|
|
165
|
+
['ARCHITECT'],
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
expect(md).toMatch(/^---\n/);
|
|
169
|
+
expect(md).toContain('role: BACKEND_PROGRAMMER');
|
|
170
|
+
expect(md).toContain('version: 1.0-project');
|
|
171
|
+
expect(md).toContain(' - endpoints');
|
|
172
|
+
expect(md).toContain(' - services');
|
|
173
|
+
expect(md).toContain(' - follow_architecture');
|
|
174
|
+
expect(md).toContain(' - must_follow_master_plan');
|
|
175
|
+
expect(md).toContain('depends_on:');
|
|
176
|
+
expect(md).toContain(' - ARCHITECT');
|
|
177
|
+
expect(md).toContain('You are the Backend Programmer.');
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('should omit depends_on when empty', () => {
|
|
181
|
+
const md = renderSkillMarkdown(
|
|
182
|
+
'DISPATCHER',
|
|
183
|
+
'You are the Dispatcher.',
|
|
184
|
+
['governance'],
|
|
185
|
+
['phase_transition'],
|
|
186
|
+
[],
|
|
187
|
+
);
|
|
188
|
+
expect(md).not.toContain('depends_on:');
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
describe('writeGenerationMarker', () => {
|
|
193
|
+
it('should write valid JSON marker file', () => {
|
|
194
|
+
const marker = {
|
|
195
|
+
timestamp: '2026-02-22T14:30:00Z',
|
|
196
|
+
pipelineVersion: '1.0',
|
|
197
|
+
activeRoles: ['DISPATCHER', 'BACKEND_PROGRAMMER'],
|
|
198
|
+
techStack: { backend: 'FastAPI' },
|
|
199
|
+
aiGenerated: true,
|
|
200
|
+
};
|
|
201
|
+
writeGenerationMarker(SKILLS_DIR, marker);
|
|
202
|
+
|
|
203
|
+
const markerPath = join(SKILLS_DIR, '.popeye-skills-generated.json');
|
|
204
|
+
expect(existsSync(markerPath)).toBe(true);
|
|
205
|
+
|
|
206
|
+
const content = JSON.parse(readFileSync(markerPath, 'utf-8'));
|
|
207
|
+
expect(content.pipelineVersion).toBe('1.0');
|
|
208
|
+
expect(content.aiGenerated).toBe(true);
|
|
209
|
+
expect(content.activeRoles).toContain('BACKEND_PROGRAMMER');
|
|
210
|
+
expect(content.techStack.backend).toBe('FastAPI');
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
});
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Role map tests — role selection, tech stack inference, template constraints.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect } from 'vitest';
|
|
6
|
+
import {
|
|
7
|
+
getActiveRoles,
|
|
8
|
+
inferTechStack,
|
|
9
|
+
getTemplateConstraints,
|
|
10
|
+
SUPPORT_ROLES,
|
|
11
|
+
IMPLEMENTATION_ROLES,
|
|
12
|
+
} from '../../../src/pipeline/skills/role-map.js';
|
|
13
|
+
import type { RepoSnapshot } from '../../../src/pipeline/types.js';
|
|
14
|
+
import type { OutputLanguage } from '../../../src/types/project.js';
|
|
15
|
+
|
|
16
|
+
function makeSnapshot(overrides: Partial<RepoSnapshot> = {}): RepoSnapshot {
|
|
17
|
+
return {
|
|
18
|
+
snapshot_id: 'test-snap',
|
|
19
|
+
timestamp: new Date().toISOString(),
|
|
20
|
+
tree_summary: '',
|
|
21
|
+
config_files: [],
|
|
22
|
+
languages_detected: [],
|
|
23
|
+
scripts: {},
|
|
24
|
+
env_files: [],
|
|
25
|
+
migrations_present: false,
|
|
26
|
+
ports_entrypoints: [],
|
|
27
|
+
total_files: 0,
|
|
28
|
+
total_lines: 0,
|
|
29
|
+
...overrides,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe('role-map', () => {
|
|
34
|
+
describe('getActiveRoles', () => {
|
|
35
|
+
it('should include support roles for all languages', () => {
|
|
36
|
+
const languages: OutputLanguage[] = ['python', 'typescript', 'fullstack', 'website', 'all'];
|
|
37
|
+
for (const lang of languages) {
|
|
38
|
+
const roles = getActiveRoles(lang);
|
|
39
|
+
for (const support of SUPPORT_ROLES) {
|
|
40
|
+
expect(roles).toContain(support);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should include DB_EXPERT for python', () => {
|
|
46
|
+
const roles = getActiveRoles('python');
|
|
47
|
+
expect(roles).toContain('DB_EXPERT');
|
|
48
|
+
expect(roles).toContain('BACKEND_PROGRAMMER');
|
|
49
|
+
expect(roles).not.toContain('FRONTEND_PROGRAMMER');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should include FRONTEND_PROGRAMMER for typescript (not BACKEND_PROGRAMMER)', () => {
|
|
53
|
+
const roles = getActiveRoles('typescript');
|
|
54
|
+
expect(roles).toContain('FRONTEND_PROGRAMMER');
|
|
55
|
+
expect(roles).toContain('UI_UX_SPECIALIST');
|
|
56
|
+
expect(roles).not.toContain('BACKEND_PROGRAMMER');
|
|
57
|
+
expect(roles).not.toContain('DB_EXPERT');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should include both FE and BE for fullstack', () => {
|
|
61
|
+
const roles = getActiveRoles('fullstack');
|
|
62
|
+
expect(roles).toContain('DB_EXPERT');
|
|
63
|
+
expect(roles).toContain('BACKEND_PROGRAMMER');
|
|
64
|
+
expect(roles).toContain('FRONTEND_PROGRAMMER');
|
|
65
|
+
expect(roles).toContain('UI_UX_SPECIALIST');
|
|
66
|
+
expect(roles).not.toContain('WEBSITE_PROGRAMMER');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should include website and marketing roles for website', () => {
|
|
70
|
+
const roles = getActiveRoles('website');
|
|
71
|
+
expect(roles).toContain('WEBSITE_PROGRAMMER');
|
|
72
|
+
expect(roles).toContain('UI_UX_SPECIALIST');
|
|
73
|
+
expect(roles).toContain('MARKETING_EXPERT');
|
|
74
|
+
expect(roles).toContain('SOCIAL_EXPERT');
|
|
75
|
+
expect(roles).not.toContain('BACKEND_PROGRAMMER');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should include all implementation roles for all', () => {
|
|
79
|
+
const roles = getActiveRoles('all');
|
|
80
|
+
for (const impl of IMPLEMENTATION_ROLES) {
|
|
81
|
+
expect(roles).toContain(impl);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe('inferTechStack', () => {
|
|
87
|
+
it('should return language defaults when no signals', () => {
|
|
88
|
+
const ts = inferTechStack('python');
|
|
89
|
+
expect(ts.backend).toBe('FastAPI');
|
|
90
|
+
expect(ts.database).toBe('PostgreSQL');
|
|
91
|
+
expect(ts.orm).toBe('SQLAlchemy');
|
|
92
|
+
expect(ts.testing).toBe('Pytest');
|
|
93
|
+
expect(ts.language).toBe('Python 3.11+');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should detect FastAPI from snapshot key_fields', () => {
|
|
97
|
+
const snapshot = makeSnapshot({
|
|
98
|
+
config_files: [{
|
|
99
|
+
path: 'pyproject.toml',
|
|
100
|
+
type: 'toml',
|
|
101
|
+
content_hash: 'abc',
|
|
102
|
+
key_fields: { dependencies: ['fastapi', 'uvicorn'] },
|
|
103
|
+
}],
|
|
104
|
+
});
|
|
105
|
+
const ts = inferTechStack('python', snapshot);
|
|
106
|
+
expect(ts.backend).toBe('FastAPI');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should detect Django from snapshot key_fields', () => {
|
|
110
|
+
const snapshot = makeSnapshot({
|
|
111
|
+
config_files: [{
|
|
112
|
+
path: 'requirements.txt',
|
|
113
|
+
type: 'txt',
|
|
114
|
+
content_hash: 'abc',
|
|
115
|
+
key_fields: { packages: ['django', 'django-rest-framework'] },
|
|
116
|
+
}],
|
|
117
|
+
});
|
|
118
|
+
const ts = inferTechStack('python', snapshot);
|
|
119
|
+
expect(ts.backend).toBe('Django');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('should detect framework from expanded spec when snapshot has no deps', () => {
|
|
123
|
+
const ts = inferTechStack('python', makeSnapshot(), 'Build a Django REST API');
|
|
124
|
+
expect(ts.backend).toBe('Django');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('should prioritize snapshot over spec mentions', () => {
|
|
128
|
+
const snapshot = makeSnapshot({
|
|
129
|
+
config_files: [{
|
|
130
|
+
path: 'pyproject.toml',
|
|
131
|
+
type: 'toml',
|
|
132
|
+
content_hash: 'abc',
|
|
133
|
+
key_fields: { dependencies: ['fastapi'] },
|
|
134
|
+
}],
|
|
135
|
+
});
|
|
136
|
+
const ts = inferTechStack('python', snapshot, 'Build a Django API');
|
|
137
|
+
// Snapshot has fastapi, spec mentions Django — snapshot wins
|
|
138
|
+
expect(ts.backend).toBe('FastAPI');
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('should return typescript defaults', () => {
|
|
142
|
+
const ts = inferTechStack('typescript');
|
|
143
|
+
expect(ts.frontend).toBe('React + Vite');
|
|
144
|
+
expect(ts.testing).toBe('Vitest');
|
|
145
|
+
expect(ts.language).toBe('TypeScript 5.x');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('should detect Next.js from snapshot', () => {
|
|
149
|
+
const snapshot = makeSnapshot({
|
|
150
|
+
config_files: [{
|
|
151
|
+
path: 'package.json',
|
|
152
|
+
type: 'json',
|
|
153
|
+
content_hash: 'abc',
|
|
154
|
+
key_fields: { dependencies: { next: '^14.0.0', react: '^18.0.0' } },
|
|
155
|
+
}],
|
|
156
|
+
});
|
|
157
|
+
const ts = inferTechStack('typescript', snapshot);
|
|
158
|
+
expect(ts.frontend).toBe('Next.js');
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
describe('getTemplateConstraints', () => {
|
|
163
|
+
it('should always include governance constraints', () => {
|
|
164
|
+
const constraints = getTemplateConstraints('BACKEND_PROGRAMMER', {});
|
|
165
|
+
expect(constraints).toContain('must_follow_master_plan');
|
|
166
|
+
expect(constraints).toContain('must_follow_architecture');
|
|
167
|
+
expect(constraints).toContain('conflicts_require_change_request');
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('should add FastAPI constraints for backend with FastAPI', () => {
|
|
171
|
+
const constraints = getTemplateConstraints('BACKEND_PROGRAMMER', {
|
|
172
|
+
backend: 'FastAPI',
|
|
173
|
+
testing: 'Pytest',
|
|
174
|
+
});
|
|
175
|
+
expect(constraints).toContain('fastapi_async_required');
|
|
176
|
+
expect(constraints).toContain('pydantic_validation');
|
|
177
|
+
expect(constraints).toContain('pytest_testing');
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('should add React constraints for frontend', () => {
|
|
181
|
+
const constraints = getTemplateConstraints('FRONTEND_PROGRAMMER', {
|
|
182
|
+
frontend: 'React + Vite',
|
|
183
|
+
testing: 'Vitest',
|
|
184
|
+
});
|
|
185
|
+
expect(constraints).toContain('react_component_pattern');
|
|
186
|
+
expect(constraints).toContain('component_testing');
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('should return only governance constraints for roles without tech constraints', () => {
|
|
190
|
+
const constraints = getTemplateConstraints('DISPATCHER', {});
|
|
191
|
+
expect(constraints).toEqual([
|
|
192
|
+
'must_follow_master_plan',
|
|
193
|
+
'must_follow_architecture',
|
|
194
|
+
'conflicts_require_change_request',
|
|
195
|
+
]);
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
});
|