sofia-cli 0.1.2 → 0.1.4
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/README.md +42 -20
- package/dist/infra/deploy.sh +193 -0
- package/dist/infra/gather-env.sh +211 -0
- package/dist/infra/infra/deploy.sh +193 -0
- package/dist/infra/infra/gather-env.sh +211 -0
- package/dist/infra/infra/main.bicep +90 -0
- package/dist/infra/infra/main.bicepparam +18 -0
- package/dist/infra/infra/resources.bicep +134 -0
- package/dist/infra/infra/teardown.sh +114 -0
- package/dist/infra/main.bicep +90 -0
- package/dist/infra/main.bicepparam +18 -0
- package/dist/infra/resources.bicep +134 -0
- package/dist/infra/teardown.sh +114 -0
- package/dist/src/cli/developCommand.js +0 -2
- package/dist/src/cli/index.js +8 -1
- package/dist/src/cli/workshopCommand.js +1 -1
- package/dist/src/develop/index.js +1 -1
- package/dist/src/develop/pocUtils.js +228 -0
- package/dist/src/develop/ralphLoop.js +8 -27
- package/dist/src/shared/data/cards.json +655 -670
- package/docs/architecture.md +2 -1
- package/package.json +5 -3
- package/src/cli/developCommand.ts +1 -3
- package/src/cli/index.ts +11 -1
- package/src/cli/workshopCommand.ts +21 -17
- package/src/develop/dynamicScaffolder.ts +36 -30
- package/src/develop/index.ts +13 -2
- package/src/develop/pocUtils.ts +296 -0
- package/src/develop/ralphLoop.ts +8 -28
- package/src/develop/templateRegistry.ts +19 -18
- package/src/shared/data/cards.json +655 -670
- package/tests/e2e/developE2e.spec.ts +3 -61
- package/tests/e2e/developFailureE2e.spec.ts +34 -38
- package/tests/integration/pocGithubMcp.spec.ts +29 -39
- package/tests/integration/pocLocalFallback.spec.ts +29 -39
- package/tests/integration/ralphLoopFlow.spec.ts +46 -66
- package/tests/integration/ralphLoopPartial.spec.ts +30 -37
- package/tests/unit/develop/githubMcpAdapter.spec.ts +0 -134
- package/tests/unit/develop/outputValidator.spec.ts +45 -21
- package/tests/unit/develop/ralphLoop.spec.ts +58 -94
- package/tsconfig.json +2 -1
- package/vitest.workspace.ts +5 -0
- package/dist/src/develop/pocScaffolder.js +0 -542
- package/dist/tests/e2e/developE2e.spec.js +0 -126
- package/dist/tests/e2e/developFailureE2e.spec.js +0 -247
- package/dist/tests/e2e/developPty.spec.js +0 -75
- package/dist/tests/e2e/discoveryWebSearchRelevance.spec.js +0 -84
- package/dist/tests/e2e/harness.spec.js +0 -83
- package/dist/tests/e2e/mcpLive.spec.js +0 -120
- package/dist/tests/e2e/newSession.e2e.spec.js +0 -177
- package/dist/tests/e2e/ralphLoopEnrichmentComparison.spec.js +0 -62
- package/dist/tests/e2e/workiqEnrichment.spec.js +0 -56
- package/dist/tests/e2e/zavaSimulation.spec.js +0 -452
- package/dist/tests/fixtures/test-fixture-project/src/add.js +0 -3
- package/dist/tests/fixtures/test-fixture-project/tests/failing.test.js +0 -6
- package/dist/tests/fixtures/test-fixture-project/tests/hanging.test.js +0 -8
- package/dist/tests/fixtures/test-fixture-project/tests/passing.test.js +0 -10
- package/dist/tests/fixtures/test-fixture-project/vitest.config.js +0 -6
- package/dist/tests/integration/autoStartConversation.spec.js +0 -138
- package/dist/tests/integration/defaultCommand.spec.js +0 -147
- package/dist/tests/integration/directCommandNonTty.spec.js +0 -224
- package/dist/tests/integration/directCommandTty.spec.js +0 -151
- package/dist/tests/integration/discoveryEnrichmentFlow.spec.js +0 -175
- package/dist/tests/integration/exportArtifacts.spec.js +0 -202
- package/dist/tests/integration/exportFallbackFlow.spec.js +0 -99
- package/dist/tests/integration/mcpDegradationFlow.spec.js +0 -190
- package/dist/tests/integration/mcpTransportFlow.spec.js +0 -139
- package/dist/tests/integration/newSessionFlow.spec.js +0 -343
- package/dist/tests/integration/pocGithubMcp.spec.js +0 -186
- package/dist/tests/integration/pocLocalFallback.spec.js +0 -171
- package/dist/tests/integration/pocScaffold.spec.js +0 -163
- package/dist/tests/integration/ralphLoopFlow.spec.js +0 -359
- package/dist/tests/integration/ralphLoopPartial.spec.js +0 -368
- package/dist/tests/integration/resumeAndBacktrack.spec.js +0 -247
- package/dist/tests/integration/spinnerLifecycle.spec.js +0 -220
- package/dist/tests/integration/summarizationFlow.spec.js +0 -115
- package/dist/tests/integration/testRunnerReal.spec.js +0 -52
- package/dist/tests/integration/webSearchAgent.spec.js +0 -128
- package/dist/tests/live/copilotSdkLive.spec.js +0 -107
- package/dist/tests/live/zavaFullWorkshop.spec.js +0 -392
- package/dist/tests/setup/loadEnv.js +0 -3
- package/dist/tests/unit/cli/developCommand.spec.js +0 -567
- package/dist/tests/unit/cli/directCommands.spec.js +0 -279
- package/dist/tests/unit/cli/envLoader.spec.js +0 -58
- package/dist/tests/unit/cli/ioContext.spec.js +0 -119
- package/dist/tests/unit/cli/preflight.spec.js +0 -108
- package/dist/tests/unit/cli/statusCommand.spec.js +0 -111
- package/dist/tests/unit/cli/workshopClientFallback.spec.js +0 -80
- package/dist/tests/unit/cli/workshopCommand.spec.js +0 -328
- package/dist/tests/unit/config/vitestEnvSetup.spec.js +0 -13
- package/dist/tests/unit/develop/checkpointState.spec.js +0 -315
- package/dist/tests/unit/develop/codeGenerator.spec.js +0 -355
- package/dist/tests/unit/develop/githubMcpAdapter.spec.js +0 -231
- package/dist/tests/unit/develop/mcpContextEnricher.spec.js +0 -433
- package/dist/tests/unit/develop/outputValidator.spec.js +0 -119
- package/dist/tests/unit/develop/pocScaffolder.spec.js +0 -353
- package/dist/tests/unit/develop/ralphLoop.spec.js +0 -1248
- package/dist/tests/unit/develop/templateRegistry.spec.js +0 -85
- package/dist/tests/unit/develop/testRunner.spec.js +0 -249
- package/dist/tests/unit/infraBicep.spec.js +0 -92
- package/dist/tests/unit/infraDeploy.spec.js +0 -82
- package/dist/tests/unit/infraTeardown.spec.js +0 -63
- package/dist/tests/unit/logging/logger.spec.js +0 -43
- package/dist/tests/unit/loop/conversationLoop.spec.js +0 -592
- package/dist/tests/unit/loop/phaseSummarizer.spec.js +0 -141
- package/dist/tests/unit/loop/streamingMarkdown.spec.js +0 -147
- package/dist/tests/unit/mcp/mcpManager.spec.js +0 -279
- package/dist/tests/unit/mcp/mcpTransport.spec.js +0 -529
- package/dist/tests/unit/mcp/retryPolicy.spec.js +0 -218
- package/dist/tests/unit/mcp/timeoutValidation.spec.js +0 -46
- package/dist/tests/unit/mcp/webSearch.spec.js +0 -567
- package/dist/tests/unit/phases/contextSummarizer.spec.js +0 -140
- package/dist/tests/unit/phases/discoveryEnricher.repeatCalls.spec.js +0 -93
- package/dist/tests/unit/phases/discoveryEnricher.spec.js +0 -411
- package/dist/tests/unit/phases/phaseExtractors.spec.js +0 -352
- package/dist/tests/unit/phases/phaseHandlers.spec.js +0 -425
- package/dist/tests/unit/prompts/promptLoader.spec.js +0 -118
- package/dist/tests/unit/schemas/pocSchemas.spec.js +0 -412
- package/dist/tests/unit/schemas/session.spec.js +0 -257
- package/dist/tests/unit/sessions/exportPaths.spec.js +0 -31
- package/dist/tests/unit/sessions/exportWriter.spec.js +0 -655
- package/dist/tests/unit/sessions/sessionManager.spec.js +0 -151
- package/dist/tests/unit/sessions/sessionStore.spec.js +0 -116
- package/dist/tests/unit/shared/activitySpinner.spec.js +0 -175
- package/dist/tests/unit/shared/cardsLoader.spec.js +0 -76
- package/dist/tests/unit/shared/copilotClient.spec.js +0 -155
- package/dist/tests/unit/shared/errorClassifier.spec.js +0 -131
- package/dist/tests/unit/shared/events.spec.js +0 -55
- package/dist/tests/unit/shared/markdownRenderer.spec.js +0 -35
- package/dist/tests/unit/shared/markdownRendererChunks.spec.js +0 -70
- package/dist/tests/unit/shared/tableRenderer.spec.js +0 -34
- package/dist/vitest.config.js +0 -14
- package/dist/vitest.live.config.js +0 -18
- package/src/develop/pocScaffolder.ts +0 -646
- package/tests/integration/pocScaffold.spec.ts +0 -220
- package/tests/unit/develop/pocScaffolder.spec.ts +0 -451
|
@@ -12,7 +12,7 @@ import { tmpdir } from 'node:os';
|
|
|
12
12
|
import { createRequire } from 'node:module';
|
|
13
13
|
|
|
14
14
|
import { RalphLoop } from '../../src/develop/ralphLoop.js';
|
|
15
|
-
import {
|
|
15
|
+
import { generateDynamicScaffold } from '../../src/develop/dynamicScaffolder.js';
|
|
16
16
|
import { TestRunner } from '../../src/develop/testRunner.js';
|
|
17
17
|
import type { WorkshopSession } from '../../src/shared/schemas/session.js';
|
|
18
18
|
import type { LoopIO } from '../../src/loop/conversationLoop.js';
|
|
@@ -42,14 +42,19 @@ vi.mock('node:child_process', async (importOriginal) => {
|
|
|
42
42
|
});
|
|
43
43
|
|
|
44
44
|
// Mock validatePocOutput to always pass in integration tests
|
|
45
|
-
vi.mock('../../src/develop/
|
|
46
|
-
const actual = await importOriginal<typeof import('../../src/develop/
|
|
45
|
+
vi.mock('../../src/develop/pocUtils.js', async (importOriginal) => {
|
|
46
|
+
const actual = await importOriginal<typeof import('../../src/develop/pocUtils.js')>();
|
|
47
47
|
return {
|
|
48
48
|
...actual,
|
|
49
49
|
validatePocOutput: vi.fn().mockResolvedValue({ valid: true, missingFiles: [], errors: [] }),
|
|
50
50
|
};
|
|
51
51
|
});
|
|
52
52
|
|
|
53
|
+
// Mock generateDynamicScaffold to create minimal files for tests
|
|
54
|
+
vi.mock('../../src/develop/dynamicScaffolder.js', () => ({
|
|
55
|
+
generateDynamicScaffold: vi.fn(),
|
|
56
|
+
}));
|
|
57
|
+
|
|
53
58
|
const require = createRequire(import.meta.url);
|
|
54
59
|
const fixtureSession: WorkshopSession =
|
|
55
60
|
require('../fixtures/completedSession.json') as WorkshopSession;
|
|
@@ -68,48 +73,36 @@ function makeIo(): LoopIO {
|
|
|
68
73
|
};
|
|
69
74
|
}
|
|
70
75
|
|
|
71
|
-
function
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
projectName: 'route-optimizer-poc',
|
|
102
|
-
ideaTitle: 'AI-Powered Route Optimizer',
|
|
103
|
-
ideaDescription: 'Optimize routes',
|
|
104
|
-
techStack: { language: 'TypeScript', runtime: 'Node.js 20', testRunner: 'npm test' },
|
|
105
|
-
planSummary: 'Route optimization',
|
|
106
|
-
sessionId: fixtureSession.sessionId,
|
|
107
|
-
outputDir,
|
|
108
|
-
},
|
|
109
|
-
};
|
|
110
|
-
}),
|
|
111
|
-
getTemplateFiles: () => ['package.json', 'src/index.ts', 'tests/index.test.ts'],
|
|
112
|
-
} as unknown as PocScaffolder;
|
|
76
|
+
function setupDynamicScaffoldMock(outputDir: string): void {
|
|
77
|
+
vi.mocked(generateDynamicScaffold).mockImplementation(async () => {
|
|
78
|
+
const { writeFile, mkdir } = await import('node:fs/promises');
|
|
79
|
+
await mkdir(join(outputDir, 'src'), { recursive: true });
|
|
80
|
+
await mkdir(join(outputDir, 'tests'), { recursive: true });
|
|
81
|
+
await writeFile(
|
|
82
|
+
join(outputDir, 'package.json'),
|
|
83
|
+
JSON.stringify({
|
|
84
|
+
name: 'route-optimizer-poc',
|
|
85
|
+
scripts: { test: 'vitest run' },
|
|
86
|
+
dependencies: {},
|
|
87
|
+
devDependencies: { vitest: '^3.0.0' },
|
|
88
|
+
}),
|
|
89
|
+
'utf-8',
|
|
90
|
+
);
|
|
91
|
+
await writeFile(
|
|
92
|
+
join(outputDir, 'src', 'index.ts'),
|
|
93
|
+
'// TODO: implement\nexport function optimize() { return []; }',
|
|
94
|
+
'utf-8',
|
|
95
|
+
);
|
|
96
|
+
await writeFile(
|
|
97
|
+
join(outputDir, 'tests', 'index.test.ts'),
|
|
98
|
+
'import { describe, it, expect } from "vitest";\nimport { optimize } from "../src/index.js";\ndescribe("optimizer", () => { it("should return stops", () => { expect(optimize().length).toBeGreaterThan(0); }); });',
|
|
99
|
+
'utf-8',
|
|
100
|
+
);
|
|
101
|
+
return {
|
|
102
|
+
createdFiles: ['package.json', 'src/index.ts', 'tests/index.test.ts'],
|
|
103
|
+
techStack: { language: 'TypeScript', runtime: 'Node.js 20', testRunner: 'npm test' },
|
|
104
|
+
};
|
|
105
|
+
});
|
|
113
106
|
}
|
|
114
107
|
|
|
115
108
|
// ── SC-002-003: Iterative refinement test ────────────────────────────────────
|
|
@@ -128,7 +121,7 @@ describe('RalphLoop integration — iterative refinement (SC-002-003)', () => {
|
|
|
128
121
|
|
|
129
122
|
it('scaffold → fail tests → LLM fix → tests pass → success', async () => {
|
|
130
123
|
const io = makeIo();
|
|
131
|
-
|
|
124
|
+
setupDynamicScaffoldMock(tmpDir);
|
|
132
125
|
|
|
133
126
|
// Test runner: fails first, passes second
|
|
134
127
|
let testCallCount = 0;
|
|
@@ -200,7 +193,6 @@ describe('RalphLoop integration — iterative refinement (SC-002-003)', () => {
|
|
|
200
193
|
outputDir: tmpDir,
|
|
201
194
|
maxIterations: 5,
|
|
202
195
|
testRunner,
|
|
203
|
-
scaffolder,
|
|
204
196
|
onSessionUpdate: async (session) => {
|
|
205
197
|
sessionUpdates.push({ ...session });
|
|
206
198
|
},
|
|
@@ -231,7 +223,7 @@ describe('RalphLoop integration — iterative refinement (SC-002-003)', () => {
|
|
|
231
223
|
|
|
232
224
|
it('verifies failing tests are passed to LLM in iteration prompt (SC-002-003)', async () => {
|
|
233
225
|
const io = makeIo();
|
|
234
|
-
|
|
226
|
+
setupDynamicScaffoldMock(tmpDir);
|
|
235
227
|
|
|
236
228
|
let testCallCount = 0;
|
|
237
229
|
const testRunner: TestRunner = {
|
|
@@ -287,7 +279,6 @@ describe('RalphLoop integration — iterative refinement (SC-002-003)', () => {
|
|
|
287
279
|
outputDir: tmpDir,
|
|
288
280
|
maxIterations: 3,
|
|
289
281
|
testRunner,
|
|
290
|
-
scaffolder,
|
|
291
282
|
});
|
|
292
283
|
|
|
293
284
|
await ralph.run();
|
|
@@ -314,9 +305,8 @@ describe('TODO tracking integration (T074)', () => {
|
|
|
314
305
|
it('writes TODO counts to .sofia-metadata.json during scaffold and updates after iteration', async () => {
|
|
315
306
|
const { writeFile, mkdir } = await import('node:fs/promises');
|
|
316
307
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
scaffold: vi.fn().mockImplementation(async () => {
|
|
308
|
+
function setupTodoScaffoldMock(): void {
|
|
309
|
+
vi.mocked(generateDynamicScaffold).mockImplementation(async () => {
|
|
320
310
|
await mkdir(join(tmpDir, 'src'), { recursive: true });
|
|
321
311
|
await mkdir(join(tmpDir, 'tests'), { recursive: true });
|
|
322
312
|
await writeFile(
|
|
@@ -344,20 +334,11 @@ describe('TODO tracking integration (T074)', () => {
|
|
|
344
334
|
);
|
|
345
335
|
return {
|
|
346
336
|
createdFiles: ['package.json', 'src/index.ts', '.sofia-metadata.json'],
|
|
347
|
-
|
|
348
|
-
context: {
|
|
349
|
-
projectName: 'todo-test-poc',
|
|
350
|
-
ideaTitle: 'Test',
|
|
351
|
-
ideaDescription: 'Test',
|
|
352
|
-
techStack: { language: 'TypeScript', runtime: 'Node.js 20', testRunner: 'npm test' },
|
|
353
|
-
planSummary: 'Test',
|
|
354
|
-
sessionId: fixtureSession.sessionId,
|
|
355
|
-
outputDir: tmpDir,
|
|
356
|
-
},
|
|
337
|
+
techStack: { language: 'TypeScript', runtime: 'Node.js 20', testRunner: 'npm test' },
|
|
357
338
|
};
|
|
358
|
-
})
|
|
359
|
-
|
|
360
|
-
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
setupTodoScaffoldMock();
|
|
361
342
|
|
|
362
343
|
// Test runner that fails on first call (triggering TODO rescan), then passes
|
|
363
344
|
let runCount = 0;
|
|
@@ -412,7 +393,6 @@ describe('TODO tracking integration (T074)', () => {
|
|
|
412
393
|
outputDir: tmpDir,
|
|
413
394
|
maxIterations: 3,
|
|
414
395
|
testRunner: failThenPassRunner,
|
|
415
|
-
scaffolder: todoScaffolder,
|
|
416
396
|
});
|
|
417
397
|
|
|
418
398
|
await ralph.run();
|
|
@@ -13,7 +13,7 @@ import { tmpdir } from 'node:os';
|
|
|
13
13
|
import { createRequire } from 'node:module';
|
|
14
14
|
|
|
15
15
|
import { RalphLoop } from '../../src/develop/ralphLoop.js';
|
|
16
|
-
import {
|
|
16
|
+
import { generateDynamicScaffold } from '../../src/develop/dynamicScaffolder.js';
|
|
17
17
|
import { TestRunner } from '../../src/develop/testRunner.js';
|
|
18
18
|
import type { WorkshopSession } from '../../src/shared/schemas/session.js';
|
|
19
19
|
import type { LoopIO } from '../../src/loop/conversationLoop.js';
|
|
@@ -42,8 +42,14 @@ vi.mock('node:child_process', async (importOriginal) => {
|
|
|
42
42
|
};
|
|
43
43
|
});
|
|
44
44
|
|
|
45
|
+
// Mock generateDynamicScaffold
|
|
46
|
+
vi.mock('../../src/develop/dynamicScaffolder.js', () => ({
|
|
47
|
+
generateDynamicScaffold: vi.fn(),
|
|
48
|
+
}));
|
|
49
|
+
|
|
45
50
|
const require = createRequire(import.meta.url);
|
|
46
|
-
const fixtureSession: WorkshopSession =
|
|
51
|
+
const fixtureSession: WorkshopSession =
|
|
52
|
+
require('../fixtures/completedSession.json') as WorkshopSession;
|
|
47
53
|
|
|
48
54
|
function makeIo(): LoopIO {
|
|
49
55
|
return {
|
|
@@ -57,34 +63,26 @@ function makeIo(): LoopIO {
|
|
|
57
63
|
};
|
|
58
64
|
}
|
|
59
65
|
|
|
60
|
-
function
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
+
function setupDynamicScaffoldMock(outputDir: string): void {
|
|
67
|
+
vi.mocked(generateDynamicScaffold).mockImplementation(async () => {
|
|
68
|
+
const { writeFile, mkdir } = await import('node:fs/promises');
|
|
69
|
+
await mkdir(join(outputDir, 'src'), { recursive: true });
|
|
70
|
+
await writeFile(
|
|
71
|
+
join(outputDir, 'package.json'),
|
|
72
|
+
JSON.stringify({
|
|
66
73
|
name: 'test-poc',
|
|
67
74
|
scripts: { test: 'vitest run' },
|
|
68
75
|
dependencies: {},
|
|
69
76
|
devDependencies: {},
|
|
70
|
-
}),
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
techStack: { language: 'TypeScript', runtime: 'Node.js 20', testRunner: 'npm test' },
|
|
80
|
-
planSummary: 'Test',
|
|
81
|
-
sessionId: fixtureSession.sessionId,
|
|
82
|
-
outputDir,
|
|
83
|
-
},
|
|
84
|
-
};
|
|
85
|
-
}),
|
|
86
|
-
getTemplateFiles: () => [],
|
|
87
|
-
} as unknown as PocScaffolder;
|
|
77
|
+
}),
|
|
78
|
+
'utf-8',
|
|
79
|
+
);
|
|
80
|
+
await writeFile(join(outputDir, 'src', 'index.ts'), 'export function main() {}', 'utf-8');
|
|
81
|
+
return {
|
|
82
|
+
createdFiles: ['package.json', 'src/index.ts'],
|
|
83
|
+
techStack: { language: 'TypeScript', runtime: 'Node.js 20', testRunner: 'npm test' },
|
|
84
|
+
};
|
|
85
|
+
});
|
|
88
86
|
}
|
|
89
87
|
|
|
90
88
|
function makeClient(): CopilotClient {
|
|
@@ -119,7 +117,7 @@ describe('RalphLoop integration — partial/failed outcomes', () => {
|
|
|
119
117
|
it('sets finalStatus=partial when some tests pass at max-iterations', async () => {
|
|
120
118
|
const io = makeIo();
|
|
121
119
|
const client = makeClient();
|
|
122
|
-
|
|
120
|
+
setupDynamicScaffoldMock(tmpDir);
|
|
123
121
|
|
|
124
122
|
const testRunner: TestRunner = {
|
|
125
123
|
run: vi.fn().mockResolvedValue({
|
|
@@ -140,7 +138,6 @@ describe('RalphLoop integration — partial/failed outcomes', () => {
|
|
|
140
138
|
outputDir: tmpDir,
|
|
141
139
|
maxIterations: 2,
|
|
142
140
|
testRunner,
|
|
143
|
-
scaffolder,
|
|
144
141
|
});
|
|
145
142
|
|
|
146
143
|
const result = await ralph.run();
|
|
@@ -154,7 +151,7 @@ describe('RalphLoop integration — partial/failed outcomes', () => {
|
|
|
154
151
|
it('sets finalStatus=failed when no tests pass at max-iterations', async () => {
|
|
155
152
|
const io = makeIo();
|
|
156
153
|
const client = makeClient();
|
|
157
|
-
|
|
154
|
+
setupDynamicScaffoldMock(tmpDir);
|
|
158
155
|
|
|
159
156
|
const testRunner: TestRunner = {
|
|
160
157
|
run: vi.fn().mockResolvedValue({
|
|
@@ -178,7 +175,6 @@ describe('RalphLoop integration — partial/failed outcomes', () => {
|
|
|
178
175
|
outputDir: tmpDir,
|
|
179
176
|
maxIterations: 2,
|
|
180
177
|
testRunner,
|
|
181
|
-
scaffolder,
|
|
182
178
|
});
|
|
183
179
|
|
|
184
180
|
const result = await ralph.run();
|
|
@@ -189,7 +185,7 @@ describe('RalphLoop integration — partial/failed outcomes', () => {
|
|
|
189
185
|
|
|
190
186
|
it('records error iteration when LLM returns empty response, continues loop', async () => {
|
|
191
187
|
const io = makeIo();
|
|
192
|
-
|
|
188
|
+
setupDynamicScaffoldMock(tmpDir);
|
|
193
189
|
|
|
194
190
|
let testCallCount = 0;
|
|
195
191
|
const testRunner: TestRunner = {
|
|
@@ -254,7 +250,6 @@ describe('RalphLoop integration — partial/failed outcomes', () => {
|
|
|
254
250
|
outputDir: tmpDir,
|
|
255
251
|
maxIterations: 5,
|
|
256
252
|
testRunner,
|
|
257
|
-
scaffolder,
|
|
258
253
|
});
|
|
259
254
|
|
|
260
255
|
const result = await ralph.run();
|
|
@@ -272,7 +267,7 @@ describe('RalphLoop integration — partial/failed outcomes', () => {
|
|
|
272
267
|
it('records terminationReason in session poc state', async () => {
|
|
273
268
|
const io = makeIo();
|
|
274
269
|
const client = makeClient();
|
|
275
|
-
|
|
270
|
+
setupDynamicScaffoldMock(tmpDir);
|
|
276
271
|
|
|
277
272
|
const testRunner: TestRunner = {
|
|
278
273
|
run: vi.fn().mockResolvedValue({
|
|
@@ -293,7 +288,6 @@ describe('RalphLoop integration — partial/failed outcomes', () => {
|
|
|
293
288
|
outputDir: tmpDir,
|
|
294
289
|
maxIterations: 2,
|
|
295
290
|
testRunner,
|
|
296
|
-
scaffolder,
|
|
297
291
|
});
|
|
298
292
|
|
|
299
293
|
const result = await ralph.run();
|
|
@@ -381,7 +375,7 @@ describe('resume from interrupted session', () => {
|
|
|
381
375
|
}),
|
|
382
376
|
} as unknown as TestRunner;
|
|
383
377
|
|
|
384
|
-
|
|
378
|
+
setupDynamicScaffoldMock(tmpDir);
|
|
385
379
|
|
|
386
380
|
const ralph = new RalphLoop({
|
|
387
381
|
client: passingClient,
|
|
@@ -390,7 +384,6 @@ describe('resume from interrupted session', () => {
|
|
|
390
384
|
outputDir: tmpDir,
|
|
391
385
|
maxIterations: 10,
|
|
392
386
|
testRunner,
|
|
393
|
-
scaffolder,
|
|
394
387
|
checkpoint: {
|
|
395
388
|
hasPriorRun: true,
|
|
396
389
|
completedIterations: 2,
|
|
@@ -405,7 +398,7 @@ describe('resume from interrupted session', () => {
|
|
|
405
398
|
const result = await ralph.run();
|
|
406
399
|
|
|
407
400
|
// Should have seeded from prior iterations and continued
|
|
408
|
-
// Note: finalStatus may be 'partial' since
|
|
401
|
+
// Note: finalStatus may be 'partial' since setupDynamicScaffoldMock doesn't create all
|
|
409
402
|
// files required by validatePocOutput (README.md, tsconfig.json, etc.)
|
|
410
403
|
expect(['success', 'partial']).toContain(result.finalStatus);
|
|
411
404
|
// Total iterations should include the 2 prior + scaffold + tests-passing
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Verifies:
|
|
5
5
|
* - isAvailable() checks McpManager.isAvailable('github')
|
|
6
|
-
* - createRepository() calls MCP tool
|
|
7
6
|
* - pushFiles() commits and pushes
|
|
8
7
|
* - Graceful fallback returns { available: false } when MCP unavailable
|
|
9
8
|
*/
|
|
@@ -48,82 +47,6 @@ describe('GitHubMcpAdapter', () => {
|
|
|
48
47
|
});
|
|
49
48
|
});
|
|
50
49
|
|
|
51
|
-
describe('createRepository()', () => {
|
|
52
|
-
it('calls mcpManager.callTool with create_repository and returns parsed result', async () => {
|
|
53
|
-
const callTool = vi.fn().mockResolvedValue({
|
|
54
|
-
html_url: 'https://github.com/acme/poc-route-optimizer',
|
|
55
|
-
name: 'poc-route-optimizer',
|
|
56
|
-
});
|
|
57
|
-
const mgr = makeMcpManager(true, callTool);
|
|
58
|
-
const adapter = new GitHubMcpAdapter(mgr);
|
|
59
|
-
const result = await adapter.createRepository({
|
|
60
|
-
name: 'poc-route-optimizer',
|
|
61
|
-
description: 'AI route optimization PoC',
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
expect(result.available).toBe(true);
|
|
65
|
-
if (result.available) {
|
|
66
|
-
expect(result.repoUrl).toBe('https://github.com/acme/poc-route-optimizer');
|
|
67
|
-
expect(result.repoName).toBe('poc-route-optimizer');
|
|
68
|
-
}
|
|
69
|
-
expect(callTool).toHaveBeenCalledWith(
|
|
70
|
-
'github',
|
|
71
|
-
'create_repository',
|
|
72
|
-
{
|
|
73
|
-
name: 'poc-route-optimizer',
|
|
74
|
-
description: 'AI route optimization PoC',
|
|
75
|
-
private: true,
|
|
76
|
-
},
|
|
77
|
-
{ timeoutMs: 60_000 },
|
|
78
|
-
);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it('returns { available: false } when GitHub MCP is unavailable', async () => {
|
|
82
|
-
const adapter = new GitHubMcpAdapter(makeMcpManager(false));
|
|
83
|
-
const result = await adapter.createRepository({
|
|
84
|
-
name: 'poc-route-optimizer',
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
expect(result.available).toBe(false);
|
|
88
|
-
if (!result.available) {
|
|
89
|
-
expect(result.reason).toBeDefined();
|
|
90
|
-
}
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it('returns { available: false } when callTool throws', async () => {
|
|
94
|
-
const callTool = vi.fn().mockRejectedValue(new Error('MCP callTool not yet wired'));
|
|
95
|
-
const adapter = new GitHubMcpAdapter(makeMcpManager(true, callTool));
|
|
96
|
-
const result = await adapter.createRepository({ name: 'poc-fail' });
|
|
97
|
-
|
|
98
|
-
expect(result.available).toBe(false);
|
|
99
|
-
if (!result.available) {
|
|
100
|
-
expect(result.reason).toContain('not yet wired');
|
|
101
|
-
}
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
it('stores repo URL after successful creation', async () => {
|
|
105
|
-
const callTool = vi.fn().mockResolvedValue({
|
|
106
|
-
html_url: 'https://github.com/acme/my-poc',
|
|
107
|
-
name: 'my-poc',
|
|
108
|
-
});
|
|
109
|
-
const adapter = new GitHubMcpAdapter(makeMcpManager(true, callTool));
|
|
110
|
-
await adapter.createRepository({ name: 'my-poc' });
|
|
111
|
-
|
|
112
|
-
expect(adapter.getRepoUrl()).toBe('https://github.com/acme/my-poc');
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
it('returns { available: false } when response has no URL', async () => {
|
|
116
|
-
const callTool = vi.fn().mockResolvedValue({ id: 123 });
|
|
117
|
-
const adapter = new GitHubMcpAdapter(makeMcpManager(true, callTool));
|
|
118
|
-
const result = await adapter.createRepository({ name: 'no-url' });
|
|
119
|
-
|
|
120
|
-
expect(result.available).toBe(false);
|
|
121
|
-
if (!result.available) {
|
|
122
|
-
expect(result.reason).toContain('missing repository URL');
|
|
123
|
-
}
|
|
124
|
-
});
|
|
125
|
-
});
|
|
126
|
-
|
|
127
50
|
describe('pushFiles()', () => {
|
|
128
51
|
it('calls mcpManager.callTool with push_files and returns commitSha', async () => {
|
|
129
52
|
const callTool = vi.fn().mockResolvedValue({ sha: 'abc123ff' });
|
|
@@ -203,63 +126,6 @@ describe('GitHubMcpAdapter', () => {
|
|
|
203
126
|
const adapter = new GitHubMcpAdapter(makeMcpManager(true));
|
|
204
127
|
expect(adapter.getRepoUrl()).toBeUndefined();
|
|
205
128
|
});
|
|
206
|
-
|
|
207
|
-
it('returns URL after successful createRepository', async () => {
|
|
208
|
-
const callTool = vi.fn().mockResolvedValue({
|
|
209
|
-
html_url: 'https://github.com/acme/test-poc',
|
|
210
|
-
name: 'test-poc',
|
|
211
|
-
});
|
|
212
|
-
const adapter = new GitHubMcpAdapter(makeMcpManager(true, callTool));
|
|
213
|
-
await adapter.createRepository({ name: 'test-poc' });
|
|
214
|
-
expect(adapter.getRepoUrl()).toBe('https://github.com/acme/test-poc');
|
|
215
|
-
});
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
// ── T008: Contract tests per contracts/github-adapter.md ────────────────
|
|
219
|
-
|
|
220
|
-
describe('createRepository() — contract: URL fallback chain', () => {
|
|
221
|
-
it('falls back to response.url when html_url is missing', async () => {
|
|
222
|
-
const callTool = vi.fn().mockResolvedValue({
|
|
223
|
-
url: 'https://api.github.com/repos/acme/poc',
|
|
224
|
-
name: 'poc',
|
|
225
|
-
});
|
|
226
|
-
const adapter = new GitHubMcpAdapter(makeMcpManager(true, callTool));
|
|
227
|
-
const result = await adapter.createRepository({ name: 'poc' });
|
|
228
|
-
|
|
229
|
-
expect(result.available).toBe(true);
|
|
230
|
-
if (result.available) {
|
|
231
|
-
expect(result.repoUrl).toBe('https://api.github.com/repos/acme/poc');
|
|
232
|
-
}
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
it('falls back to response.clone_url when html_url and url are missing', async () => {
|
|
236
|
-
const callTool = vi.fn().mockResolvedValue({
|
|
237
|
-
clone_url: 'https://github.com/acme/poc.git',
|
|
238
|
-
name: 'poc',
|
|
239
|
-
});
|
|
240
|
-
const adapter = new GitHubMcpAdapter(makeMcpManager(true, callTool));
|
|
241
|
-
const result = await adapter.createRepository({ name: 'poc' });
|
|
242
|
-
|
|
243
|
-
expect(result.available).toBe(true);
|
|
244
|
-
if (result.available) {
|
|
245
|
-
expect(result.repoUrl).toBe('https://github.com/acme/poc.git');
|
|
246
|
-
}
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
it('extracts repoName from response.full_name as fallback', async () => {
|
|
250
|
-
const callTool = vi.fn().mockResolvedValue({
|
|
251
|
-
html_url: 'https://github.com/acme/poc',
|
|
252
|
-
full_name: 'acme/poc',
|
|
253
|
-
});
|
|
254
|
-
const adapter = new GitHubMcpAdapter(makeMcpManager(true, callTool));
|
|
255
|
-
const result = await adapter.createRepository({ name: 'poc' });
|
|
256
|
-
|
|
257
|
-
expect(result.available).toBe(true);
|
|
258
|
-
if (result.available) {
|
|
259
|
-
// name not in response → full_name fallback returns 'acme/poc'
|
|
260
|
-
expect(result.repoName).toBe('acme/poc');
|
|
261
|
-
}
|
|
262
|
-
});
|
|
263
129
|
});
|
|
264
130
|
|
|
265
131
|
describe('pushFiles() — contract: commitSha extraction', () => {
|
|
@@ -8,7 +8,7 @@ import { mkdtemp, rm, writeFile, mkdir } from 'node:fs/promises';
|
|
|
8
8
|
import { join } from 'node:path';
|
|
9
9
|
import { tmpdir } from 'node:os';
|
|
10
10
|
|
|
11
|
-
import { validatePocOutput } from '../../../src/develop/
|
|
11
|
+
import { validatePocOutput } from '../../../src/develop/pocUtils.js';
|
|
12
12
|
|
|
13
13
|
describe('validatePocOutput', () => {
|
|
14
14
|
let tmpDir: string;
|
|
@@ -24,24 +24,44 @@ describe('validatePocOutput', () => {
|
|
|
24
24
|
async function createCompleteScaffold(): Promise<void> {
|
|
25
25
|
await mkdir(join(tmpDir, 'src'), { recursive: true });
|
|
26
26
|
await mkdir(join(tmpDir, 'tests'), { recursive: true });
|
|
27
|
-
await writeFile(
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
27
|
+
await writeFile(
|
|
28
|
+
join(tmpDir, 'package.json'),
|
|
29
|
+
JSON.stringify({
|
|
30
|
+
name: 'test-poc',
|
|
31
|
+
version: '0.1.0',
|
|
32
|
+
scripts: { test: 'vitest run', build: 'tsc', start: 'node dist/index.js' },
|
|
33
|
+
dependencies: {},
|
|
34
|
+
devDependencies: { vitest: '^3.0.0', typescript: '^5.0.0' },
|
|
35
|
+
}),
|
|
36
|
+
'utf-8',
|
|
37
|
+
);
|
|
34
38
|
await writeFile(join(tmpDir, 'README.md'), '# Test PoC\n\nA test proof of concept.', 'utf-8');
|
|
35
|
-
await writeFile(
|
|
36
|
-
|
|
37
|
-
|
|
39
|
+
await writeFile(
|
|
40
|
+
join(tmpDir, 'tsconfig.json'),
|
|
41
|
+
JSON.stringify({
|
|
42
|
+
compilerOptions: { target: 'ES2022', module: 'Node16', strict: true },
|
|
43
|
+
}),
|
|
44
|
+
'utf-8',
|
|
45
|
+
);
|
|
38
46
|
await writeFile(join(tmpDir, '.gitignore'), 'node_modules/\ndist/\ncoverage/\n', 'utf-8');
|
|
39
|
-
await writeFile(
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
47
|
+
await writeFile(
|
|
48
|
+
join(tmpDir, '.sofia-metadata.json'),
|
|
49
|
+
JSON.stringify({
|
|
50
|
+
sessionId: 'test-001',
|
|
51
|
+
featureSpec: '002-poc-generation',
|
|
52
|
+
}),
|
|
53
|
+
'utf-8',
|
|
54
|
+
);
|
|
55
|
+
await writeFile(
|
|
56
|
+
join(tmpDir, 'src', 'index.ts'),
|
|
57
|
+
'export function main() { return "ok"; }',
|
|
58
|
+
'utf-8',
|
|
59
|
+
);
|
|
60
|
+
await writeFile(
|
|
61
|
+
join(tmpDir, 'tests', 'index.test.ts'),
|
|
62
|
+
'import { test, expect } from "vitest"; test("works", () => { expect(true).toBe(true); });',
|
|
63
|
+
'utf-8',
|
|
64
|
+
);
|
|
45
65
|
}
|
|
46
66
|
|
|
47
67
|
it('1. validates package.json exists and has test script', async () => {
|
|
@@ -115,10 +135,14 @@ describe('validatePocOutput', () => {
|
|
|
115
135
|
|
|
116
136
|
it('reports error when package.json has no test script', async () => {
|
|
117
137
|
await createCompleteScaffold();
|
|
118
|
-
await writeFile(
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
138
|
+
await writeFile(
|
|
139
|
+
join(tmpDir, 'package.json'),
|
|
140
|
+
JSON.stringify({
|
|
141
|
+
name: 'test-poc',
|
|
142
|
+
scripts: { build: 'tsc' }, // no test script
|
|
143
|
+
}),
|
|
144
|
+
'utf-8',
|
|
145
|
+
);
|
|
122
146
|
const result = await validatePocOutput(tmpDir);
|
|
123
147
|
expect(result.valid).toBe(false);
|
|
124
148
|
expect(result.errors).toContain('package.json is missing "test" script');
|