popeye-cli 1.7.0 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. package/README.md +148 -7
  2. package/cheatsheet.md +440 -0
  3. package/dist/cli/commands/db.d.ts +10 -0
  4. package/dist/cli/commands/db.d.ts.map +1 -0
  5. package/dist/cli/commands/db.js +240 -0
  6. package/dist/cli/commands/db.js.map +1 -0
  7. package/dist/cli/commands/doctor.d.ts +18 -0
  8. package/dist/cli/commands/doctor.d.ts.map +1 -0
  9. package/dist/cli/commands/doctor.js +255 -0
  10. package/dist/cli/commands/doctor.js.map +1 -0
  11. package/dist/cli/commands/index.d.ts +3 -0
  12. package/dist/cli/commands/index.d.ts.map +1 -1
  13. package/dist/cli/commands/index.js +3 -0
  14. package/dist/cli/commands/index.js.map +1 -1
  15. package/dist/cli/commands/review.d.ts +31 -0
  16. package/dist/cli/commands/review.d.ts.map +1 -0
  17. package/dist/cli/commands/review.js +156 -0
  18. package/dist/cli/commands/review.js.map +1 -0
  19. package/dist/cli/index.d.ts.map +1 -1
  20. package/dist/cli/index.js +4 -1
  21. package/dist/cli/index.js.map +1 -1
  22. package/dist/cli/interactive.d.ts.map +1 -1
  23. package/dist/cli/interactive.js +218 -61
  24. package/dist/cli/interactive.js.map +1 -1
  25. package/dist/generators/admin-wizard.d.ts +25 -0
  26. package/dist/generators/admin-wizard.d.ts.map +1 -0
  27. package/dist/generators/admin-wizard.js +123 -0
  28. package/dist/generators/admin-wizard.js.map +1 -0
  29. package/dist/generators/all.d.ts.map +1 -1
  30. package/dist/generators/all.js +10 -3
  31. package/dist/generators/all.js.map +1 -1
  32. package/dist/generators/database.d.ts +58 -0
  33. package/dist/generators/database.d.ts.map +1 -0
  34. package/dist/generators/database.js +229 -0
  35. package/dist/generators/database.js.map +1 -0
  36. package/dist/generators/fullstack.d.ts.map +1 -1
  37. package/dist/generators/fullstack.js +23 -7
  38. package/dist/generators/fullstack.js.map +1 -1
  39. package/dist/generators/index.d.ts +2 -0
  40. package/dist/generators/index.d.ts.map +1 -1
  41. package/dist/generators/index.js +2 -0
  42. package/dist/generators/index.js.map +1 -1
  43. package/dist/generators/templates/admin-wizard-python.d.ts +32 -0
  44. package/dist/generators/templates/admin-wizard-python.d.ts.map +1 -0
  45. package/dist/generators/templates/admin-wizard-python.js +425 -0
  46. package/dist/generators/templates/admin-wizard-python.js.map +1 -0
  47. package/dist/generators/templates/admin-wizard-react.d.ts +48 -0
  48. package/dist/generators/templates/admin-wizard-react.d.ts.map +1 -0
  49. package/dist/generators/templates/admin-wizard-react.js +554 -0
  50. package/dist/generators/templates/admin-wizard-react.js.map +1 -0
  51. package/dist/generators/templates/database-docker.d.ts +23 -0
  52. package/dist/generators/templates/database-docker.d.ts.map +1 -0
  53. package/dist/generators/templates/database-docker.js +221 -0
  54. package/dist/generators/templates/database-docker.js.map +1 -0
  55. package/dist/generators/templates/database-python.d.ts +54 -0
  56. package/dist/generators/templates/database-python.d.ts.map +1 -0
  57. package/dist/generators/templates/database-python.js +723 -0
  58. package/dist/generators/templates/database-python.js.map +1 -0
  59. package/dist/generators/templates/database-typescript.d.ts +34 -0
  60. package/dist/generators/templates/database-typescript.d.ts.map +1 -0
  61. package/dist/generators/templates/database-typescript.js +232 -0
  62. package/dist/generators/templates/database-typescript.js.map +1 -0
  63. package/dist/generators/templates/fullstack.d.ts.map +1 -1
  64. package/dist/generators/templates/fullstack.js +29 -0
  65. package/dist/generators/templates/fullstack.js.map +1 -1
  66. package/dist/generators/templates/index.d.ts +5 -0
  67. package/dist/generators/templates/index.d.ts.map +1 -1
  68. package/dist/generators/templates/index.js +5 -0
  69. package/dist/generators/templates/index.js.map +1 -1
  70. package/dist/state/index.d.ts +10 -0
  71. package/dist/state/index.d.ts.map +1 -1
  72. package/dist/state/index.js +21 -0
  73. package/dist/state/index.js.map +1 -1
  74. package/dist/types/audit.d.ts +623 -0
  75. package/dist/types/audit.d.ts.map +1 -0
  76. package/dist/types/audit.js +240 -0
  77. package/dist/types/audit.js.map +1 -0
  78. package/dist/types/database-runtime.d.ts +86 -0
  79. package/dist/types/database-runtime.d.ts.map +1 -0
  80. package/dist/types/database-runtime.js +61 -0
  81. package/dist/types/database-runtime.js.map +1 -0
  82. package/dist/types/database.d.ts +85 -0
  83. package/dist/types/database.d.ts.map +1 -0
  84. package/dist/types/database.js +71 -0
  85. package/dist/types/database.js.map +1 -0
  86. package/dist/types/index.d.ts +2 -0
  87. package/dist/types/index.d.ts.map +1 -1
  88. package/dist/types/index.js +4 -0
  89. package/dist/types/index.js.map +1 -1
  90. package/dist/types/workflow.d.ts +36 -0
  91. package/dist/types/workflow.d.ts.map +1 -1
  92. package/dist/types/workflow.js +7 -0
  93. package/dist/types/workflow.js.map +1 -1
  94. package/dist/workflow/audit-analyzer.d.ts +58 -0
  95. package/dist/workflow/audit-analyzer.d.ts.map +1 -0
  96. package/dist/workflow/audit-analyzer.js +420 -0
  97. package/dist/workflow/audit-analyzer.js.map +1 -0
  98. package/dist/workflow/audit-mode.d.ts +28 -0
  99. package/dist/workflow/audit-mode.d.ts.map +1 -0
  100. package/dist/workflow/audit-mode.js +169 -0
  101. package/dist/workflow/audit-mode.js.map +1 -0
  102. package/dist/workflow/audit-recovery.d.ts +61 -0
  103. package/dist/workflow/audit-recovery.d.ts.map +1 -0
  104. package/dist/workflow/audit-recovery.js +242 -0
  105. package/dist/workflow/audit-recovery.js.map +1 -0
  106. package/dist/workflow/audit-reporter.d.ts +65 -0
  107. package/dist/workflow/audit-reporter.d.ts.map +1 -0
  108. package/dist/workflow/audit-reporter.js +301 -0
  109. package/dist/workflow/audit-reporter.js.map +1 -0
  110. package/dist/workflow/audit-scanner.d.ts +87 -0
  111. package/dist/workflow/audit-scanner.d.ts.map +1 -0
  112. package/dist/workflow/audit-scanner.js +768 -0
  113. package/dist/workflow/audit-scanner.js.map +1 -0
  114. package/dist/workflow/db-setup-runner.d.ts +63 -0
  115. package/dist/workflow/db-setup-runner.d.ts.map +1 -0
  116. package/dist/workflow/db-setup-runner.js +336 -0
  117. package/dist/workflow/db-setup-runner.js.map +1 -0
  118. package/dist/workflow/db-state-machine.d.ts +30 -0
  119. package/dist/workflow/db-state-machine.d.ts.map +1 -0
  120. package/dist/workflow/db-state-machine.js +51 -0
  121. package/dist/workflow/db-state-machine.js.map +1 -0
  122. package/dist/workflow/index.d.ts +7 -0
  123. package/dist/workflow/index.d.ts.map +1 -1
  124. package/dist/workflow/index.js +7 -0
  125. package/dist/workflow/index.js.map +1 -1
  126. package/package.json +1 -1
  127. package/src/cli/commands/db.ts +281 -0
  128. package/src/cli/commands/doctor.ts +273 -0
  129. package/src/cli/commands/index.ts +3 -0
  130. package/src/cli/commands/review.ts +187 -0
  131. package/src/cli/index.ts +6 -0
  132. package/src/cli/interactive.ts +174 -4
  133. package/src/generators/admin-wizard.ts +146 -0
  134. package/src/generators/all.ts +10 -3
  135. package/src/generators/database.ts +286 -0
  136. package/src/generators/fullstack.ts +26 -9
  137. package/src/generators/index.ts +12 -0
  138. package/src/generators/templates/admin-wizard-python.ts +431 -0
  139. package/src/generators/templates/admin-wizard-react.ts +560 -0
  140. package/src/generators/templates/database-docker.ts +227 -0
  141. package/src/generators/templates/database-python.ts +734 -0
  142. package/src/generators/templates/database-typescript.ts +238 -0
  143. package/src/generators/templates/fullstack.ts +29 -0
  144. package/src/generators/templates/index.ts +5 -0
  145. package/src/state/index.ts +28 -0
  146. package/src/types/audit.ts +294 -0
  147. package/src/types/database-runtime.ts +69 -0
  148. package/src/types/database.ts +84 -0
  149. package/src/types/index.ts +29 -0
  150. package/src/types/workflow.ts +20 -0
  151. package/src/workflow/audit-analyzer.ts +491 -0
  152. package/src/workflow/audit-mode.ts +240 -0
  153. package/src/workflow/audit-recovery.ts +284 -0
  154. package/src/workflow/audit-reporter.ts +370 -0
  155. package/src/workflow/audit-scanner.ts +873 -0
  156. package/src/workflow/db-setup-runner.ts +391 -0
  157. package/src/workflow/db-state-machine.ts +58 -0
  158. package/src/workflow/index.ts +7 -0
  159. package/tests/cli/commands/review.test.ts +52 -0
  160. package/tests/generators/admin-wizard-orchestrator.test.ts +64 -0
  161. package/tests/generators/admin-wizard-templates.test.ts +366 -0
  162. package/tests/generators/cross-phase-integration.test.ts +383 -0
  163. package/tests/generators/database.test.ts +456 -0
  164. package/tests/generators/fe-be-db-integration.test.ts +613 -0
  165. package/tests/types/audit.test.ts +250 -0
  166. package/tests/types/database-runtime.test.ts +158 -0
  167. package/tests/types/database.test.ts +187 -0
  168. package/tests/workflow/audit-analyzer.test.ts +281 -0
  169. package/tests/workflow/audit-mode.test.ts +114 -0
  170. package/tests/workflow/audit-recovery.test.ts +237 -0
  171. package/tests/workflow/audit-reporter.test.ts +254 -0
  172. package/tests/workflow/audit-scanner.test.ts +270 -0
  173. package/tests/workflow/db-setup-runner.test.ts +211 -0
  174. package/tests/workflow/db-state-machine.test.ts +117 -0
@@ -0,0 +1,613 @@
1
+ /**
2
+ * Frontend ↔ Backend ↔ Database integration tests
3
+ * Verifies that generated FE, BE, and DB layers are properly wired
4
+ * and that Popeye's test runner validates fullstack projects correctly.
5
+ */
6
+
7
+ import { describe, it, expect } from 'vitest';
8
+
9
+ // Generated frontend templates
10
+ import {
11
+ generateAppTsxWithAdmin,
12
+ generateUseAdminApiHook,
13
+ generateDbStatusBanner,
14
+ generateConnectionForm,
15
+ generateMigrationProgress,
16
+ } from '../../src/generators/templates/admin-wizard-react.js';
17
+
18
+ // Generated backend templates
19
+ import {
20
+ generateFastAPIMainWithAdmin,
21
+ generateAdminDbRoutes,
22
+ generateAdminAuthMiddleware,
23
+ } from '../../src/generators/templates/admin-wizard-python.js';
24
+
25
+ // Generated DB templates
26
+ import {
27
+ generateDbConnection,
28
+ generateDbStartupHook,
29
+ generateDbHealthRoute,
30
+ generateDbConftest,
31
+ } from '../../src/generators/templates/database-python.js';
32
+
33
+ // Generated infra templates
34
+ import {
35
+ generateDockerComposeWithDb,
36
+ generateAllDockerComposeWithDb,
37
+ generateDbEnvExample,
38
+ } from '../../src/generators/templates/database-docker.js';
39
+
40
+ import {
41
+ generateViteConfigReact,
42
+ generateNginxConfig,
43
+ generateFrontendTest,
44
+ } from '../../src/generators/templates/fullstack.js';
45
+
46
+ // Test runner
47
+ import {
48
+ buildTestCommand,
49
+ parseTestOutput,
50
+ DEFAULT_TEST_COMMANDS,
51
+ } from '../../src/workflow/test-runner.js';
52
+
53
+ const TEST_PACKAGE = 'my_project';
54
+ const TEST_PROJECT = 'my-project';
55
+
56
+ // ============================================================
57
+ // FE → BE: API endpoint path alignment
58
+ // ============================================================
59
+
60
+ describe('FE → BE: Admin API endpoint paths match', () => {
61
+ const adminRoutes = generateAdminDbRoutes(TEST_PACKAGE);
62
+ const statusBanner = generateDbStatusBanner();
63
+ const connectionForm = generateConnectionForm();
64
+ const migrationProgress = generateMigrationProgress();
65
+
66
+ it('admin routes should use /api/admin/db prefix', () => {
67
+ expect(adminRoutes).toContain('prefix="/api/admin/db"');
68
+ });
69
+
70
+ it('FE status banner should call /api/admin/db/status', () => {
71
+ expect(statusBanner).toContain('/api/admin/db/status');
72
+ });
73
+
74
+ it('admin routes should have GET /status endpoint', () => {
75
+ expect(adminRoutes).toContain('@router.get("/status")');
76
+ });
77
+
78
+ it('FE connection form should call /api/admin/db/test', () => {
79
+ expect(connectionForm).toContain('/api/admin/db/test');
80
+ });
81
+
82
+ it('admin routes should have POST /test endpoint', () => {
83
+ expect(adminRoutes).toContain('@router.post("/test")');
84
+ });
85
+
86
+ it('FE migration progress should call /api/admin/db/apply', () => {
87
+ expect(migrationProgress).toContain('/api/admin/db/apply');
88
+ });
89
+
90
+ it('admin routes should have POST /apply endpoint', () => {
91
+ expect(adminRoutes).toContain('@router.post("/apply")');
92
+ });
93
+
94
+ it('admin routes should have POST /retry endpoint', () => {
95
+ expect(adminRoutes).toContain('@router.post("/retry")');
96
+ });
97
+ });
98
+
99
+ describe('FE → BE: Health endpoint path alignment', () => {
100
+ const appTsx = generateAppTsxWithAdmin(TEST_PROJECT);
101
+ const mainPy = generateFastAPIMainWithAdmin(TEST_PROJECT, TEST_PACKAGE);
102
+ const healthRoute = generateDbHealthRoute(TEST_PACKAGE);
103
+
104
+ it('FE App.tsx should call /health endpoint', () => {
105
+ expect(appTsx).toContain('/health');
106
+ });
107
+
108
+ it('BE main.py should expose /health endpoint', () => {
109
+ expect(mainPy).toContain('@app.get("/health")');
110
+ });
111
+
112
+ it('BE health_db route should expose /health/db endpoint', () => {
113
+ expect(healthRoute).toContain('/health/db');
114
+ });
115
+
116
+ it('BE main.py should include health_db_router', () => {
117
+ expect(mainPy).toContain('app.include_router(health_db_router)');
118
+ });
119
+ });
120
+
121
+ // ============================================================
122
+ // FE → BE: Auth token header alignment
123
+ // ============================================================
124
+
125
+ describe('FE → BE: Admin token header alignment', () => {
126
+ const apiHook = generateUseAdminApiHook();
127
+ const authMiddleware = generateAdminAuthMiddleware();
128
+
129
+ it('FE should send X-Admin-Token header', () => {
130
+ expect(apiHook).toContain('X-Admin-Token');
131
+ });
132
+
133
+ it('BE should check X-Admin-Token header', () => {
134
+ expect(authMiddleware).toContain('X-Admin-Token');
135
+ });
136
+
137
+ it('FE should read token from VITE_ADMIN_TOKEN env var', () => {
138
+ expect(apiHook).toContain('VITE_ADMIN_TOKEN');
139
+ });
140
+
141
+ it('BE should read token from ADMIN_SETUP_TOKEN env var', () => {
142
+ expect(authMiddleware).toContain('ADMIN_SETUP_TOKEN');
143
+ });
144
+
145
+ it('BE should return 403 when token is invalid', () => {
146
+ expect(authMiddleware).toContain('403');
147
+ });
148
+ });
149
+
150
+ // ============================================================
151
+ // FE → BE: API URL configuration alignment
152
+ // ============================================================
153
+
154
+ describe('FE → BE: API URL configuration', () => {
155
+ const appTsx = generateAppTsxWithAdmin(TEST_PROJECT);
156
+ const apiHook = generateUseAdminApiHook();
157
+ const viteConfig = generateViteConfigReact();
158
+
159
+ it('App.tsx should use VITE_API_URL env var', () => {
160
+ expect(appTsx).toContain('VITE_API_URL');
161
+ });
162
+
163
+ it('useAdminApi hook should use VITE_API_URL env var', () => {
164
+ expect(apiHook).toContain('VITE_API_URL');
165
+ });
166
+
167
+ it('both should default to http://localhost:8000', () => {
168
+ expect(appTsx).toContain('http://localhost:8000');
169
+ expect(apiHook).toContain('http://localhost:8000');
170
+ });
171
+
172
+ it('Vite dev proxy should forward /api to backend', () => {
173
+ expect(viteConfig).toContain("'/api'");
174
+ expect(viteConfig).toContain('http://localhost:8000');
175
+ });
176
+
177
+ it('Vite dev server should run on port 5173', () => {
178
+ expect(viteConfig).toContain('port: 5173');
179
+ });
180
+ });
181
+
182
+ // ============================================================
183
+ // BE → DB: Database connection wiring
184
+ // ============================================================
185
+
186
+ describe('BE → DB: Database connection wiring', () => {
187
+ const dbConnection = generateDbConnection(TEST_PACKAGE);
188
+ const dbStartup = generateDbStartupHook(TEST_PACKAGE);
189
+ const mainPy = generateFastAPIMainWithAdmin(TEST_PROJECT, TEST_PACKAGE);
190
+
191
+ it('DB connection should read DATABASE_URL from env', () => {
192
+ expect(dbConnection).toContain('DATABASE_URL');
193
+ expect(dbConnection).toContain('os.getenv');
194
+ });
195
+
196
+ it('DB connection should use async SQLAlchemy engine', () => {
197
+ expect(dbConnection).toContain('create_async_engine');
198
+ expect(dbConnection).toContain('AsyncSession');
199
+ });
200
+
201
+ it('DB startup hook should check connectivity on startup', () => {
202
+ expect(dbStartup).toContain('DATABASE_URL');
203
+ expect(dbStartup).toContain('check_db_connection');
204
+ });
205
+
206
+ it('main.py should include startup hook or lifecycle', () => {
207
+ // The main.py should set up the app with some startup wiring
208
+ expect(mainPy).toContain('FastAPI');
209
+ });
210
+ });
211
+
212
+ describe('BE → DB: Admin routes use asyncpg for direct DB access', () => {
213
+ const adminRoutes = generateAdminDbRoutes(TEST_PACKAGE);
214
+
215
+ it('admin /test should use asyncpg.connect()', () => {
216
+ expect(adminRoutes).toContain('asyncpg.connect');
217
+ });
218
+
219
+ it('admin /test should execute SELECT 1 for connectivity check', () => {
220
+ expect(adminRoutes).toContain('SELECT 1');
221
+ });
222
+
223
+ it('admin /apply should run alembic upgrade head', () => {
224
+ expect(adminRoutes).toContain('alembic upgrade head');
225
+ });
226
+
227
+ it('admin /status should check alembic_version table', () => {
228
+ expect(adminRoutes).toContain('alembic_version');
229
+ });
230
+
231
+ it('admin routes should convert SQLAlchemy URL to asyncpg format', () => {
232
+ // The admin routes need to strip "postgresql+asyncpg://" to "postgresql://"
233
+ expect(adminRoutes).toContain('postgresql+asyncpg://');
234
+ expect(adminRoutes).toContain('postgresql://');
235
+ });
236
+ });
237
+
238
+ // ============================================================
239
+ // BE → DB: Health check validates real DB connectivity
240
+ // ============================================================
241
+
242
+ describe('BE → DB: Health check route validates DB', () => {
243
+ const healthRoute = generateDbHealthRoute(TEST_PACKAGE);
244
+
245
+ it('should check if DATABASE_URL is configured', () => {
246
+ expect(healthRoute).toContain('DATABASE_URL');
247
+ });
248
+
249
+ it('should return 503 when DB is not configured', () => {
250
+ expect(healthRoute).toContain('503');
251
+ expect(healthRoute).toContain('DB_NOT_READY');
252
+ });
253
+
254
+ it('should execute SELECT 1 to verify connectivity', () => {
255
+ expect(healthRoute).toContain('SELECT 1');
256
+ });
257
+
258
+ it('should check alembic_version for migration status', () => {
259
+ expect(healthRoute).toContain('alembic_version');
260
+ });
261
+ });
262
+
263
+ // ============================================================
264
+ // CORS: Frontend ports match backend CORS origins
265
+ // ============================================================
266
+
267
+ describe('CORS: FE dev ports match BE CORS origins', () => {
268
+ const mainPy = generateFastAPIMainWithAdmin(TEST_PROJECT, TEST_PACKAGE);
269
+ const viteConfig = generateViteConfigReact();
270
+
271
+ it('BE CORS should allow Vite dev port 5173', () => {
272
+ expect(mainPy).toContain('http://localhost:5173');
273
+ });
274
+
275
+ it('BE CORS should allow production port 3000', () => {
276
+ expect(mainPy).toContain('http://localhost:3000');
277
+ });
278
+
279
+ it('Vite config should use port 5173 (matching CORS)', () => {
280
+ expect(viteConfig).toContain('port: 5173');
281
+ });
282
+
283
+ it('CORS should allow all methods and headers', () => {
284
+ expect(mainPy).toContain('allow_methods=["*"]');
285
+ expect(mainPy).toContain('allow_headers=["*"]');
286
+ });
287
+ });
288
+
289
+ // ============================================================
290
+ // Docker Compose: Service wiring FE → BE → DB
291
+ // ============================================================
292
+
293
+ describe('Docker Compose: Fullstack service wiring', () => {
294
+ const compose = generateDockerComposeWithDb(TEST_PROJECT);
295
+
296
+ it('frontend should depend on backend', () => {
297
+ expect(compose).toContain('depends_on:\n - backend');
298
+ });
299
+
300
+ it('frontend should use backend service URL for API', () => {
301
+ expect(compose).toContain('VITE_API_URL=http://backend:8000');
302
+ });
303
+
304
+ it('backend should depend on postgres with health condition', () => {
305
+ expect(compose).toContain('condition: service_healthy');
306
+ });
307
+
308
+ it('backend should use postgres service in DATABASE_URL', () => {
309
+ expect(compose).toContain('postgres:5432');
310
+ });
311
+
312
+ it('backend DATABASE_URL should use correct DB name', () => {
313
+ const dbName = TEST_PROJECT.replace(/-/g, '_') + '_db';
314
+ expect(compose).toContain(dbName);
315
+ });
316
+
317
+ it('postgres should have healthcheck', () => {
318
+ expect(compose).toContain('pg_isready');
319
+ });
320
+
321
+ it('backend-dev should also depend on healthy postgres', () => {
322
+ // Verify the dev backend also waits for postgres
323
+ const devSection = compose.split('backend-dev:')[1];
324
+ expect(devSection).toContain('condition: service_healthy');
325
+ });
326
+ });
327
+
328
+ describe('Docker Compose: All project service wiring', () => {
329
+ const compose = generateAllDockerComposeWithDb(TEST_PROJECT);
330
+
331
+ it('should include frontend, backend, website, and postgres services', () => {
332
+ expect(compose).toContain('frontend:');
333
+ expect(compose).toContain('backend:');
334
+ expect(compose).toContain('website:');
335
+ expect(compose).toContain('postgres:');
336
+ });
337
+
338
+ it('backend should depend on postgres with health condition', () => {
339
+ expect(compose).toContain('condition: service_healthy');
340
+ });
341
+
342
+ it('all services should be on the same network', () => {
343
+ const networkName = `${TEST_PROJECT}-network`;
344
+ expect(compose).toContain(networkName);
345
+ // Count network references - should appear in services + network definition
346
+ const networkCount = (compose.match(new RegExp(networkName, 'g')) || []).length;
347
+ expect(networkCount).toBeGreaterThanOrEqual(5); // 4 services + 1 definition
348
+ });
349
+
350
+ it('frontend API URL should point to backend service', () => {
351
+ expect(compose).toContain('VITE_API_URL=http://backend:8000');
352
+ });
353
+ });
354
+
355
+ // ============================================================
356
+ // Nginx: Proxy paths match backend routes
357
+ // ============================================================
358
+
359
+ describe('Nginx: Proxy config matches BE route structure', () => {
360
+ const nginx = generateNginxConfig();
361
+ const mainPy = generateFastAPIMainWithAdmin(TEST_PROJECT, TEST_PACKAGE);
362
+
363
+ it('nginx should proxy /api to backend:8000', () => {
364
+ expect(nginx).toContain('location /api');
365
+ expect(nginx).toContain('proxy_pass http://backend:8000');
366
+ });
367
+
368
+ it('admin routes start with /api prefix (matching nginx proxy)', () => {
369
+ // Admin routes use prefix="/api/admin/db" which falls under nginx /api proxy
370
+ const adminRoutes = generateAdminDbRoutes(TEST_PACKAGE);
371
+ expect(adminRoutes).toContain('prefix="/api/admin/db"');
372
+ });
373
+
374
+ it('nginx should handle SPA routing (catch-all to index.html)', () => {
375
+ expect(nginx).toContain('try_files $uri $uri/ /index.html');
376
+ });
377
+ });
378
+
379
+ // ============================================================
380
+ // Env vars: FE and BE env examples are consistent
381
+ // ============================================================
382
+
383
+ describe('Env vars: Backend and frontend env alignment', () => {
384
+ const backendEnv = generateDbEnvExample(TEST_PROJECT);
385
+
386
+ it('backend env should have DATABASE_URL for DB connection', () => {
387
+ expect(backendEnv).toContain('DATABASE_URL=postgresql+asyncpg://');
388
+ });
389
+
390
+ it('backend env should have ADMIN_SETUP_TOKEN for admin auth', () => {
391
+ expect(backendEnv).toContain('ADMIN_SETUP_TOKEN=');
392
+ });
393
+
394
+ it('DATABASE_URL format should match admin route URL conversion', () => {
395
+ // Backend env uses postgresql+asyncpg:// format
396
+ // Admin routes convert this to postgresql:// for asyncpg
397
+ expect(backendEnv).toContain('postgresql+asyncpg://');
398
+ const adminRoutes = generateAdminDbRoutes(TEST_PACKAGE);
399
+ expect(adminRoutes).toContain('.replace("postgresql+asyncpg://", "postgresql://")');
400
+ });
401
+ });
402
+
403
+ // ============================================================
404
+ // Generated test files: Backend tests match endpoints
405
+ // ============================================================
406
+
407
+ describe('Generated backend test validates real endpoints', () => {
408
+ const frontendTest = generateFrontendTest(TEST_PROJECT);
409
+ const appTsx = generateAppTsxWithAdmin(TEST_PROJECT);
410
+
411
+ it('generated FE test should check for project name text', () => {
412
+ expect(frontendTest).toContain(TEST_PROJECT);
413
+ // App.tsx should also contain the project name
414
+ expect(appTsx).toContain(TEST_PROJECT);
415
+ });
416
+
417
+ it('generated FE test should check for loading state', () => {
418
+ expect(frontendTest).toContain('Checking...');
419
+ // App.tsx should also have this text
420
+ expect(appTsx).toContain('Checking...');
421
+ });
422
+ });
423
+
424
+ describe('Generated DB conftest provides proper test fixtures', () => {
425
+ const conftest = generateDbConftest(TEST_PACKAGE);
426
+
427
+ it('should use TEST_DATABASE_URL (not production DATABASE_URL)', () => {
428
+ expect(conftest).toContain('TEST_DATABASE_URL');
429
+ });
430
+
431
+ it('should create async engine for tests', () => {
432
+ expect(conftest).toContain('create_async_engine');
433
+ });
434
+
435
+ it('should provide db_session fixture', () => {
436
+ expect(conftest).toContain('db_session');
437
+ });
438
+
439
+ it('should create tables before tests and drop after', () => {
440
+ expect(conftest).toContain('Base.metadata.create_all');
441
+ expect(conftest).toContain('Base.metadata.drop_all');
442
+ });
443
+
444
+ it('should import models from the correct package', () => {
445
+ expect(conftest).toContain(`from src.${TEST_PACKAGE}.database.models import Base`);
446
+ });
447
+ });
448
+
449
+ // ============================================================
450
+ // Popeye test runner: Fullstack/All project support
451
+ // ============================================================
452
+
453
+ describe('Popeye test runner: Fullstack project commands', () => {
454
+ it('fullstack should use test:all command', () => {
455
+ expect(DEFAULT_TEST_COMMANDS.fullstack).toBe('npm run test:all');
456
+ });
457
+
458
+ it('all should use test:all command', () => {
459
+ expect(DEFAULT_TEST_COMMANDS.all).toBe('npm run test:all');
460
+ });
461
+
462
+ it('buildTestCommand should return test:all for fullstack', () => {
463
+ const cmd = buildTestCommand({ language: 'fullstack' });
464
+ expect(cmd).toBe('npm run test:all');
465
+ });
466
+
467
+ it('buildTestCommand should return test:all for all', () => {
468
+ const cmd = buildTestCommand({ language: 'all' });
469
+ expect(cmd).toBe('npm run test:all');
470
+ });
471
+ });
472
+
473
+ describe('Popeye test runner: Fullstack output parsing', () => {
474
+ it('should parse combined pytest + jest output for fullstack', () => {
475
+ const output = `
476
+ === Backend Tests ===
477
+ ============================= test session starts ==============================
478
+ tests/test_main.py .. [100%]
479
+ ============================== 2 passed in 0.54s ==============================
480
+
481
+ === Frontend Tests ===
482
+ PASS tests/App.test.tsx
483
+ App
484
+ v renders the project name (25 ms)
485
+ v shows loading state initially (12 ms)
486
+
487
+ Tests: 2 passed, 2 total
488
+ `;
489
+ const result = parseTestOutput(output, 'fullstack');
490
+
491
+ expect(result.success).toBe(true);
492
+ expect(result.passed).toBe(4); // 2 pytest + 2 jest
493
+ expect(result.failed).toBe(0);
494
+ expect(result.total).toBe(4);
495
+ });
496
+
497
+ it('should detect backend failures in fullstack output', () => {
498
+ const output = `
499
+ === Backend Tests ===
500
+ FAILED tests/test_main.py::test_health_check
501
+ ============================== 1 failed, 1 passed in 0.54s ==============================
502
+
503
+ === Frontend Tests ===
504
+ Tests: 2 passed, 2 total
505
+ `;
506
+ const result = parseTestOutput(output, 'fullstack');
507
+
508
+ expect(result.success).toBe(false);
509
+ expect(result.failed).toBe(1);
510
+ expect(result.passed).toBe(3); // 1 pytest + 2 jest
511
+ expect(result.failedTests).toContain('tests/test_main.py::test_health_check');
512
+ });
513
+
514
+ it('should detect frontend failures in fullstack output', () => {
515
+ // Note: pytest regex `(\d+)\s+failed` can match jest "1 failed" line too,
516
+ // causing double-count. The parser counts pytest failed (1) + jest failed (1) = 2.
517
+ const output = `
518
+ === Backend Tests ===
519
+ ============================== 2 passed in 0.54s ==============================
520
+
521
+ === Frontend Tests ===
522
+ FAIL tests/App.test.tsx
523
+ App
524
+ ✕ renders the project name (25 ms)
525
+ v shows loading state initially (12 ms)
526
+
527
+ Tests: 1 failed, 1 passed, 2 total
528
+ `;
529
+ const result = parseTestOutput(output, 'fullstack');
530
+
531
+ expect(result.success).toBe(false);
532
+ expect(result.passed).toBe(3); // 2 pytest + 1 jest
533
+ expect(result.failed).toBe(2); // 1 jest + 1 double-counted by pytest regex
534
+ expect(result.failedTests?.some((t) => t.includes('renders the project name'))).toBe(true);
535
+ });
536
+
537
+ it('should parse all project output (multiple jest outputs)', () => {
538
+ const output = `
539
+ === Backend Tests ===
540
+ ============================== 3 passed in 0.54s ==============================
541
+
542
+ === Frontend Tests ===
543
+ Tests: 2 passed, 2 total
544
+
545
+ === Website Tests ===
546
+ Tests: 4 passed, 4 total
547
+ `;
548
+ const result = parseTestOutput(output, 'all');
549
+
550
+ expect(result.success).toBe(true);
551
+ expect(result.passed).toBe(9); // 3 pytest + 2 jest + 4 jest
552
+ expect(result.failed).toBe(0);
553
+ });
554
+ });
555
+
556
+ // ============================================================
557
+ // FE ↔ BE ↔ DB: Full-stack data flow alignment
558
+ // ============================================================
559
+
560
+ describe('Full-stack data flow: Admin wizard lifecycle', () => {
561
+ const apiHook = generateUseAdminApiHook();
562
+ const adminRoutes = generateAdminDbRoutes(TEST_PACKAGE);
563
+ const statusBanner = generateDbStatusBanner();
564
+
565
+ it('FE sends database_url in test request body', () => {
566
+ expect(generateConnectionForm()).toContain('database_url');
567
+ });
568
+
569
+ it('BE /test expects database_url field', () => {
570
+ expect(adminRoutes).toContain('database_url');
571
+ });
572
+
573
+ it('FE sends database_url in apply request body', () => {
574
+ expect(generateMigrationProgress()).toContain('database_url');
575
+ });
576
+
577
+ it('BE /apply expects database_url field', () => {
578
+ expect(adminRoutes).toContain('database_url');
579
+ });
580
+
581
+ it('BE /status returns status field that FE checks', () => {
582
+ // BE returns { "status": "ready"|"unconfigured"|... }
583
+ expect(adminRoutes).toContain('"status"');
584
+ // FE checks status.status
585
+ expect(statusBanner).toContain('status.status');
586
+ });
587
+
588
+ it('FE hides banner when status is ready (matches BE response)', () => {
589
+ expect(statusBanner).toContain("status === 'ready'");
590
+ });
591
+ });
592
+
593
+ describe('Full-stack data flow: DB URL format consistency', () => {
594
+ const backendEnv = generateDbEnvExample(TEST_PROJECT);
595
+ const dockerCompose = generateDockerComposeWithDb(TEST_PROJECT);
596
+ const dbConnection = generateDbConnection(TEST_PACKAGE);
597
+ const adminRoutes = generateAdminDbRoutes(TEST_PACKAGE);
598
+
599
+ it('all DATABASE_URL values use postgresql+asyncpg:// scheme', () => {
600
+ expect(backendEnv).toContain('postgresql+asyncpg://');
601
+ expect(dockerCompose).toContain('postgresql+asyncpg://');
602
+ });
603
+
604
+ it('DB connection module reads DATABASE_URL', () => {
605
+ expect(dbConnection).toContain('DATABASE_URL');
606
+ });
607
+
608
+ it('admin routes convert URL scheme for asyncpg direct use', () => {
609
+ expect(adminRoutes).toContain(
610
+ 'replace("postgresql+asyncpg://", "postgresql://")'
611
+ );
612
+ });
613
+ });