sunpeak 0.18.14 → 0.19.2

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 (53) hide show
  1. package/README.md +37 -134
  2. package/bin/commands/new.mjs +3 -1
  3. package/bin/commands/test-init.mjs +305 -0
  4. package/bin/commands/test.mjs +144 -0
  5. package/bin/lib/inspect/inspect-config.d.mts +4 -0
  6. package/bin/lib/inspect/inspect-config.mjs +18 -24
  7. package/bin/lib/test/base-config.mjs +75 -0
  8. package/bin/lib/test/matchers.mjs +99 -0
  9. package/bin/lib/test/test-config.d.mts +66 -0
  10. package/bin/lib/test/test-config.mjs +125 -0
  11. package/bin/lib/test/test-fixtures.d.mts +129 -0
  12. package/bin/lib/test/test-fixtures.mjs +232 -0
  13. package/bin/sunpeak.js +18 -5
  14. package/package.json +22 -10
  15. package/template/README.md +18 -8
  16. package/template/dist/albums/albums.json +1 -1
  17. package/template/dist/carousel/carousel.json +1 -1
  18. package/template/dist/map/map.html +468 -280
  19. package/template/dist/map/map.json +1 -1
  20. package/template/dist/review/review.json +1 -1
  21. package/template/node_modules/.bin/playwright +2 -2
  22. package/template/node_modules/.bin/vite +2 -2
  23. package/template/node_modules/.bin/vitest +2 -2
  24. package/template/node_modules/.vite/deps/_metadata.json +4 -4
  25. package/template/node_modules/.vite-mcp/deps/_metadata.json +22 -22
  26. package/template/node_modules/.vite-mcp/deps/mapbox-gl.js +15924 -14588
  27. package/template/node_modules/.vite-mcp/deps/mapbox-gl.js.map +1 -1
  28. package/template/node_modules/.vite-mcp/deps/vitest.js +8 -8
  29. package/template/node_modules/.vite-mcp/deps/vitest.js.map +1 -1
  30. package/template/package.json +9 -7
  31. package/template/playwright.config.ts +2 -40
  32. package/template/test-results/.last-run.json +4 -0
  33. package/template/tests/e2e/albums.spec.ts +114 -245
  34. package/template/tests/e2e/carousel.spec.ts +189 -313
  35. package/template/tests/e2e/map.spec.ts +177 -300
  36. package/template/tests/e2e/review.spec.ts +232 -423
  37. package/template/tests/e2e/visual.spec.ts +36 -0
  38. package/template/tests/e2e/visual.spec.ts-snapshots/albums-dark-chatgpt-linux.png +0 -0
  39. package/template/tests/e2e/visual.spec.ts-snapshots/albums-dark-claude-linux.png +0 -0
  40. package/template/tests/e2e/visual.spec.ts-snapshots/albums-fullscreen-chatgpt-linux.png +0 -0
  41. package/template/tests/e2e/visual.spec.ts-snapshots/albums-fullscreen-claude-linux.png +0 -0
  42. package/template/tests/e2e/visual.spec.ts-snapshots/albums-light-chatgpt-linux.png +0 -0
  43. package/template/tests/e2e/visual.spec.ts-snapshots/albums-light-claude-linux.png +0 -0
  44. package/template/tests/e2e/visual.spec.ts-snapshots/albums-page-light-chatgpt-linux.png +0 -0
  45. package/template/tests/e2e/visual.spec.ts-snapshots/albums-page-light-claude-linux.png +0 -0
  46. package/template/tests/live/albums.spec.ts +1 -1
  47. package/template/tests/live/carousel.spec.ts +1 -1
  48. package/template/tests/live/map.spec.ts +1 -1
  49. package/template/tests/live/playwright.config.ts +1 -1
  50. package/template/tests/live/review.spec.ts +1 -1
  51. package/template/vitest.config.ts +1 -1
  52. package/template/tests/e2e/global-setup.ts +0 -10
  53. package/template/tests/e2e/helpers.ts +0 -13
package/README.md CHANGED
@@ -16,7 +16,7 @@
16
16
  [![TypeScript](https://img.shields.io/badge/TypeScript-5.9-blue?style=flat&logo=typescript&label=ts&color=FFB800&logoColor=white&labelColor=000035)](https://www.typescriptlang.org/)
17
17
  [![React](https://img.shields.io/badge/React-19-blue?style=flat&logo=react&label=react&color=FFB800&logoColor=white&labelColor=000035)](https://reactjs.org/)
18
18
 
19
- Inspector, testing framework, and app framework for MCP Apps.
19
+ Inspector, testing framework, and runtime framework for MCP servers and MCP Apps.
20
20
 
21
21
  [Demo (Hosted)](https://sunpeak.ai/inspector) ~
22
22
  [Demo (Video)](https://cdn.sunpeak.ai/sunpeak-demo-prod.mp4) ~
@@ -28,7 +28,7 @@ Inspector, testing framework, and app framework for MCP Apps.
28
28
 
29
29
  ### 1. Inspector
30
30
 
31
- Test any MCP server in replicated ChatGPT and Claude runtimes — no sunpeak project required.
31
+ Manually test any MCP server in replicated ChatGPT and Claude runtimes.
32
32
 
33
33
  ```bash
34
34
  sunpeak inspect --server http://localhost:8000/mcp
@@ -45,15 +45,25 @@ sunpeak inspect --server http://localhost:8000/mcp
45
45
  - Multi-host inspector replicating ChatGPT and Claude runtimes
46
46
  - Toggle themes, display modes, device types from the sidebar or URL params
47
47
  - Call real tool handlers or use simulation fixtures for mock data
48
- - Built into `sunpeak dev` for framework users
49
48
 
50
49
  ### 2. Testing Framework
51
50
 
52
- E2E tests against simulated hosts and live tests against real production hosts.
51
+ Automatically test any MCP server against replicated ChatGPT and Claude runtimes.
53
52
 
54
- - **Simulations**: JSON fixtures defining reproducible tool states ([example below](#simulation))
55
- - **E2E tests**: Playwright + `createInspectorUrl` against the inspector ([example below](#inspector))
56
- - **Live tests**: Automated browser tests against real ChatGPT via `sunpeak/test`
53
+ ```ts
54
+ import { test, expect } from 'sunpeak/test';
55
+
56
+ test('review tool renders title', async ({ mcp }) => {
57
+ const result = await mcp.callTool('review-diff');
58
+ const app = result.app();
59
+ await expect(app.locator('h1:has-text("Refactor")')).toBeVisible();
60
+ });
61
+ ```
62
+
63
+ - **Works for any MCP server**: `sunpeak test init` scaffolds tests for Python, Go, TS, or any language
64
+ - **MCP-native assertions**: `toBeError()`, `toHaveTextContent()`, `toHaveStructuredContent()`
65
+ - **Multi-host**: Tests run against ChatGPT and Claude hosts automatically
66
+ - **Live tests**: Automated browser tests against real ChatGPT via `sunpeak/test/live`
57
67
 
58
68
  ### 3. App Framework
59
69
 
@@ -92,135 +102,28 @@ sunpeak new
92
102
 
93
103
  ## CLI
94
104
 
105
+ **Testing** (works with any MCP server):
106
+
95
107
  | Command | Description |
96
108
  | ------------------------------------- | ------------------------------------------- |
97
- | `sunpeak new [name] [resources]` | Create a new project |
98
- | `sunpeak dev` | Start dev server + inspector + MCP endpoint |
99
- | `sunpeak inspect --server <url\|cmd>` | Inspect any MCP server (standalone) |
100
- | `sunpeak build` | Build resources + tools for production |
101
- | `sunpeak start` | Start production MCP server |
102
- | `sunpeak upgrade` | Upgrade sunpeak to latest version |
103
-
104
- ## Example App
105
-
106
- Example `Resource`, `Simulation`, and testing file (using the `Inspector`) for an [MCP resource](https://sunpeak.ai/docs/mcp-apps/mcp/resources) called "Review".
107
-
108
- ### `Resource` Component
109
-
110
- Each resource `.tsx` file exports both the React component and the [MCP resource](https://sunpeak.ai/docs/mcp-apps/mcp/resources) metadata:
111
-
112
- ```tsx
113
- // src/resources/review/review.tsx
114
-
115
- import { useToolData } from 'sunpeak';
116
- import type { ResourceConfig } from 'sunpeak';
117
-
118
- export const resource: ResourceConfig = {
119
- description: 'Visualize and review a code change',
120
- _meta: { ui: { csp: { resourceDomains: ['https://cdn.example.com'] } } },
121
- };
122
-
123
- export function ReviewResource() {
124
- const { output: data } = useToolData<unknown, { title: string }>();
125
-
126
- return <h1>Review: {data?.title}</h1>;
127
- }
128
- ```
129
-
130
- ### Tool File
131
-
132
- Each tool `.ts` file exports metadata (with an optional resource link for UI tools), a Zod schema, and a handler:
133
-
134
- ```ts
135
- // src/tools/review-diff.ts
136
-
137
- import { z } from 'zod';
138
- import type { AppToolConfig, ToolHandlerExtra } from 'sunpeak/mcp';
139
-
140
- export const tool: AppToolConfig = {
141
- resource: 'review',
142
- title: 'Diff Review',
143
- description: 'Show a review dialog for a proposed code diff',
144
- annotations: { readOnlyHint: false },
145
- _meta: { ui: { visibility: ['model', 'app'] } },
146
- };
147
-
148
- export const schema = {
149
- changesetId: z.string().describe('Unique identifier for the changeset'),
150
- title: z.string().describe('Title describing the changes'),
151
- };
152
-
153
- type Args = z.infer<z.ZodObject<typeof schema>>;
154
-
155
- export default async function (args: Args, extra: ToolHandlerExtra) {
156
- return { structuredContent: { title: args.title, sections: [] } };
157
- }
158
- ```
159
-
160
- ### `Simulation`
161
-
162
- Simulation files provide fixture data for testing UIs. Each references a tool by filename and contains the mock input/output:
163
-
164
- ```jsonc
165
- // tests/simulations/review-diff.json
166
-
167
- {
168
- "tool": "review-diff", // References src/tools/review-diff.ts
169
- "userMessage": "Refactor the auth module to use JWT tokens.",
170
- "toolInput": {
171
- "changesetId": "cs_789",
172
- "title": "Refactor Authentication Module"
173
- },
174
- "toolResult": {
175
- "structuredContent": {
176
- "title": "Refactor Authentication Module",
177
- "sections": [...]
178
- }
179
- }
180
- }
181
- ```
182
-
183
- ### `Inspector`
184
-
185
- ```bash
186
- ├── tests/e2e/
187
- │ └── review.spec.ts # This! (not pictured above for simplicity)
188
- └── package.json
189
- ```
190
-
191
- The `Inspector` allows you to set **host state** (like host platform, light/dark mode) via URL params, which can be rendered alongside your `Simulation`s and tested via pre-configured Playwright end-to-end tests (`.spec.ts`).
192
-
193
- Using the `Inspector` and `Simulation`s, you can test all possible App states locally and automatically across hosts (ChatGPT, Claude)!
194
-
195
- ```ts
196
- // tests/e2e/review.spec.ts
197
-
198
- import { test, expect } from '@playwright/test';
199
- import { createInspectorUrl } from 'sunpeak/inspector';
200
-
201
- const hosts = ['chatgpt', 'claude'] as const;
202
-
203
- for (const host of hosts) {
204
- test.describe(`Review Resource [${host}]`, () => {
205
- test.describe('Light Mode', () => {
206
- test('should render review title with correct styles', async ({ page }) => {
207
- const params = { simulation: 'review-diff', theme: 'light', host }; // Set sim & host state.
208
- await page.goto(createInspectorUrl(params));
209
-
210
- // Resource content renders inside an iframe
211
- const iframe = page.frameLocator('iframe');
212
- const title = iframe.locator('h1:has-text("Refactor Authentication Module")');
213
- await expect(title).toBeVisible();
214
-
215
- const color = await title.evaluate((el) => window.getComputedStyle(el).color);
216
-
217
- // Light mode should render dark text.
218
- expect(color).toBe('rgb(13, 13, 13)');
219
- });
220
- });
221
- });
222
- }
223
- ```
109
+ | `sunpeak inspect --server <url\|cmd>` | Inspect any MCP server in the inspector |
110
+ | `sunpeak test` | Run unit + e2e tests |
111
+ | `sunpeak test --unit` | Run unit tests only (Vitest) |
112
+ | `sunpeak test --e2e` | Run e2e tests only (Playwright) |
113
+ | `sunpeak test --visual` | Run e2e tests with visual regression |
114
+ | `sunpeak test --visual --update` | Update visual regression baselines |
115
+ | `sunpeak test --live` | Run live tests against real hosts |
116
+ | `sunpeak test init` | Scaffold test infrastructure into a project |
117
+
118
+ **App framework** (for sunpeak projects):
119
+
120
+ | Command | Description |
121
+ | -------------------------------- | ------------------------------------------- |
122
+ | `sunpeak new [name] [resources]` | Create a new project |
123
+ | `sunpeak dev` | Start dev server + inspector + MCP endpoint |
124
+ | `sunpeak build` | Build resources + tools for production |
125
+ | `sunpeak start` | Start production MCP server |
126
+ | `sunpeak upgrade` | Upgrade sunpeak to latest version |
224
127
 
225
128
  ## Coding Agent Skill
226
129
 
@@ -176,7 +176,9 @@ export async function init(projectName, resourcesArg, deps = defaultDeps) {
176
176
  }
177
177
 
178
178
  // Skip framework-internal test files (dev overlay tests are for sunpeak development, not user projects)
179
- if ((src.includes('/tests/e2e/') || src.includes('/tests/live/')) && name.startsWith('dev-')) {
179
+ // Skip visual.spec.ts it references specific resources and serves as a template/example.
180
+ // Users should write their own visual tests for their selected resources.
181
+ if ((src.includes('/tests/e2e/') || src.includes('/tests/live/')) && (name.startsWith('dev-') || name === 'visual.spec.ts')) {
180
182
  return false;
181
183
  }
182
184
 
@@ -0,0 +1,305 @@
1
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
2
+ import { join } from 'path';
3
+ import * as p from '@clack/prompts';
4
+
5
+ /**
6
+ * sunpeak test init — Scaffold test infrastructure for MCP servers.
7
+ *
8
+ * Detects project type and scaffolds accordingly:
9
+ * - Non-JS projects: self-contained tests/sunpeak/ directory
10
+ * - JS/TS projects: root-level config + test files
11
+ * - sunpeak projects: migrate to defineConfig()
12
+ */
13
+ export async function testInit(args = []) {
14
+ p.intro('Setting up sunpeak tests');
15
+
16
+ // Parse --server flag from CLI args
17
+ const serverIdx = args.indexOf('--server');
18
+ const cliServer =
19
+ serverIdx !== -1 && args[serverIdx + 1]
20
+ ? args[serverIdx + 1]
21
+ : undefined;
22
+
23
+ const projectType = detectProjectType();
24
+
25
+ if (projectType === 'sunpeak') {
26
+ await initSunpeakProject();
27
+ } else if (projectType === 'js') {
28
+ await initJsProject(cliServer);
29
+ } else {
30
+ await initExternalProject(cliServer);
31
+ }
32
+
33
+ p.outro('Done!');
34
+ }
35
+
36
+ function detectProjectType() {
37
+ const cwd = process.cwd();
38
+ const pkgPath = join(cwd, 'package.json');
39
+
40
+ if (existsSync(pkgPath)) {
41
+ try {
42
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
43
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
44
+ if ('sunpeak' in deps) return 'sunpeak';
45
+ return 'js';
46
+ } catch {
47
+ return 'js';
48
+ }
49
+ }
50
+
51
+ // Non-JS project (Python, Go, Rust, etc.)
52
+ return 'external';
53
+ }
54
+
55
+ async function getServerConfig(cliServer) {
56
+ // If provided via --server flag, detect type automatically
57
+ if (cliServer) {
58
+ if (cliServer.startsWith('http://') || cliServer.startsWith('https://')) {
59
+ return { type: 'url', value: cliServer };
60
+ }
61
+ return { type: 'command', value: cliServer };
62
+ }
63
+
64
+ const serverType = await p.select({
65
+ message: 'How does your MCP server start?',
66
+ options: [
67
+ { value: 'command', label: 'Command (e.g., python server.py)' },
68
+ { value: 'url', label: 'HTTP URL (e.g., http://localhost:8000/mcp)' },
69
+ { value: 'later', label: 'Configure later' },
70
+ ],
71
+ });
72
+
73
+ if (p.isCancel(serverType)) process.exit(0);
74
+
75
+ if (serverType === 'command') {
76
+ const command = await p.text({
77
+ message: 'Server start command:',
78
+ placeholder: 'python src/server.py',
79
+ });
80
+ if (p.isCancel(command)) process.exit(0);
81
+ return { type: 'command', value: command };
82
+ }
83
+
84
+ if (serverType === 'url') {
85
+ const url = await p.text({
86
+ message: 'Server URL:',
87
+ placeholder: 'http://localhost:8000/mcp',
88
+ });
89
+ if (p.isCancel(url)) process.exit(0);
90
+ return { type: 'url', value: url };
91
+ }
92
+
93
+ return { type: 'later' };
94
+ }
95
+
96
+ function generateServerConfigBlock(server, relativeTo = '.') {
97
+ if (server.type === 'later') {
98
+ return ` // TODO: Configure your MCP server connection
99
+ // server: {
100
+ // command: 'python',
101
+ // args: ['server.py'],
102
+ // },`;
103
+ }
104
+ if (server.type === 'url') {
105
+ return ` server: {
106
+ url: '${server.value}',
107
+ },`;
108
+ }
109
+ // Parse command into command + args
110
+ const parts = server.value.split(/\s+/);
111
+ const cmd = parts[0];
112
+ const args = parts.slice(1);
113
+ // Make paths relative from test directory
114
+ const relativeArgs = args.map((a) =>
115
+ a.startsWith('/') || a.startsWith('./') || a.startsWith('../')
116
+ ? `'${relativeTo}/${a}'`
117
+ : `'${a}'`
118
+ );
119
+ return ` server: {
120
+ command: '${cmd}',
121
+ args: [${relativeArgs.join(', ')}],
122
+ },`;
123
+ }
124
+
125
+ async function initExternalProject(cliServer) {
126
+ p.log.info('Detected non-JS project. Creating self-contained test directory.');
127
+
128
+ const server = await getServerConfig(cliServer);
129
+ const testDir = join(process.cwd(), 'tests', 'sunpeak');
130
+
131
+ if (existsSync(testDir)) {
132
+ p.log.warn('tests/sunpeak/ already exists. Skipping scaffold.');
133
+ return;
134
+ }
135
+
136
+ mkdirSync(testDir, { recursive: true });
137
+
138
+ // package.json
139
+ writeFileSync(
140
+ join(testDir, 'package.json'),
141
+ JSON.stringify(
142
+ {
143
+ private: true,
144
+ type: 'module',
145
+ devDependencies: {
146
+ sunpeak: 'latest',
147
+ '@playwright/test': 'latest',
148
+ },
149
+ scripts: {
150
+ test: 'sunpeak test',
151
+ },
152
+ },
153
+ null,
154
+ 2
155
+ ) + '\n'
156
+ );
157
+
158
+ // sunpeak.config.ts (used as playwright config)
159
+ const serverBlock = generateServerConfigBlock(server, '../..');
160
+ writeFileSync(
161
+ join(testDir, 'playwright.config.ts'),
162
+ `import { defineConfig } from 'sunpeak/test/config';
163
+
164
+ export default defineConfig({
165
+ ${serverBlock}
166
+ });
167
+ `
168
+ );
169
+
170
+ // tsconfig.json
171
+ writeFileSync(
172
+ join(testDir, 'tsconfig.json'),
173
+ JSON.stringify(
174
+ {
175
+ compilerOptions: {
176
+ target: 'ES2022',
177
+ module: 'ESNext',
178
+ moduleResolution: 'bundler',
179
+ strict: true,
180
+ esModuleInterop: true,
181
+ },
182
+ },
183
+ null,
184
+ 2
185
+ ) + '\n'
186
+ );
187
+
188
+ // smoke test — runnable out of the box, verifies the server is reachable
189
+ writeFileSync(
190
+ join(testDir, 'smoke.test.ts'),
191
+ `import { test, expect } from 'sunpeak/test';
192
+
193
+ test('server is reachable and inspector loads', async ({ mcp }) => {
194
+ // Verify the inspector page loads successfully
195
+ await expect(mcp.page.locator('#root')).not.toBeEmpty();
196
+ });
197
+
198
+ // Uncomment and customize for your tools:
199
+ // test('my tool renders correctly', async ({ mcp }) => {
200
+ // const result = await mcp.callTool('your-tool', { key: 'value' });
201
+ // expect(result).not.toBeError();
202
+ //
203
+ // // If your tool has a UI:
204
+ // // const app = result.app();
205
+ // // await expect(app.getByText('Hello')).toBeVisible();
206
+ // });
207
+ `
208
+ );
209
+
210
+ p.log.success('Created tests/sunpeak/ with config and starter test.');
211
+ p.log.step('Next steps:');
212
+ p.log.message(' cd tests/sunpeak');
213
+ p.log.message(' npm install');
214
+ p.log.message(' npx playwright install chromium');
215
+ p.log.message(' npx sunpeak test');
216
+ }
217
+
218
+ async function initJsProject(cliServer) {
219
+ p.log.info('Detected JS/TS project. Adding test config at project root.');
220
+
221
+ const server = await getServerConfig(cliServer);
222
+ const cwd = process.cwd();
223
+
224
+ // Create playwright.config.ts
225
+ const configPath = join(cwd, 'playwright.config.ts');
226
+ if (existsSync(configPath)) {
227
+ p.log.warn('playwright.config.ts already exists. Skipping config creation.');
228
+ } else {
229
+ const serverBlock = generateServerConfigBlock(server);
230
+ writeFileSync(
231
+ configPath,
232
+ `import { defineConfig } from 'sunpeak/test/config';
233
+
234
+ export default defineConfig({
235
+ ${serverBlock}
236
+ });
237
+ `
238
+ );
239
+ p.log.success('Created playwright.config.ts');
240
+ }
241
+
242
+ // Create test directory and smoke test
243
+ const testDir = join(cwd, 'tests', 'e2e');
244
+ mkdirSync(testDir, { recursive: true });
245
+
246
+ const testPath = join(testDir, 'smoke.test.ts');
247
+ if (!existsSync(testPath)) {
248
+ writeFileSync(
249
+ testPath,
250
+ `import { test, expect } from 'sunpeak/test';
251
+
252
+ test('server is reachable and inspector loads', async ({ mcp }) => {
253
+ await expect(mcp.page.locator('#root')).not.toBeEmpty();
254
+ });
255
+
256
+ // Uncomment and customize for your tools:
257
+ // test('my tool renders correctly', async ({ mcp }) => {
258
+ // const result = await mcp.callTool('your-tool', { key: 'value' });
259
+ // expect(result).not.toBeError();
260
+ //
261
+ // // If your tool has a UI:
262
+ // // const app = result.app();
263
+ // // await expect(app.getByText('Hello')).toBeVisible();
264
+ // });
265
+ `
266
+ );
267
+ p.log.success('Created tests/e2e/smoke.test.ts');
268
+ }
269
+
270
+ p.log.step('Next steps:');
271
+ p.log.message(' npm install -D sunpeak @playwright/test');
272
+ p.log.message(' npx playwright install chromium');
273
+ p.log.message(' npx sunpeak test');
274
+ }
275
+
276
+ async function initSunpeakProject() {
277
+ p.log.info('Detected sunpeak project. Updating config to use defineConfig().');
278
+
279
+ const cwd = process.cwd();
280
+ const configPath = join(cwd, 'playwright.config.ts');
281
+
282
+ if (existsSync(configPath)) {
283
+ const content = readFileSync(configPath, 'utf-8');
284
+ if (content.includes('sunpeak/test/config')) {
285
+ p.log.info('Config already uses sunpeak/test/config. Nothing to do.');
286
+ return;
287
+ }
288
+ }
289
+
290
+ writeFileSync(
291
+ configPath,
292
+ `import { defineConfig } from 'sunpeak/test/config';
293
+
294
+ export default defineConfig();
295
+ `
296
+ );
297
+
298
+ p.log.success('Updated playwright.config.ts to use defineConfig()');
299
+ p.log.step('Migrate test files:');
300
+ p.log.message(' Replace: import { test, expect } from "@playwright/test"');
301
+ p.log.message(' With: import { test, expect } from "sunpeak/test"');
302
+ p.log.message('');
303
+ p.log.message(' Use the `mcp` fixture instead of raw page navigation.');
304
+ p.log.message(' See sunpeak docs for migration examples.');
305
+ }
@@ -0,0 +1,144 @@
1
+ import { spawn } from 'child_process';
2
+ import { existsSync } from 'fs';
3
+ import { join } from 'path';
4
+
5
+ /**
6
+ * sunpeak test — Run MCP server tests.
7
+ *
8
+ * No flags: Run unit + e2e tests
9
+ * sunpeak test init Scaffold test infrastructure
10
+ * sunpeak test --unit Run unit tests (vitest)
11
+ * sunpeak test --e2e Run e2e tests (Playwright)
12
+ * sunpeak test --live Run live tests against real hosts
13
+ * sunpeak test --visual Run e2e tests with visual regression comparison
14
+ * sunpeak test --visual --update Update visual regression baselines
15
+ * sunpeak test [pattern] Pass through to the relevant runner
16
+ *
17
+ * Flags are additive: --unit --e2e --live runs all three.
18
+ * --visual implies --e2e and enables screenshot comparison.
19
+ * --update implies --visual.
20
+ */
21
+ export async function runTest(args) {
22
+ // Handle `sunpeak test init` subcommand
23
+ if (args[0] === 'init') {
24
+ const { testInit } = await import('./test-init.mjs');
25
+ await testInit(args.slice(1));
26
+ return;
27
+ }
28
+
29
+ const isUnit = args.includes('--unit');
30
+ const isE2e = args.includes('--e2e');
31
+ const isLive = args.includes('--live');
32
+ let isVisual = args.includes('--visual');
33
+ const isUpdate = args.includes('--update');
34
+ const filteredArgs = args.filter(
35
+ (a) => !['--unit', '--e2e', '--live', '--visual', '--update'].includes(a)
36
+ );
37
+
38
+ // --update implies --visual (no point updating without enabling visual)
39
+ if (isUpdate) isVisual = true;
40
+
41
+ const hasAnyScope = isUnit || isE2e || isLive || isVisual;
42
+
43
+ // When extra args are present (file patterns, etc.) and no scope flags given,
44
+ // default to e2e only — passing Playwright file patterns to vitest would fail.
45
+ const hasExtraArgs = filteredArgs.length > 0;
46
+
47
+ // Determine which suites to run.
48
+ // No scope flags → unit + e2e (unless extra args narrow to e2e).
49
+ // --visual implies e2e.
50
+ const runUnit = hasAnyScope ? isUnit : !hasExtraArgs;
51
+ const runE2e = hasAnyScope ? (isE2e || isVisual) : true;
52
+ const runLive = isLive;
53
+
54
+ const results = [];
55
+
56
+ if (runUnit) {
57
+ const code = await runChild('pnpm', ['exec', 'vitest', 'run', ...filteredArgs]);
58
+ results.push({ suite: 'unit', code });
59
+ }
60
+
61
+ if (runE2e) {
62
+ const code = await runPlaywright(filteredArgs, {
63
+ configCandidates: [
64
+ 'playwright.config.ts',
65
+ 'playwright.config.js',
66
+ 'sunpeak.config.ts',
67
+ 'sunpeak.config.js',
68
+ ],
69
+ visual: isVisual,
70
+ updateSnapshots: isVisual && isUpdate,
71
+ });
72
+ results.push({ suite: 'e2e', code });
73
+ }
74
+
75
+ if (runLive) {
76
+ const code = await runPlaywright(filteredArgs, {
77
+ configCandidates: [
78
+ 'tests/live/playwright.config.ts',
79
+ 'tests/live/playwright.config.js',
80
+ ],
81
+ configRequired: true,
82
+ configErrorMessage: 'No live test config found at tests/live/playwright.config.ts',
83
+ });
84
+ results.push({ suite: 'live', code });
85
+ }
86
+
87
+ // Exit with the first non-zero code, or 0 if all passed
88
+ const failed = results.find((r) => r.code !== 0);
89
+ process.exit(failed ? failed.code : 0);
90
+ }
91
+
92
+ /**
93
+ * Spawn a child process and return its exit code.
94
+ */
95
+ function runChild(command, args, env) {
96
+ return new Promise((resolve) => {
97
+ const child = spawn(command, args, {
98
+ stdio: 'inherit',
99
+ env: { ...process.env, ...env },
100
+ });
101
+ child.on('error', () => resolve(1));
102
+ child.on('exit', (code) => resolve(code ?? 1));
103
+ });
104
+ }
105
+
106
+ /**
107
+ * Run Playwright and return the exit code.
108
+ */
109
+ function runPlaywright(args, options = {}) {
110
+ const {
111
+ configCandidates = [],
112
+ configRequired = false,
113
+ configErrorMessage,
114
+ visual = false,
115
+ updateSnapshots = false,
116
+ } = options;
117
+
118
+ const config = findConfig(configCandidates);
119
+
120
+ if (!config && configRequired) {
121
+ console.error(configErrorMessage);
122
+ return Promise.resolve(1);
123
+ }
124
+
125
+ const configArgs = config ? ['--config', config] : [];
126
+ const extraArgs = updateSnapshots ? ['--update-snapshots'] : [];
127
+
128
+ return runChild(
129
+ 'pnpm',
130
+ ['exec', 'playwright', 'test', ...configArgs, ...extraArgs, ...args],
131
+ {
132
+ SUNPEAK_DEV_OVERLAY: process.env.SUNPEAK_DEV_OVERLAY ?? 'false',
133
+ ...(visual ? { SUNPEAK_VISUAL: 'true' } : {}),
134
+ }
135
+ );
136
+ }
137
+
138
+ function findConfig(candidates) {
139
+ for (const candidate of candidates) {
140
+ const full = join(process.cwd(), candidate);
141
+ if (existsSync(full)) return candidate;
142
+ }
143
+ return null;
144
+ }
@@ -1,3 +1,5 @@
1
+ import type { VisualConfig } from '../test/test-config.d.mts';
2
+
1
3
  export interface InspectConfigOptions {
2
4
  /** MCP server URL or stdio command string (required) */
3
5
  server: string;
@@ -11,6 +13,8 @@ export interface InspectConfigOptions {
11
13
  name?: string;
12
14
  /** Additional Playwright `use` options */
13
15
  use?: Record<string, unknown>;
16
+ /** Visual regression testing configuration */
17
+ visual?: VisualConfig;
14
18
  }
15
19
 
16
20
  /**