sunpeak 0.18.13 → 0.19.1
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 +34 -134
- package/bin/commands/test-init.mjs +305 -0
- package/bin/commands/test.mjs +83 -0
- package/bin/lib/inspect/inspect-config.mjs +16 -24
- package/bin/lib/test/base-config.mjs +60 -0
- package/bin/lib/test/matchers.mjs +99 -0
- package/bin/lib/test/test-config.d.mts +44 -0
- package/bin/lib/test/test-config.mjs +123 -0
- package/bin/lib/test/test-fixtures.d.mts +96 -0
- package/bin/lib/test/test-fixtures.mjs +189 -0
- package/bin/sunpeak.js +18 -5
- package/dist/mcp/index.cjs +58 -16
- package/dist/mcp/index.cjs.map +1 -1
- package/dist/mcp/index.js +58 -16
- package/dist/mcp/index.js.map +1 -1
- package/package.json +22 -10
- package/template/README.md +15 -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 +4 -4
- package/template/playwright.config.ts +2 -40
- 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/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,25 @@ 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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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 e2e tests against the inspector |
|
|
111
|
+
| `sunpeak test init` | Scaffold test infrastructure into a project |
|
|
112
|
+
| `sunpeak test --unit` | Run unit tests (Vitest) |
|
|
113
|
+
| `sunpeak test --live` | Run live tests against real hosts |
|
|
114
|
+
|
|
115
|
+
**App framework** (for sunpeak projects):
|
|
116
|
+
|
|
117
|
+
| Command | Description |
|
|
118
|
+
| -------------------------------- | ------------------------------------------- |
|
|
119
|
+
| `sunpeak new [name] [resources]` | Create a new project |
|
|
120
|
+
| `sunpeak dev` | Start dev server + inspector + MCP endpoint |
|
|
121
|
+
| `sunpeak build` | Build resources + tools for production |
|
|
122
|
+
| `sunpeak start` | Start production MCP server |
|
|
123
|
+
| `sunpeak upgrade` | Upgrade sunpeak to latest version |
|
|
224
124
|
|
|
225
125
|
## Coding Agent Skill
|
|
226
126
|
|
|
@@ -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,83 @@
|
|
|
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
|
+
* sunpeak test Run e2e tests (Playwright)
|
|
9
|
+
* sunpeak test init Scaffold test infrastructure
|
|
10
|
+
* sunpeak test --unit Run unit tests (vitest)
|
|
11
|
+
* sunpeak test --live Run live tests against real hosts
|
|
12
|
+
* sunpeak test [pattern] Pass through to Playwright
|
|
13
|
+
*/
|
|
14
|
+
export async function runTest(args) {
|
|
15
|
+
// Handle `sunpeak test init` subcommand
|
|
16
|
+
if (args[0] === 'init') {
|
|
17
|
+
const { testInit } = await import('./test-init.mjs');
|
|
18
|
+
await testInit(args.slice(1));
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const isUnit = args.includes('--unit');
|
|
23
|
+
const isLive = args.includes('--live');
|
|
24
|
+
const filteredArgs = args.filter((a) => a !== '--unit' && a !== '--live');
|
|
25
|
+
|
|
26
|
+
if (isUnit) {
|
|
27
|
+
// Run vitest
|
|
28
|
+
const child = spawn('pnpm', ['exec', 'vitest', 'run', ...filteredArgs], {
|
|
29
|
+
stdio: 'inherit',
|
|
30
|
+
env: { ...process.env },
|
|
31
|
+
});
|
|
32
|
+
child.on('exit', (code) => process.exit(code ?? 1));
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Find the appropriate Playwright config
|
|
37
|
+
let configArgs = [];
|
|
38
|
+
if (isLive) {
|
|
39
|
+
const liveConfig = findConfig([
|
|
40
|
+
'tests/live/playwright.config.ts',
|
|
41
|
+
'tests/live/playwright.config.js',
|
|
42
|
+
]);
|
|
43
|
+
if (liveConfig) {
|
|
44
|
+
configArgs = ['--config', liveConfig];
|
|
45
|
+
} else {
|
|
46
|
+
console.error('No live test config found at tests/live/playwright.config.ts');
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
} else {
|
|
50
|
+
// Default: e2e tests — look for config in standard locations
|
|
51
|
+
const e2eConfig = findConfig([
|
|
52
|
+
'playwright.config.ts',
|
|
53
|
+
'playwright.config.js',
|
|
54
|
+
'sunpeak.config.ts',
|
|
55
|
+
'sunpeak.config.js',
|
|
56
|
+
]);
|
|
57
|
+
if (e2eConfig) {
|
|
58
|
+
configArgs = ['--config', e2eConfig];
|
|
59
|
+
}
|
|
60
|
+
// If no config found, let Playwright use its defaults
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const child = spawn(
|
|
64
|
+
'pnpm',
|
|
65
|
+
['exec', 'playwright', 'test', ...configArgs, ...filteredArgs],
|
|
66
|
+
{
|
|
67
|
+
stdio: 'inherit',
|
|
68
|
+
env: {
|
|
69
|
+
...process.env,
|
|
70
|
+
SUNPEAK_DEV_OVERLAY: process.env.SUNPEAK_DEV_OVERLAY ?? 'false',
|
|
71
|
+
},
|
|
72
|
+
}
|
|
73
|
+
);
|
|
74
|
+
child.on('exit', (code) => process.exit(code ?? 1));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function findConfig(candidates) {
|
|
78
|
+
for (const candidate of candidates) {
|
|
79
|
+
const full = join(process.cwd(), candidate);
|
|
80
|
+
if (existsSync(full)) return candidate;
|
|
81
|
+
}
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
@@ -2,16 +2,19 @@
|
|
|
2
2
|
* Playwright config factory for inspect mode (BYOS — Bring Your Own Server).
|
|
3
3
|
*
|
|
4
4
|
* Generates a complete Playwright config that starts `sunpeak inspect` as the
|
|
5
|
-
* webServer and runs e2e tests against the inspector.
|
|
6
|
-
* as `defineLiveConfig` for live tests.
|
|
5
|
+
* webServer and runs e2e tests against the inspector.
|
|
7
6
|
*
|
|
8
7
|
* Usage in playwright.config.ts:
|
|
9
8
|
* import { defineInspectConfig } from 'sunpeak/test/inspect/config';
|
|
10
9
|
* export default defineInspectConfig({
|
|
11
10
|
* server: 'http://localhost:8000/mcp',
|
|
12
11
|
* });
|
|
12
|
+
*
|
|
13
|
+
* Note: For new projects, prefer `defineConfig` from 'sunpeak/test/config'
|
|
14
|
+
* which auto-detects the project type and handles both sunpeak projects
|
|
15
|
+
* and external servers.
|
|
13
16
|
*/
|
|
14
|
-
import {
|
|
17
|
+
import { createBaseConfig, resolvePorts } from '../test/base-config.mjs';
|
|
15
18
|
|
|
16
19
|
/**
|
|
17
20
|
* Create a complete Playwright config for testing an external MCP server.
|
|
@@ -19,7 +22,7 @@ import { getPortSync } from '../get-port.mjs';
|
|
|
19
22
|
* @param {Object} options
|
|
20
23
|
* @param {string} options.server - MCP server URL or stdio command (required)
|
|
21
24
|
* @param {string} [options.testDir='tests/e2e'] - Test directory
|
|
22
|
-
* @param {string} [options.simulationsDir
|
|
25
|
+
* @param {string} [options.simulationsDir] - Simulation JSON directory
|
|
23
26
|
* @param {string[]} [options.hosts=['chatgpt', 'claude']] - Host shells to test
|
|
24
27
|
* @param {string} [options.name] - App name in inspector chrome
|
|
25
28
|
* @param {Object} [options.use] - Additional Playwright `use` options
|
|
@@ -39,12 +42,12 @@ export function defineInspectConfig(options) {
|
|
|
39
42
|
throw new Error('defineInspectConfig: `server` option is required');
|
|
40
43
|
}
|
|
41
44
|
|
|
42
|
-
const port
|
|
43
|
-
const sandboxPort = Number(process.env.SUNPEAK_SANDBOX_PORT) || getPortSync(24680);
|
|
45
|
+
const { port, sandboxPort } = resolvePorts();
|
|
44
46
|
|
|
45
47
|
// Build the sunpeak inspect command
|
|
46
48
|
const serverArg = server.includes(' ') ? `"${server}"` : server;
|
|
47
49
|
const command = [
|
|
50
|
+
`SUNPEAK_SANDBOX_PORT=${sandboxPort}`,
|
|
48
51
|
'npx sunpeak inspect',
|
|
49
52
|
`--server ${serverArg}`,
|
|
50
53
|
...(simulationsDir ? [`--simulations ${simulationsDir}`] : []),
|
|
@@ -52,25 +55,14 @@ export function defineInspectConfig(options) {
|
|
|
52
55
|
...(name ? [`--name "${name}"`] : []),
|
|
53
56
|
].join(' ');
|
|
54
57
|
|
|
55
|
-
return {
|
|
58
|
+
return createBaseConfig({
|
|
59
|
+
hosts,
|
|
56
60
|
testDir,
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
retries: process.env.CI ? 2 : 1,
|
|
60
|
-
// Limit workers to avoid overwhelming the double-iframe sandbox proxy.
|
|
61
|
-
workers: process.env.CI ? 1 : 2,
|
|
62
|
-
reporter: 'list',
|
|
63
|
-
use: {
|
|
64
|
-
baseURL: `http://localhost:${port}`,
|
|
65
|
-
trace: 'on-first-retry',
|
|
66
|
-
...userUse,
|
|
67
|
-
},
|
|
68
|
-
projects: hosts.map((host) => ({ name: host })),
|
|
61
|
+
port,
|
|
62
|
+
use: userUse,
|
|
69
63
|
webServer: {
|
|
70
|
-
command
|
|
71
|
-
|
|
72
|
-
reuseExistingServer: !process.env.CI,
|
|
73
|
-
timeout: 60_000,
|
|
64
|
+
command,
|
|
65
|
+
healthUrl: `http://localhost:${port}/health`,
|
|
74
66
|
},
|
|
75
|
-
};
|
|
67
|
+
});
|
|
76
68
|
}
|