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.
- package/README.md +37 -134
- package/bin/commands/new.mjs +3 -1
- package/bin/commands/test-init.mjs +305 -0
- package/bin/commands/test.mjs +144 -0
- package/bin/lib/inspect/inspect-config.d.mts +4 -0
- package/bin/lib/inspect/inspect-config.mjs +18 -24
- package/bin/lib/test/base-config.mjs +75 -0
- package/bin/lib/test/matchers.mjs +99 -0
- package/bin/lib/test/test-config.d.mts +66 -0
- package/bin/lib/test/test-config.mjs +125 -0
- package/bin/lib/test/test-fixtures.d.mts +129 -0
- package/bin/lib/test/test-fixtures.mjs +232 -0
- package/bin/sunpeak.js +18 -5
- package/package.json +22 -10
- package/template/README.md +18 -8
- package/template/dist/albums/albums.json +1 -1
- package/template/dist/carousel/carousel.json +1 -1
- package/template/dist/map/map.html +468 -280
- package/template/dist/map/map.json +1 -1
- package/template/dist/review/review.json +1 -1
- package/template/node_modules/.bin/playwright +2 -2
- package/template/node_modules/.bin/vite +2 -2
- package/template/node_modules/.bin/vitest +2 -2
- package/template/node_modules/.vite/deps/_metadata.json +4 -4
- package/template/node_modules/.vite-mcp/deps/_metadata.json +22 -22
- package/template/node_modules/.vite-mcp/deps/mapbox-gl.js +15924 -14588
- package/template/node_modules/.vite-mcp/deps/mapbox-gl.js.map +1 -1
- package/template/node_modules/.vite-mcp/deps/vitest.js +8 -8
- package/template/node_modules/.vite-mcp/deps/vitest.js.map +1 -1
- package/template/package.json +9 -7
- package/template/playwright.config.ts +2 -40
- package/template/test-results/.last-run.json +4 -0
- package/template/tests/e2e/albums.spec.ts +114 -245
- package/template/tests/e2e/carousel.spec.ts +189 -313
- package/template/tests/e2e/map.spec.ts +177 -300
- package/template/tests/e2e/review.spec.ts +232 -423
- package/template/tests/e2e/visual.spec.ts +36 -0
- package/template/tests/e2e/visual.spec.ts-snapshots/albums-dark-chatgpt-linux.png +0 -0
- package/template/tests/e2e/visual.spec.ts-snapshots/albums-dark-claude-linux.png +0 -0
- package/template/tests/e2e/visual.spec.ts-snapshots/albums-fullscreen-chatgpt-linux.png +0 -0
- package/template/tests/e2e/visual.spec.ts-snapshots/albums-fullscreen-claude-linux.png +0 -0
- package/template/tests/e2e/visual.spec.ts-snapshots/albums-light-chatgpt-linux.png +0 -0
- package/template/tests/e2e/visual.spec.ts-snapshots/albums-light-claude-linux.png +0 -0
- package/template/tests/e2e/visual.spec.ts-snapshots/albums-page-light-chatgpt-linux.png +0 -0
- package/template/tests/e2e/visual.spec.ts-snapshots/albums-page-light-claude-linux.png +0 -0
- package/template/tests/live/albums.spec.ts +1 -1
- package/template/tests/live/carousel.spec.ts +1 -1
- package/template/tests/live/map.spec.ts +1 -1
- package/template/tests/live/playwright.config.ts +1 -1
- package/template/tests/live/review.spec.ts +1 -1
- package/template/vitest.config.ts +1 -1
- package/template/tests/e2e/global-setup.ts +0 -10
- package/template/tests/e2e/helpers.ts +0 -13
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
[](https://www.typescriptlang.org/)
|
|
17
17
|
[](https://reactjs.org/)
|
|
18
18
|
|
|
19
|
-
Inspector, testing framework, and
|
|
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
|
-
|
|
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
|
-
|
|
51
|
+
Automatically test any MCP server against replicated ChatGPT and Claude runtimes.
|
|
53
52
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
|
98
|
-
| `sunpeak
|
|
99
|
-
| `sunpeak
|
|
100
|
-
| `sunpeak
|
|
101
|
-
| `sunpeak
|
|
102
|
-
| `sunpeak
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
|
package/bin/commands/new.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
/**
|