popeye-cli 2.0.0 → 2.1.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/CHANGELOG.md +55 -0
- package/CONTRIBUTING.md +23 -2
- package/README.md +47 -18
- package/dist/adapters/gemini.js +3 -3
- package/dist/adapters/openai.js +2 -2
- package/dist/adapters/openai.js.map +1 -1
- package/dist/auth/gemini.js +1 -1
- package/dist/cli/commands/create.d.ts.map +1 -1
- package/dist/cli/commands/create.js +11 -5
- package/dist/cli/commands/create.js.map +1 -1
- package/dist/cli/commands/resume.d.ts.map +1 -1
- package/dist/cli/commands/resume.js +9 -1
- package/dist/cli/commands/resume.js.map +1 -1
- package/dist/cli/interactive.d.ts.map +1 -1
- package/dist/cli/interactive.js +29 -3
- package/dist/cli/interactive.js.map +1 -1
- package/dist/config/defaults.d.ts.map +1 -1
- package/dist/config/defaults.js +7 -2
- package/dist/config/defaults.js.map +1 -1
- package/dist/config/index.d.ts +1 -7
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/popeye-md.d.ts +32 -0
- package/dist/config/popeye-md.d.ts.map +1 -0
- package/dist/config/popeye-md.js +111 -0
- package/dist/config/popeye-md.js.map +1 -0
- package/dist/config/schema.d.ts +3 -21
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +21 -8
- package/dist/config/schema.js.map +1 -1
- package/dist/pipeline/bridges/review-bridge.d.ts +70 -0
- package/dist/pipeline/bridges/review-bridge.d.ts.map +1 -0
- package/dist/pipeline/bridges/review-bridge.js +266 -0
- package/dist/pipeline/bridges/review-bridge.js.map +1 -0
- package/dist/pipeline/consensus/consensus-runner.js +3 -3
- package/dist/pipeline/consensus/consensus-runner.js.map +1 -1
- package/dist/pipeline/orchestrator.d.ts +2 -0
- package/dist/pipeline/orchestrator.d.ts.map +1 -1
- package/dist/pipeline/orchestrator.js +5 -1
- package/dist/pipeline/orchestrator.js.map +1 -1
- package/dist/pipeline/phases/implementation.d.ts.map +1 -1
- package/dist/pipeline/phases/implementation.js +5 -2
- package/dist/pipeline/phases/implementation.js.map +1 -1
- package/dist/pipeline/phases/intake.d.ts.map +1 -1
- package/dist/pipeline/phases/intake.js +13 -4
- package/dist/pipeline/phases/intake.js.map +1 -1
- package/dist/pipeline/phases/recovery-loop.d.ts.map +1 -1
- package/dist/pipeline/phases/recovery-loop.js +2 -0
- package/dist/pipeline/phases/recovery-loop.js.map +1 -1
- 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 +6 -0
- package/dist/pipeline/type-defs/state.d.ts.map +1 -1
- package/dist/pipeline/type-defs/state.js +2 -0
- package/dist/pipeline/type-defs/state.js.map +1 -1
- package/dist/types/consensus.d.ts +5 -1
- package/dist/types/consensus.d.ts.map +1 -1
- package/dist/types/consensus.js +15 -4
- package/dist/types/consensus.js.map +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +1 -1
- package/dist/types/index.js.map +1 -1
- package/dist/types/project.d.ts +1 -1
- package/dist/types/project.d.ts.map +1 -1
- package/dist/types/project.js +39 -10
- package/dist/types/project.js.map +1 -1
- package/dist/types/workflow.d.ts +1 -7
- package/dist/types/workflow.d.ts.map +1 -1
- package/dist/types/workflow.js +1 -1
- package/dist/types/workflow.js.map +1 -1
- package/dist/upgrade/handlers.js +5 -5
- package/dist/upgrade/handlers.js.map +1 -1
- package/dist/workflow/index.d.ts.map +1 -1
- package/dist/workflow/index.js +18 -14
- package/dist/workflow/index.js.map +1 -1
- package/dist/workflow/website-strategy.js +1 -1
- package/dist/workflow/website-strategy.js.map +1 -1
- package/package.json +1 -1
- package/src/adapters/gemini.ts +3 -3
- package/src/adapters/openai.ts +2 -2
- package/src/auth/gemini.ts +1 -1
- package/src/cli/commands/create.ts +12 -6
- package/src/cli/commands/resume.ts +9 -1
- package/src/cli/interactive.ts +32 -3
- package/src/config/defaults.ts +7 -2
- package/src/config/popeye-md.ts +139 -0
- package/src/config/schema.ts +21 -8
- package/src/pipeline/bridges/review-bridge.ts +371 -0
- package/src/pipeline/consensus/consensus-runner.ts +3 -3
- package/src/pipeline/orchestrator.ts +8 -0
- package/src/pipeline/phases/implementation.ts +6 -2
- package/src/pipeline/phases/intake.ts +18 -4
- package/src/pipeline/phases/recovery-loop.ts +2 -0
- package/src/pipeline/type-defs/artifacts.ts +1 -0
- package/src/pipeline/type-defs/state.ts +2 -0
- package/src/types/consensus.ts +16 -4
- package/src/types/index.ts +1 -0
- package/src/types/project.ts +39 -10
- package/src/types/workflow.ts +1 -1
- package/src/upgrade/handlers.ts +5 -5
- package/src/workflow/index.ts +18 -14
- package/src/workflow/website-strategy.ts +1 -1
- package/tests/cli/model-command.test.ts +19 -9
- package/tests/config/config.test.ts +3 -3
- package/tests/config/popeye-md.test.ts +168 -0
- package/tests/pipeline/bridges/review-bridge.test.ts +243 -0
- package/tests/pipeline/session-guidance.test.ts +205 -0
- package/tests/types/consensus.test.ts +1 -1
- package/tests/workflow/pipeline-bootstrap.test.ts +162 -0
package/src/upgrade/handlers.ts
CHANGED
|
@@ -349,7 +349,7 @@ export async function upgradeFullstackToAll(
|
|
|
349
349
|
idea: 'Marketing website',
|
|
350
350
|
name: projectName,
|
|
351
351
|
language: 'all',
|
|
352
|
-
openaiModel: 'gpt-
|
|
352
|
+
openaiModel: 'gpt-4.1',
|
|
353
353
|
};
|
|
354
354
|
|
|
355
355
|
// Build content context from user docs, brand assets, and strategy
|
|
@@ -421,7 +421,7 @@ export async function upgradeSingleToFullstack(
|
|
|
421
421
|
if (!(await pathExists(frontendDir))) {
|
|
422
422
|
const spec: ProjectSpec = {
|
|
423
423
|
idea: 'Frontend application', name: projectName,
|
|
424
|
-
language: 'fullstack', openaiModel: 'gpt-
|
|
424
|
+
language: 'fullstack', openaiModel: 'gpt-4.1',
|
|
425
425
|
};
|
|
426
426
|
const result = await generateTypeScriptProject(spec, path.join(projectDir, 'apps'), {
|
|
427
427
|
baseDir: frontendDir,
|
|
@@ -439,7 +439,7 @@ export async function upgradeSingleToFullstack(
|
|
|
439
439
|
if (!(await pathExists(backendDir))) {
|
|
440
440
|
const spec: ProjectSpec = {
|
|
441
441
|
idea: 'Backend API', name: projectName,
|
|
442
|
-
language: 'fullstack', openaiModel: 'gpt-
|
|
442
|
+
language: 'fullstack', openaiModel: 'gpt-4.1',
|
|
443
443
|
};
|
|
444
444
|
const result = await generatePythonProject(spec, path.join(projectDir, 'apps'), {
|
|
445
445
|
baseDir: backendDir,
|
|
@@ -508,7 +508,7 @@ export async function upgradeWebsiteToAll(
|
|
|
508
508
|
if (!(await pathExists(frontendDir))) {
|
|
509
509
|
const spec: ProjectSpec = {
|
|
510
510
|
idea: 'Frontend application', name: projectName,
|
|
511
|
-
language: 'all', openaiModel: 'gpt-
|
|
511
|
+
language: 'all', openaiModel: 'gpt-4.1',
|
|
512
512
|
};
|
|
513
513
|
const result = await generateTypeScriptProject(spec, path.join(projectDir, 'apps'), {
|
|
514
514
|
baseDir: frontendDir,
|
|
@@ -520,7 +520,7 @@ export async function upgradeWebsiteToAll(
|
|
|
520
520
|
if (!(await pathExists(backendDir))) {
|
|
521
521
|
const spec: ProjectSpec = {
|
|
522
522
|
idea: 'Backend API', name: projectName,
|
|
523
|
-
language: 'all', openaiModel: 'gpt-
|
|
523
|
+
language: 'all', openaiModel: 'gpt-4.1',
|
|
524
524
|
};
|
|
525
525
|
const result = await generatePythonProject(spec, path.join(projectDir, 'apps'), {
|
|
526
526
|
baseDir: backendDir,
|
package/src/workflow/index.ts
CHANGED
|
@@ -104,21 +104,24 @@ export async function runWorkflow(
|
|
|
104
104
|
const useLegacy = process.env.POPEYE_LEGACY_WORKFLOW === '1' || process.env.POPEYE_LEGACY_WORKFLOW === 'true';
|
|
105
105
|
if (!useLegacy) {
|
|
106
106
|
try {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
consensusConfig,
|
|
113
|
-
onPhaseStart: (phase) => onProgress?.('pipeline', `Starting phase: ${phase}`),
|
|
114
|
-
onProgress: (msg) => onProgress?.('pipeline', msg),
|
|
115
|
-
});
|
|
116
|
-
return {
|
|
117
|
-
success: result.success,
|
|
118
|
-
state: await loadProject(projectDir).catch(() => ({} as ProjectState)),
|
|
119
|
-
error: result.error,
|
|
120
|
-
};
|
|
107
|
+
// Bootstrap state if it doesn't exist yet (new projects)
|
|
108
|
+
let state = await loadProject(projectDir).catch(() => null);
|
|
109
|
+
if (!state) {
|
|
110
|
+
const { createProject } = await import('../state/index.js');
|
|
111
|
+
state = await createProject(spec, projectDir);
|
|
121
112
|
}
|
|
113
|
+
const result = await runPipeline({
|
|
114
|
+
projectDir,
|
|
115
|
+
state,
|
|
116
|
+
consensusConfig,
|
|
117
|
+
onPhaseStart: (phase) => onProgress?.('pipeline', `Starting phase: ${phase}`),
|
|
118
|
+
onProgress: (msg) => onProgress?.('pipeline', msg),
|
|
119
|
+
});
|
|
120
|
+
return {
|
|
121
|
+
success: result.success,
|
|
122
|
+
state: await loadProject(projectDir).catch(() => ({} as ProjectState)),
|
|
123
|
+
error: result.error,
|
|
124
|
+
};
|
|
122
125
|
} catch {
|
|
123
126
|
// Fall through to legacy workflow on pipeline error
|
|
124
127
|
onProgress?.('workflow', 'Pipeline mode failed, falling back to legacy workflow...');
|
|
@@ -241,6 +244,7 @@ export async function resumeWorkflow(
|
|
|
241
244
|
projectDir,
|
|
242
245
|
state,
|
|
243
246
|
consensusConfig,
|
|
247
|
+
additionalContext,
|
|
244
248
|
onPhaseStart: (phase) => onProgress?.('pipeline', `Resuming phase: ${phase}`),
|
|
245
249
|
onProgress: (msg) => onProgress?.('pipeline', msg),
|
|
246
250
|
});
|
|
@@ -142,7 +142,7 @@ Respond with ONLY valid JSON, no markdown code fences or explanation.`;
|
|
|
142
142
|
onProgress?.('Generating website strategy via AI...');
|
|
143
143
|
|
|
144
144
|
const completion = await client.chat.completions.create({
|
|
145
|
-
model: 'gpt-
|
|
145
|
+
model: 'gpt-4.1',
|
|
146
146
|
messages: [{ role: 'user', content: prompt }],
|
|
147
147
|
temperature: 0.4,
|
|
148
148
|
max_tokens: 4096,
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { describe, it, expect } from 'vitest';
|
|
7
7
|
import { OpenAIModelSchema, KNOWN_OPENAI_MODELS } from '../../src/types/project.js';
|
|
8
|
-
import { GeminiModelSchema, GrokModelSchema, KNOWN_GEMINI_MODELS } from '../../src/types/consensus.js';
|
|
8
|
+
import { GeminiModelSchema, GrokModelSchema, KNOWN_GEMINI_MODELS, KNOWN_GROK_MODELS } from '../../src/types/consensus.js';
|
|
9
9
|
|
|
10
10
|
describe('OpenAI model validation', () => {
|
|
11
11
|
it('should accept known OpenAI models', () => {
|
|
@@ -17,7 +17,7 @@ describe('OpenAI model validation', () => {
|
|
|
17
17
|
it('should accept unknown/new OpenAI models (flexible)', () => {
|
|
18
18
|
expect(OpenAIModelSchema.safeParse('gpt-5').success).toBe(true);
|
|
19
19
|
expect(OpenAIModelSchema.safeParse('gpt-5.2-turbo').success).toBe(true);
|
|
20
|
-
expect(OpenAIModelSchema.safeParse('
|
|
20
|
+
expect(OpenAIModelSchema.safeParse('some-future-model').success).toBe(true);
|
|
21
21
|
});
|
|
22
22
|
|
|
23
23
|
it('should reject empty string', () => {
|
|
@@ -33,8 +33,8 @@ describe('Gemini model validation', () => {
|
|
|
33
33
|
});
|
|
34
34
|
|
|
35
35
|
it('should accept unknown/new Gemini models (flexible)', () => {
|
|
36
|
-
expect(GeminiModelSchema.safeParse('gemini-2.5-pro').success).toBe(true);
|
|
37
36
|
expect(GeminiModelSchema.safeParse('gemini-3.0-ultra').success).toBe(true);
|
|
37
|
+
expect(GeminiModelSchema.safeParse('gemini-4.0-flash').success).toBe(true);
|
|
38
38
|
});
|
|
39
39
|
|
|
40
40
|
it('should reject empty string', () => {
|
|
@@ -44,9 +44,9 @@ describe('Gemini model validation', () => {
|
|
|
44
44
|
|
|
45
45
|
describe('Grok model validation', () => {
|
|
46
46
|
it('should accept any non-empty string as Grok model', () => {
|
|
47
|
+
expect(GrokModelSchema.safeParse('grok-4-0709').success).toBe(true);
|
|
47
48
|
expect(GrokModelSchema.safeParse('grok-3').success).toBe(true);
|
|
48
49
|
expect(GrokModelSchema.safeParse('grok-3-mini').success).toBe(true);
|
|
49
|
-
expect(GrokModelSchema.safeParse('grok-2').success).toBe(true);
|
|
50
50
|
expect(GrokModelSchema.safeParse('some-future-model').success).toBe(true);
|
|
51
51
|
});
|
|
52
52
|
|
|
@@ -62,15 +62,25 @@ describe('Grok model validation', () => {
|
|
|
62
62
|
|
|
63
63
|
describe('known models lists', () => {
|
|
64
64
|
it('should have known OpenAI models', () => {
|
|
65
|
+
expect(KNOWN_OPENAI_MODELS).toContain('gpt-4.1');
|
|
65
66
|
expect(KNOWN_OPENAI_MODELS).toContain('gpt-4o');
|
|
66
|
-
expect(KNOWN_OPENAI_MODELS).toContain('
|
|
67
|
-
expect(KNOWN_OPENAI_MODELS
|
|
67
|
+
expect(KNOWN_OPENAI_MODELS).toContain('o3');
|
|
68
|
+
expect(KNOWN_OPENAI_MODELS).toContain('o4-mini');
|
|
69
|
+
expect(KNOWN_OPENAI_MODELS.length).toBeGreaterThanOrEqual(8);
|
|
68
70
|
});
|
|
69
71
|
|
|
70
72
|
it('should have known Gemini models', () => {
|
|
73
|
+
expect(KNOWN_GEMINI_MODELS).toContain('gemini-2.5-flash');
|
|
74
|
+
expect(KNOWN_GEMINI_MODELS).toContain('gemini-2.5-pro');
|
|
71
75
|
expect(KNOWN_GEMINI_MODELS).toContain('gemini-2.0-flash');
|
|
72
|
-
expect(KNOWN_GEMINI_MODELS).
|
|
73
|
-
|
|
76
|
+
expect(KNOWN_GEMINI_MODELS.length).toBeGreaterThanOrEqual(5);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should have known Grok models', () => {
|
|
80
|
+
expect(KNOWN_GROK_MODELS).toContain('grok-4-0709');
|
|
81
|
+
expect(KNOWN_GROK_MODELS).toContain('grok-3');
|
|
82
|
+
expect(KNOWN_GROK_MODELS).toContain('grok-3-mini');
|
|
83
|
+
expect(KNOWN_GROK_MODELS.length).toBeGreaterThanOrEqual(4);
|
|
74
84
|
});
|
|
75
85
|
});
|
|
76
86
|
|
|
@@ -84,7 +94,7 @@ describe('backward compatibility', () => {
|
|
|
84
94
|
});
|
|
85
95
|
|
|
86
96
|
it('should not auto-detect non-OpenAI models as known OpenAI', () => {
|
|
87
|
-
const nonOpenAI = ['gemini-2.
|
|
97
|
+
const nonOpenAI = ['gemini-2.5-flash', 'grok-3', 'grok-4-0709'];
|
|
88
98
|
for (const model of nonOpenAI) {
|
|
89
99
|
const isKnown = (KNOWN_OPENAI_MODELS as readonly string[]).includes(model);
|
|
90
100
|
expect(isKnown).toBe(false);
|
|
@@ -23,7 +23,7 @@ describe('DEFAULT_CONFIG', () => {
|
|
|
23
23
|
});
|
|
24
24
|
|
|
25
25
|
it('should have valid API defaults', () => {
|
|
26
|
-
expect(DEFAULT_CONFIG.apis.openai.model).toBe('gpt-
|
|
26
|
+
expect(DEFAULT_CONFIG.apis.openai.model).toBe('gpt-4.1');
|
|
27
27
|
expect(DEFAULT_CONFIG.apis.openai.temperature).toBe(0.3);
|
|
28
28
|
expect(DEFAULT_CONFIG.apis.openai.max_tokens).toBe(4096);
|
|
29
29
|
});
|
|
@@ -122,11 +122,11 @@ describe('ConfigSchema', () => {
|
|
|
122
122
|
expect(result.success).toBe(false);
|
|
123
123
|
});
|
|
124
124
|
|
|
125
|
-
it('should reject
|
|
125
|
+
it('should reject empty model string', () => {
|
|
126
126
|
const config = {
|
|
127
127
|
apis: {
|
|
128
128
|
openai: {
|
|
129
|
-
model: '
|
|
129
|
+
model: '',
|
|
130
130
|
},
|
|
131
131
|
},
|
|
132
132
|
};
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fix C tests — readPopeyeMdConfig shared config reader.
|
|
3
|
+
* Verifies popeye.md parsing for CLI commands.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
7
|
+
import { promises as fs } from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import os from 'node:os';
|
|
10
|
+
import { readPopeyeMdConfig } from '../../src/config/popeye-md.js';
|
|
11
|
+
|
|
12
|
+
describe('Fix C: readPopeyeMdConfig', () => {
|
|
13
|
+
let tmpDir: string;
|
|
14
|
+
|
|
15
|
+
beforeEach(async () => {
|
|
16
|
+
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'popeye-md-test-'));
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterEach(async () => {
|
|
20
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should return null when popeye.md does not exist', async () => {
|
|
24
|
+
const config = await readPopeyeMdConfig(tmpDir);
|
|
25
|
+
expect(config).toBeNull();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should parse basic config with reviewer and language', async () => {
|
|
29
|
+
await fs.writeFile(
|
|
30
|
+
path.join(tmpDir, 'popeye.md'),
|
|
31
|
+
[
|
|
32
|
+
'---',
|
|
33
|
+
'language: python',
|
|
34
|
+
'reviewer: openai',
|
|
35
|
+
'arbitrator: gemini',
|
|
36
|
+
'---',
|
|
37
|
+
'',
|
|
38
|
+
'# Project Config',
|
|
39
|
+
].join('\n'),
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
const config = await readPopeyeMdConfig(tmpDir);
|
|
43
|
+
expect(config).not.toBeNull();
|
|
44
|
+
expect(config!.language).toBe('python');
|
|
45
|
+
expect(config!.reviewer).toBe('openai');
|
|
46
|
+
expect(config!.arbitrator).toBe('gemini');
|
|
47
|
+
expect(config!.enableArbitration).toBe(true);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should parse model fields from popeye.md', async () => {
|
|
51
|
+
await fs.writeFile(
|
|
52
|
+
path.join(tmpDir, 'popeye.md'),
|
|
53
|
+
[
|
|
54
|
+
'---',
|
|
55
|
+
'language: typescript',
|
|
56
|
+
'reviewer: gemini',
|
|
57
|
+
'arbitrator: grok',
|
|
58
|
+
'openaiModel: gpt-4o-mini',
|
|
59
|
+
'geminiModel: gemini-2.0-flash',
|
|
60
|
+
'grokModel: grok-3',
|
|
61
|
+
'---',
|
|
62
|
+
].join('\n'),
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const config = await readPopeyeMdConfig(tmpDir);
|
|
66
|
+
expect(config).not.toBeNull();
|
|
67
|
+
expect(config!.openaiModel).toBe('gpt-4o-mini');
|
|
68
|
+
expect(config!.geminiModel).toBe('gemini-2.0-flash');
|
|
69
|
+
expect(config!.grokModel).toBe('grok-3');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should handle arbitrator: off', async () => {
|
|
73
|
+
await fs.writeFile(
|
|
74
|
+
path.join(tmpDir, 'popeye.md'),
|
|
75
|
+
[
|
|
76
|
+
'---',
|
|
77
|
+
'language: fullstack',
|
|
78
|
+
'reviewer: openai',
|
|
79
|
+
'arbitrator: off',
|
|
80
|
+
'---',
|
|
81
|
+
].join('\n'),
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
const config = await readPopeyeMdConfig(tmpDir);
|
|
85
|
+
expect(config).not.toBeNull();
|
|
86
|
+
expect(config!.enableArbitration).toBe(false);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('should return null when frontmatter is missing', async () => {
|
|
90
|
+
await fs.writeFile(
|
|
91
|
+
path.join(tmpDir, 'popeye.md'),
|
|
92
|
+
'# Just a markdown file\n\nNo frontmatter here.',
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
const config = await readPopeyeMdConfig(tmpDir);
|
|
96
|
+
expect(config).toBeNull();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('should return null when essential fields are missing', async () => {
|
|
100
|
+
await fs.writeFile(
|
|
101
|
+
path.join(tmpDir, 'popeye.md'),
|
|
102
|
+
[
|
|
103
|
+
'---',
|
|
104
|
+
'projectName: my-app',
|
|
105
|
+
'created: 2024-01-01',
|
|
106
|
+
'---',
|
|
107
|
+
].join('\n'),
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
const config = await readPopeyeMdConfig(tmpDir);
|
|
111
|
+
// Missing language and reviewer -> null
|
|
112
|
+
expect(config).toBeNull();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('should extract notes section', async () => {
|
|
116
|
+
await fs.writeFile(
|
|
117
|
+
path.join(tmpDir, 'popeye.md'),
|
|
118
|
+
[
|
|
119
|
+
'---',
|
|
120
|
+
'language: website',
|
|
121
|
+
'reviewer: openai',
|
|
122
|
+
'---',
|
|
123
|
+
'',
|
|
124
|
+
'## Notes',
|
|
125
|
+
'This project uses Tailwind CSS.',
|
|
126
|
+
'Deploy to Vercel.',
|
|
127
|
+
].join('\n'),
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
const config = await readPopeyeMdConfig(tmpDir);
|
|
131
|
+
expect(config).not.toBeNull();
|
|
132
|
+
expect(config!.notes).toContain('Tailwind CSS');
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('should reject invalid language values', async () => {
|
|
136
|
+
await fs.writeFile(
|
|
137
|
+
path.join(tmpDir, 'popeye.md'),
|
|
138
|
+
[
|
|
139
|
+
'---',
|
|
140
|
+
'language: rust',
|
|
141
|
+
'reviewer: openai',
|
|
142
|
+
'---',
|
|
143
|
+
].join('\n'),
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
const config = await readPopeyeMdConfig(tmpDir);
|
|
147
|
+
// Invalid language means essential field is missing
|
|
148
|
+
expect(config).toBeNull();
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('should return model fields as undefined when not specified', async () => {
|
|
152
|
+
await fs.writeFile(
|
|
153
|
+
path.join(tmpDir, 'popeye.md'),
|
|
154
|
+
[
|
|
155
|
+
'---',
|
|
156
|
+
'language: python',
|
|
157
|
+
'reviewer: openai',
|
|
158
|
+
'---',
|
|
159
|
+
].join('\n'),
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
const config = await readPopeyeMdConfig(tmpDir);
|
|
163
|
+
expect(config).not.toBeNull();
|
|
164
|
+
expect(config!.openaiModel).toBeUndefined();
|
|
165
|
+
expect(config!.geminiModel).toBeUndefined();
|
|
166
|
+
expect(config!.grokModel).toBeUndefined();
|
|
167
|
+
});
|
|
168
|
+
});
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Review Bridge tests — severity mapping, category mapping, CR routing,
|
|
3
|
+
* pipeline detection, finding conversion.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect } from 'vitest';
|
|
7
|
+
import {
|
|
8
|
+
mapSeverity,
|
|
9
|
+
mapCategory,
|
|
10
|
+
categoryToChangeType,
|
|
11
|
+
convertFinding,
|
|
12
|
+
isPipelineManaged,
|
|
13
|
+
extractPipelineState,
|
|
14
|
+
} from '../../../src/pipeline/bridges/review-bridge.js';
|
|
15
|
+
import { createDefaultPipelineState } from '../../../src/pipeline/types.js';
|
|
16
|
+
import type { AuditFinding as WorkflowFinding } from '../../../src/types/audit.js';
|
|
17
|
+
import type { ArtifactRef } from '../../../src/pipeline/types.js';
|
|
18
|
+
import type { ProjectState } from '../../../src/types/workflow.js';
|
|
19
|
+
|
|
20
|
+
// ─── Test Data ───────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
function makeSnapshotRef(): ArtifactRef {
|
|
23
|
+
return {
|
|
24
|
+
artifact_id: 'snap-001',
|
|
25
|
+
path: 'docs/snapshots/repo_snapshot_001.json',
|
|
26
|
+
sha256: 'abc123',
|
|
27
|
+
version: 1,
|
|
28
|
+
type: 'repo_snapshot',
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function makeWorkflowFinding(overrides: Partial<WorkflowFinding> = {}): WorkflowFinding {
|
|
33
|
+
return {
|
|
34
|
+
id: 'finding-1',
|
|
35
|
+
category: 'integration-wiring',
|
|
36
|
+
severity: 'critical',
|
|
37
|
+
title: 'API endpoint mismatch',
|
|
38
|
+
description: 'Frontend calls /api/users but backend serves /api/v1/users',
|
|
39
|
+
evidence: [{ file: 'src/api.ts', line: 42, snippet: 'fetch("/api/users")' }],
|
|
40
|
+
recommendation: 'Update frontend API base URL',
|
|
41
|
+
autoFixable: true,
|
|
42
|
+
...overrides,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function makeProjectState(withPipeline = false): ProjectState {
|
|
47
|
+
const base: ProjectState = {
|
|
48
|
+
id: 'test-id',
|
|
49
|
+
name: 'test-project',
|
|
50
|
+
idea: 'Build something',
|
|
51
|
+
language: 'python',
|
|
52
|
+
phase: 'execution',
|
|
53
|
+
status: 'in-progress',
|
|
54
|
+
milestones: [],
|
|
55
|
+
currentMilestone: null,
|
|
56
|
+
currentTask: null,
|
|
57
|
+
consensusHistory: [],
|
|
58
|
+
createdAt: new Date().toISOString(),
|
|
59
|
+
updatedAt: new Date().toISOString(),
|
|
60
|
+
} as ProjectState;
|
|
61
|
+
|
|
62
|
+
if (withPipeline) {
|
|
63
|
+
(base as unknown as { pipeline: unknown }).pipeline = createDefaultPipelineState();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return base;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ─── Tests ───────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
describe('Review Bridge', () => {
|
|
72
|
+
describe('severity mapping', () => {
|
|
73
|
+
it('should map critical to P0', () => {
|
|
74
|
+
expect(mapSeverity('critical')).toBe('P0');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should map major to P1', () => {
|
|
78
|
+
expect(mapSeverity('major')).toBe('P1');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should map minor to P2', () => {
|
|
82
|
+
expect(mapSeverity('minor')).toBe('P2');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should map info to P3', () => {
|
|
86
|
+
expect(mapSeverity('info')).toBe('P3');
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe('category mapping', () => {
|
|
91
|
+
it('should map integration-wiring to integration', () => {
|
|
92
|
+
expect(mapCategory('integration-wiring')).toBe('integration');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should map feature-completeness to integration', () => {
|
|
96
|
+
expect(mapCategory('feature-completeness')).toBe('integration');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('should map test-coverage to tests', () => {
|
|
100
|
+
expect(mapCategory('test-coverage')).toBe('tests');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should map config-deployment to config', () => {
|
|
104
|
+
expect(mapCategory('config-deployment')).toBe('config');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('should map security to security', () => {
|
|
108
|
+
expect(mapCategory('security')).toBe('security');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('should map dependency-sanity to deployment', () => {
|
|
112
|
+
expect(mapCategory('dependency-sanity')).toBe('deployment');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('should map consistency to schema', () => {
|
|
116
|
+
expect(mapCategory('consistency')).toBe('schema');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('should map documentation to deployment', () => {
|
|
120
|
+
expect(mapCategory('documentation')).toBe('deployment');
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe('CR change type routing', () => {
|
|
125
|
+
it('should route integration to architecture CR', () => {
|
|
126
|
+
expect(categoryToChangeType('integration')).toBe('architecture');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('should route schema to architecture CR', () => {
|
|
130
|
+
expect(categoryToChangeType('schema')).toBe('architecture');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should route security to requirement CR', () => {
|
|
134
|
+
expect(categoryToChangeType('security')).toBe('requirement');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('should route tests to config CR', () => {
|
|
138
|
+
expect(categoryToChangeType('tests')).toBe('config');
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('should route config to config CR', () => {
|
|
142
|
+
expect(categoryToChangeType('config')).toBe('config');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('should route deployment to config CR', () => {
|
|
146
|
+
expect(categoryToChangeType('deployment')).toBe('config');
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe('finding conversion', () => {
|
|
151
|
+
it('should convert a critical workflow finding to P0 pipeline finding', () => {
|
|
152
|
+
const wf = makeWorkflowFinding({ severity: 'critical', category: 'integration-wiring' });
|
|
153
|
+
const ref = makeSnapshotRef();
|
|
154
|
+
|
|
155
|
+
const pf = convertFinding(wf, ref);
|
|
156
|
+
|
|
157
|
+
expect(pf.id).toBe('finding-1');
|
|
158
|
+
expect(pf.severity).toBe('P0');
|
|
159
|
+
expect(pf.category).toBe('integration');
|
|
160
|
+
expect(pf.blocking).toBe(true);
|
|
161
|
+
expect(pf.description).toContain('API endpoint mismatch');
|
|
162
|
+
expect(pf.file_path).toBe('src/api.ts');
|
|
163
|
+
expect(pf.line_number).toBe(42);
|
|
164
|
+
expect(pf.evidence).toEqual([ref]);
|
|
165
|
+
expect(pf.suggested_owner).toBe('AUDITOR');
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('should convert an info finding to P3 non-blocking', () => {
|
|
169
|
+
const wf = makeWorkflowFinding({ severity: 'info', category: 'documentation' });
|
|
170
|
+
const ref = makeSnapshotRef();
|
|
171
|
+
|
|
172
|
+
const pf = convertFinding(wf, ref);
|
|
173
|
+
|
|
174
|
+
expect(pf.severity).toBe('P3');
|
|
175
|
+
expect(pf.blocking).toBe(false);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('should mark P0 and P1 as blocking, P2 and P3 as non-blocking', () => {
|
|
179
|
+
const ref = makeSnapshotRef();
|
|
180
|
+
|
|
181
|
+
expect(convertFinding(makeWorkflowFinding({ severity: 'critical' }), ref).blocking).toBe(true);
|
|
182
|
+
expect(convertFinding(makeWorkflowFinding({ severity: 'major' }), ref).blocking).toBe(true);
|
|
183
|
+
expect(convertFinding(makeWorkflowFinding({ severity: 'minor' }), ref).blocking).toBe(false);
|
|
184
|
+
expect(convertFinding(makeWorkflowFinding({ severity: 'info' }), ref).blocking).toBe(false);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('should handle finding with no evidence', () => {
|
|
188
|
+
const wf = makeWorkflowFinding({ evidence: [] });
|
|
189
|
+
const ref = makeSnapshotRef();
|
|
190
|
+
|
|
191
|
+
const pf = convertFinding(wf, ref);
|
|
192
|
+
|
|
193
|
+
expect(pf.file_path).toBeUndefined();
|
|
194
|
+
expect(pf.line_number).toBeUndefined();
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
describe('pipeline detection', () => {
|
|
199
|
+
it('should detect pipeline-managed state', () => {
|
|
200
|
+
const state = makeProjectState(true);
|
|
201
|
+
expect(isPipelineManaged(state)).toBe(true);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('should detect non-pipeline state', () => {
|
|
205
|
+
const state = makeProjectState(false);
|
|
206
|
+
expect(isPipelineManaged(state)).toBe(false);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('should extract pipeline state when present', () => {
|
|
210
|
+
const state = makeProjectState(true);
|
|
211
|
+
const pipeline = extractPipelineState(state);
|
|
212
|
+
|
|
213
|
+
expect(pipeline).toBeDefined();
|
|
214
|
+
expect(pipeline!.pipelinePhase).toBe('INTAKE');
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('should return undefined when no pipeline state', () => {
|
|
218
|
+
const state = makeProjectState(false);
|
|
219
|
+
expect(extractPipelineState(state)).toBeUndefined();
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
describe('CR routing determinism', () => {
|
|
224
|
+
it('should route integration findings to CONSENSUS_ARCHITECTURE', () => {
|
|
225
|
+
// Integration → architecture CR → CONSENSUS_ARCHITECTURE (via change-request.ts routing)
|
|
226
|
+
const changeType = categoryToChangeType('integration');
|
|
227
|
+
expect(changeType).toBe('architecture');
|
|
228
|
+
// architecture routes to CONSENSUS_ARCHITECTURE per CHANGE_TYPE_ROUTING
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('should route security findings to CONSENSUS_MASTER_PLAN', () => {
|
|
232
|
+
const changeType = categoryToChangeType('security');
|
|
233
|
+
expect(changeType).toBe('requirement');
|
|
234
|
+
// requirement routes to CONSENSUS_MASTER_PLAN per CHANGE_TYPE_ROUTING
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('should route test findings to QA_VALIDATION', () => {
|
|
238
|
+
const changeType = categoryToChangeType('tests');
|
|
239
|
+
expect(changeType).toBe('config');
|
|
240
|
+
// config routes to QA_VALIDATION per CHANGE_TYPE_ROUTING
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
});
|