Skip to content

Commit 5357a54

Browse files
committed
init tests
1 parent 4015465 commit 5357a54

File tree

46 files changed

+3238
-382
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+3238
-382
lines changed

exercises/01.simple/01.problem.raw-html/test/globalSetup.ts

Lines changed: 137 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,19 @@ declare module 'vitest' {
88
}
99
}
1010

11+
type OutputBuffer = Array<{ channel: 'stdout' | 'stderr'; data: Buffer }>
12+
1113
export default async function setup(project: TestProject) {
1214
const mcpServerPort = await getPort()
1315

1416
project.provide('mcpServerPort', mcpServerPort)
1517

18+
let appServerProcess: ReturnType<typeof execa> | null = null
1619
let mcpServerProcess: ReturnType<typeof execa> | null = null
1720

1821
// Buffers to store output for potential error display
19-
const mcpServerOutput: Array<string> = []
22+
const appServerOutput: OutputBuffer = []
23+
const mcpServerOutput: OutputBuffer = []
2024

2125
/**
2226
* Wait for a server to be ready by monitoring its output for a specific text pattern
@@ -30,7 +34,7 @@ export default async function setup(project: TestProject) {
3034
process: ReturnType<typeof execa> | null
3135
textMatch: string
3236
name: string
33-
outputBuffer: Array<string>
37+
outputBuffer: OutputBuffer
3438
}) {
3539
if (!childProcess) return
3640

@@ -40,9 +44,12 @@ export default async function setup(project: TestProject) {
4044
reject(new Error(`${name} failed to start within 10 seconds`))
4145
}, 10_000)
4246

43-
function searchForMatch(data: Buffer) {
47+
function searchForMatch(
48+
channel: OutputBuffer[number]['channel'],
49+
data: Buffer,
50+
) {
51+
outputBuffer.push({ channel, data })
4452
const str = data.toString()
45-
outputBuffer.push(str)
4653
if (str.includes(textMatch)) {
4754
clearTimeout(timeout)
4855
// Remove the listeners after finding the match
@@ -51,8 +58,9 @@ export default async function setup(project: TestProject) {
5158
resolve()
5259
}
5360
}
54-
childProcess?.stdout?.on('data', searchForMatch)
55-
childProcess?.stderr?.on('data', searchForMatch)
61+
62+
childProcess?.stdout?.on('data', searchForMatch.bind(null, 'stdout'))
63+
childProcess?.stderr?.on('data', searchForMatch.bind(null, 'stderr'))
5664

5765
childProcess?.on('error', (err) => {
5866
clearTimeout(timeout)
@@ -72,17 +80,54 @@ export default async function setup(project: TestProject) {
7280
* Display buffered output when there's a failure
7381
*/
7482
function displayBufferedOutput() {
83+
if (appServerOutput.length > 0) {
84+
console.log('=== App Server Output ===')
85+
for (const { channel, data } of appServerOutput) {
86+
process[channel].write(data)
87+
}
88+
}
7589
if (mcpServerOutput.length > 0) {
7690
console.log('=== MCP Server Output ===')
77-
for (const line of mcpServerOutput) {
78-
process.stdout.write(line)
91+
for (const { channel, data } of mcpServerOutput) {
92+
process[channel].write(data)
7993
}
8094
}
8195
}
8296

97+
async function startAppServerIfNecessary() {
98+
const isAppRunning = await fetch('http://localhost:7787/healthcheck').catch(
99+
() => ({ ok: false }),
100+
)
101+
if (isAppRunning.ok) {
102+
return
103+
}
104+
105+
const rootDir = process.cwd().replace(/exercises\/.*$/, '')
106+
107+
// Start the app server from the root directory
108+
console.log(`Starting app server on port 7787...`)
109+
const command = 'npm'
110+
// prettier-ignore
111+
const args = [
112+
'run', 'dev',
113+
'--prefix', './epicshop/epic-me',
114+
'--',
115+
'--clearScreen=false',
116+
'--strictPort',
117+
]
118+
119+
appServerProcess = execa(command, args, {
120+
cwd: rootDir,
121+
stdio: ['ignore', 'pipe', 'pipe'],
122+
})
123+
}
124+
83125
async function startServers() {
84126
console.log('Starting servers...')
85127

128+
// Start app server if necessary
129+
await startAppServerIfNecessary()
130+
86131
// Start the MCP server from the exercise directory
87132
console.log(`Starting MCP server on port ${mcpServerPort}...`)
88133
mcpServerProcess = execa(
@@ -99,13 +144,25 @@ export default async function setup(project: TestProject) {
99144
)
100145

101146
try {
102-
// Wait for MCP server to be ready
103-
await waitForServerReady({
104-
process: mcpServerProcess,
105-
textMatch: mcpServerPort.toString(),
106-
name: '[MCP-SERVER]',
107-
outputBuffer: mcpServerOutput,
108-
})
147+
// Wait for both servers to be ready simultaneously
148+
await Promise.all([
149+
appServerProcess
150+
? waitForServerReady({
151+
process: appServerProcess,
152+
textMatch: '7787',
153+
name: '[APP-SERVER]',
154+
outputBuffer: appServerOutput,
155+
}).then(() =>
156+
waitForResourceReady('http://localhost:7787/healthcheck'),
157+
)
158+
: Promise.resolve(),
159+
waitForServerReady({
160+
process: mcpServerProcess,
161+
textMatch: mcpServerPort.toString(),
162+
name: '[MCP-SERVER]',
163+
outputBuffer: mcpServerOutput,
164+
}),
165+
])
109166

110167
console.log('Servers started successfully')
111168
} catch (error) {
@@ -124,12 +181,12 @@ export default async function setup(project: TestProject) {
124181
cleanupPromises.push(
125182
(async () => {
126183
mcpServerProcess.kill('SIGTERM')
127-
// Give it 2 seconds to gracefully shutdown, then force kill
184+
// Give it time to gracefully shutdown, then force kill
128185
const timeout = setTimeout(() => {
129186
if (mcpServerProcess && !mcpServerProcess.killed) {
130187
mcpServerProcess.kill('SIGKILL')
131188
}
132-
}, 2000)
189+
}, 500)
133190

134191
try {
135192
await mcpServerProcess
@@ -142,6 +199,28 @@ export default async function setup(project: TestProject) {
142199
)
143200
}
144201

202+
if (appServerProcess && !appServerProcess.killed) {
203+
cleanupPromises.push(
204+
(async () => {
205+
appServerProcess.kill('SIGTERM')
206+
// Give time to gracefully shutdown, then force kill
207+
const timeout = setTimeout(() => {
208+
if (appServerProcess && !appServerProcess.killed) {
209+
appServerProcess.kill('SIGKILL')
210+
}
211+
}, 500)
212+
213+
try {
214+
await appServerProcess
215+
} catch {
216+
// Process was killed, which is expected
217+
} finally {
218+
clearTimeout(timeout)
219+
}
220+
})(),
221+
)
222+
}
223+
145224
// Wait for all cleanup to complete, but with an overall timeout
146225
try {
147226
await Promise.race([
@@ -159,6 +238,9 @@ export default async function setup(project: TestProject) {
159238
if (mcpServerProcess && !mcpServerProcess.killed) {
160239
mcpServerProcess.kill('SIGKILL')
161240
}
241+
if (appServerProcess && !appServerProcess.killed) {
242+
appServerProcess.kill('SIGKILL')
243+
}
162244
}
163245

164246
console.log('Servers cleaned up')
@@ -170,3 +252,41 @@ export default async function setup(project: TestProject) {
170252
// Return cleanup function
171253
return cleanup
172254
}
255+
256+
function waitForResourceReady(resourceUrl: string) {
257+
const timeoutSignal = AbortSignal.timeout(10_000)
258+
let lastError: Error | null = null
259+
return new Promise<void>((resolve, reject) => {
260+
timeoutSignal.addEventListener('abort', () => {
261+
const error = lastError ?? new Error('No other errors detected')
262+
error.message = `Timed out waiting for ${resourceUrl}:\n Last Error:${error.message}`
263+
reject(error)
264+
})
265+
async function checkResource() {
266+
try {
267+
const response = await fetch(resourceUrl)
268+
if (response.ok) return resolve()
269+
} catch (error) {
270+
lastError = error instanceof Error ? error : new Error(String(error))
271+
}
272+
await sleep(100, timeoutSignal)
273+
await checkResource()
274+
}
275+
return checkResource()
276+
})
277+
}
278+
279+
function sleep(ms: number, signal?: AbortSignal) {
280+
return new Promise<void>((resolve, reject) => {
281+
const timeout = setTimeout(() => {
282+
signal?.removeEventListener('abort', onAbort)
283+
resolve()
284+
}, ms)
285+
286+
function onAbort() {
287+
clearTimeout(timeout)
288+
reject(new Error('Sleep aborted'))
289+
}
290+
signal?.addEventListener('abort', onAbort)
291+
})
292+
}

exercises/01.simple/01.problem.raw-html/test/index.test.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
1+
import { Client } from '@modelcontextprotocol/sdk/client'
22
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
33
import { test, expect, inject } from 'vitest'
44

@@ -27,10 +27,15 @@ async function setupClient() {
2727
}
2828
}
2929

30-
test('listing tools works', async () => {
30+
test('get raw html', async () => {
3131
await using setup = await setupClient()
3232
const { client } = setup
3333

34-
const result = await client.listTools()
35-
expect(result.tools.length).toBeGreaterThan(0)
34+
const result = await client.callTool({
35+
name: 'view_tag',
36+
params: {
37+
id: 1,
38+
},
39+
})
40+
console.log(result)
3641
})
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { defineConfig } from 'vitest/config'
2+
3+
export default defineConfig({
4+
test: {
5+
globalSetup: './test/globalSetup.ts',
6+
},
7+
})

0 commit comments

Comments
 (0)