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.
Files changed (136) hide show
  1. package/README.md +42 -20
  2. package/dist/infra/deploy.sh +193 -0
  3. package/dist/infra/gather-env.sh +211 -0
  4. package/dist/infra/infra/deploy.sh +193 -0
  5. package/dist/infra/infra/gather-env.sh +211 -0
  6. package/dist/infra/infra/main.bicep +90 -0
  7. package/dist/infra/infra/main.bicepparam +18 -0
  8. package/dist/infra/infra/resources.bicep +134 -0
  9. package/dist/infra/infra/teardown.sh +114 -0
  10. package/dist/infra/main.bicep +90 -0
  11. package/dist/infra/main.bicepparam +18 -0
  12. package/dist/infra/resources.bicep +134 -0
  13. package/dist/infra/teardown.sh +114 -0
  14. package/dist/src/cli/developCommand.js +0 -2
  15. package/dist/src/cli/index.js +8 -1
  16. package/dist/src/cli/workshopCommand.js +1 -1
  17. package/dist/src/develop/index.js +1 -1
  18. package/dist/src/develop/pocUtils.js +228 -0
  19. package/dist/src/develop/ralphLoop.js +8 -27
  20. package/dist/src/shared/data/cards.json +655 -670
  21. package/docs/architecture.md +2 -1
  22. package/package.json +5 -3
  23. package/src/cli/developCommand.ts +1 -3
  24. package/src/cli/index.ts +11 -1
  25. package/src/cli/workshopCommand.ts +21 -17
  26. package/src/develop/dynamicScaffolder.ts +36 -30
  27. package/src/develop/index.ts +13 -2
  28. package/src/develop/pocUtils.ts +296 -0
  29. package/src/develop/ralphLoop.ts +8 -28
  30. package/src/develop/templateRegistry.ts +19 -18
  31. package/src/shared/data/cards.json +655 -670
  32. package/tests/e2e/developE2e.spec.ts +3 -61
  33. package/tests/e2e/developFailureE2e.spec.ts +34 -38
  34. package/tests/integration/pocGithubMcp.spec.ts +29 -39
  35. package/tests/integration/pocLocalFallback.spec.ts +29 -39
  36. package/tests/integration/ralphLoopFlow.spec.ts +46 -66
  37. package/tests/integration/ralphLoopPartial.spec.ts +30 -37
  38. package/tests/unit/develop/githubMcpAdapter.spec.ts +0 -134
  39. package/tests/unit/develop/outputValidator.spec.ts +45 -21
  40. package/tests/unit/develop/ralphLoop.spec.ts +58 -94
  41. package/tsconfig.json +2 -1
  42. package/vitest.workspace.ts +5 -0
  43. package/dist/src/develop/pocScaffolder.js +0 -542
  44. package/dist/tests/e2e/developE2e.spec.js +0 -126
  45. package/dist/tests/e2e/developFailureE2e.spec.js +0 -247
  46. package/dist/tests/e2e/developPty.spec.js +0 -75
  47. package/dist/tests/e2e/discoveryWebSearchRelevance.spec.js +0 -84
  48. package/dist/tests/e2e/harness.spec.js +0 -83
  49. package/dist/tests/e2e/mcpLive.spec.js +0 -120
  50. package/dist/tests/e2e/newSession.e2e.spec.js +0 -177
  51. package/dist/tests/e2e/ralphLoopEnrichmentComparison.spec.js +0 -62
  52. package/dist/tests/e2e/workiqEnrichment.spec.js +0 -56
  53. package/dist/tests/e2e/zavaSimulation.spec.js +0 -452
  54. package/dist/tests/fixtures/test-fixture-project/src/add.js +0 -3
  55. package/dist/tests/fixtures/test-fixture-project/tests/failing.test.js +0 -6
  56. package/dist/tests/fixtures/test-fixture-project/tests/hanging.test.js +0 -8
  57. package/dist/tests/fixtures/test-fixture-project/tests/passing.test.js +0 -10
  58. package/dist/tests/fixtures/test-fixture-project/vitest.config.js +0 -6
  59. package/dist/tests/integration/autoStartConversation.spec.js +0 -138
  60. package/dist/tests/integration/defaultCommand.spec.js +0 -147
  61. package/dist/tests/integration/directCommandNonTty.spec.js +0 -224
  62. package/dist/tests/integration/directCommandTty.spec.js +0 -151
  63. package/dist/tests/integration/discoveryEnrichmentFlow.spec.js +0 -175
  64. package/dist/tests/integration/exportArtifacts.spec.js +0 -202
  65. package/dist/tests/integration/exportFallbackFlow.spec.js +0 -99
  66. package/dist/tests/integration/mcpDegradationFlow.spec.js +0 -190
  67. package/dist/tests/integration/mcpTransportFlow.spec.js +0 -139
  68. package/dist/tests/integration/newSessionFlow.spec.js +0 -343
  69. package/dist/tests/integration/pocGithubMcp.spec.js +0 -186
  70. package/dist/tests/integration/pocLocalFallback.spec.js +0 -171
  71. package/dist/tests/integration/pocScaffold.spec.js +0 -163
  72. package/dist/tests/integration/ralphLoopFlow.spec.js +0 -359
  73. package/dist/tests/integration/ralphLoopPartial.spec.js +0 -368
  74. package/dist/tests/integration/resumeAndBacktrack.spec.js +0 -247
  75. package/dist/tests/integration/spinnerLifecycle.spec.js +0 -220
  76. package/dist/tests/integration/summarizationFlow.spec.js +0 -115
  77. package/dist/tests/integration/testRunnerReal.spec.js +0 -52
  78. package/dist/tests/integration/webSearchAgent.spec.js +0 -128
  79. package/dist/tests/live/copilotSdkLive.spec.js +0 -107
  80. package/dist/tests/live/zavaFullWorkshop.spec.js +0 -392
  81. package/dist/tests/setup/loadEnv.js +0 -3
  82. package/dist/tests/unit/cli/developCommand.spec.js +0 -567
  83. package/dist/tests/unit/cli/directCommands.spec.js +0 -279
  84. package/dist/tests/unit/cli/envLoader.spec.js +0 -58
  85. package/dist/tests/unit/cli/ioContext.spec.js +0 -119
  86. package/dist/tests/unit/cli/preflight.spec.js +0 -108
  87. package/dist/tests/unit/cli/statusCommand.spec.js +0 -111
  88. package/dist/tests/unit/cli/workshopClientFallback.spec.js +0 -80
  89. package/dist/tests/unit/cli/workshopCommand.spec.js +0 -328
  90. package/dist/tests/unit/config/vitestEnvSetup.spec.js +0 -13
  91. package/dist/tests/unit/develop/checkpointState.spec.js +0 -315
  92. package/dist/tests/unit/develop/codeGenerator.spec.js +0 -355
  93. package/dist/tests/unit/develop/githubMcpAdapter.spec.js +0 -231
  94. package/dist/tests/unit/develop/mcpContextEnricher.spec.js +0 -433
  95. package/dist/tests/unit/develop/outputValidator.spec.js +0 -119
  96. package/dist/tests/unit/develop/pocScaffolder.spec.js +0 -353
  97. package/dist/tests/unit/develop/ralphLoop.spec.js +0 -1248
  98. package/dist/tests/unit/develop/templateRegistry.spec.js +0 -85
  99. package/dist/tests/unit/develop/testRunner.spec.js +0 -249
  100. package/dist/tests/unit/infraBicep.spec.js +0 -92
  101. package/dist/tests/unit/infraDeploy.spec.js +0 -82
  102. package/dist/tests/unit/infraTeardown.spec.js +0 -63
  103. package/dist/tests/unit/logging/logger.spec.js +0 -43
  104. package/dist/tests/unit/loop/conversationLoop.spec.js +0 -592
  105. package/dist/tests/unit/loop/phaseSummarizer.spec.js +0 -141
  106. package/dist/tests/unit/loop/streamingMarkdown.spec.js +0 -147
  107. package/dist/tests/unit/mcp/mcpManager.spec.js +0 -279
  108. package/dist/tests/unit/mcp/mcpTransport.spec.js +0 -529
  109. package/dist/tests/unit/mcp/retryPolicy.spec.js +0 -218
  110. package/dist/tests/unit/mcp/timeoutValidation.spec.js +0 -46
  111. package/dist/tests/unit/mcp/webSearch.spec.js +0 -567
  112. package/dist/tests/unit/phases/contextSummarizer.spec.js +0 -140
  113. package/dist/tests/unit/phases/discoveryEnricher.repeatCalls.spec.js +0 -93
  114. package/dist/tests/unit/phases/discoveryEnricher.spec.js +0 -411
  115. package/dist/tests/unit/phases/phaseExtractors.spec.js +0 -352
  116. package/dist/tests/unit/phases/phaseHandlers.spec.js +0 -425
  117. package/dist/tests/unit/prompts/promptLoader.spec.js +0 -118
  118. package/dist/tests/unit/schemas/pocSchemas.spec.js +0 -412
  119. package/dist/tests/unit/schemas/session.spec.js +0 -257
  120. package/dist/tests/unit/sessions/exportPaths.spec.js +0 -31
  121. package/dist/tests/unit/sessions/exportWriter.spec.js +0 -655
  122. package/dist/tests/unit/sessions/sessionManager.spec.js +0 -151
  123. package/dist/tests/unit/sessions/sessionStore.spec.js +0 -116
  124. package/dist/tests/unit/shared/activitySpinner.spec.js +0 -175
  125. package/dist/tests/unit/shared/cardsLoader.spec.js +0 -76
  126. package/dist/tests/unit/shared/copilotClient.spec.js +0 -155
  127. package/dist/tests/unit/shared/errorClassifier.spec.js +0 -131
  128. package/dist/tests/unit/shared/events.spec.js +0 -55
  129. package/dist/tests/unit/shared/markdownRenderer.spec.js +0 -35
  130. package/dist/tests/unit/shared/markdownRendererChunks.spec.js +0 -70
  131. package/dist/tests/unit/shared/tableRenderer.spec.js +0 -34
  132. package/dist/vitest.config.js +0 -14
  133. package/dist/vitest.live.config.js +0 -18
  134. package/src/develop/pocScaffolder.ts +0 -646
  135. package/tests/integration/pocScaffold.spec.ts +0 -220
  136. 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
- });