sofia-cli 0.1.1 → 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 +3 -3
- 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 -329
- 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
|
@@ -1,567 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* T014: Unit tests for developCommand.
|
|
3
|
-
*
|
|
4
|
-
* Verifies:
|
|
5
|
-
* - Session validation (rejects sessions without selection/plan)
|
|
6
|
-
* - --session, --max-iterations, --output option parsing
|
|
7
|
-
* - Error messages for invalid sessions
|
|
8
|
-
*/
|
|
9
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
10
|
-
import { existsSync, rmSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
11
|
-
import { join } from 'node:path';
|
|
12
|
-
// Mock RalphLoop so tests don't run the real loop
|
|
13
|
-
vi.mock('../../../src/develop/ralphLoop.js');
|
|
14
|
-
import { validateSessionForDevelop, developCommand } from '../../../src/cli/developCommand.js';
|
|
15
|
-
import { RalphLoop } from '../../../src/develop/ralphLoop.js';
|
|
16
|
-
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
17
|
-
function makeSession(overrides) {
|
|
18
|
-
const now = new Date().toISOString();
|
|
19
|
-
return {
|
|
20
|
-
sessionId: 'test-dev-session',
|
|
21
|
-
schemaVersion: '1.0.0',
|
|
22
|
-
createdAt: now,
|
|
23
|
-
updatedAt: now,
|
|
24
|
-
phase: 'Develop',
|
|
25
|
-
status: 'Active',
|
|
26
|
-
participants: [],
|
|
27
|
-
artifacts: { generatedFiles: [] },
|
|
28
|
-
ideas: [
|
|
29
|
-
{
|
|
30
|
-
id: 'idea-1',
|
|
31
|
-
title: 'AI Route Optimizer',
|
|
32
|
-
description: 'Optimize delivery routes.',
|
|
33
|
-
workflowStepIds: [],
|
|
34
|
-
},
|
|
35
|
-
],
|
|
36
|
-
selection: {
|
|
37
|
-
ideaId: 'idea-1',
|
|
38
|
-
selectionRationale: 'Best idea',
|
|
39
|
-
confirmedByUser: true,
|
|
40
|
-
},
|
|
41
|
-
plan: {
|
|
42
|
-
milestones: [{ id: 'm1', title: 'Setup', items: [] }],
|
|
43
|
-
architectureNotes: 'Node.js + TypeScript',
|
|
44
|
-
},
|
|
45
|
-
...overrides,
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
function makeIo() {
|
|
49
|
-
return {
|
|
50
|
-
write: vi.fn(),
|
|
51
|
-
writeActivity: vi.fn(),
|
|
52
|
-
writeToolSummary: vi.fn(),
|
|
53
|
-
readInput: vi.fn().mockResolvedValue(null),
|
|
54
|
-
showDecisionGate: vi.fn().mockResolvedValue({ choice: 'exit' }),
|
|
55
|
-
isJsonMode: false,
|
|
56
|
-
isTTY: false,
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
function makeFakeClient() {
|
|
60
|
-
return {
|
|
61
|
-
createSession: vi.fn().mockResolvedValue({
|
|
62
|
-
send: vi.fn().mockReturnValue({
|
|
63
|
-
async *[Symbol.asyncIterator]() {
|
|
64
|
-
yield {
|
|
65
|
-
type: 'TextDelta',
|
|
66
|
-
text: '```typescript file=src/index.ts\nexport function main() { return "ok"; }\n```',
|
|
67
|
-
timestamp: '',
|
|
68
|
-
};
|
|
69
|
-
},
|
|
70
|
-
}),
|
|
71
|
-
getHistory: () => [],
|
|
72
|
-
}),
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
// ── validateSessionForDevelop ─────────────────────────────────────────────────
|
|
76
|
-
describe('validateSessionForDevelop', () => {
|
|
77
|
-
it('returns null for a valid session with selection and plan', () => {
|
|
78
|
-
const session = makeSession();
|
|
79
|
-
expect(validateSessionForDevelop(session)).toBeNull();
|
|
80
|
-
});
|
|
81
|
-
it('returns error when selection is missing', () => {
|
|
82
|
-
const session = makeSession({ selection: undefined });
|
|
83
|
-
const error = validateSessionForDevelop(session);
|
|
84
|
-
expect(error).not.toBeNull();
|
|
85
|
-
expect(error).toContain('missing an idea selection');
|
|
86
|
-
});
|
|
87
|
-
it('returns error when plan is missing', () => {
|
|
88
|
-
const session = makeSession({ plan: undefined });
|
|
89
|
-
const error = validateSessionForDevelop(session);
|
|
90
|
-
expect(error).not.toBeNull();
|
|
91
|
-
expect(error).toContain('missing an implementation plan');
|
|
92
|
-
});
|
|
93
|
-
it('error message includes guidance to run Select phase when selection missing', () => {
|
|
94
|
-
const session = makeSession({ selection: undefined });
|
|
95
|
-
const error = validateSessionForDevelop(session);
|
|
96
|
-
expect(error).toContain('Select');
|
|
97
|
-
});
|
|
98
|
-
it('error message includes guidance to run Plan phase when plan missing', () => {
|
|
99
|
-
const session = makeSession({ plan: undefined });
|
|
100
|
-
const error = validateSessionForDevelop(session);
|
|
101
|
-
expect(error).toContain('Plan');
|
|
102
|
-
});
|
|
103
|
-
});
|
|
104
|
-
// ── developCommand ────────────────────────────────────────────────────────────
|
|
105
|
-
describe('developCommand', () => {
|
|
106
|
-
let originalExitCode;
|
|
107
|
-
let stdoutOutput;
|
|
108
|
-
let stderrOutput;
|
|
109
|
-
beforeEach(() => {
|
|
110
|
-
originalExitCode = process.exitCode;
|
|
111
|
-
process.exitCode = undefined;
|
|
112
|
-
stdoutOutput = [];
|
|
113
|
-
stderrOutput = [];
|
|
114
|
-
vi.spyOn(process.stdout, 'write').mockImplementation((chunk) => {
|
|
115
|
-
stdoutOutput.push(chunk.toString());
|
|
116
|
-
return true;
|
|
117
|
-
});
|
|
118
|
-
vi.spyOn(process.stderr, 'write').mockImplementation((chunk) => {
|
|
119
|
-
stderrOutput.push(chunk.toString());
|
|
120
|
-
return true;
|
|
121
|
-
});
|
|
122
|
-
});
|
|
123
|
-
afterEach(() => {
|
|
124
|
-
process.exitCode = originalExitCode;
|
|
125
|
-
vi.restoreAllMocks();
|
|
126
|
-
});
|
|
127
|
-
it('fails with exitCode 1 when session not found', async () => {
|
|
128
|
-
const io = makeIo();
|
|
129
|
-
const client = makeFakeClient();
|
|
130
|
-
const store = {
|
|
131
|
-
load: vi.fn().mockResolvedValue(null),
|
|
132
|
-
save: vi.fn(),
|
|
133
|
-
list: vi.fn().mockResolvedValue([]),
|
|
134
|
-
};
|
|
135
|
-
await developCommand({ session: 'nonexistent' }, { store, io, client });
|
|
136
|
-
expect(process.exitCode).toBe(1);
|
|
137
|
-
});
|
|
138
|
-
it('fails with exitCode 1 when no sessions exist', async () => {
|
|
139
|
-
const io = makeIo();
|
|
140
|
-
const client = makeFakeClient();
|
|
141
|
-
const store = {
|
|
142
|
-
load: vi.fn().mockResolvedValue(null),
|
|
143
|
-
save: vi.fn(),
|
|
144
|
-
list: vi.fn().mockResolvedValue([]),
|
|
145
|
-
};
|
|
146
|
-
await developCommand({}, { store, io, client });
|
|
147
|
-
expect(process.exitCode).toBe(1);
|
|
148
|
-
});
|
|
149
|
-
it('outputs error as JSON when --json flag is set and session not found', async () => {
|
|
150
|
-
const io = makeIo();
|
|
151
|
-
const client = makeFakeClient();
|
|
152
|
-
const store = {
|
|
153
|
-
load: vi.fn().mockResolvedValue(null),
|
|
154
|
-
save: vi.fn(),
|
|
155
|
-
list: vi.fn().mockResolvedValue([]),
|
|
156
|
-
};
|
|
157
|
-
await developCommand({ session: 'missing', json: true }, { store, io, client });
|
|
158
|
-
expect(stdoutOutput.some((o) => o.includes('"error"'))).toBe(true);
|
|
159
|
-
});
|
|
160
|
-
it('fails with exitCode 1 when session has no selection', async () => {
|
|
161
|
-
const io = makeIo();
|
|
162
|
-
const client = makeFakeClient();
|
|
163
|
-
const session = makeSession({ selection: undefined });
|
|
164
|
-
const store = {
|
|
165
|
-
load: vi.fn().mockResolvedValue(session),
|
|
166
|
-
save: vi.fn(),
|
|
167
|
-
list: vi.fn().mockResolvedValue(['test-dev-session']),
|
|
168
|
-
};
|
|
169
|
-
await developCommand({ session: 'test-dev-session' }, { store, io, client });
|
|
170
|
-
expect(process.exitCode).toBe(1);
|
|
171
|
-
});
|
|
172
|
-
it('fails with exitCode 1 when session has no plan', async () => {
|
|
173
|
-
const io = makeIo();
|
|
174
|
-
const client = makeFakeClient();
|
|
175
|
-
const session = makeSession({ plan: undefined });
|
|
176
|
-
const store = {
|
|
177
|
-
load: vi.fn().mockResolvedValue(session),
|
|
178
|
-
save: vi.fn(),
|
|
179
|
-
list: vi.fn().mockResolvedValue(['test-dev-session']),
|
|
180
|
-
};
|
|
181
|
-
await developCommand({ session: 'test-dev-session' }, { store, io, client });
|
|
182
|
-
expect(process.exitCode).toBe(1);
|
|
183
|
-
});
|
|
184
|
-
it('outputs validation error as JSON when --json flag set and selection missing', async () => {
|
|
185
|
-
const io = makeIo();
|
|
186
|
-
const client = makeFakeClient();
|
|
187
|
-
const session = makeSession({ selection: undefined });
|
|
188
|
-
const store = {
|
|
189
|
-
load: vi.fn().mockResolvedValue(session),
|
|
190
|
-
save: vi.fn(),
|
|
191
|
-
list: vi.fn().mockResolvedValue(['test-dev-session']),
|
|
192
|
-
};
|
|
193
|
-
await developCommand({ session: 'test-dev-session', json: true }, { store, io, client });
|
|
194
|
-
const jsonOutput = stdoutOutput.find((o) => o.includes('"error"'));
|
|
195
|
-
expect(jsonOutput).toBeDefined();
|
|
196
|
-
const parsed = JSON.parse(jsonOutput);
|
|
197
|
-
expect(parsed.error).toContain('selection');
|
|
198
|
-
});
|
|
199
|
-
it('uses most recent session when no --session specified', async () => {
|
|
200
|
-
const io = makeIo();
|
|
201
|
-
const client = makeFakeClient();
|
|
202
|
-
const session = makeSession({ plan: undefined }); // Will fail validation
|
|
203
|
-
const store = {
|
|
204
|
-
load: vi.fn().mockResolvedValue(session),
|
|
205
|
-
save: vi.fn(),
|
|
206
|
-
list: vi.fn().mockResolvedValue(['session-1', 'session-2', 'test-dev-session']),
|
|
207
|
-
};
|
|
208
|
-
await developCommand({}, { store, io, client });
|
|
209
|
-
// Should try to load the last session in the list
|
|
210
|
-
expect(store.load).toHaveBeenCalledWith('test-dev-session');
|
|
211
|
-
});
|
|
212
|
-
});
|
|
213
|
-
// ── McpManager wiring ─────────────────────────────────────────────────────────
|
|
214
|
-
describe('developCommand — MCP wiring', () => {
|
|
215
|
-
let capturedOptions;
|
|
216
|
-
let savedExitCode;
|
|
217
|
-
beforeEach(() => {
|
|
218
|
-
capturedOptions = undefined;
|
|
219
|
-
savedExitCode = process.exitCode;
|
|
220
|
-
process.exitCode = undefined;
|
|
221
|
-
vi.mocked(RalphLoop).mockImplementation(function (options) {
|
|
222
|
-
capturedOptions = options;
|
|
223
|
-
const fakeResult = {
|
|
224
|
-
session: makeSession(),
|
|
225
|
-
finalStatus: 'success',
|
|
226
|
-
terminationReason: 'tests-passing',
|
|
227
|
-
iterationsCompleted: 1,
|
|
228
|
-
outputDir: '/tmp/poc-test',
|
|
229
|
-
};
|
|
230
|
-
return { run: vi.fn().mockResolvedValue(fakeResult) };
|
|
231
|
-
});
|
|
232
|
-
});
|
|
233
|
-
afterEach(() => {
|
|
234
|
-
process.exitCode = savedExitCode;
|
|
235
|
-
capturedOptions = undefined;
|
|
236
|
-
vi.mocked(RalphLoop).mockReset();
|
|
237
|
-
});
|
|
238
|
-
it('passes non-undefined enricher to RalphLoop when mcpManager provided', async () => {
|
|
239
|
-
const io = makeIo();
|
|
240
|
-
const client = makeFakeClient();
|
|
241
|
-
const mockMcpManager = {
|
|
242
|
-
isAvailable: (name) => name === 'github',
|
|
243
|
-
listServers: () => ['github'],
|
|
244
|
-
getServerConfig: () => undefined,
|
|
245
|
-
markConnected: vi.fn(),
|
|
246
|
-
markDisconnected: vi.fn(),
|
|
247
|
-
getAllConfigs: () => [],
|
|
248
|
-
};
|
|
249
|
-
const session = makeSession(); // valid — has both selection and plan
|
|
250
|
-
const store = {
|
|
251
|
-
load: vi.fn().mockResolvedValue(session),
|
|
252
|
-
save: vi.fn(),
|
|
253
|
-
list: vi.fn().mockResolvedValue(['test-dev-session']),
|
|
254
|
-
};
|
|
255
|
-
await developCommand({ session: 'test-dev-session' }, { store, io, client, mcpManager: mockMcpManager });
|
|
256
|
-
expect(capturedOptions).toBeDefined();
|
|
257
|
-
expect(capturedOptions?.enricher).toBeDefined();
|
|
258
|
-
});
|
|
259
|
-
it('passes undefined enricher to RalphLoop when no mcpManager provided', async () => {
|
|
260
|
-
const io = makeIo();
|
|
261
|
-
const client = makeFakeClient();
|
|
262
|
-
const session = makeSession(); // valid
|
|
263
|
-
const store = {
|
|
264
|
-
load: vi.fn().mockResolvedValue(session),
|
|
265
|
-
save: vi.fn(),
|
|
266
|
-
list: vi.fn().mockResolvedValue(['test-dev-session']),
|
|
267
|
-
};
|
|
268
|
-
await developCommand({ session: 'test-dev-session' }, { store, io, client });
|
|
269
|
-
expect(capturedOptions).toBeDefined();
|
|
270
|
-
expect(capturedOptions?.enricher).toBeUndefined();
|
|
271
|
-
});
|
|
272
|
-
});
|
|
273
|
-
// ── --force option ────────────────────────────────────────────────────────────
|
|
274
|
-
describe('developCommand — --force option', () => {
|
|
275
|
-
let tmpDir;
|
|
276
|
-
let relOutput;
|
|
277
|
-
let _capturedOptions;
|
|
278
|
-
let savedExitCode;
|
|
279
|
-
beforeEach(() => {
|
|
280
|
-
_capturedOptions = undefined;
|
|
281
|
-
savedExitCode = process.exitCode;
|
|
282
|
-
process.exitCode = undefined;
|
|
283
|
-
relOutput = `tmp/sofia-dev-test-${Date.now()}`;
|
|
284
|
-
tmpDir = join(process.cwd(), relOutput);
|
|
285
|
-
vi.mocked(RalphLoop).mockImplementation(function (options) {
|
|
286
|
-
_capturedOptions = options;
|
|
287
|
-
const fakeResult = {
|
|
288
|
-
session: makeSession(),
|
|
289
|
-
finalStatus: 'success',
|
|
290
|
-
terminationReason: 'tests-passing',
|
|
291
|
-
iterationsCompleted: 1,
|
|
292
|
-
outputDir: options.outputDir ?? tmpDir,
|
|
293
|
-
};
|
|
294
|
-
return { run: vi.fn().mockResolvedValue(fakeResult) };
|
|
295
|
-
});
|
|
296
|
-
vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
297
|
-
vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
|
|
298
|
-
});
|
|
299
|
-
afterEach(() => {
|
|
300
|
-
process.exitCode = savedExitCode;
|
|
301
|
-
_capturedOptions = undefined;
|
|
302
|
-
vi.mocked(RalphLoop).mockReset();
|
|
303
|
-
vi.restoreAllMocks();
|
|
304
|
-
// Clean up temp dir
|
|
305
|
-
if (existsSync(tmpDir)) {
|
|
306
|
-
rmSync(tmpDir, { recursive: true, force: true });
|
|
307
|
-
}
|
|
308
|
-
});
|
|
309
|
-
it('resumes from existing directory when --force is not set', async () => {
|
|
310
|
-
// Create output directory with metadata matching the session
|
|
311
|
-
mkdirSync(tmpDir, { recursive: true });
|
|
312
|
-
writeFileSync(join(tmpDir, '.sofia-metadata.json'), JSON.stringify({ sessionId: 'test-dev-session' }));
|
|
313
|
-
const io = makeIo();
|
|
314
|
-
const client = makeFakeClient();
|
|
315
|
-
// Session with prior iterations to trigger resume
|
|
316
|
-
const session = makeSession({
|
|
317
|
-
poc: {
|
|
318
|
-
repoSource: 'local',
|
|
319
|
-
repoPath: tmpDir,
|
|
320
|
-
iterations: [
|
|
321
|
-
{
|
|
322
|
-
iteration: 1,
|
|
323
|
-
startedAt: new Date().toISOString(),
|
|
324
|
-
endedAt: new Date().toISOString(),
|
|
325
|
-
outcome: 'scaffold',
|
|
326
|
-
filesChanged: [],
|
|
327
|
-
},
|
|
328
|
-
],
|
|
329
|
-
},
|
|
330
|
-
});
|
|
331
|
-
const store = {
|
|
332
|
-
load: vi.fn().mockResolvedValue(session),
|
|
333
|
-
save: vi.fn(),
|
|
334
|
-
list: vi.fn().mockResolvedValue(['test-dev-session']),
|
|
335
|
-
};
|
|
336
|
-
await developCommand({ session: 'test-dev-session', output: relOutput }, { store, io, client });
|
|
337
|
-
// Should have called writeActivity with "Resuming" message
|
|
338
|
-
expect(io.writeActivity).toHaveBeenCalledWith(expect.stringContaining('Resuming'));
|
|
339
|
-
// Directory should still exist
|
|
340
|
-
expect(existsSync(tmpDir)).toBe(true);
|
|
341
|
-
expect(existsSync(join(tmpDir, '.sofia-metadata.json'))).toBe(true);
|
|
342
|
-
});
|
|
343
|
-
it('clears directory when --force is set', async () => {
|
|
344
|
-
// Create output directory with some files
|
|
345
|
-
mkdirSync(tmpDir, { recursive: true });
|
|
346
|
-
writeFileSync(join(tmpDir, '.sofia-metadata.json'), JSON.stringify({ scaffold: true }));
|
|
347
|
-
writeFileSync(join(tmpDir, 'old-file.ts'), 'old code');
|
|
348
|
-
const io = makeIo();
|
|
349
|
-
const client = makeFakeClient();
|
|
350
|
-
const session = makeSession();
|
|
351
|
-
const store = {
|
|
352
|
-
load: vi.fn().mockResolvedValue(session),
|
|
353
|
-
save: vi.fn(),
|
|
354
|
-
list: vi.fn().mockResolvedValue(['test-dev-session']),
|
|
355
|
-
};
|
|
356
|
-
await developCommand({ session: 'test-dev-session', output: relOutput, force: true }, { store, io, client });
|
|
357
|
-
// Should have called writeActivity with "Cleared" message
|
|
358
|
-
expect(io.writeActivity).toHaveBeenCalledWith(expect.stringContaining('Cleared'));
|
|
359
|
-
// Old file was removed (RalphLoop mock doesn't recreate it)
|
|
360
|
-
expect(existsSync(join(tmpDir, 'old-file.ts'))).toBe(false);
|
|
361
|
-
});
|
|
362
|
-
it('--force clears session.poc and calls store.save() before creating RalphLoop (T027, FR-008)', async () => {
|
|
363
|
-
const io = makeIo();
|
|
364
|
-
const client = makeFakeClient();
|
|
365
|
-
const session = makeSession({
|
|
366
|
-
poc: {
|
|
367
|
-
repoSource: 'local',
|
|
368
|
-
iterations: [
|
|
369
|
-
{
|
|
370
|
-
iteration: 1,
|
|
371
|
-
startedAt: new Date().toISOString(),
|
|
372
|
-
endedAt: new Date().toISOString(),
|
|
373
|
-
outcome: 'scaffold',
|
|
374
|
-
filesChanged: [],
|
|
375
|
-
},
|
|
376
|
-
],
|
|
377
|
-
finalStatus: 'failed',
|
|
378
|
-
},
|
|
379
|
-
});
|
|
380
|
-
const store = {
|
|
381
|
-
load: vi.fn().mockResolvedValue(session),
|
|
382
|
-
save: vi.fn(),
|
|
383
|
-
list: vi.fn().mockResolvedValue(['test-dev-session']),
|
|
384
|
-
};
|
|
385
|
-
await developCommand({ session: 'test-dev-session', output: relOutput, force: true }, { store, io, client });
|
|
386
|
-
// store.save should have been called with poc cleared
|
|
387
|
-
expect(store.save).toHaveBeenCalledWith(expect.objectContaining({ poc: undefined }));
|
|
388
|
-
});
|
|
389
|
-
it('--force on a success session clears status and starts fresh (T028, FR-010)', async () => {
|
|
390
|
-
const io = makeIo();
|
|
391
|
-
const client = makeFakeClient();
|
|
392
|
-
const session = makeSession({
|
|
393
|
-
poc: {
|
|
394
|
-
repoSource: 'local',
|
|
395
|
-
iterations: [
|
|
396
|
-
{
|
|
397
|
-
iteration: 1,
|
|
398
|
-
startedAt: new Date().toISOString(),
|
|
399
|
-
endedAt: new Date().toISOString(),
|
|
400
|
-
outcome: 'tests-passing',
|
|
401
|
-
filesChanged: [],
|
|
402
|
-
testResults: {
|
|
403
|
-
passed: 2,
|
|
404
|
-
failed: 0,
|
|
405
|
-
skipped: 0,
|
|
406
|
-
total: 2,
|
|
407
|
-
durationMs: 100,
|
|
408
|
-
failures: [],
|
|
409
|
-
},
|
|
410
|
-
},
|
|
411
|
-
],
|
|
412
|
-
finalStatus: 'success',
|
|
413
|
-
},
|
|
414
|
-
});
|
|
415
|
-
const store = {
|
|
416
|
-
load: vi.fn().mockResolvedValue(session),
|
|
417
|
-
save: vi.fn(),
|
|
418
|
-
list: vi.fn().mockResolvedValue(['test-dev-session']),
|
|
419
|
-
};
|
|
420
|
-
await developCommand({ session: 'test-dev-session', output: relOutput, force: true }, { store, io, client });
|
|
421
|
-
// Should have cleared poc before running loop
|
|
422
|
-
expect(store.save).toHaveBeenCalledWith(expect.objectContaining({ poc: undefined }));
|
|
423
|
-
// RalphLoop should have been constructed and run
|
|
424
|
-
expect(RalphLoop).toHaveBeenCalled();
|
|
425
|
-
});
|
|
426
|
-
it('--force on a session with no prior poc behaves identically to first run (T029)', async () => {
|
|
427
|
-
const io = makeIo();
|
|
428
|
-
const client = makeFakeClient();
|
|
429
|
-
const session = makeSession(); // No poc
|
|
430
|
-
const store = {
|
|
431
|
-
load: vi.fn().mockResolvedValue(session),
|
|
432
|
-
save: vi.fn(),
|
|
433
|
-
list: vi.fn().mockResolvedValue(['test-dev-session']),
|
|
434
|
-
};
|
|
435
|
-
await developCommand({ session: 'test-dev-session', output: relOutput, force: true }, { store, io, client });
|
|
436
|
-
// store.save should have been called (poc was already undefined)
|
|
437
|
-
expect(store.save).toHaveBeenCalled();
|
|
438
|
-
// RalphLoop should have been created
|
|
439
|
-
expect(RalphLoop).toHaveBeenCalled();
|
|
440
|
-
});
|
|
441
|
-
});
|
|
442
|
-
// ── Resume behavior (US1) ─────────────────────────────────────────────────
|
|
443
|
-
describe('developCommand — resume behavior', () => {
|
|
444
|
-
let savedExitCode;
|
|
445
|
-
beforeEach(() => {
|
|
446
|
-
savedExitCode = process.exitCode;
|
|
447
|
-
process.exitCode = undefined;
|
|
448
|
-
vi.mocked(RalphLoop).mockImplementation(function (options) {
|
|
449
|
-
const fakeResult = {
|
|
450
|
-
session: makeSession(),
|
|
451
|
-
finalStatus: 'success',
|
|
452
|
-
terminationReason: 'tests-passing',
|
|
453
|
-
iterationsCompleted: 1,
|
|
454
|
-
outputDir: options.outputDir ?? '/tmp/poc-test',
|
|
455
|
-
};
|
|
456
|
-
return { run: vi.fn().mockResolvedValue(fakeResult) };
|
|
457
|
-
});
|
|
458
|
-
vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
459
|
-
vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
|
|
460
|
-
});
|
|
461
|
-
afterEach(() => {
|
|
462
|
-
process.exitCode = savedExitCode;
|
|
463
|
-
vi.mocked(RalphLoop).mockReset();
|
|
464
|
-
vi.restoreAllMocks();
|
|
465
|
-
});
|
|
466
|
-
it('exits with completion message when poc.finalStatus is success (T016, FR-005)', async () => {
|
|
467
|
-
const io = makeIo();
|
|
468
|
-
const client = makeFakeClient();
|
|
469
|
-
const session = makeSession({
|
|
470
|
-
poc: {
|
|
471
|
-
repoSource: 'local',
|
|
472
|
-
iterations: [
|
|
473
|
-
{
|
|
474
|
-
iteration: 1,
|
|
475
|
-
startedAt: new Date().toISOString(),
|
|
476
|
-
endedAt: new Date().toISOString(),
|
|
477
|
-
outcome: 'tests-passing',
|
|
478
|
-
filesChanged: [],
|
|
479
|
-
testResults: {
|
|
480
|
-
passed: 2,
|
|
481
|
-
failed: 0,
|
|
482
|
-
skipped: 0,
|
|
483
|
-
total: 2,
|
|
484
|
-
durationMs: 100,
|
|
485
|
-
failures: [],
|
|
486
|
-
},
|
|
487
|
-
},
|
|
488
|
-
],
|
|
489
|
-
finalStatus: 'success',
|
|
490
|
-
},
|
|
491
|
-
});
|
|
492
|
-
const store = {
|
|
493
|
-
load: vi.fn().mockResolvedValue(session),
|
|
494
|
-
save: vi.fn(),
|
|
495
|
-
list: vi.fn().mockResolvedValue(['test-dev-session']),
|
|
496
|
-
};
|
|
497
|
-
await developCommand({ session: 'test-dev-session' }, { store, io, client });
|
|
498
|
-
// Should NOT have created a RalphLoop
|
|
499
|
-
expect(RalphLoop).not.toHaveBeenCalled();
|
|
500
|
-
// Should have displayed completion message
|
|
501
|
-
expect(io.writeActivity).toHaveBeenCalledWith(expect.stringContaining('already complete'));
|
|
502
|
-
});
|
|
503
|
-
it('defaults to resume when poc.finalStatus is failed (T017, FR-006)', async () => {
|
|
504
|
-
const io = makeIo();
|
|
505
|
-
const client = makeFakeClient();
|
|
506
|
-
const session = makeSession({
|
|
507
|
-
poc: {
|
|
508
|
-
repoSource: 'local',
|
|
509
|
-
iterations: [
|
|
510
|
-
{
|
|
511
|
-
iteration: 1,
|
|
512
|
-
startedAt: new Date().toISOString(),
|
|
513
|
-
endedAt: new Date().toISOString(),
|
|
514
|
-
outcome: 'scaffold',
|
|
515
|
-
filesChanged: [],
|
|
516
|
-
},
|
|
517
|
-
],
|
|
518
|
-
finalStatus: 'failed',
|
|
519
|
-
},
|
|
520
|
-
});
|
|
521
|
-
const store = {
|
|
522
|
-
load: vi.fn().mockResolvedValue(session),
|
|
523
|
-
save: vi.fn(),
|
|
524
|
-
list: vi.fn().mockResolvedValue(['test-dev-session']),
|
|
525
|
-
};
|
|
526
|
-
await developCommand({ session: 'test-dev-session' }, { store, io, client });
|
|
527
|
-
// Should have displayed resume message
|
|
528
|
-
expect(io.writeActivity).toHaveBeenCalledWith(expect.stringContaining('Resuming session'));
|
|
529
|
-
// RalphLoop should have been created
|
|
530
|
-
expect(RalphLoop).toHaveBeenCalled();
|
|
531
|
-
});
|
|
532
|
-
it('defaults to resume when poc.finalStatus is partial (T017, FR-006)', async () => {
|
|
533
|
-
const io = makeIo();
|
|
534
|
-
const client = makeFakeClient();
|
|
535
|
-
const session = makeSession({
|
|
536
|
-
poc: {
|
|
537
|
-
repoSource: 'local',
|
|
538
|
-
iterations: [
|
|
539
|
-
{
|
|
540
|
-
iteration: 1,
|
|
541
|
-
startedAt: new Date().toISOString(),
|
|
542
|
-
endedAt: new Date().toISOString(),
|
|
543
|
-
outcome: 'tests-failing',
|
|
544
|
-
filesChanged: [],
|
|
545
|
-
testResults: {
|
|
546
|
-
passed: 1,
|
|
547
|
-
failed: 1,
|
|
548
|
-
skipped: 0,
|
|
549
|
-
total: 2,
|
|
550
|
-
durationMs: 100,
|
|
551
|
-
failures: [{ testName: 'test1', message: 'fail' }],
|
|
552
|
-
},
|
|
553
|
-
},
|
|
554
|
-
],
|
|
555
|
-
finalStatus: 'partial',
|
|
556
|
-
},
|
|
557
|
-
});
|
|
558
|
-
const store = {
|
|
559
|
-
load: vi.fn().mockResolvedValue(session),
|
|
560
|
-
save: vi.fn(),
|
|
561
|
-
list: vi.fn().mockResolvedValue(['test-dev-session']),
|
|
562
|
-
};
|
|
563
|
-
await developCommand({ session: 'test-dev-session' }, { store, io, client });
|
|
564
|
-
expect(io.writeActivity).toHaveBeenCalledWith(expect.stringContaining('Resuming session'));
|
|
565
|
-
expect(RalphLoop).toHaveBeenCalled();
|
|
566
|
-
});
|
|
567
|
-
});
|