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.
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 +3 -3
  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 -329
  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,220 +0,0 @@
1
- /**
2
- * T015: Integration test for scaffold-only flow.
3
- *
4
- * Runs scaffolder with fixture session → verify output directory structure
5
- * matches poc-output contract → verify package.json has test script →
6
- * verify .sofia-metadata.json links to session.
7
- */
8
- import { describe, it, expect, beforeEach, afterEach } from 'vitest';
9
- import { mkdtemp, rm, readFile } from 'node:fs/promises';
10
- import { join } from 'node:path';
11
- import { tmpdir } from 'node:os';
12
- import { existsSync } from 'node:fs';
13
- import { createRequire } from 'node:module';
14
-
15
- import { PocScaffolder, validatePocOutput } from '../../src/develop/pocScaffolder.js';
16
- import type { WorkshopSession } from '../../src/shared/schemas/session.js';
17
-
18
- // ── Load fixture session ──────────────────────────────────────────────────────
19
-
20
- const require = createRequire(import.meta.url);
21
- const fixtureSession: WorkshopSession = require('../fixtures/completedSession.json') as WorkshopSession;
22
-
23
- describe('PoC Scaffold Integration', () => {
24
- let tmpDir: string;
25
-
26
- beforeEach(async () => {
27
- tmpDir = await mkdtemp(join(tmpdir(), 'sofia-scaffold-integration-'));
28
- });
29
-
30
- afterEach(async () => {
31
- await rm(tmpDir, { recursive: true, force: true });
32
- });
33
-
34
- it('creates valid directory structure from fixture session', async () => {
35
- const scaffolder = new PocScaffolder();
36
- const ctx = PocScaffolder.buildContext(fixtureSession, tmpDir);
37
- const result = await scaffolder.scaffold(ctx);
38
-
39
- // Verify files were created
40
- expect(result.createdFiles.length).toBeGreaterThan(0);
41
-
42
- // Verify all required files per poc-output contract
43
- const requiredFiles = [
44
- '.gitignore',
45
- 'README.md',
46
- 'package.json',
47
- 'tsconfig.json',
48
- '.sofia-metadata.json',
49
- ];
50
-
51
- for (const file of requiredFiles) {
52
- expect(existsSync(join(tmpDir, file)), `Expected ${file} to exist`).toBe(true);
53
- }
54
-
55
- // Verify src/ and tests/ directories
56
- expect(existsSync(join(tmpDir, 'src', 'index.ts'))).toBe(true);
57
- expect(existsSync(join(tmpDir, 'tests', 'index.test.ts'))).toBe(true);
58
- });
59
-
60
- it('package.json has required test script', async () => {
61
- const scaffolder = new PocScaffolder();
62
- const ctx = PocScaffolder.buildContext(fixtureSession, tmpDir);
63
- await scaffolder.scaffold(ctx);
64
-
65
- const pkgContent = await readFile(join(tmpDir, 'package.json'), 'utf-8');
66
- const pkg = JSON.parse(pkgContent) as {
67
- name: string;
68
- scripts: Record<string, string>;
69
- type: string;
70
- };
71
-
72
- expect(pkg.scripts.test).toBeDefined();
73
- expect(pkg.scripts.test).toBe('vitest run');
74
- expect(pkg.type).toBe('module');
75
- expect(pkg.name).toBe('ai-powered-route-optimizer'); // from fixture
76
- });
77
-
78
- it('.sofia-metadata.json links to session', async () => {
79
- const scaffolder = new PocScaffolder();
80
- const ctx = PocScaffolder.buildContext(fixtureSession, tmpDir);
81
- await scaffolder.scaffold(ctx);
82
-
83
- const metaContent = await readFile(join(tmpDir, '.sofia-metadata.json'), 'utf-8');
84
- const meta = JSON.parse(metaContent) as {
85
- sessionId: string;
86
- featureSpec: string;
87
- ideaTitle: string;
88
- generatedAt: string;
89
- };
90
-
91
- expect(meta.sessionId).toBe(fixtureSession.sessionId);
92
- expect(meta.featureSpec).toBe('002-poc-generation');
93
- expect(meta.ideaTitle).toBe('AI-Powered Route Optimizer');
94
- expect(meta.generatedAt).toBeDefined();
95
- });
96
-
97
- it('tsconfig.json is valid JSON with strict mode', async () => {
98
- const scaffolder = new PocScaffolder();
99
- const ctx = PocScaffolder.buildContext(fixtureSession, tmpDir);
100
- await scaffolder.scaffold(ctx);
101
-
102
- const tsconfigContent = await readFile(join(tmpDir, 'tsconfig.json'), 'utf-8');
103
- const tsconfig = JSON.parse(tsconfigContent) as {
104
- compilerOptions: { strict: boolean; module: string };
105
- };
106
-
107
- expect(tsconfig.compilerOptions.strict).toBe(true);
108
- expect(tsconfig.compilerOptions.module).toBe('Node16');
109
- });
110
-
111
- it('.gitignore contains required patterns', async () => {
112
- const scaffolder = new PocScaffolder();
113
- const ctx = PocScaffolder.buildContext(fixtureSession, tmpDir);
114
- await scaffolder.scaffold(ctx);
115
-
116
- const gitignoreContent = await readFile(join(tmpDir, '.gitignore'), 'utf-8');
117
- expect(gitignoreContent).toContain('node_modules/');
118
- expect(gitignoreContent).toContain('dist/');
119
- expect(gitignoreContent).toContain('coverage/');
120
- });
121
-
122
- it('README.md contains idea title and generated-by attribution', async () => {
123
- const scaffolder = new PocScaffolder();
124
- const ctx = PocScaffolder.buildContext(fixtureSession, tmpDir);
125
- await scaffolder.scaffold(ctx);
126
-
127
- const readmeContent = await readFile(join(tmpDir, 'README.md'), 'utf-8');
128
- expect(readmeContent).toContain('AI-Powered Route Optimizer');
129
- expect(readmeContent).toContain(fixtureSession.sessionId);
130
- expect(readmeContent).toContain('sofIA');
131
- });
132
-
133
- it('validatePocOutput returns valid=true for complete scaffold', async () => {
134
- const scaffolder = new PocScaffolder();
135
- const ctx = PocScaffolder.buildContext(fixtureSession, tmpDir);
136
- await scaffolder.scaffold(ctx);
137
-
138
- const validation = await validatePocOutput(tmpDir);
139
-
140
- expect(validation.valid).toBe(true);
141
- expect(validation.missingFiles).toHaveLength(0);
142
- expect(validation.errors).toHaveLength(0);
143
- });
144
-
145
- it('infers tech stack from plan architecture notes', async () => {
146
- const _scaffolder = new PocScaffolder();
147
- const ctx = PocScaffolder.buildContext(fixtureSession, tmpDir);
148
-
149
- // Fixture session has 'express' in architectureNotes
150
- expect(ctx.techStack.language).toBe('TypeScript');
151
- expect(ctx.techStack.runtime).toBe('Node.js 20');
152
- expect(ctx.techStack.framework).toBe('Express');
153
- });
154
-
155
- it('src/index.ts exports a main function', async () => {
156
- const scaffolder = new PocScaffolder();
157
- const ctx = PocScaffolder.buildContext(fixtureSession, tmpDir);
158
- await scaffolder.scaffold(ctx);
159
-
160
- const indexContent = await readFile(join(tmpDir, 'src', 'index.ts'), 'utf-8');
161
- expect(indexContent).toContain('export async function main');
162
- });
163
-
164
- it('tests/index.test.ts contains vitest imports', async () => {
165
- const scaffolder = new PocScaffolder();
166
- const ctx = PocScaffolder.buildContext(fixtureSession, tmpDir);
167
- await scaffolder.scaffold(ctx);
168
-
169
- const testContent = await readFile(join(tmpDir, 'tests', 'index.test.ts'), 'utf-8');
170
- expect(testContent).toContain('vitest');
171
- expect(testContent).toContain('describe');
172
- expect(testContent).toContain('expect');
173
- });
174
- });
175
-
176
- // ── python-pytest template integration (T037) ────────────────────────────
177
-
178
- describe('python-pytest template', () => {
179
- let tmpDir: string;
180
-
181
- beforeEach(async () => {
182
- tmpDir = await mkdtemp(join(tmpdir(), 'sofia-python-scaffold-'));
183
- });
184
-
185
- afterEach(async () => {
186
- await rm(tmpDir, { recursive: true, force: true });
187
- });
188
-
189
- it('scaffolds expected Python project structure (T037)', async () => {
190
- const { PYTHON_PYTEST_TEMPLATE } = await import('../../src/develop/templateRegistry.js');
191
-
192
- const scaffolder = new PocScaffolder(PYTHON_PYTEST_TEMPLATE);
193
- const ctx = PocScaffolder.buildContext(fixtureSession, tmpDir, PYTHON_PYTEST_TEMPLATE);
194
-
195
- const result = await scaffolder.scaffold(ctx);
196
-
197
- expect(result.createdFiles).toContain('requirements.txt');
198
- expect(result.createdFiles).toContain('pytest.ini');
199
- expect(result.createdFiles).toContain('src/__init__.py');
200
- expect(result.createdFiles).toContain('src/main.py');
201
- expect(result.createdFiles).toContain('tests/test_main.py');
202
- expect(result.createdFiles).toContain('.sofia-metadata.json');
203
- expect(result.createdFiles).toContain('README.md');
204
-
205
- // Verify files exist on disk
206
- expect(existsSync(join(tmpDir, 'requirements.txt'))).toBe(true);
207
- expect(existsSync(join(tmpDir, 'src', 'main.py'))).toBe(true);
208
- expect(existsSync(join(tmpDir, 'tests', 'test_main.py'))).toBe(true);
209
-
210
- // Verify content
211
- const reqContent = await readFile(join(tmpDir, 'requirements.txt'), 'utf-8');
212
- expect(reqContent).toContain('pytest');
213
-
214
- const mainContent = await readFile(join(tmpDir, 'src', 'main.py'), 'utf-8');
215
- expect(mainContent).toContain('def main');
216
-
217
- // Verify techStack uses Python
218
- expect(ctx.techStack.language).toBe('Python');
219
- });
220
- });
@@ -1,451 +0,0 @@
1
- /**
2
- * T011: Unit tests for PocScaffolder.
3
- *
4
- * Verifies scaffold creates all required files, skip-if-exists behavior,
5
- * and ScaffoldContext population from session.
6
- */
7
- import { describe, it, expect, beforeEach, afterEach } from 'vitest';
8
- import { mkdtemp, rm, readFile, readdir, stat } from 'node:fs/promises';
9
- import { join } from 'node:path';
10
- import { tmpdir } from 'node:os';
11
-
12
- import { PocScaffolder, toKebabCase, validatePocOutput } from '../../../src/develop/pocScaffolder.js';
13
- import type { WorkshopSession } from '../../../src/shared/schemas/session.js';
14
-
15
- // ── Helpers ──────────────────────────────────────────────────────────────────
16
-
17
- function makeSession(overrides?: Partial<WorkshopSession>): WorkshopSession {
18
- const now = new Date().toISOString();
19
- return {
20
- sessionId: 'scaffold-test-001',
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 using AI.',
33
- workflowStepIds: ['a1'],
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: ['Init project'] }],
43
- architectureNotes: 'Node.js 20 + TypeScript + Express. Use vitest for tests.',
44
- dependencies: ['express', 'typescript', 'vitest'],
45
- },
46
- ...overrides,
47
- };
48
- }
49
-
50
- describe('toKebabCase', () => {
51
- it('converts a title to kebab case', () => {
52
- expect(toKebabCase('AI Route Optimizer')).toBe('ai-route-optimizer');
53
- });
54
-
55
- it('removes special characters', () => {
56
- expect(toKebabCase('AI-Powered Delivery (Beta)')).toBe('ai-powered-delivery-beta');
57
- });
58
-
59
- it('handles leading/trailing spaces', () => {
60
- expect(toKebabCase(' My Project ')).toBe('my-project');
61
- });
62
-
63
- it('truncates to 64 characters', () => {
64
- const long = 'a'.repeat(100);
65
- expect(toKebabCase(long)).toHaveLength(64);
66
- });
67
- });
68
-
69
- describe('PocScaffolder', () => {
70
- let tmpDir: string;
71
-
72
- beforeEach(async () => {
73
- tmpDir = await mkdtemp(join(tmpdir(), 'sofia-scaffold-test-'));
74
- });
75
-
76
- afterEach(async () => {
77
- await rm(tmpDir, { recursive: true, force: true });
78
- });
79
-
80
- describe('buildContext', () => {
81
- it('builds a context from a session', () => {
82
- const session = makeSession();
83
- const ctx = PocScaffolder.buildContext(session, tmpDir);
84
-
85
- expect(ctx.sessionId).toBe('scaffold-test-001');
86
- expect(ctx.projectName).toBe('ai-route-optimizer');
87
- expect(ctx.ideaTitle).toBe('AI Route Optimizer');
88
- expect(ctx.ideaDescription).toBe('Optimize delivery routes using AI.');
89
- expect(ctx.outputDir).toBe(tmpDir);
90
- });
91
-
92
- it('infers Express framework from architecture notes', () => {
93
- const session = makeSession();
94
- const ctx = PocScaffolder.buildContext(session, tmpDir);
95
- expect(ctx.techStack.framework).toBe('Express');
96
- });
97
-
98
- it('uses idea title as project name', () => {
99
- const session = makeSession({
100
- ideas: [{ id: 'idea-1', title: 'My Cool AI App', description: 'desc', workflowStepIds: [] }],
101
- });
102
- const ctx = PocScaffolder.buildContext(session, tmpDir);
103
- expect(ctx.projectName).toBe('my-cool-ai-app');
104
- });
105
-
106
- it('falls back gracefully when no ideas or selection', () => {
107
- const session = makeSession({ ideas: [], selection: undefined });
108
- const ctx = PocScaffolder.buildContext(session, tmpDir);
109
- expect(ctx.ideaTitle).toBe('AI PoC');
110
- expect(ctx.projectName).toBe('ai-poc');
111
- });
112
- });
113
-
114
- describe('scaffold', () => {
115
- it('creates all required files', async () => {
116
- const session = makeSession();
117
- const scaffolder = new PocScaffolder();
118
- const ctx = PocScaffolder.buildContext(session, tmpDir);
119
-
120
- const result = await scaffolder.scaffold(ctx);
121
-
122
- // Check all required files were created
123
- const requiredFiles = [
124
- '.gitignore',
125
- 'package.json',
126
- 'tsconfig.json',
127
- 'README.md',
128
- 'src/index.ts',
129
- 'tests/index.test.ts',
130
- '.sofia-metadata.json',
131
- ];
132
-
133
- for (const file of requiredFiles) {
134
- const fullPath = join(tmpDir, file);
135
- const exists = await stat(fullPath).then(() => true).catch(() => false);
136
- expect(exists, `Expected ${file} to exist`).toBe(true);
137
- }
138
-
139
- expect(result.createdFiles).toContain('package.json');
140
- expect(result.createdFiles).toContain('README.md');
141
- expect(result.createdFiles).toContain('.gitignore');
142
- expect(result.createdFiles).toContain('.sofia-metadata.json');
143
- });
144
-
145
- it('package.json has required structure', async () => {
146
- const session = makeSession();
147
- const scaffolder = new PocScaffolder();
148
- const ctx = PocScaffolder.buildContext(session, tmpDir);
149
- await scaffolder.scaffold(ctx);
150
-
151
- const content = await readFile(join(tmpDir, 'package.json'), 'utf-8');
152
- const pkg = JSON.parse(content) as Record<string, unknown>;
153
-
154
- expect(pkg.type).toBe('module');
155
- expect((pkg.scripts as Record<string, string>).test).toBe('vitest run');
156
- expect((pkg.scripts as Record<string, string>).build).toBeDefined();
157
- expect(pkg.version).toBe('0.1.0');
158
- expect(pkg.name).toBe('ai-route-optimizer');
159
- });
160
-
161
- it('tsconfig.json has strict TypeScript config', async () => {
162
- const session = makeSession();
163
- const scaffolder = new PocScaffolder();
164
- const ctx = PocScaffolder.buildContext(session, tmpDir);
165
- await scaffolder.scaffold(ctx);
166
-
167
- const content = await readFile(join(tmpDir, 'tsconfig.json'), 'utf-8');
168
- const tsconfig = JSON.parse(content) as {
169
- compilerOptions: { strict: boolean; target: string };
170
- };
171
-
172
- expect(tsconfig.compilerOptions.strict).toBe(true);
173
- expect(tsconfig.compilerOptions.target).toBe('ES2022');
174
- });
175
-
176
- it('.gitignore excludes node_modules, dist, coverage', async () => {
177
- const session = makeSession();
178
- const scaffolder = new PocScaffolder();
179
- const ctx = PocScaffolder.buildContext(session, tmpDir);
180
- await scaffolder.scaffold(ctx);
181
-
182
- const content = await readFile(join(tmpDir, '.gitignore'), 'utf-8');
183
- expect(content).toContain('node_modules/');
184
- expect(content).toContain('dist/');
185
- expect(content).toContain('coverage/');
186
- });
187
-
188
- it('.sofia-metadata.json links to session', async () => {
189
- const session = makeSession();
190
- const scaffolder = new PocScaffolder();
191
- const ctx = PocScaffolder.buildContext(session, tmpDir);
192
- await scaffolder.scaffold(ctx);
193
-
194
- const content = await readFile(join(tmpDir, '.sofia-metadata.json'), 'utf-8');
195
- const meta = JSON.parse(content) as Record<string, unknown>;
196
-
197
- expect(meta.sessionId).toBe('scaffold-test-001');
198
- expect(meta.featureSpec).toBe('002-poc-generation');
199
- expect(meta.ideaTitle).toBe('AI Route Optimizer');
200
- expect(meta.generatedAt).toBeDefined();
201
- });
202
-
203
- it('README.md contains idea title and session ID', async () => {
204
- const session = makeSession();
205
- const scaffolder = new PocScaffolder();
206
- const ctx = PocScaffolder.buildContext(session, tmpDir);
207
- await scaffolder.scaffold(ctx);
208
-
209
- const content = await readFile(join(tmpDir, 'README.md'), 'utf-8');
210
- expect(content).toContain('AI Route Optimizer');
211
- expect(content).toContain('scaffold-test-001');
212
- });
213
-
214
- it('creates src/ and tests/ directories', async () => {
215
- const session = makeSession();
216
- const scaffolder = new PocScaffolder();
217
- const ctx = PocScaffolder.buildContext(session, tmpDir);
218
- await scaffolder.scaffold(ctx);
219
-
220
- const srcEntries = await readdir(join(tmpDir, 'src'));
221
- const testEntries = await readdir(join(tmpDir, 'tests'));
222
-
223
- expect(srcEntries.some((f) => f.endsWith('.ts'))).toBe(true);
224
- expect(testEntries.some((f) => f.endsWith('.test.ts'))).toBe(true);
225
- });
226
-
227
- it('returns list of created files', async () => {
228
- const session = makeSession();
229
- const scaffolder = new PocScaffolder();
230
- const ctx = PocScaffolder.buildContext(session, tmpDir);
231
- const result = await scaffolder.scaffold(ctx);
232
-
233
- expect(result.createdFiles.length).toBeGreaterThan(0);
234
- expect(result.skippedFiles).toEqual([]);
235
- });
236
- });
237
-
238
- describe('skip-if-exists behavior', () => {
239
- it('skips package.json if it already exists (skipIfExists: true)', async () => {
240
- const session = makeSession();
241
- const scaffolder = new PocScaffolder();
242
- const ctx = PocScaffolder.buildContext(session, tmpDir);
243
-
244
- // Create a custom package.json first
245
- const { writeFile, mkdir } = await import('node:fs/promises');
246
- await mkdir(tmpDir, { recursive: true });
247
- await writeFile(
248
- join(tmpDir, 'package.json'),
249
- JSON.stringify({ name: 'custom', scripts: { test: 'echo custom' } }),
250
- 'utf-8',
251
- );
252
-
253
- // Scaffold should skip existing package.json
254
- const result = await scaffolder.scaffold(ctx);
255
- expect(result.skippedFiles).toContain('package.json');
256
-
257
- // Custom content should be preserved
258
- const content = await readFile(join(tmpDir, 'package.json'), 'utf-8');
259
- const pkg = JSON.parse(content) as { name: string };
260
- expect(pkg.name).toBe('custom');
261
- });
262
-
263
- it('always overwrites .gitignore (skipIfExists: false)', async () => {
264
- const session = makeSession();
265
- const scaffolder = new PocScaffolder();
266
- const ctx = PocScaffolder.buildContext(session, tmpDir);
267
-
268
- // Create a custom .gitignore first
269
- const { writeFile } = await import('node:fs/promises');
270
- await writeFile(join(tmpDir, '.gitignore'), '# custom\n', 'utf-8');
271
-
272
- // Scaffold should overwrite .gitignore
273
- await scaffolder.scaffold(ctx);
274
-
275
- const content = await readFile(join(tmpDir, '.gitignore'), 'utf-8');
276
- expect(content).toContain('node_modules/'); // Generated content
277
- });
278
- });
279
-
280
- describe('getTemplateFiles', () => {
281
- it('returns list of template file paths', () => {
282
- const scaffolder = new PocScaffolder();
283
- const files = scaffolder.getTemplateFiles();
284
- expect(files).toContain('package.json');
285
- expect(files).toContain('README.md');
286
- expect(files).toContain('.gitignore');
287
- expect(files).toContain('src/index.ts');
288
- expect(files).toContain('tests/index.test.ts');
289
- });
290
- });
291
- });
292
-
293
- describe('validatePocOutput', () => {
294
- let tmpDir: string;
295
-
296
- beforeEach(async () => {
297
- tmpDir = await mkdtemp(join(tmpdir(), 'sofia-validate-test-'));
298
- });
299
-
300
- afterEach(async () => {
301
- await rm(tmpDir, { recursive: true, force: true });
302
- });
303
-
304
- it('returns valid=true for a complete scaffold', async () => {
305
- const session = makeSession();
306
- const scaffolder = new PocScaffolder();
307
- const ctx = PocScaffolder.buildContext(session, tmpDir);
308
- await scaffolder.scaffold(ctx);
309
-
310
- const result = await validatePocOutput(tmpDir);
311
- expect(result.valid).toBe(true);
312
- expect(result.missingFiles).toEqual([]);
313
- expect(result.errors).toEqual([]);
314
- });
315
-
316
- it('reports missing required files', async () => {
317
- const result = await validatePocOutput(tmpDir);
318
- expect(result.valid).toBe(false);
319
- expect(result.missingFiles).toContain('package.json');
320
- expect(result.missingFiles).toContain('README.md');
321
- });
322
-
323
- it('reports error when package.json lacks test script', async () => {
324
- const { writeFile, mkdir } = await import('node:fs/promises');
325
- await mkdir(join(tmpDir, 'src'), { recursive: true });
326
- await mkdir(join(tmpDir, 'tests'), { recursive: true });
327
- await writeFile(join(tmpDir, 'package.json'), JSON.stringify({ name: 'test', scripts: {} }), 'utf-8');
328
- await writeFile(join(tmpDir, 'README.md'), '# Test', 'utf-8');
329
- await writeFile(join(tmpDir, 'tsconfig.json'), JSON.stringify({}), 'utf-8');
330
- await writeFile(join(tmpDir, '.gitignore'), 'node_modules/', 'utf-8');
331
- await writeFile(join(tmpDir, '.sofia-metadata.json'), JSON.stringify({ sessionId: 'x' }), 'utf-8');
332
- await writeFile(join(tmpDir, 'src/index.ts'), 'export function main() {}', 'utf-8');
333
- await writeFile(join(tmpDir, 'tests/index.test.ts'), 'import { test } from "vitest"', 'utf-8');
334
-
335
- const result = await validatePocOutput(tmpDir);
336
- expect(result.errors).toContain('package.json is missing "test" script');
337
- });
338
- });
339
-
340
- // ── Template entry construction (T036) ────────────────────────────────────
341
-
342
- describe('PocScaffolder with TemplateEntry', () => {
343
- let tmpDir: string;
344
-
345
- beforeEach(async () => {
346
- tmpDir = await mkdtemp(join(tmpdir(), 'scaffolder-template-'));
347
- });
348
-
349
- afterEach(async () => {
350
- await rm(tmpDir, { recursive: true, force: true });
351
- });
352
-
353
- it('uses TemplateEntry.files when constructed with a template entry (T036)', async () => {
354
- const { PYTHON_PYTEST_TEMPLATE } = await import('../../../src/develop/templateRegistry.js');
355
- const scaffolder = new PocScaffolder(PYTHON_PYTEST_TEMPLATE);
356
-
357
- // The template should contain Python-specific file paths
358
- const filePaths = scaffolder.getTemplateFiles();
359
- expect(filePaths).toContain('requirements.txt');
360
- expect(filePaths).toContain('src/main.py');
361
- expect(filePaths).toContain('tests/test_main.py');
362
- // Should NOT contain TypeScript files
363
- expect(filePaths).not.toContain('tsconfig.json');
364
- expect(filePaths).not.toContain('src/index.ts');
365
- });
366
-
367
- it('uses TemplateEntry.techStack in buildContext (T010)', async () => {
368
- const { PYTHON_PYTEST_TEMPLATE } = await import('../../../src/develop/templateRegistry.js');
369
- const session = makeSession();
370
- const ctx = PocScaffolder.buildContext(session, tmpDir, PYTHON_PYTEST_TEMPLATE);
371
- expect(ctx.techStack.language).toBe('Python');
372
- expect(ctx.techStack.runtime).toBe('Python 3.11');
373
- });
374
-
375
- it('falls back to default TypeScript techStack when no template entry provided', () => {
376
- const session = makeSession();
377
- const ctx = PocScaffolder.buildContext(session, tmpDir);
378
- expect(ctx.techStack.language).toBe('TypeScript');
379
- expect(ctx.techStack.runtime).toBe('Node.js 20');
380
- });
381
- });
382
-
383
- // ── T072: TODO marker scanning records totalInitial, remaining, markers ───
384
-
385
- describe('PocScaffolder.scanAndRecordTodos (T072)', () => {
386
- let tmpDir: string;
387
-
388
- beforeEach(async () => {
389
- tmpDir = await mkdtemp(join(tmpdir(), 'todo-scan-'));
390
- });
391
-
392
- afterEach(async () => {
393
- await rm(tmpDir, { recursive: true, force: true });
394
- });
395
-
396
- it('scans scaffold files for TODO markers and records counts in .sofia-metadata.json', async () => {
397
- const { writeFile, mkdir } = await import('node:fs/promises');
398
-
399
- // Create a minimal project with TODO markers
400
- await mkdir(join(tmpDir, 'src'), { recursive: true });
401
- await writeFile(
402
- join(tmpDir, 'src/index.ts'),
403
- '// TODO: Implement the core functionality\nexport function main() {}\n// TODO: Add error handling\n',
404
- );
405
- await writeFile(
406
- join(tmpDir, 'src/utils.ts'),
407
- 'export function helper() { return 42; }\n',
408
- );
409
- await writeFile(
410
- join(tmpDir, '.sofia-metadata.json'),
411
- JSON.stringify({ sessionId: 'test-001', scaffoldedAt: new Date().toISOString() }),
412
- );
413
-
414
- const result = await PocScaffolder.scanAndRecordTodos(tmpDir);
415
-
416
- // Verify return value
417
- expect(result.totalInitial).toBe(2);
418
- expect(result.remaining).toBe(2);
419
- expect(result.markers).toHaveLength(2);
420
- expect(result.markers[0]).toContain('src/index.ts:1');
421
- expect(result.markers[0]).toContain('TODO:');
422
- expect(result.markers[1]).toContain('src/index.ts:3');
423
-
424
- // Verify .sofia-metadata.json was updated
425
- const metaRaw = await readFile(join(tmpDir, '.sofia-metadata.json'), 'utf-8');
426
- const metadata = JSON.parse(metaRaw);
427
- expect(metadata.todos).toBeDefined();
428
- expect(metadata.todos.totalInitial).toBe(2);
429
- expect(metadata.todos.remaining).toBe(2);
430
- expect(metadata.todos.markers).toHaveLength(2);
431
- });
432
-
433
- it('records zero TODOs when no markers exist', async () => {
434
- const { writeFile } = await import('node:fs/promises');
435
-
436
- await writeFile(
437
- join(tmpDir, 'index.ts'),
438
- 'export function main() { return "clean"; }\n',
439
- );
440
- await writeFile(
441
- join(tmpDir, '.sofia-metadata.json'),
442
- JSON.stringify({ sessionId: 'test-002' }),
443
- );
444
-
445
- const result = await PocScaffolder.scanAndRecordTodos(tmpDir);
446
-
447
- expect(result.totalInitial).toBe(0);
448
- expect(result.remaining).toBe(0);
449
- expect(result.markers).toHaveLength(0);
450
- });
451
- });