specforge-mcp 0.2.2 → 0.3.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 (64) hide show
  1. package/dist/engine/skill-generator/sections-skills.d.ts +4 -0
  2. package/dist/engine/skill-generator/sections-skills.d.ts.map +1 -0
  3. package/dist/engine/skill-generator/sections-skills.js +51 -0
  4. package/dist/engine/skill-generator/sections-skills.js.map +1 -0
  5. package/dist/engine/skill-generator/sections.d.ts +1 -0
  6. package/dist/engine/skill-generator/sections.d.ts.map +1 -1
  7. package/dist/engine/skill-generator/sections.js +2 -1
  8. package/dist/engine/skill-generator/sections.js.map +1 -1
  9. package/dist/engine/skill-generator.d.ts +3 -1
  10. package/dist/engine/skill-generator.d.ts.map +1 -1
  11. package/dist/engine/skill-generator.js +11 -2
  12. package/dist/engine/skill-generator.js.map +1 -1
  13. package/dist/engine/test-plan-generator.d.ts +3 -0
  14. package/dist/engine/test-plan-generator.d.ts.map +1 -0
  15. package/dist/engine/test-plan-generator.js +166 -0
  16. package/dist/engine/test-plan-generator.js.map +1 -0
  17. package/dist/engine/test-spec-generator.d.ts +8 -0
  18. package/dist/engine/test-spec-generator.d.ts.map +1 -0
  19. package/dist/engine/test-spec-generator.js +348 -0
  20. package/dist/engine/test-spec-generator.js.map +1 -0
  21. package/dist/tools/generate-rules.d.ts.map +1 -1
  22. package/dist/tools/generate-rules.js +20 -0
  23. package/dist/tools/generate-rules.js.map +1 -1
  24. package/dist/tools/generate-tests/generators/graphql-test-generator.d.ts +17 -0
  25. package/dist/tools/generate-tests/generators/graphql-test-generator.d.ts.map +1 -0
  26. package/dist/tools/generate-tests/generators/graphql-test-generator.js +235 -0
  27. package/dist/tools/generate-tests/generators/graphql-test-generator.js.map +1 -0
  28. package/dist/tools/generate-tests/generators/grpc-test-generator.d.ts +17 -0
  29. package/dist/tools/generate-tests/generators/grpc-test-generator.d.ts.map +1 -0
  30. package/dist/tools/generate-tests/generators/grpc-test-generator.js +283 -0
  31. package/dist/tools/generate-tests/generators/grpc-test-generator.js.map +1 -0
  32. package/dist/tools/generate-tests/generators/visual-regression-generator.d.ts +19 -0
  33. package/dist/tools/generate-tests/generators/visual-regression-generator.d.ts.map +1 -0
  34. package/dist/tools/generate-tests/generators/visual-regression-generator.js +304 -0
  35. package/dist/tools/generate-tests/generators/visual-regression-generator.js.map +1 -0
  36. package/dist/tools/generate-tests/generators/websocket-test-generator.d.ts +17 -0
  37. package/dist/tools/generate-tests/generators/websocket-test-generator.d.ts.map +1 -0
  38. package/dist/tools/generate-tests/generators/websocket-test-generator.js +243 -0
  39. package/dist/tools/generate-tests/generators/websocket-test-generator.js.map +1 -0
  40. package/dist/tools/generate-tests/plan-mode-handler.d.ts +3 -0
  41. package/dist/tools/generate-tests/plan-mode-handler.d.ts.map +1 -0
  42. package/dist/tools/generate-tests/plan-mode-handler.js +54 -0
  43. package/dist/tools/generate-tests/plan-mode-handler.js.map +1 -0
  44. package/dist/tools/generate-tests/spec-dispatcher.d.ts.map +1 -1
  45. package/dist/tools/generate-tests/spec-dispatcher.js +25 -0
  46. package/dist/tools/generate-tests/spec-dispatcher.js.map +1 -1
  47. package/dist/tools/generate-tests/test-helpers.d.ts +8 -0
  48. package/dist/tools/generate-tests/test-helpers.d.ts.map +1 -0
  49. package/dist/tools/generate-tests/test-helpers.js +120 -0
  50. package/dist/tools/generate-tests/test-helpers.js.map +1 -0
  51. package/dist/tools/generate-tests.d.ts.map +1 -1
  52. package/dist/tools/generate-tests.js +6 -118
  53. package/dist/tools/generate-tests.js.map +1 -1
  54. package/dist/tools/init-project/handler.d.ts.map +1 -1
  55. package/dist/tools/init-project/handler.js +29 -0
  56. package/dist/tools/init-project/handler.js.map +1 -1
  57. package/dist/types/stack/recommend.d.ts +2 -0
  58. package/dist/types/stack/recommend.d.ts.map +1 -1
  59. package/dist/types/testing.d.ts +51 -0
  60. package/dist/types/testing.d.ts.map +1 -1
  61. package/package.json +3 -2
  62. package/src/i18n/messages/en.json +333 -0
  63. package/src/i18n/messages/es.json +333 -0
  64. package/src/i18n/messages/pt.json +333 -0
@@ -0,0 +1,304 @@
1
+ // tools/generate-tests/generators/visual-regression-generator.ts — SPEC-058a Section C
2
+ // Generates visual regression and frontend screenshot test scaffolds.
3
+ const UI_FRAMEWORKS = [
4
+ 'react',
5
+ 'vue',
6
+ 'angular',
7
+ 'svelte',
8
+ 'next',
9
+ 'nextjs',
10
+ 'next.js',
11
+ 'nuxt',
12
+ 'sveltekit',
13
+ 'svelte-kit',
14
+ 'solid',
15
+ 'solidjs',
16
+ 'preact',
17
+ 'qwik',
18
+ 'remix',
19
+ 'gatsby',
20
+ 'astro',
21
+ ];
22
+ const DARK_MODE_SIGNALS = [
23
+ 'tailwind',
24
+ 'shadcn',
25
+ 'radix',
26
+ 'mui',
27
+ 'chakra',
28
+ 'mantine',
29
+ 'daisy',
30
+ 'daisyui',
31
+ 'styled-components',
32
+ 'emotion',
33
+ ];
34
+ const BP_MOBILE = { label: 'mobile', width: 375, height: 812 };
35
+ const BP_TABLET = { label: 'tablet', width: 768, height: 1024 };
36
+ const BP_DESKTOP = { label: 'desktop', width: 1280, height: 800 };
37
+ /**
38
+ * Returns true when the project has at least one frontend/fullstack app with a UI framework.
39
+ */
40
+ export function isVisualRegressionProject(knowledge) {
41
+ const frontendApps = knowledge.apps.filter((app) => app.type === 'frontend');
42
+ for (const app of frontendApps) {
43
+ const fw = (app.framework ?? '').toLowerCase();
44
+ if (UI_FRAMEWORKS.some((uif) => fw.includes(uif))) {
45
+ return true;
46
+ }
47
+ }
48
+ // Fall back to top-level framework when no frontend apps matched
49
+ if (frontendApps.length === 0) {
50
+ const topFw = (knowledge.framework ?? '').toLowerCase();
51
+ return UI_FRAMEWORKS.some((uif) => topFw.includes(uif));
52
+ }
53
+ return false;
54
+ }
55
+ function hasDarkModeSupport(knowledge) {
56
+ const stackLower = knowledge.stack.map((s) => s.toLowerCase());
57
+ return DARK_MODE_SIGNALS.some((sig) => stackLower.some((s) => s.includes(sig)));
58
+ }
59
+ /**
60
+ * Generate visual regression test definitions.
61
+ */
62
+ export function generateVisualRegressionTestDefs(title, testDir, _testExt) {
63
+ const defs = [
64
+ {
65
+ name: `${title} — Visual: screenshot baseline matches approved snapshot`,
66
+ type: 'e2e',
67
+ file: `${testDir}/visual/`,
68
+ description: 'Capture full-page screenshot and compare against approved baseline snapshot — fails on unexpected visual changes',
69
+ priority: 'critical',
70
+ automatable: true,
71
+ },
72
+ {
73
+ name: `${title} — Visual: responsive layout at 375px, 768px, 1280px`,
74
+ type: 'e2e',
75
+ file: `${testDir}/visual/`,
76
+ description: 'Verify layout integrity across mobile (375px), tablet (768px), and desktop (1280px) breakpoints',
77
+ priority: 'high',
78
+ automatable: true,
79
+ },
80
+ {
81
+ name: `${title} — Visual: component renders without layout shift`,
82
+ type: 'unit',
83
+ file: `${testDir}/visual/`,
84
+ description: 'Component snapshot test — verify no unintended style or markup changes in isolated render',
85
+ priority: 'medium',
86
+ automatable: true,
87
+ },
88
+ ];
89
+ // Dark mode def added unconditionally — caller controls knowledge, but we always include it
90
+ // since isVisualRegressionProject already filtered for UI projects that commonly support dark mode
91
+ defs.push({
92
+ name: `${title} — Visual: dark mode toggle does not break layout`,
93
+ type: 'e2e',
94
+ file: `${testDir}/visual/`,
95
+ description: 'Toggle dark/light mode and verify screenshot matches approved dark-mode snapshot without overflow or invisible text',
96
+ priority: 'medium',
97
+ automatable: true,
98
+ });
99
+ return defs;
100
+ }
101
+ // === Language-specific content generators ===
102
+ function generatePlaywrightTest(title, slug) {
103
+ return `// Visual regression tests for ${title}
104
+ // Requires: @playwright/test with --update-snapshots for initial baseline
105
+ import { test, expect } from '@playwright/test';
106
+
107
+ const BASE_URL = process.env.BASE_URL ?? 'http://localhost:3000';
108
+
109
+ test.describe('${title} — Visual Regression', () => {
110
+ test('full-page screenshot matches baseline', async ({ page }) => {
111
+ await page.goto(BASE_URL);
112
+ await page.waitForLoadState('networkidle');
113
+ await expect(page).toHaveScreenshot('${slug}-baseline.png', {
114
+ fullPage: true,
115
+ maxDiffPixelRatio: 0.02,
116
+ });
117
+ });
118
+
119
+ test('responsive — mobile 375px', async ({ page }) => {
120
+ await page.setViewportSize({ width: ${BP_MOBILE.width}, height: ${BP_MOBILE.height} });
121
+ await page.goto(BASE_URL);
122
+ await page.waitForLoadState('networkidle');
123
+ await expect(page).toHaveScreenshot('${slug}-mobile.png', {
124
+ maxDiffPixelRatio: 0.02,
125
+ });
126
+ });
127
+
128
+ test('responsive — tablet 768px', async ({ page }) => {
129
+ await page.setViewportSize({ width: ${BP_TABLET.width}, height: ${BP_TABLET.height} });
130
+ await page.goto(BASE_URL);
131
+ await page.waitForLoadState('networkidle');
132
+ await expect(page).toHaveScreenshot('${slug}-tablet.png', {
133
+ maxDiffPixelRatio: 0.02,
134
+ });
135
+ });
136
+
137
+ test('responsive — desktop 1280px', async ({ page }) => {
138
+ await page.setViewportSize({ width: ${BP_DESKTOP.width}, height: ${BP_DESKTOP.height} });
139
+ await page.goto(BASE_URL);
140
+ await page.waitForLoadState('networkidle');
141
+ await expect(page).toHaveScreenshot('${slug}-desktop.png', {
142
+ maxDiffPixelRatio: 0.02,
143
+ });
144
+ });
145
+
146
+ test('component renders without layout shift', async ({ page }) => {
147
+ await page.goto(\`\${BASE_URL}/components\`);
148
+ // TODO: navigate to your component preview/Storybook URL
149
+ await page.waitForLoadState('networkidle');
150
+ await expect(page).toHaveScreenshot('${slug}-component.png');
151
+ });
152
+
153
+ test('dark mode toggle does not break layout', async ({ page }) => {
154
+ await page.goto(BASE_URL);
155
+ await page.waitForLoadState('networkidle');
156
+ // TODO: adjust selector to match your dark mode toggle
157
+ await page.click('[data-testid="theme-toggle"], button[aria-label*="dark"], button[aria-label*="theme"]');
158
+ await page.waitForTimeout(300); // allow CSS transition
159
+ await expect(page).toHaveScreenshot('${slug}-dark-mode.png', {
160
+ maxDiffPixelRatio: 0.02,
161
+ });
162
+ });
163
+ });
164
+ `;
165
+ }
166
+ function generatePytestPlaywrightTest(title, slug) {
167
+ const pySlug = slug.replace(/-/g, '_');
168
+ return `"""Visual regression tests for ${title} — pytest-playwright."""
169
+ # Requires: pytest-playwright + pytest-playwright-snapshot
170
+ # Install: pip install pytest-playwright playwright-pytest-snapshot
171
+ # Run: pytest --update-snapshots (first time to create baseline)
172
+ import pytest
173
+ from playwright.sync_api import Page, expect
174
+
175
+
176
+ BASE_URL = "http://localhost:3000"
177
+
178
+
179
+ @pytest.fixture(scope="session")
180
+ def base_url() -> str:
181
+ import os
182
+ return os.environ.get("BASE_URL", BASE_URL)
183
+
184
+
185
+ def test_${pySlug}_full_page_screenshot(page: Page, base_url: str, assert_snapshot: object) -> None:
186
+ """Full-page screenshot matches approved baseline."""
187
+ page.goto(base_url)
188
+ page.wait_for_load_state("networkidle")
189
+ assert_snapshot(page.screenshot(full_page=True), name="${slug}-baseline.png")
190
+
191
+
192
+ def test_${pySlug}_responsive_mobile(page: Page, base_url: str, assert_snapshot: object) -> None:
193
+ """Layout intact at 375px (mobile)."""
194
+ page.set_viewport_size({"width": ${BP_MOBILE.width}, "height": ${BP_MOBILE.height}})
195
+ page.goto(base_url)
196
+ page.wait_for_load_state("networkidle")
197
+ assert_snapshot(page.screenshot(), name="${slug}-mobile.png")
198
+
199
+
200
+ def test_${pySlug}_responsive_tablet(page: Page, base_url: str, assert_snapshot: object) -> None:
201
+ """Layout intact at 768px (tablet)."""
202
+ page.set_viewport_size({"width": ${BP_TABLET.width}, "height": ${BP_TABLET.height}})
203
+ page.goto(base_url)
204
+ page.wait_for_load_state("networkidle")
205
+ assert_snapshot(page.screenshot(), name="${slug}-tablet.png")
206
+
207
+
208
+ def test_${pySlug}_responsive_desktop(page: Page, base_url: str, assert_snapshot: object) -> None:
209
+ """Layout intact at 1280px (desktop)."""
210
+ page.set_viewport_size({"width": ${BP_DESKTOP.width}, "height": ${BP_DESKTOP.height}})
211
+ page.goto(base_url)
212
+ page.wait_for_load_state("networkidle")
213
+ assert_snapshot(page.screenshot(), name="${slug}-desktop.png")
214
+
215
+
216
+ def test_${pySlug}_dark_mode(page: Page, base_url: str, assert_snapshot: object) -> None:
217
+ """Dark mode toggle does not break layout."""
218
+ page.goto(base_url)
219
+ page.wait_for_load_state("networkidle")
220
+ # TODO: adjust selector to match your dark mode toggle
221
+ page.click('[data-testid="theme-toggle"]')
222
+ page.wait_for_timeout(300)
223
+ assert_snapshot(page.screenshot(), name="${slug}-dark-mode.png")
224
+ `;
225
+ }
226
+ function generateCypressVisualTest(title, slug) {
227
+ return `// Visual regression tests for ${title} — Cypress + cypress-image-snapshot
228
+ // Install: npm install --save-dev cypress-image-snapshot
229
+ // Config: import 'cypress-image-snapshot/command' in cypress/support/commands.ts
230
+ describe('${title} — Visual Regression', () => {
231
+ const BASE_URL = Cypress.env('BASE_URL') ?? 'http://localhost:3000';
232
+
233
+ it('full-page screenshot matches baseline', () => {
234
+ cy.visit(BASE_URL);
235
+ cy.matchImageSnapshot('${slug}-baseline', { failureThreshold: 0.02 });
236
+ });
237
+
238
+ it('responsive — mobile 375px', () => {
239
+ cy.viewport(${BP_MOBILE.width}, ${BP_MOBILE.height});
240
+ cy.visit(BASE_URL);
241
+ cy.matchImageSnapshot('${slug}-mobile', { failureThreshold: 0.02 });
242
+ });
243
+
244
+ it('responsive — tablet 768px', () => {
245
+ cy.viewport(${BP_TABLET.width}, ${BP_TABLET.height});
246
+ cy.visit(BASE_URL);
247
+ cy.matchImageSnapshot('${slug}-tablet', { failureThreshold: 0.02 });
248
+ });
249
+
250
+ it('responsive — desktop 1280px', () => {
251
+ cy.viewport(${BP_DESKTOP.width}, ${BP_DESKTOP.height});
252
+ cy.visit(BASE_URL);
253
+ cy.matchImageSnapshot('${slug}-desktop', { failureThreshold: 0.02 });
254
+ });
255
+
256
+ it('dark mode toggle does not break layout', () => {
257
+ cy.visit(BASE_URL);
258
+ // TODO: adjust selector to match your dark mode toggle
259
+ cy.get('[data-testid="theme-toggle"]').click();
260
+ cy.wait(300); // allow CSS transition
261
+ cy.matchImageSnapshot('${slug}-dark-mode', { failureThreshold: 0.02 });
262
+ });
263
+ });
264
+ `;
265
+ }
266
+ function generateVisualTestContent(title, slug, framework, language) {
267
+ if (language === 'python') {
268
+ return generatePytestPlaywrightTest(title, slug);
269
+ }
270
+ if (framework === 'cypress') {
271
+ return generateCypressVisualTest(title, slug);
272
+ }
273
+ return generatePlaywrightTest(title, slug);
274
+ }
275
+ function getFileExtension(language) {
276
+ const exts = {
277
+ python: 'py',
278
+ go: 'go',
279
+ ruby: 'rb',
280
+ java: 'java',
281
+ javascript: 'test.ts',
282
+ typescript: 'test.ts',
283
+ };
284
+ return exts[language] ?? 'test.ts';
285
+ }
286
+ /**
287
+ * Generate visual regression test files for the given spec.
288
+ */
289
+ export function generateVisualRegressionTestFiles(spec, testDir, framework, language, testExt, autoGenerate) {
290
+ const ext = language === 'python' ? 'py' : getFileExtension(language);
291
+ const resolvedFramework = framework === 'cypress' ? 'cypress' : 'playwright';
292
+ const content = generateVisualTestContent(spec.title, spec.slug, framework, language);
293
+ return [
294
+ {
295
+ path: `${testDir}/visual/${spec.slug}.visual.${ext === 'test.ts' ? testExt : ext}`,
296
+ framework: resolvedFramework,
297
+ content,
298
+ ready: autoGenerate,
299
+ },
300
+ ];
301
+ }
302
+ // Re-export for dark mode detection utility (used in spec-dispatcher if needed)
303
+ export { hasDarkModeSupport };
304
+ //# sourceMappingURL=visual-regression-generator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"visual-regression-generator.js","sourceRoot":"","sources":["../../../../src/tools/generate-tests/generators/visual-regression-generator.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ import type { TestDefinition, TestFile, ProjectKnowledge } from '../../../types/index.js';
2
+ /**
3
+ * Returns true when the project uses WebSocket technology.
4
+ */
5
+ export declare function isWebSocketProject(knowledge: ProjectKnowledge): boolean;
6
+ /**
7
+ * Generate WebSocket test definitions.
8
+ */
9
+ export declare function generateWebSocketTestDefs(title: string, testDir: string, _testExt: string): TestDefinition[];
10
+ /**
11
+ * Generate WebSocket test file scaffolds for the target language.
12
+ */
13
+ export declare function generateWebSocketTestFiles(spec: {
14
+ title: string;
15
+ slug: string;
16
+ }, testDir: string, framework: string, language: string, testExt: string, autoGenerate: boolean): TestFile[];
17
+ //# sourceMappingURL=websocket-test-generator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"websocket-test-generator.d.ts","sourceRoot":"","sources":["../../../../src/tools/generate-tests/generators/websocket-test-generator.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,QAAQ,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAgB1F;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,gBAAgB,GAAG,OAAO,CAGvE;AAED;;GAEG;AACH,wBAAgB,yBAAyB,CACvC,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,GACf,cAAc,EAAE,CA2ClB;AA+JD;;GAEG;AACH,wBAAgB,0BAA0B,CACxC,IAAI,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EACrC,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,YAAY,EAAE,OAAO,GACpB,QAAQ,EAAE,CAgBZ"}
@@ -0,0 +1,243 @@
1
+ // tools/generate-tests/generators/websocket-test-generator.ts — SPEC-058a Section D
2
+ // Generates WebSocket test scaffolds: connection lifecycle, reconnection, ordering, heartbeat.
3
+ const WS_SIGNALS = [
4
+ 'ws',
5
+ 'socket.io',
6
+ 'socketio',
7
+ 'signalr',
8
+ 'websocket',
9
+ 'sockjs',
10
+ 'phoenix-channels',
11
+ 'phoenix',
12
+ 'actioncable',
13
+ 'pusher',
14
+ 'ably',
15
+ ];
16
+ /**
17
+ * Returns true when the project uses WebSocket technology.
18
+ */
19
+ export function isWebSocketProject(knowledge) {
20
+ const stackLower = knowledge.stack.map((s) => s.toLowerCase());
21
+ return WS_SIGNALS.some((sig) => stackLower.some((s) => s.includes(sig)));
22
+ }
23
+ /**
24
+ * Generate WebSocket test definitions.
25
+ */
26
+ export function generateWebSocketTestDefs(title, testDir, _testExt) {
27
+ return [
28
+ {
29
+ name: `${title} — WebSocket: connection lifecycle (open/close/error)`,
30
+ type: 'integration',
31
+ file: `${testDir}/websocket/`,
32
+ description: 'Verify WebSocket connection opens, exchanges messages, and closes cleanly',
33
+ priority: 'critical',
34
+ automatable: true,
35
+ },
36
+ {
37
+ name: `${title} — WebSocket: reconnection with backoff`,
38
+ type: 'integration',
39
+ file: `${testDir}/websocket/`,
40
+ description: 'Verify client reconnects with exponential backoff after unexpected disconnect',
41
+ priority: 'high',
42
+ automatable: true,
43
+ },
44
+ {
45
+ name: `${title} — WebSocket: message ordering guarantee`,
46
+ type: 'integration',
47
+ file: `${testDir}/websocket/`,
48
+ description: 'Send N messages sequentially and verify they arrive in FIFO order',
49
+ priority: 'high',
50
+ automatable: true,
51
+ },
52
+ {
53
+ name: `${title} — WebSocket: heartbeat/ping-pong keeps connection alive`,
54
+ type: 'integration',
55
+ file: `${testDir}/websocket/`,
56
+ description: 'Verify ping/pong mechanism keeps the connection alive beyond idle timeout',
57
+ priority: 'medium',
58
+ automatable: true,
59
+ },
60
+ {
61
+ name: `${title} — WebSocket: auth rejection on invalid token`,
62
+ type: 'integration',
63
+ file: `${testDir}/websocket/`,
64
+ description: 'Verify connection is rejected or closed when an invalid auth token is provided',
65
+ priority: 'critical',
66
+ automatable: true,
67
+ },
68
+ ];
69
+ }
70
+ // ---------------------------------------------------------------------------
71
+ // Content generators
72
+ // ---------------------------------------------------------------------------
73
+ function buildTsWebSocketTest(title) {
74
+ return `// WebSocket tests for: ${title}
75
+ // Generated by SpecForge SDD MCP Server (SPEC-058a)
76
+ import { describe, it, expect, beforeAll, afterAll } from 'vitest';
77
+ // import WebSocket from 'ws';
78
+
79
+ const WS_URL = process.env['WS_URL'] ?? 'ws://localhost:3000';
80
+
81
+ describe('${title} — WebSocket Tests', () => {
82
+ // let ws: WebSocket;
83
+
84
+ beforeAll(() => {
85
+ // ws = new WebSocket(WS_URL);
86
+ });
87
+
88
+ afterAll(() => {
89
+ // ws?.close();
90
+ });
91
+
92
+ describe('Connection lifecycle', () => {
93
+ it('opens connection successfully', async () => {
94
+ // await new Promise<void>((resolve, reject) => {
95
+ // ws.on('open', resolve);
96
+ // ws.on('error', reject);
97
+ // });
98
+ // expect(ws.readyState).toBe(WebSocket.OPEN);
99
+ expect(true).toBe(true); // Replace with real connection assertion
100
+ });
101
+
102
+ it('closes connection cleanly', async () => {
103
+ // ws.close(1000, 'Normal closure');
104
+ // await new Promise<void>((resolve) => ws.on('close', resolve));
105
+ // expect(ws.readyState).toBe(WebSocket.CLOSED);
106
+ expect(true).toBe(true); // Replace with real close assertion
107
+ });
108
+ });
109
+
110
+ describe('Reconnection', () => {
111
+ it('reconnects with exponential backoff after disconnect', async () => {
112
+ // Simulate abrupt disconnect and verify reconnection attempt
113
+ // const attempts: number[] = [];
114
+ // client.on('reconnect_attempt', (n) => attempts.push(n));
115
+ // await simulateDisconnect();
116
+ // expect(attempts.length).toBeGreaterThan(0);
117
+ expect(true).toBe(true); // Replace with real reconnect test
118
+ });
119
+ });
120
+
121
+ describe('Message ordering', () => {
122
+ it('messages arrive in FIFO order', async () => {
123
+ // const received: string[] = [];
124
+ // ws.on('message', (data) => received.push(data.toString()));
125
+ // for (let i = 0; i < 10; i++) ws.send(\`msg-\${i}\`);
126
+ // await new Promise((r) => setTimeout(r, 500));
127
+ // expect(received).toEqual(Array.from({ length: 10 }, (_, i) => \`msg-\${i}\`));
128
+ expect(true).toBe(true); // Replace with real ordering test
129
+ });
130
+ });
131
+
132
+ describe('Heartbeat', () => {
133
+ it('ping/pong keeps connection alive', async () => {
134
+ // let pongReceived = false;
135
+ // ws.on('pong', () => { pongReceived = true; });
136
+ // ws.ping();
137
+ // await new Promise((r) => setTimeout(r, 1000));
138
+ // expect(pongReceived).toBe(true);
139
+ expect(true).toBe(true); // Replace with real heartbeat test
140
+ });
141
+ });
142
+
143
+ describe('Auth', () => {
144
+ it('rejects connection with invalid token', async () => {
145
+ // const badWs = new WebSocket(WS_URL, { headers: { Authorization: 'Bearer invalid' } });
146
+ // await expect(
147
+ // new Promise<void>((_, reject) => {
148
+ // badWs.on('error', reject);
149
+ // badWs.on('close', (code) => reject(new Error(\`closed: \${code}\`)));
150
+ // }),
151
+ // ).rejects.toThrow();
152
+ expect(true).toBe(true); // Replace with real auth rejection test
153
+ });
154
+ });
155
+ });
156
+ `;
157
+ }
158
+ function buildPythonWebSocketTest(title) {
159
+ const cls = title.replace(/\s+/g, '');
160
+ return `"""WebSocket tests for: ${title}
161
+ Generated by SpecForge SDD MCP Server (SPEC-058a).
162
+ Uses websockets library for Python.
163
+ """
164
+ import pytest
165
+ # import asyncio
166
+ # import websockets
167
+
168
+
169
+ WS_URL = "ws://localhost:3000"
170
+
171
+
172
+ class Test${cls}WebSocket:
173
+ """WebSocket test suite for ${title}."""
174
+
175
+ def test_connection_opens_successfully(self) -> None:
176
+ """WebSocket connection opens and receives welcome message."""
177
+ # async def _test():
178
+ # async with websockets.connect(WS_URL) as ws:
179
+ # assert ws.open
180
+ # asyncio.get_event_loop().run_until_complete(_test())
181
+ pass
182
+
183
+ def test_connection_closes_cleanly(self) -> None:
184
+ """WebSocket connection closes with normal code 1000."""
185
+ # async def _test():
186
+ # async with websockets.connect(WS_URL) as ws:
187
+ # await ws.close(1000, "Normal closure")
188
+ # assert ws.closed
189
+ # asyncio.get_event_loop().run_until_complete(_test())
190
+ pass
191
+
192
+ def test_messages_arrive_in_order(self) -> None:
193
+ """Messages sent sequentially arrive in FIFO order."""
194
+ # async def _test():
195
+ # async with websockets.connect(WS_URL) as ws:
196
+ # for i in range(10):
197
+ # await ws.send(f"msg-{i}")
198
+ # received = [await ws.recv() for _ in range(10)]
199
+ # assert received == [f"msg-{i}" for i in range(10)]
200
+ # asyncio.get_event_loop().run_until_complete(_test())
201
+ pass
202
+
203
+ def test_heartbeat_keeps_connection_alive(self) -> None:
204
+ """Ping/pong keeps the connection alive beyond idle timeout."""
205
+ # async def _test():
206
+ # async with websockets.connect(WS_URL) as ws:
207
+ # pong = await ws.ping()
208
+ # await pong # wait for pong response
209
+ # asyncio.get_event_loop().run_until_complete(_test())
210
+ pass
211
+
212
+ def test_invalid_token_rejects_connection(self) -> None:
213
+ """Connection with invalid auth token is rejected."""
214
+ # async def _test():
215
+ # with pytest.raises(websockets.exceptions.InvalidStatusCode):
216
+ # await websockets.connect(
217
+ # WS_URL,
218
+ # extra_headers={"Authorization": "Bearer invalid"},
219
+ # )
220
+ # asyncio.get_event_loop().run_until_complete(_test())
221
+ pass
222
+ `;
223
+ }
224
+ /**
225
+ * Generate WebSocket test file scaffolds for the target language.
226
+ */
227
+ export function generateWebSocketTestFiles(spec, testDir, framework, language, testExt, autoGenerate) {
228
+ const isPython = language === 'python';
229
+ const resolvedExt = isPython ? 'py' : testExt;
230
+ const resolvedFramework = isPython ? 'pytest' : framework;
231
+ const content = isPython
232
+ ? buildPythonWebSocketTest(spec.title)
233
+ : buildTsWebSocketTest(spec.title);
234
+ return [
235
+ {
236
+ path: `${testDir}/websocket/${spec.slug}.ws.test.${resolvedExt}`,
237
+ framework: resolvedFramework,
238
+ content,
239
+ ready: autoGenerate,
240
+ },
241
+ ];
242
+ }
243
+ //# sourceMappingURL=websocket-test-generator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"websocket-test-generator.js","sourceRoot":"","sources":["../../../../src/tools/generate-tests/generators/websocket-test-generator.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import type { ProjectKnowledge, ToolResult } from '../../types/index.js';
2
+ export declare function handlePlanMode(projectId: string, knowledge: ProjectKnowledge): ToolResult;
3
+ //# sourceMappingURL=plan-mode-handler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plan-mode-handler.d.ts","sourceRoot":"","sources":["../../../src/tools/generate-tests/plan-mode-handler.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,gBAAgB,EAAiB,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAwBxF,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,gBAAgB,GAAG,UAAU,CAyCzF"}
@@ -0,0 +1,54 @@
1
+ // tools/generate-tests/plan-mode-handler.ts — SPEC-058a Plan mode handler
2
+ // Extracted from generate-tests.ts to stay under 400-line limit
3
+ import { generateTestPlan } from '../../engine/test-plan-generator.js';
4
+ function formatLayerSection(layer) {
5
+ const statusIcon = layer.needed ? '✓' : '–';
6
+ const lines = [
7
+ `### ${statusIcon} ${layer.type.toUpperCase()} tests (${layer.priority})`,
8
+ '',
9
+ `**Needed:** ${layer.needed ? 'Yes' : 'No'}`,
10
+ `**Reason:** ${layer.reason}`,
11
+ ];
12
+ if (layer.needed) {
13
+ lines.push(`**Estimated hours:** ${layer.estimatedHours}h`);
14
+ if (layer.frameworks.length > 0) {
15
+ lines.push(`**Frameworks:** ${layer.frameworks.join(', ')}`);
16
+ }
17
+ if (layer.tools.length > 0) {
18
+ lines.push(`**Tools:** ${layer.tools.join(', ')}`);
19
+ }
20
+ }
21
+ lines.push('');
22
+ return lines;
23
+ }
24
+ export function handlePlanMode(projectId, knowledge) {
25
+ const plan = generateTestPlan(knowledge);
26
+ const lines = [
27
+ '## Test Plan',
28
+ '',
29
+ `**Project:** \`${projectId}\``,
30
+ `**Generated at:** ${plan.generatedAt}`,
31
+ `**Total estimated hours:** ${plan.totalEstimatedHours}h`,
32
+ `**Starter kit mode:** ${plan.starterKit ? 'Yes (no apps detected)' : 'No'}`,
33
+ '',
34
+ '## Layers',
35
+ '',
36
+ ];
37
+ for (const layer of plan.layers) {
38
+ lines.push(...formatLayerSection(layer));
39
+ }
40
+ if (plan.mcpRecommendations.length > 0) {
41
+ lines.push('## MCP Recommendations', '');
42
+ for (const rec of plan.mcpRecommendations) {
43
+ lines.push(`- ${rec}`);
44
+ }
45
+ lines.push('');
46
+ }
47
+ if (plan.starterKit) {
48
+ lines.push('## Starter Kit', '', 'No apps were detected. Use the starter kit templates to bootstrap your test setup:', '- Add a test runner to your project (vitest, pytest, go test, etc.)', '- Configure coverage thresholds (aim for 80%+ lines, 70%+ branches)', '- Set up CI/CD to run tests on every push', '');
49
+ }
50
+ return {
51
+ content: [{ type: 'text', text: lines.join('\n') }],
52
+ };
53
+ }
54
+ //# sourceMappingURL=plan-mode-handler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plan-mode-handler.js","sourceRoot":"","sources":["../../../src/tools/generate-tests/plan-mode-handler.ts"],"names":[],"mappings":"AAAA,0EAA0E;AAC1E,gEAAgE;AAGhE,OAAO,EAAE,gBAAgB,EAAE,MAAM,qCAAqC,CAAC;AAEvE,SAAS,kBAAkB,CAAC,KAAoB;IAC9C,MAAM,UAAU,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC;IAC5C,MAAM,KAAK,GAAa;QACtB,OAAO,UAAU,IAAI,KAAK,CAAC,IAAI,CAAC,WAAW,EAAE,WAAW,KAAK,CAAC,QAAQ,GAAG;QACzE,EAAE;QACF,eAAe,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE;QAC5C,eAAe,KAAK,CAAC,MAAM,EAAE;KAC9B,CAAC;IACF,IAAI,KAAK,CAAC,MAAM,EAAE,CAAC;QACjB,KAAK,CAAC,IAAI,CAAC,wBAAwB,KAAK,CAAC,cAAc,GAAG,CAAC,CAAC;QAC5D,IAAI,KAAK,CAAC,UAAU,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAChC,KAAK,CAAC,IAAI,CAAC,mBAAmB,KAAK,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAC/D,CAAC;QACD,IAAI,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC3B,KAAK,CAAC,IAAI,CAAC,cAAc,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACrD,CAAC;IACH,CAAC;IACD,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACf,OAAO,KAAK,CAAC;AACf,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,SAAiB,EAAE,SAA2B;IAC3E,MAAM,IAAI,GAAG,gBAAgB,CAAC,SAAS,CAAC,CAAC;IACzC,MAAM,KAAK,GAAa;QACtB,cAAc;QACd,EAAE;QACF,kBAAkB,SAAS,IAAI;QAC/B,qBAAqB,IAAI,CAAC,WAAW,EAAE;QACvC,8BAA8B,IAAI,CAAC,mBAAmB,GAAG;QACzD,yBAAyB,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,wBAAwB,CAAC,CAAC,CAAC,IAAI,EAAE;QAC5E,EAAE;QACF,WAAW;QACX,EAAE;KACH,CAAC;IAEF,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;QAChC,KAAK,CAAC,IAAI,CAAC,GAAG,kBAAkB,CAAC,KAAK,CAAC,CAAC,CAAC;IAC3C,CAAC;IAED,IAAI,IAAI,CAAC,kBAAkB,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACvC,KAAK,CAAC,IAAI,CAAC,wBAAwB,EAAE,EAAE,CAAC,CAAC;QACzC,KAAK,MAAM,GAAG,IAAI,IAAI,CAAC,kBAAkB,EAAE,CAAC;YAC1C,KAAK,CAAC,IAAI,CAAC,KAAK,GAAG,EAAE,CAAC,CAAC;QACzB,CAAC;QACD,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACjB,CAAC;IAED,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;QACpB,KAAK,CAAC,IAAI,CACR,gBAAgB,EAChB,EAAE,EACF,oFAAoF,EACpF,qEAAqE,EACrE,qEAAqE,EACrE,2CAA2C,EAC3C,EAAE,CACH,CAAC;IACJ,CAAC;IAED,OAAO;QACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAe,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;KAC7D,CAAC;AACJ,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"spec-dispatcher.d.ts","sourceRoot":"","sources":["../../../src/tools/generate-tests/spec-dispatcher.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,IAAI,EAAE,gBAAgB,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAyE7F;;;;GAIG;AACH,wBAAgB,wBAAwB,CACtC,IAAI,EAAE;IACJ,IAAI,EAAE,IAAI,CAAC;IACX,SAAS,EAAE,gBAAgB,CAAC;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,OAAO,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;CACjB,EACD,SAAS,EAAE,cAAc,EAAE,EAC3B,gBAAgB,EAAE,cAAc,EAAE,EAClC,SAAS,EAAE,QAAQ,EAAE,GACpB;IAAE,SAAS,EAAE,OAAO,CAAA;CAAE,CA0JxB"}
1
+ {"version":3,"file":"spec-dispatcher.d.ts","sourceRoot":"","sources":["../../../src/tools/generate-tests/spec-dispatcher.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,IAAI,EAAE,gBAAgB,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AA6F7F;;;;GAIG;AACH,wBAAgB,wBAAwB,CACtC,IAAI,EAAE;IACJ,IAAI,EAAE,IAAI,CAAC;IACX,SAAS,EAAE,gBAAgB,CAAC;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,OAAO,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;CACjB,EACD,SAAS,EAAE,cAAc,EAAE,EAC3B,gBAAgB,EAAE,cAAc,EAAE,EAClC,SAAS,EAAE,QAAQ,EAAE,GACpB;IAAE,SAAS,EAAE,OAAO,CAAA;CAAE,CAuNxB"}