@imgly/codesign-mcp 0.1.1 → 0.1.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/dist/cli/stdio.d.ts.map +1 -1
- package/dist/cli/stdio.js +54 -21
- package/dist/cli/stdio.js.map +1 -1
- package/dist/engine/cesdk-adapter.d.ts +7 -1
- package/dist/engine/cesdk-adapter.d.ts.map +1 -1
- package/dist/engine/cesdk-adapter.js +7 -1
- package/dist/engine/cesdk-adapter.js.map +1 -1
- package/dist/http/server.d.ts.map +1 -1
- package/dist/http/server.js +0 -1
- package/dist/http/server.js.map +1 -1
- package/dist/server.d.ts +0 -2
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js.map +1 -1
- package/dist/test/test/cli/redirect-console.test.d.ts +2 -0
- package/dist/test/test/cli/redirect-console.test.d.ts.map +1 -0
- package/dist/test/test/cli/redirect-console.test.js +47 -0
- package/dist/test/test/cli/redirect-console.test.js.map +1 -0
- package/dist/test/test/cli/resolve-license.test.d.ts +2 -0
- package/dist/test/test/cli/resolve-license.test.d.ts.map +1 -0
- package/dist/test/test/cli/resolve-license.test.js +20 -0
- package/dist/test/test/cli/resolve-license.test.js.map +1 -0
- package/dist/test/test/cli/stdio-license.test.d.ts +2 -0
- package/dist/test/test/cli/stdio-license.test.d.ts.map +1 -0
- package/dist/test/test/cli/stdio-license.test.js +19 -0
- package/dist/test/test/cli/stdio-license.test.js.map +1 -0
- package/dist/test/test/config.test.d.ts +2 -0
- package/dist/test/test/config.test.d.ts.map +1 -0
- package/dist/test/test/config.test.js +91 -0
- package/dist/test/test/config.test.js.map +1 -0
- package/dist/test/test/engine/asset-sources.test.d.ts +2 -0
- package/dist/test/test/engine/asset-sources.test.d.ts.map +1 -0
- package/dist/test/test/engine/asset-sources.test.js +100 -0
- package/dist/test/test/engine/asset-sources.test.js.map +1 -0
- package/dist/test/test/engine/mutex.test.d.ts +2 -0
- package/dist/test/test/engine/mutex.test.d.ts.map +1 -0
- package/dist/test/test/engine/mutex.test.js +63 -0
- package/dist/test/test/engine/mutex.test.js.map +1 -0
- package/dist/test/test/engine/session.test.js +15 -0
- package/dist/test/test/engine/session.test.js.map +1 -1
- package/dist/test/test/engine/unsplash-source.test.d.ts +2 -0
- package/dist/test/test/engine/unsplash-source.test.d.ts.map +1 -0
- package/dist/test/test/engine/unsplash-source.test.js +129 -0
- package/dist/test/test/engine/unsplash-source.test.js.map +1 -0
- package/dist/test/test/handbook-contract.test.js +8 -4
- package/dist/test/test/handbook-contract.test.js.map +1 -1
- package/dist/test/test/http/server.test.js +129 -0
- package/dist/test/test/http/server.test.js.map +1 -1
- package/dist/test/test/http/session-id-header.test.d.ts +2 -0
- package/dist/test/test/http/session-id-header.test.d.ts.map +1 -0
- package/dist/test/test/http/session-id-header.test.js +76 -0
- package/dist/test/test/http/session-id-header.test.js.map +1 -0
- package/dist/test/test/http/session-manager.test.js +14 -0
- package/dist/test/test/http/session-manager.test.js.map +1 -1
- package/dist/test/test/http/workspace-auth.test.d.ts +2 -0
- package/dist/test/test/http/workspace-auth.test.d.ts.map +1 -0
- package/dist/test/test/http/workspace-auth.test.js +41 -0
- package/dist/test/test/http/workspace-auth.test.js.map +1 -0
- package/dist/test/test/metrics/mount.test.d.ts +2 -0
- package/dist/test/test/metrics/mount.test.d.ts.map +1 -0
- package/dist/test/test/metrics/mount.test.js +59 -0
- package/dist/test/test/metrics/mount.test.js.map +1 -0
- package/dist/test/test/metrics/recorder.test.d.ts +2 -0
- package/dist/test/test/metrics/recorder.test.d.ts.map +1 -0
- package/dist/test/test/metrics/recorder.test.js +118 -0
- package/dist/test/test/metrics/recorder.test.js.map +1 -0
- package/dist/test/test/revisions/store.test.js +81 -11
- package/dist/test/test/revisions/store.test.js.map +1 -1
- package/dist/test/test/server.metrics.test.d.ts +2 -0
- package/dist/test/test/server.metrics.test.d.ts.map +1 -0
- package/dist/test/test/server.metrics.test.js +87 -0
- package/dist/test/test/server.metrics.test.js.map +1 -0
- package/dist/test/test/server.test.js +33 -4
- package/dist/test/test/server.test.js.map +1 -1
- package/dist/test/test/tools/asset-search.test.d.ts +2 -0
- package/dist/test/test/tools/asset-search.test.d.ts.map +1 -0
- package/dist/test/test/tools/asset-search.test.js +121 -0
- package/dist/test/test/tools/asset-search.test.js.map +1 -0
- package/dist/test/test/tools/edit.test.js +71 -9
- package/dist/test/test/tools/edit.test.js.map +1 -1
- package/dist/test/test/tools/export.test.js +60 -30
- package/dist/test/test/tools/export.test.js.map +1 -1
- package/dist/test/test/tools/index.test.d.ts +2 -0
- package/dist/test/test/tools/index.test.d.ts.map +1 -0
- package/dist/test/test/tools/index.test.js +55 -0
- package/dist/test/test/tools/index.test.js.map +1 -0
- package/dist/test/test/tools/metrics.test.d.ts +2 -0
- package/dist/test/test/tools/metrics.test.d.ts.map +1 -0
- package/dist/test/test/tools/metrics.test.js +29 -0
- package/dist/test/test/tools/metrics.test.js.map +1 -0
- package/dist/test/test/tools/preview.test.js +6 -4
- package/dist/test/test/tools/preview.test.js.map +1 -1
- package/dist/test/test/tools/rename.test.js +1 -2
- package/dist/test/test/tools/rename.test.js.map +1 -1
- package/dist/test/test/tools/save.test.d.ts +2 -0
- package/dist/test/test/tools/save.test.d.ts.map +1 -0
- package/dist/test/test/tools/save.test.js +137 -0
- package/dist/test/test/tools/save.test.js.map +1 -0
- package/dist/test/test/viewer/router.test.js +56 -0
- package/dist/test/test/viewer/router.test.js.map +1 -1
- package/dist/test/test/viewer/session-mount.test.d.ts +2 -0
- package/dist/test/test/viewer/session-mount.test.d.ts.map +1 -0
- package/dist/test/test/viewer/session-mount.test.js +52 -0
- package/dist/test/test/viewer/session-mount.test.js.map +1 -0
- package/dist/test/test/viewer/url-builder.test.js +76 -9
- package/dist/test/test/viewer/url-builder.test.js.map +1 -1
- package/dist/test/test/workspace/s3.test.d.ts +2 -0
- package/dist/test/test/workspace/s3.test.d.ts.map +1 -0
- package/dist/test/test/workspace/s3.test.js +153 -0
- package/dist/test/test/workspace/s3.test.js.map +1 -0
- package/dist/test/test/workspace/scoped.test.d.ts +2 -0
- package/dist/test/test/workspace/scoped.test.d.ts.map +1 -0
- package/dist/test/test/workspace/scoped.test.js +59 -0
- package/dist/test/test/workspace/scoped.test.js.map +1 -0
- package/dist/test/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +1 -1
|
@@ -3,6 +3,9 @@ import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js';
|
|
|
3
3
|
import { describe, expect, it } from 'vitest';
|
|
4
4
|
import { loadNodeAssets } from '../src/assets/load-node.js';
|
|
5
5
|
import { createCodesignMcpServer } from '../src/server.js';
|
|
6
|
+
// asset_generate is intentionally absent: it is hidden for the beta (see the
|
|
7
|
+
// ASSET_GENERATE_ENABLED kill-switch in src/tools/index.ts and the commented-out
|
|
8
|
+
// section in the handbook). Re-add it here when the feature ships.
|
|
6
9
|
const TOOL_NAMES_IN_HANDBOOK = [
|
|
7
10
|
'handbook',
|
|
8
11
|
'api',
|
|
@@ -10,12 +13,13 @@ const TOOL_NAMES_IN_HANDBOOK = [
|
|
|
10
13
|
'edit',
|
|
11
14
|
'preview',
|
|
12
15
|
'export',
|
|
16
|
+
'save',
|
|
13
17
|
'list',
|
|
14
18
|
'history',
|
|
15
19
|
'inspect',
|
|
16
20
|
'rename',
|
|
17
21
|
'open_editor',
|
|
18
|
-
'
|
|
22
|
+
'asset_search'
|
|
19
23
|
];
|
|
20
24
|
describe('handbook contract', () => {
|
|
21
25
|
it('the handbook prose references every tool name we ship', () => {
|
|
@@ -28,7 +32,6 @@ describe('handbook contract', () => {
|
|
|
28
32
|
});
|
|
29
33
|
it('every tool the handbook names is in tools/list', async () => {
|
|
30
34
|
const server = createCodesignMcpServer({
|
|
31
|
-
engine: {},
|
|
32
35
|
assets: loadNodeAssets(),
|
|
33
36
|
tools: [
|
|
34
37
|
// Stubs with the right names so tools/list returns them. The
|
|
@@ -38,12 +41,13 @@ describe('handbook contract', () => {
|
|
|
38
41
|
'edit',
|
|
39
42
|
'preview',
|
|
40
43
|
'export',
|
|
44
|
+
'save',
|
|
41
45
|
'list',
|
|
42
46
|
'history',
|
|
43
47
|
'inspect',
|
|
44
48
|
'rename',
|
|
45
49
|
'open_editor',
|
|
46
|
-
'
|
|
50
|
+
'asset_search'
|
|
47
51
|
].map((name) => ({
|
|
48
52
|
name,
|
|
49
53
|
description: `${name} tool stub for handbook-contract check`,
|
|
@@ -62,7 +66,7 @@ describe('handbook contract', () => {
|
|
|
62
66
|
expect(names, `tools/list missing "${expected}"`).toContain(expected);
|
|
63
67
|
}
|
|
64
68
|
});
|
|
65
|
-
it('documents the httpUrl field for export and
|
|
69
|
+
it('documents the httpUrl field for export and save', () => {
|
|
66
70
|
const assets = loadNodeAssets();
|
|
67
71
|
const handbookText = assets['docs/handbook/SKILL.md'];
|
|
68
72
|
expect(handbookText).toContain('httpUrl');
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"handbook-contract.test.js","sourceRoot":"","sources":["../../../test/handbook-contract.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AACnE,OAAO,EAAE,iBAAiB,EAAE,MAAM,uCAAuC,CAAC;AAC1E,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAC5D,OAAO,EAAE,uBAAuB,EAAE,MAAM,kBAAkB,CAAC;AAE3D,MAAM,sBAAsB,GAAG;IAC7B,UAAU;IACV,KAAK;IACL,OAAO;IACP,MAAM;IACN,SAAS;IACT,QAAQ;IACR,MAAM;IACN,SAAS;IACT,SAAS;IACT,QAAQ;IACR,aAAa;IACb,
|
|
1
|
+
{"version":3,"file":"handbook-contract.test.js","sourceRoot":"","sources":["../../../test/handbook-contract.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { Client } from '@modelcontextprotocol/sdk/client/index.js';\nimport { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js';\nimport { describe, expect, it } from 'vitest';\nimport { loadNodeAssets } from '../src/assets/load-node.js';\nimport { createCodesignMcpServer } from '../src/server.js';\n\n// asset_generate is intentionally absent: it is hidden for the beta (see the\n// ASSET_GENERATE_ENABLED kill-switch in src/tools/index.ts and the commented-out\n// section in the handbook). Re-add it here when the feature ships.\nconst TOOL_NAMES_IN_HANDBOOK = [\n 'handbook',\n 'api',\n 'guide',\n 'edit',\n 'preview',\n 'export',\n 'save',\n 'list',\n 'history',\n 'inspect',\n 'rename',\n 'open_editor',\n 'asset_search'\n];\n\ndescribe('handbook contract', () => {\n it('the handbook prose references every tool name we ship', () => {\n const assets = loadNodeAssets();\n const handbookText = assets['docs/handbook/SKILL.md'];\n expect(handbookText).toBeTruthy();\n for (const tool of TOOL_NAMES_IN_HANDBOOK) {\n expect(handbookText, `handbook is missing tool name \"${tool}\"`).toContain(\n tool\n );\n }\n });\n\n it('every tool the handbook names is in tools/list', async () => {\n const server = createCodesignMcpServer({\n assets: loadNodeAssets(),\n tools: [\n // Stubs with the right names so tools/list returns them. The\n // handbook contract checks NAMES, not behaviour — full behaviour\n // is covered in tools/*.test.ts and the evals.\n ...[\n 'edit',\n 'preview',\n 'export',\n 'save',\n 'list',\n 'history',\n 'inspect',\n 'rename',\n 'open_editor',\n 'asset_search'\n ].map((name) => ({\n name,\n description: `${name} tool stub for handbook-contract check`,\n inputSchema: { type: 'object' as const, properties: {} },\n call: async () => ({ content: [{ type: 'text' as const, text: '' }] })\n }))\n ]\n });\n const [c, s] = InMemoryTransport.createLinkedPair();\n await server.connect(s);\n const client = new Client({ name: 'test', version: '0' });\n await client.connect(c);\n const tools = await client.listTools();\n const names = tools.tools.map((t) => t.name);\n for (const expected of TOOL_NAMES_IN_HANDBOOK) {\n expect(names, `tools/list missing \"${expected}\"`).toContain(expected);\n }\n });\n\n it('documents the httpUrl field for export and save', () => {\n const assets = loadNodeAssets();\n const handbookText = assets['docs/handbook/SKILL.md'];\n expect(handbookText).toContain('httpUrl');\n });\n});\n"]}
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
2
2
|
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
|
3
3
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
4
|
+
import request from 'supertest';
|
|
4
5
|
import { createCodesignHttpServer } from '../../src/http/server.js';
|
|
6
|
+
import { WorkspaceAuthError } from '../../src/http/workspace-auth.js';
|
|
7
|
+
import { ScopedWorkspaceFS } from '../../src/workspace/scoped.js';
|
|
8
|
+
import { InMemoryWorkspaceFS } from '../../src/workspace/in-memory.js';
|
|
5
9
|
const FIXTURES = {
|
|
6
10
|
'docs/alpha/SKILL.md': '---\nname: alpha\ndescription: First.\n---\n# Alpha'
|
|
7
11
|
};
|
|
@@ -178,6 +182,35 @@ describe('createCodesignHttpServer', () => {
|
|
|
178
182
|
expect(json.error.code).toBe(-32001);
|
|
179
183
|
expect(json.error.message).toMatch(/unknown session/);
|
|
180
184
|
});
|
|
185
|
+
it('graceful DELETE /mcp terminates the session and frees its slot', async () => {
|
|
186
|
+
const { sessionId } = await connect();
|
|
187
|
+
expect(sessionId).toBeDefined();
|
|
188
|
+
expect(app.sessions.size).toBe(1);
|
|
189
|
+
const r = await fetch(baseUrl, {
|
|
190
|
+
method: 'DELETE',
|
|
191
|
+
headers: { 'mcp-session-id': sessionId }
|
|
192
|
+
});
|
|
193
|
+
expect(r.status).toBe(200);
|
|
194
|
+
// onsessionclosed → sessionMgr.delete: the slot (and its engine) is
|
|
195
|
+
// reclaimed immediately, not left squatting until idle eviction.
|
|
196
|
+
expect(app.sessions.size).toBe(0);
|
|
197
|
+
// The id is now unknown — a follow-up request 404s.
|
|
198
|
+
const followUp = await fetch(baseUrl, {
|
|
199
|
+
method: 'POST',
|
|
200
|
+
headers: {
|
|
201
|
+
'content-type': 'application/json',
|
|
202
|
+
accept: 'application/json, text/event-stream',
|
|
203
|
+
'mcp-session-id': sessionId
|
|
204
|
+
},
|
|
205
|
+
body: JSON.stringify({
|
|
206
|
+
jsonrpc: '2.0',
|
|
207
|
+
id: 1,
|
|
208
|
+
method: 'tools/list',
|
|
209
|
+
params: {}
|
|
210
|
+
})
|
|
211
|
+
});
|
|
212
|
+
expect(followUp.status).toBe(404);
|
|
213
|
+
});
|
|
181
214
|
it('auth middleware rejects requests with 401 when configured to', async () => {
|
|
182
215
|
await app.shutdown();
|
|
183
216
|
await new Promise((resolve) => server.close(() => resolve()));
|
|
@@ -241,5 +274,101 @@ describe('createCodesignHttpServer', () => {
|
|
|
241
274
|
await a.client.close();
|
|
242
275
|
await b.client.close();
|
|
243
276
|
});
|
|
277
|
+
it('passes the resolved workspaceId to createSession on initialize', async () => {
|
|
278
|
+
const seen = [];
|
|
279
|
+
const testApp = await createCodesignHttpServer({
|
|
280
|
+
assets: {},
|
|
281
|
+
maxSessions: 4,
|
|
282
|
+
idleTimeoutMs: 60_000,
|
|
283
|
+
resolveWorkspaceId: (req) => req.header('x-workspace-id') ?? 'fallback',
|
|
284
|
+
createSession: async ({ workspaceId }) => {
|
|
285
|
+
seen.push(workspaceId);
|
|
286
|
+
return { engine: {}, tools: [] };
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
await request(testApp)
|
|
290
|
+
.post('/mcp')
|
|
291
|
+
.set('x-workspace-id', 'u/acme')
|
|
292
|
+
.set('content-type', 'application/json')
|
|
293
|
+
.send({ jsonrpc: '2.0', id: 1, method: 'initialize', params: {} });
|
|
294
|
+
expect(seen).toEqual(['u/acme']);
|
|
295
|
+
await testApp.shutdown();
|
|
296
|
+
});
|
|
297
|
+
it('returns 401 when resolveWorkspaceId throws WorkspaceAuthError', async () => {
|
|
298
|
+
const testApp = await createCodesignHttpServer({
|
|
299
|
+
assets: {},
|
|
300
|
+
maxSessions: 4,
|
|
301
|
+
idleTimeoutMs: 60_000,
|
|
302
|
+
resolveWorkspaceId: () => {
|
|
303
|
+
throw new WorkspaceAuthError('missing header');
|
|
304
|
+
},
|
|
305
|
+
createSession: async () => ({ engine: {}, tools: [] })
|
|
306
|
+
});
|
|
307
|
+
const res = await request(testApp)
|
|
308
|
+
.post('/mcp')
|
|
309
|
+
.set('content-type', 'application/json')
|
|
310
|
+
.send({ jsonrpc: '2.0', id: 1, method: 'initialize', params: {} });
|
|
311
|
+
expect(res.status).toBe(401);
|
|
312
|
+
expect(res.body.error.code).toBe(-32002);
|
|
313
|
+
expect(res.body.error.message).toBe('missing header');
|
|
314
|
+
await testApp.shutdown();
|
|
315
|
+
});
|
|
316
|
+
it('returns 400 (not a hang) when the resolved workspaceId is malformed', async () => {
|
|
317
|
+
const testApp = await createCodesignHttpServer({
|
|
318
|
+
assets: {},
|
|
319
|
+
maxSessions: 4,
|
|
320
|
+
idleTimeoutMs: 60_000,
|
|
321
|
+
// header mode-style: return an illegal id that ScopedWorkspaceFS will reject
|
|
322
|
+
resolveWorkspaceId: () => '../escape',
|
|
323
|
+
createSession: async ({ workspaceId }) => {
|
|
324
|
+
// mirror cli/http.ts: constructing the scoped fs validates the id
|
|
325
|
+
new ScopedWorkspaceFS(new InMemoryWorkspaceFS(), workspaceId);
|
|
326
|
+
return { engine: {}, tools: [] };
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
const res = await request(testApp)
|
|
330
|
+
.post('/mcp')
|
|
331
|
+
.set('content-type', 'application/json')
|
|
332
|
+
.send({ jsonrpc: '2.0', id: 1, method: 'initialize', params: {} });
|
|
333
|
+
expect(res.status).toBe(400);
|
|
334
|
+
expect(res.body.error.code).toBe(-32602); // invalid params
|
|
335
|
+
await testApp.shutdown();
|
|
336
|
+
});
|
|
337
|
+
it('forwards the metrics recorder so tool calls are timed', async () => {
|
|
338
|
+
const { MetricsRecorder } = await import('../../src/metrics/recorder.js');
|
|
339
|
+
const recorder = new MetricsRecorder();
|
|
340
|
+
const a2 = await createCodesignHttpServer({
|
|
341
|
+
assets: {},
|
|
342
|
+
maxSessions: 4,
|
|
343
|
+
idleTimeoutMs: 60_000,
|
|
344
|
+
metrics: recorder,
|
|
345
|
+
createSession: async () => ({
|
|
346
|
+
engine: {},
|
|
347
|
+
tools: [
|
|
348
|
+
{
|
|
349
|
+
name: 'ping',
|
|
350
|
+
description: 'ping',
|
|
351
|
+
inputSchema: { type: 'object', properties: {} },
|
|
352
|
+
call: async () => ({
|
|
353
|
+
content: [{ type: 'text', text: 'pong' }]
|
|
354
|
+
})
|
|
355
|
+
}
|
|
356
|
+
]
|
|
357
|
+
})
|
|
358
|
+
});
|
|
359
|
+
const srv = await new Promise((resolve) => {
|
|
360
|
+
const s = a2.listen(0, '127.0.0.1', () => resolve(s));
|
|
361
|
+
});
|
|
362
|
+
const addr = srv.address();
|
|
363
|
+
const url = `http://127.0.0.1:${addr.port}/mcp`;
|
|
364
|
+
const transport = new StreamableHTTPClientTransport(new URL(url));
|
|
365
|
+
const client = new Client({ name: 'test', version: '0' });
|
|
366
|
+
await client.connect(transport);
|
|
367
|
+
await client.callTool({ name: 'ping', arguments: {} });
|
|
368
|
+
await client.close();
|
|
369
|
+
await new Promise((resolve) => srv.close(() => resolve()));
|
|
370
|
+
await a2.shutdown();
|
|
371
|
+
expect(recorder.snapshot().tools.find((t) => t.tool === 'ping')?.count).toBe(1);
|
|
372
|
+
});
|
|
244
373
|
});
|
|
245
374
|
//# sourceMappingURL=server.test.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.test.js","sourceRoot":"","sources":["../../../../test/http/server.test.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AddressInfo } from 'node:net';\nimport { Client } from '@modelcontextprotocol/sdk/client/index.js';\nimport { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';\nimport { afterEach, beforeEach, describe, expect, it } from 'vitest';\nimport { createCodesignHttpServer } from '../../src/http/server.js';\n\nconst FIXTURES = {\n 'docs/alpha/SKILL.md': '---\\nname: alpha\\ndescription: First.\\n---\\n# Alpha'\n};\n\ninterface FakeEngine {\n id: number;\n scriptsRun: string[];\n}\n\nlet nextEngineId = 0;\n\nfunction makeServer() {\n return createCodesignHttpServer({\n assets: FIXTURES,\n maxSessions: 5,\n idleTimeoutMs: 60_000,\n // Replace the real CE.SDK headless bootstrap with a deterministic fake so\n // the integration test runs in milliseconds, not seconds. The fake engine\n // owns its own scriptsRun array, asserting per-session isolation.\n createSession: async () => {\n const engine: FakeEngine = { id: nextEngineId++, scriptsRun: [] };\n const tools = [\n {\n name: 'echo',\n description: 'Echo arg back',\n inputSchema: {\n type: 'object',\n properties: { msg: { type: 'string' } },\n required: ['msg']\n },\n call: async (args: unknown) => {\n const msg = (args as { msg: string }).msg;\n return { content: [{ type: 'text' as const, text: msg }] };\n }\n },\n {\n // Records the input on this session's engine and reads back the\n // list of all scripts seen so far. Used by the isolation test to\n // prove two sessions don't share engine state.\n name: 'engine_record',\n description:\n 'Record a string into the session engine, return all recorded so far',\n inputSchema: {\n type: 'object',\n properties: { msg: { type: 'string' } },\n required: ['msg']\n },\n call: async (args: unknown) => {\n const msg = (args as { msg: string }).msg;\n engine.scriptsRun.push(msg);\n return {\n content: [\n { type: 'text' as const, text: engine.scriptsRun.join(',') }\n ]\n };\n }\n }\n ];\n return { engine, tools };\n }\n });\n}\n\ndescribe('createCodesignHttpServer', () => {\n let app: Awaited<ReturnType<typeof createCodesignHttpServer>>;\n let baseUrl: string;\n let server: ReturnType<typeof app.listen>;\n\n beforeEach(async () => {\n nextEngineId = 0;\n app = await makeServer();\n await new Promise<void>((resolve) => {\n server = app.listen(0, '127.0.0.1', resolve);\n });\n const addr = server.address() as AddressInfo;\n baseUrl = `http://127.0.0.1:${addr.port}/mcp`;\n });\n\n afterEach(async () => {\n await new Promise<void>((resolve) => server.close(() => resolve()));\n await app.shutdown();\n });\n\n async function connect(): Promise<{\n client: Client;\n sessionId: string | undefined;\n }> {\n const transport = new StreamableHTTPClientTransport(new URL(baseUrl));\n const client = new Client({ name: 'test', version: '0' });\n await client.connect(transport);\n return { client, sessionId: transport.sessionId };\n }\n\n it('initialize creates a new session and returns its id', async () => {\n const { client, sessionId } = await connect();\n expect(sessionId).toMatch(/[0-9a-f-]{36}/i); // uuid shape\n expect(app.sessions.size).toBe(1);\n await client.close();\n });\n\n it(\"listResources returns the asset bundle's skill files for that session\", async () => {\n const { client } = await connect();\n const list = await client.listResources();\n expect(list.resources.map((r) => r.uri)).toEqual([\n 'codesign://docs/alpha/SKILL.md'\n ]);\n await client.close();\n });\n\n it(\"callTool routes to the session's own engine\", async () => {\n const { client } = await connect();\n const result = await client.callTool({\n name: 'echo',\n arguments: { msg: 'hi' }\n });\n expect(result.content).toEqual([{ type: 'text', text: 'hi' }]);\n await client.close();\n });\n\n it('two parallel clients get distinct sessions and engines', async () => {\n const a = await connect();\n const b = await connect();\n expect(a.sessionId).toBeDefined();\n expect(b.sessionId).toBeDefined();\n expect(a.sessionId).not.toBe(b.sessionId);\n expect(app.sessions.size).toBe(2);\n await a.client.close();\n await b.client.close();\n });\n\n it('session cap: returns 503 when maxSessions is reached', async () => {\n await app.shutdown(); // dismiss the default 5-session app\n app = await createCodesignHttpServer({\n assets: FIXTURES,\n maxSessions: 2,\n idleTimeoutMs: 60_000,\n createSession: async () => ({\n engine: { id: nextEngineId++, scriptsRun: [] },\n tools: []\n })\n });\n await new Promise<void>((resolve) => server.close(() => resolve()));\n await new Promise<void>((resolve) => {\n server = app.listen(0, '127.0.0.1', resolve);\n });\n const addr = server.address() as AddressInfo;\n baseUrl = `http://127.0.0.1:${addr.port}/mcp`;\n\n const a = await connect();\n const b = await connect();\n // 3rd init request should be rejected at the HTTP layer with 503.\n const initBody = {\n jsonrpc: '2.0',\n id: 1,\n method: 'initialize',\n params: {\n protocolVersion: '2025-03-26',\n capabilities: {},\n clientInfo: { name: 't', version: '0' }\n }\n };\n const r = await fetch(baseUrl, {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n accept: 'application/json, text/event-stream'\n },\n body: JSON.stringify(initBody)\n });\n expect(r.status).toBe(503);\n expect(r.headers.get('retry-after')).toBe('30');\n const json = (await r.json()) as {\n error: { code: number; message: string };\n };\n expect(json.error.code).toBe(-32000);\n expect(json.error.message).toMatch(/session cap/i);\n\n await a.client.close();\n await b.client.close();\n });\n\n it('unknown session id: returns 404 with -32001', async () => {\n const r = await fetch(baseUrl, {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n accept: 'application/json, text/event-stream',\n 'mcp-session-id': '00000000-0000-0000-0000-000000000000'\n },\n body: JSON.stringify({\n jsonrpc: '2.0',\n id: 1,\n method: 'tools/list',\n params: {}\n })\n });\n expect(r.status).toBe(404);\n const json = (await r.json()) as {\n error: { code: number; message: string };\n };\n expect(json.error.code).toBe(-32001);\n expect(json.error.message).toMatch(/unknown session/);\n });\n\n it('auth middleware rejects requests with 401 when configured to', async () => {\n await app.shutdown();\n await new Promise<void>((resolve) => server.close(() => resolve()));\n app = await createCodesignHttpServer({\n assets: FIXTURES,\n maxSessions: 5,\n idleTimeoutMs: 60_000,\n createSession: async () => ({\n engine: { id: nextEngineId++, scriptsRun: [] },\n tools: []\n }),\n auth: (req, res, next) => {\n if (req.header('authorization') !== 'Bearer testtoken') {\n res.status(401).json({ error: 'unauthorized' });\n return;\n }\n next();\n }\n });\n await new Promise<void>((resolve) => {\n server = app.listen(0, '127.0.0.1', resolve);\n });\n const addr = server.address() as AddressInfo;\n baseUrl = `http://127.0.0.1:${addr.port}/mcp`;\n\n const r = await fetch(baseUrl, {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n accept: 'application/json, text/event-stream'\n },\n body: JSON.stringify({\n jsonrpc: '2.0',\n id: 1,\n method: 'initialize',\n params: {}\n })\n });\n expect(r.status).toBe(401);\n });\n\n it('engine state is isolated per session: mutation in one is invisible to the other', async () => {\n const a = await connect();\n const b = await connect();\n\n // Record on a — only a should see it.\n const aResult = await a.client.callTool({\n name: 'engine_record',\n arguments: { msg: 'from-a' }\n });\n expect(aResult.content).toEqual([{ type: 'text', text: 'from-a' }]);\n\n // Record on b — should NOT include from-a.\n const bResult = await b.client.callTool({\n name: 'engine_record',\n arguments: { msg: 'from-b' }\n });\n expect(bResult.content).toEqual([{ type: 'text', text: 'from-b' }]);\n\n // Re-record on a — should append to a's history, still no b.\n const aResult2 = await a.client.callTool({\n name: 'engine_record',\n arguments: { msg: 'again' }\n });\n expect(aResult2.content).toEqual([{ type: 'text', text: 'from-a,again' }]);\n\n await a.client.close();\n await b.client.close();\n });\n});\n"]}
|
|
1
|
+
{"version":3,"file":"server.test.js","sourceRoot":"","sources":["../../../../test/http/server.test.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AddressInfo } from 'node:net';\nimport { Client } from '@modelcontextprotocol/sdk/client/index.js';\nimport { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';\nimport { afterEach, beforeEach, describe, expect, it } from 'vitest';\nimport request from 'supertest';\nimport { createCodesignHttpServer } from '../../src/http/server.js';\nimport { WorkspaceAuthError } from '../../src/http/workspace-auth.js';\nimport { ScopedWorkspaceFS } from '../../src/workspace/scoped.js';\nimport { InMemoryWorkspaceFS } from '../../src/workspace/in-memory.js';\n\nconst FIXTURES = {\n 'docs/alpha/SKILL.md': '---\\nname: alpha\\ndescription: First.\\n---\\n# Alpha'\n};\n\ninterface FakeEngine {\n id: number;\n scriptsRun: string[];\n}\n\nlet nextEngineId = 0;\n\nfunction makeServer() {\n return createCodesignHttpServer({\n assets: FIXTURES,\n maxSessions: 5,\n idleTimeoutMs: 60_000,\n // Replace the real CE.SDK headless bootstrap with a deterministic fake so\n // the integration test runs in milliseconds, not seconds. The fake engine\n // owns its own scriptsRun array, asserting per-session isolation.\n createSession: async () => {\n const engine: FakeEngine = { id: nextEngineId++, scriptsRun: [] };\n const tools = [\n {\n name: 'echo',\n description: 'Echo arg back',\n inputSchema: {\n type: 'object',\n properties: { msg: { type: 'string' } },\n required: ['msg']\n },\n call: async (args: unknown) => {\n const msg = (args as { msg: string }).msg;\n return { content: [{ type: 'text' as const, text: msg }] };\n }\n },\n {\n // Records the input on this session's engine and reads back the\n // list of all scripts seen so far. Used by the isolation test to\n // prove two sessions don't share engine state.\n name: 'engine_record',\n description:\n 'Record a string into the session engine, return all recorded so far',\n inputSchema: {\n type: 'object',\n properties: { msg: { type: 'string' } },\n required: ['msg']\n },\n call: async (args: unknown) => {\n const msg = (args as { msg: string }).msg;\n engine.scriptsRun.push(msg);\n return {\n content: [\n { type: 'text' as const, text: engine.scriptsRun.join(',') }\n ]\n };\n }\n }\n ];\n return { engine, tools };\n }\n });\n}\n\ndescribe('createCodesignHttpServer', () => {\n let app: Awaited<ReturnType<typeof createCodesignHttpServer>>;\n let baseUrl: string;\n let server: ReturnType<typeof app.listen>;\n\n beforeEach(async () => {\n nextEngineId = 0;\n app = await makeServer();\n await new Promise<void>((resolve) => {\n server = app.listen(0, '127.0.0.1', resolve);\n });\n const addr = server.address() as AddressInfo;\n baseUrl = `http://127.0.0.1:${addr.port}/mcp`;\n });\n\n afterEach(async () => {\n await new Promise<void>((resolve) => server.close(() => resolve()));\n await app.shutdown();\n });\n\n async function connect(): Promise<{\n client: Client;\n sessionId: string | undefined;\n }> {\n const transport = new StreamableHTTPClientTransport(new URL(baseUrl));\n const client = new Client({ name: 'test', version: '0' });\n await client.connect(transport);\n return { client, sessionId: transport.sessionId };\n }\n\n it('initialize creates a new session and returns its id', async () => {\n const { client, sessionId } = await connect();\n expect(sessionId).toMatch(/[0-9a-f-]{36}/i); // uuid shape\n expect(app.sessions.size).toBe(1);\n await client.close();\n });\n\n it(\"listResources returns the asset bundle's skill files for that session\", async () => {\n const { client } = await connect();\n const list = await client.listResources();\n expect(list.resources.map((r) => r.uri)).toEqual([\n 'codesign://docs/alpha/SKILL.md'\n ]);\n await client.close();\n });\n\n it(\"callTool routes to the session's own engine\", async () => {\n const { client } = await connect();\n const result = await client.callTool({\n name: 'echo',\n arguments: { msg: 'hi' }\n });\n expect(result.content).toEqual([{ type: 'text', text: 'hi' }]);\n await client.close();\n });\n\n it('two parallel clients get distinct sessions and engines', async () => {\n const a = await connect();\n const b = await connect();\n expect(a.sessionId).toBeDefined();\n expect(b.sessionId).toBeDefined();\n expect(a.sessionId).not.toBe(b.sessionId);\n expect(app.sessions.size).toBe(2);\n await a.client.close();\n await b.client.close();\n });\n\n it('session cap: returns 503 when maxSessions is reached', async () => {\n await app.shutdown(); // dismiss the default 5-session app\n app = await createCodesignHttpServer({\n assets: FIXTURES,\n maxSessions: 2,\n idleTimeoutMs: 60_000,\n createSession: async () => ({\n engine: { id: nextEngineId++, scriptsRun: [] },\n tools: []\n })\n });\n await new Promise<void>((resolve) => server.close(() => resolve()));\n await new Promise<void>((resolve) => {\n server = app.listen(0, '127.0.0.1', resolve);\n });\n const addr = server.address() as AddressInfo;\n baseUrl = `http://127.0.0.1:${addr.port}/mcp`;\n\n const a = await connect();\n const b = await connect();\n // 3rd init request should be rejected at the HTTP layer with 503.\n const initBody = {\n jsonrpc: '2.0',\n id: 1,\n method: 'initialize',\n params: {\n protocolVersion: '2025-03-26',\n capabilities: {},\n clientInfo: { name: 't', version: '0' }\n }\n };\n const r = await fetch(baseUrl, {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n accept: 'application/json, text/event-stream'\n },\n body: JSON.stringify(initBody)\n });\n expect(r.status).toBe(503);\n expect(r.headers.get('retry-after')).toBe('30');\n const json = (await r.json()) as {\n error: { code: number; message: string };\n };\n expect(json.error.code).toBe(-32000);\n expect(json.error.message).toMatch(/session cap/i);\n\n await a.client.close();\n await b.client.close();\n });\n\n it('unknown session id: returns 404 with -32001', async () => {\n const r = await fetch(baseUrl, {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n accept: 'application/json, text/event-stream',\n 'mcp-session-id': '00000000-0000-0000-0000-000000000000'\n },\n body: JSON.stringify({\n jsonrpc: '2.0',\n id: 1,\n method: 'tools/list',\n params: {}\n })\n });\n expect(r.status).toBe(404);\n const json = (await r.json()) as {\n error: { code: number; message: string };\n };\n expect(json.error.code).toBe(-32001);\n expect(json.error.message).toMatch(/unknown session/);\n });\n\n it('graceful DELETE /mcp terminates the session and frees its slot', async () => {\n const { sessionId } = await connect();\n expect(sessionId).toBeDefined();\n expect(app.sessions.size).toBe(1);\n\n const r = await fetch(baseUrl, {\n method: 'DELETE',\n headers: { 'mcp-session-id': sessionId as string }\n });\n expect(r.status).toBe(200);\n // onsessionclosed → sessionMgr.delete: the slot (and its engine) is\n // reclaimed immediately, not left squatting until idle eviction.\n expect(app.sessions.size).toBe(0);\n\n // The id is now unknown — a follow-up request 404s.\n const followUp = await fetch(baseUrl, {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n accept: 'application/json, text/event-stream',\n 'mcp-session-id': sessionId as string\n },\n body: JSON.stringify({\n jsonrpc: '2.0',\n id: 1,\n method: 'tools/list',\n params: {}\n })\n });\n expect(followUp.status).toBe(404);\n });\n\n it('auth middleware rejects requests with 401 when configured to', async () => {\n await app.shutdown();\n await new Promise<void>((resolve) => server.close(() => resolve()));\n app = await createCodesignHttpServer({\n assets: FIXTURES,\n maxSessions: 5,\n idleTimeoutMs: 60_000,\n createSession: async () => ({\n engine: { id: nextEngineId++, scriptsRun: [] },\n tools: []\n }),\n auth: (req, res, next) => {\n if (req.header('authorization') !== 'Bearer testtoken') {\n res.status(401).json({ error: 'unauthorized' });\n return;\n }\n next();\n }\n });\n await new Promise<void>((resolve) => {\n server = app.listen(0, '127.0.0.1', resolve);\n });\n const addr = server.address() as AddressInfo;\n baseUrl = `http://127.0.0.1:${addr.port}/mcp`;\n\n const r = await fetch(baseUrl, {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n accept: 'application/json, text/event-stream'\n },\n body: JSON.stringify({\n jsonrpc: '2.0',\n id: 1,\n method: 'initialize',\n params: {}\n })\n });\n expect(r.status).toBe(401);\n });\n\n it('engine state is isolated per session: mutation in one is invisible to the other', async () => {\n const a = await connect();\n const b = await connect();\n\n // Record on a — only a should see it.\n const aResult = await a.client.callTool({\n name: 'engine_record',\n arguments: { msg: 'from-a' }\n });\n expect(aResult.content).toEqual([{ type: 'text', text: 'from-a' }]);\n\n // Record on b — should NOT include from-a.\n const bResult = await b.client.callTool({\n name: 'engine_record',\n arguments: { msg: 'from-b' }\n });\n expect(bResult.content).toEqual([{ type: 'text', text: 'from-b' }]);\n\n // Re-record on a — should append to a's history, still no b.\n const aResult2 = await a.client.callTool({\n name: 'engine_record',\n arguments: { msg: 'again' }\n });\n expect(aResult2.content).toEqual([{ type: 'text', text: 'from-a,again' }]);\n\n await a.client.close();\n await b.client.close();\n });\n\n it('passes the resolved workspaceId to createSession on initialize', async () => {\n const seen: string[] = [];\n const testApp = await createCodesignHttpServer({\n assets: {},\n maxSessions: 4,\n idleTimeoutMs: 60_000,\n resolveWorkspaceId: (req) => req.header('x-workspace-id') ?? 'fallback',\n createSession: async ({ workspaceId }) => {\n seen.push(workspaceId);\n return { engine: {}, tools: [] };\n }\n });\n await request(testApp)\n .post('/mcp')\n .set('x-workspace-id', 'u/acme')\n .set('content-type', 'application/json')\n .send({ jsonrpc: '2.0', id: 1, method: 'initialize', params: {} });\n expect(seen).toEqual(['u/acme']);\n await testApp.shutdown();\n });\n\n it('returns 401 when resolveWorkspaceId throws WorkspaceAuthError', async () => {\n const testApp = await createCodesignHttpServer({\n assets: {},\n maxSessions: 4,\n idleTimeoutMs: 60_000,\n resolveWorkspaceId: () => {\n throw new WorkspaceAuthError('missing header');\n },\n createSession: async () => ({ engine: {}, tools: [] })\n });\n const res = await request(testApp)\n .post('/mcp')\n .set('content-type', 'application/json')\n .send({ jsonrpc: '2.0', id: 1, method: 'initialize', params: {} });\n expect(res.status).toBe(401);\n expect(res.body.error.code).toBe(-32002);\n expect(res.body.error.message).toBe('missing header');\n await testApp.shutdown();\n });\n\n it('returns 400 (not a hang) when the resolved workspaceId is malformed', async () => {\n const testApp = await createCodesignHttpServer({\n assets: {},\n maxSessions: 4,\n idleTimeoutMs: 60_000,\n // header mode-style: return an illegal id that ScopedWorkspaceFS will reject\n resolveWorkspaceId: () => '../escape',\n createSession: async ({ workspaceId }) => {\n // mirror cli/http.ts: constructing the scoped fs validates the id\n new ScopedWorkspaceFS(new InMemoryWorkspaceFS(), workspaceId);\n return { engine: {}, tools: [] };\n }\n });\n const res = await request(testApp)\n .post('/mcp')\n .set('content-type', 'application/json')\n .send({ jsonrpc: '2.0', id: 1, method: 'initialize', params: {} });\n expect(res.status).toBe(400);\n expect(res.body.error.code).toBe(-32602); // invalid params\n await testApp.shutdown();\n });\n\n it('forwards the metrics recorder so tool calls are timed', async () => {\n const { MetricsRecorder } = await import('../../src/metrics/recorder.js');\n const recorder = new MetricsRecorder();\n const a2 = await createCodesignHttpServer({\n assets: {},\n maxSessions: 4,\n idleTimeoutMs: 60_000,\n metrics: recorder,\n createSession: async () => ({\n engine: {},\n tools: [\n {\n name: 'ping',\n description: 'ping',\n inputSchema: { type: 'object', properties: {} },\n call: async () => ({\n content: [{ type: 'text' as const, text: 'pong' }]\n })\n }\n ]\n })\n });\n const srv = await new Promise<ReturnType<typeof a2.listen>>((resolve) => {\n const s = a2.listen(0, '127.0.0.1', () => resolve(s));\n });\n const addr = srv.address() as AddressInfo;\n const url = `http://127.0.0.1:${addr.port}/mcp`;\n const transport = new StreamableHTTPClientTransport(new URL(url));\n const client = new Client({ name: 'test', version: '0' });\n await client.connect(transport);\n await client.callTool({ name: 'ping', arguments: {} });\n await client.close();\n await new Promise<void>((resolve) => srv.close(() => resolve()));\n await a2.shutdown();\n expect(\n recorder.snapshot().tools.find((t) => t.tool === 'ping')?.count\n ).toBe(1);\n });\n});\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-id-header.test.d.ts","sourceRoot":"","sources":["../../../../test/http/session-id-header.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import request from 'supertest';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { createCodesignHttpServer } from '../../src/http/server.js';
|
|
4
|
+
// The transport echoes the session id it assigned in the `mcp-session-id`
|
|
5
|
+
// response header. We assert that, with `trustedSessionIdHeader` configured,
|
|
6
|
+
// an inbound header value becomes the session id verbatim — and without it,
|
|
7
|
+
// the server still mints its own (a normal uuid, not our injected value).
|
|
8
|
+
const INIT_BODY = {
|
|
9
|
+
jsonrpc: '2.0',
|
|
10
|
+
id: 1,
|
|
11
|
+
method: 'initialize',
|
|
12
|
+
params: {
|
|
13
|
+
protocolVersion: '2024-11-05',
|
|
14
|
+
capabilities: {},
|
|
15
|
+
clientInfo: { name: 'test', version: '0.0.0' }
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
describe('trustedSessionIdHeader', () => {
|
|
19
|
+
it('uses the inbound header value as the session id on initialize', async () => {
|
|
20
|
+
const app = await createCodesignHttpServer({
|
|
21
|
+
assets: {},
|
|
22
|
+
maxSessions: 4,
|
|
23
|
+
idleTimeoutMs: 60_000,
|
|
24
|
+
trustedSessionIdHeader: 'x-mcp-session-id',
|
|
25
|
+
resolveWorkspaceId: (_req, sid) => `s/${sid}`,
|
|
26
|
+
createSession: async () => ({ engine: {}, tools: [] })
|
|
27
|
+
});
|
|
28
|
+
const res = await request(app)
|
|
29
|
+
.post('/mcp')
|
|
30
|
+
.set('accept', 'application/json, text/event-stream')
|
|
31
|
+
.set('content-type', 'application/json')
|
|
32
|
+
.set('x-mcp-session-id', 'worker-minted-123')
|
|
33
|
+
.send(INIT_BODY);
|
|
34
|
+
expect(res.headers['mcp-session-id']).toBe('worker-minted-123');
|
|
35
|
+
await app.shutdown();
|
|
36
|
+
});
|
|
37
|
+
it('mints its own id when the header is absent', async () => {
|
|
38
|
+
const app = await createCodesignHttpServer({
|
|
39
|
+
assets: {},
|
|
40
|
+
maxSessions: 4,
|
|
41
|
+
idleTimeoutMs: 60_000,
|
|
42
|
+
trustedSessionIdHeader: 'x-mcp-session-id',
|
|
43
|
+
resolveWorkspaceId: (_req, sid) => `s/${sid}`,
|
|
44
|
+
createSession: async () => ({ engine: {}, tools: [] })
|
|
45
|
+
});
|
|
46
|
+
const res = await request(app)
|
|
47
|
+
.post('/mcp')
|
|
48
|
+
.set('accept', 'application/json, text/event-stream')
|
|
49
|
+
.set('content-type', 'application/json')
|
|
50
|
+
.send(INIT_BODY);
|
|
51
|
+
expect(res.headers['mcp-session-id']).toBeDefined();
|
|
52
|
+
expect(res.headers['mcp-session-id']).not.toBe('worker-minted-123');
|
|
53
|
+
expect(res.headers['mcp-session-id']).toMatch(/^[0-9a-f-]{36}$/i);
|
|
54
|
+
await app.shutdown();
|
|
55
|
+
});
|
|
56
|
+
it('mints its own id when the header is present but empty', async () => {
|
|
57
|
+
const app = await createCodesignHttpServer({
|
|
58
|
+
assets: {},
|
|
59
|
+
maxSessions: 4,
|
|
60
|
+
idleTimeoutMs: 60_000,
|
|
61
|
+
trustedSessionIdHeader: 'x-mcp-session-id',
|
|
62
|
+
resolveWorkspaceId: (_req, sid) => `s/${sid}`,
|
|
63
|
+
createSession: async () => ({ engine: {}, tools: [] })
|
|
64
|
+
});
|
|
65
|
+
const res = await request(app)
|
|
66
|
+
.post('/mcp')
|
|
67
|
+
.set('accept', 'application/json, text/event-stream')
|
|
68
|
+
.set('content-type', 'application/json')
|
|
69
|
+
.set('x-mcp-session-id', '')
|
|
70
|
+
.send(INIT_BODY);
|
|
71
|
+
expect(res.headers['mcp-session-id']).toBeDefined();
|
|
72
|
+
expect(res.headers['mcp-session-id']).toMatch(/^[0-9a-f-]{36}$/i);
|
|
73
|
+
await app.shutdown();
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
//# sourceMappingURL=session-id-header.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-id-header.test.js","sourceRoot":"","sources":["../../../../test/http/session-id-header.test.ts"],"names":[],"mappings":"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","sourcesContent":["import request from 'supertest';\nimport { describe, expect, it } from 'vitest';\nimport { createCodesignHttpServer } from '../../src/http/server.js';\n\n// The transport echoes the session id it assigned in the `mcp-session-id`\n// response header. We assert that, with `trustedSessionIdHeader` configured,\n// an inbound header value becomes the session id verbatim — and without it,\n// the server still mints its own (a normal uuid, not our injected value).\nconst INIT_BODY = {\n jsonrpc: '2.0',\n id: 1,\n method: 'initialize',\n params: {\n protocolVersion: '2024-11-05',\n capabilities: {},\n clientInfo: { name: 'test', version: '0.0.0' }\n }\n};\n\ndescribe('trustedSessionIdHeader', () => {\n it('uses the inbound header value as the session id on initialize', async () => {\n const app = await createCodesignHttpServer({\n assets: {},\n maxSessions: 4,\n idleTimeoutMs: 60_000,\n trustedSessionIdHeader: 'x-mcp-session-id',\n resolveWorkspaceId: (_req, sid) => `s/${sid}`,\n createSession: async () => ({ engine: {}, tools: [] })\n });\n const res = await request(app)\n .post('/mcp')\n .set('accept', 'application/json, text/event-stream')\n .set('content-type', 'application/json')\n .set('x-mcp-session-id', 'worker-minted-123')\n .send(INIT_BODY);\n expect(res.headers['mcp-session-id']).toBe('worker-minted-123');\n await app.shutdown();\n });\n\n it('mints its own id when the header is absent', async () => {\n const app = await createCodesignHttpServer({\n assets: {},\n maxSessions: 4,\n idleTimeoutMs: 60_000,\n trustedSessionIdHeader: 'x-mcp-session-id',\n resolveWorkspaceId: (_req, sid) => `s/${sid}`,\n createSession: async () => ({ engine: {}, tools: [] })\n });\n const res = await request(app)\n .post('/mcp')\n .set('accept', 'application/json, text/event-stream')\n .set('content-type', 'application/json')\n .send(INIT_BODY);\n expect(res.headers['mcp-session-id']).toBeDefined();\n expect(res.headers['mcp-session-id']).not.toBe('worker-minted-123');\n expect(res.headers['mcp-session-id']).toMatch(/^[0-9a-f-]{36}$/i);\n await app.shutdown();\n });\n\n it('mints its own id when the header is present but empty', async () => {\n const app = await createCodesignHttpServer({\n assets: {},\n maxSessions: 4,\n idleTimeoutMs: 60_000,\n trustedSessionIdHeader: 'x-mcp-session-id',\n resolveWorkspaceId: (_req, sid) => `s/${sid}`,\n createSession: async () => ({ engine: {}, tools: [] })\n });\n const res = await request(app)\n .post('/mcp')\n .set('accept', 'application/json, text/event-stream')\n .set('content-type', 'application/json')\n .set('x-mcp-session-id', '')\n .send(INIT_BODY);\n expect(res.headers['mcp-session-id']).toBeDefined();\n expect(res.headers['mcp-session-id']).toMatch(/^[0-9a-f-]{36}$/i);\n await app.shutdown();\n });\n});\n"]}
|
|
@@ -79,5 +79,19 @@ describe('SessionManager', () => {
|
|
|
79
79
|
expect(a.engine.disposed).toBe(true);
|
|
80
80
|
expect(b.engine.disposed).toBe(true);
|
|
81
81
|
});
|
|
82
|
+
it('forwards a context object to the create callback', async () => {
|
|
83
|
+
const seen = [];
|
|
84
|
+
const mgr = new SessionManager({
|
|
85
|
+
maxSessions: 10,
|
|
86
|
+
idleTimeoutMs: 60_000,
|
|
87
|
+
create: (ctx) => {
|
|
88
|
+
seen.push(ctx?.workspaceId);
|
|
89
|
+
return { id: ctx?.workspaceId ?? 'none' };
|
|
90
|
+
},
|
|
91
|
+
destroy: () => { }
|
|
92
|
+
});
|
|
93
|
+
await mgr.create({ workspaceId: 'u/a' });
|
|
94
|
+
expect(seen).toEqual(['u/a']);
|
|
95
|
+
});
|
|
82
96
|
});
|
|
83
97
|
//# sourceMappingURL=session-manager.test.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-manager.test.js","sourceRoot":"","sources":["../../../../test/http/session-manager.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';\nimport {\n SessionCapExceededError,\n SessionManager,\n SessionNotFoundError\n} from '../../src/http/session-manager.js';\n\ninterface FakeEngine {\n id: number;\n disposed: boolean;\n}\n\ninterface FakeSession {\n id: string;\n engine: FakeEngine;\n}\n\ndescribe('SessionManager', () => {\n let nextEngineId: number;\n let createdEngines: FakeEngine[];\n let mgr: SessionManager<FakeSession>;\n\n beforeEach(() => {\n vi.useFakeTimers();\n vi.setSystemTime(new Date('2026-05-09T00:00:00Z'));\n nextEngineId = 0;\n createdEngines = [];\n mgr = new SessionManager<FakeSession>({\n maxSessions: 3,\n idleTimeoutMs: 60_000,\n create: async () => {\n const engine: FakeEngine = { id: nextEngineId++, disposed: false };\n createdEngines.push(engine);\n return { id: `s-${engine.id}`, engine };\n },\n destroy: async (s) => {\n s.engine.disposed = true;\n }\n });\n });\n\n afterEach(() => {\n vi.useRealTimers();\n });\n\n it('create() returns a fresh session and registers it under its id', async () => {\n const s = await mgr.create();\n expect(s.id).toBe('s-0');\n expect(s.engine.id).toBe(0);\n expect(mgr.get(s.id)).toBe(s);\n });\n\n it('get() returns undefined for unknown ids', () => {\n expect(mgr.get('nope')).toBeUndefined();\n });\n\n it('touch() updates lastActivityAt so the session survives idle eviction', async () => {\n const a = await mgr.create();\n vi.advanceTimersByTime(50_000);\n mgr.touch(a.id);\n vi.advanceTimersByTime(50_000); // total 100s; without touch a would be evicted\n await mgr.evictIdle();\n expect(mgr.get(a.id)).toBe(a);\n expect(a.engine.disposed).toBe(false);\n });\n\n it('evictIdle() destroys sessions older than idleTimeoutMs', async () => {\n const a = await mgr.create();\n const b = await mgr.create();\n vi.advanceTimersByTime(30_000);\n mgr.touch(b.id); // bump b only\n vi.advanceTimersByTime(40_000); // a is now 70s idle, b only 40s\n await mgr.evictIdle();\n expect(mgr.get(a.id)).toBeUndefined();\n expect(a.engine.disposed).toBe(true);\n expect(mgr.get(b.id)).toBe(b);\n expect(b.engine.disposed).toBe(false);\n });\n\n it('create() throws SessionCapExceededError once maxSessions is reached', async () => {\n await mgr.create();\n await mgr.create();\n await mgr.create();\n await expect(mgr.create()).rejects.toBeInstanceOf(SessionCapExceededError);\n });\n\n it('delete() destroys a session and removes it', async () => {\n const a = await mgr.create();\n await mgr.delete(a.id);\n expect(mgr.get(a.id)).toBeUndefined();\n expect(a.engine.disposed).toBe(true);\n });\n\n it('delete() of an unknown id throws SessionNotFoundError', async () => {\n await expect(mgr.delete('nope')).rejects.toBeInstanceOf(\n SessionNotFoundError\n );\n });\n\n it('destroyAll() shuts down every active session', async () => {\n const a = await mgr.create();\n const b = await mgr.create();\n await mgr.destroyAll();\n expect(mgr.get(a.id)).toBeUndefined();\n expect(mgr.get(b.id)).toBeUndefined();\n expect(a.engine.disposed).toBe(true);\n expect(b.engine.disposed).toBe(true);\n });\n});\n"]}
|
|
1
|
+
{"version":3,"file":"session-manager.test.js","sourceRoot":"","sources":["../../../../test/http/session-manager.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';\nimport {\n SessionCapExceededError,\n SessionManager,\n SessionNotFoundError\n} from '../../src/http/session-manager.js';\n\ninterface FakeEngine {\n id: number;\n disposed: boolean;\n}\n\ninterface FakeSession {\n id: string;\n engine: FakeEngine;\n}\n\ndescribe('SessionManager', () => {\n let nextEngineId: number;\n let createdEngines: FakeEngine[];\n let mgr: SessionManager<FakeSession>;\n\n beforeEach(() => {\n vi.useFakeTimers();\n vi.setSystemTime(new Date('2026-05-09T00:00:00Z'));\n nextEngineId = 0;\n createdEngines = [];\n mgr = new SessionManager<FakeSession>({\n maxSessions: 3,\n idleTimeoutMs: 60_000,\n create: async () => {\n const engine: FakeEngine = { id: nextEngineId++, disposed: false };\n createdEngines.push(engine);\n return { id: `s-${engine.id}`, engine };\n },\n destroy: async (s) => {\n s.engine.disposed = true;\n }\n });\n });\n\n afterEach(() => {\n vi.useRealTimers();\n });\n\n it('create() returns a fresh session and registers it under its id', async () => {\n const s = await mgr.create();\n expect(s.id).toBe('s-0');\n expect(s.engine.id).toBe(0);\n expect(mgr.get(s.id)).toBe(s);\n });\n\n it('get() returns undefined for unknown ids', () => {\n expect(mgr.get('nope')).toBeUndefined();\n });\n\n it('touch() updates lastActivityAt so the session survives idle eviction', async () => {\n const a = await mgr.create();\n vi.advanceTimersByTime(50_000);\n mgr.touch(a.id);\n vi.advanceTimersByTime(50_000); // total 100s; without touch a would be evicted\n await mgr.evictIdle();\n expect(mgr.get(a.id)).toBe(a);\n expect(a.engine.disposed).toBe(false);\n });\n\n it('evictIdle() destroys sessions older than idleTimeoutMs', async () => {\n const a = await mgr.create();\n const b = await mgr.create();\n vi.advanceTimersByTime(30_000);\n mgr.touch(b.id); // bump b only\n vi.advanceTimersByTime(40_000); // a is now 70s idle, b only 40s\n await mgr.evictIdle();\n expect(mgr.get(a.id)).toBeUndefined();\n expect(a.engine.disposed).toBe(true);\n expect(mgr.get(b.id)).toBe(b);\n expect(b.engine.disposed).toBe(false);\n });\n\n it('create() throws SessionCapExceededError once maxSessions is reached', async () => {\n await mgr.create();\n await mgr.create();\n await mgr.create();\n await expect(mgr.create()).rejects.toBeInstanceOf(SessionCapExceededError);\n });\n\n it('delete() destroys a session and removes it', async () => {\n const a = await mgr.create();\n await mgr.delete(a.id);\n expect(mgr.get(a.id)).toBeUndefined();\n expect(a.engine.disposed).toBe(true);\n });\n\n it('delete() of an unknown id throws SessionNotFoundError', async () => {\n await expect(mgr.delete('nope')).rejects.toBeInstanceOf(\n SessionNotFoundError\n );\n });\n\n it('destroyAll() shuts down every active session', async () => {\n const a = await mgr.create();\n const b = await mgr.create();\n await mgr.destroyAll();\n expect(mgr.get(a.id)).toBeUndefined();\n expect(mgr.get(b.id)).toBeUndefined();\n expect(a.engine.disposed).toBe(true);\n expect(b.engine.disposed).toBe(true);\n });\n\n it('forwards a context object to the create callback', async () => {\n const seen: Array<string | undefined> = [];\n const mgr = new SessionManager<{ id: string }, { workspaceId: string }>({\n maxSessions: 10,\n idleTimeoutMs: 60_000,\n create: (ctx) => {\n seen.push(ctx?.workspaceId);\n return { id: ctx?.workspaceId ?? 'none' };\n },\n destroy: () => {}\n });\n await mgr.create({ workspaceId: 'u/a' });\n expect(seen).toEqual(['u/a']);\n });\n});\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workspace-auth.test.d.ts","sourceRoot":"","sources":["../../../../test/http/workspace-auth.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { makeWorkspaceResolver, WorkspaceAuthError } from '../../src/http/workspace-auth.js';
|
|
3
|
+
function cfg(partial) {
|
|
4
|
+
return {
|
|
5
|
+
cesdkLicense: 'x',
|
|
6
|
+
buildId: 'test',
|
|
7
|
+
port: 8080,
|
|
8
|
+
maxEngines: 8,
|
|
9
|
+
idleTimeoutMs: 900_000,
|
|
10
|
+
logLevel: 'info',
|
|
11
|
+
metricsEnabled: false,
|
|
12
|
+
workspace: { backend: 'local', dir: '/data' },
|
|
13
|
+
auth: { mode: 'header', header: 'X-Workspace-Id', ...partial },
|
|
14
|
+
gateway: {}
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
function fakeReq(headers) {
|
|
18
|
+
return {
|
|
19
|
+
header: (name) => headers[name.toLowerCase()]
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
describe('makeWorkspaceResolver', () => {
|
|
23
|
+
it('header mode returns the trusted header value (opaque, unmodified)', () => {
|
|
24
|
+
const resolve = makeWorkspaceResolver(cfg({ mode: 'header', header: 'X-Workspace-Id' }));
|
|
25
|
+
const req = fakeReq({ 'x-workspace-id': 'u/usr_8842' });
|
|
26
|
+
expect(resolve(req, 'sess-1')).toBe('u/usr_8842');
|
|
27
|
+
});
|
|
28
|
+
it('header mode throws WorkspaceAuthError when the header is absent', () => {
|
|
29
|
+
const resolve = makeWorkspaceResolver(cfg({ mode: 'header' }));
|
|
30
|
+
expect(() => resolve(fakeReq({}), 'sess-1')).toThrow(WorkspaceAuthError);
|
|
31
|
+
});
|
|
32
|
+
it('session mode derives the ephemeral s/<sessionId> id', () => {
|
|
33
|
+
const resolve = makeWorkspaceResolver(cfg({ mode: 'session' }));
|
|
34
|
+
expect(resolve(fakeReq({}), 'sess-42')).toBe('s/sess-42');
|
|
35
|
+
});
|
|
36
|
+
it('single mode returns the fixed id', () => {
|
|
37
|
+
const resolve = makeWorkspaceResolver(cfg({ mode: 'single', fixedWorkspaceId: 'acme' }));
|
|
38
|
+
expect(resolve(fakeReq({}), 'sess-1')).toBe('acme');
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
//# sourceMappingURL=workspace-auth.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workspace-auth.test.js","sourceRoot":"","sources":["../../../../test/http/workspace-auth.test.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAE9C,OAAO,EACL,qBAAqB,EACrB,kBAAkB,EACnB,MAAM,kCAAkC,CAAC;AAE1C,SAAS,GAAG,CAAC,OAAsC;IACjD,OAAO;QACL,YAAY,EAAE,GAAG;QACjB,OAAO,EAAE,MAAM;QACf,IAAI,EAAE,IAAI;QACV,UAAU,EAAE,CAAC;QACb,aAAa,EAAE,OAAO;QACtB,QAAQ,EAAE,MAAM;QAChB,cAAc,EAAE,KAAK;QACrB,SAAS,EAAE,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE;QAC7C,IAAI,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,gBAAgB,EAAE,GAAG,OAAO,EAAE;QAC9D,OAAO,EAAE,EAAE;KACZ,CAAC;AACJ,CAAC;AAED,SAAS,OAAO,CAAC,OAA+B;IAC9C,OAAO;QACL,MAAM,EAAE,CAAC,IAAY,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC;KAChC,CAAC;AAC1B,CAAC;AAED,QAAQ,CAAC,uBAAuB,EAAE,GAAG,EAAE;IACrC,EAAE,CAAC,mEAAmE,EAAE,GAAG,EAAE;QAC3E,MAAM,OAAO,GAAG,qBAAqB,CACnC,GAAG,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,gBAAgB,EAAE,CAAC,CAClD,CAAC;QACF,MAAM,GAAG,GAAG,OAAO,CAAC,EAAE,gBAAgB,EAAE,YAAY,EAAE,CAAC,CAAC;QACxD,MAAM,CAAC,OAAO,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;IACpD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,iEAAiE,EAAE,GAAG,EAAE;QACzE,MAAM,OAAO,GAAG,qBAAqB,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC;QAC/D,MAAM,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,EAAE,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,kBAAkB,CAAC,CAAC;IAC3E,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,qDAAqD,EAAE,GAAG,EAAE;QAC7D,MAAM,OAAO,GAAG,qBAAqB,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC;QAChE,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,EAAE,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;IAC5D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,kCAAkC,EAAE,GAAG,EAAE;QAC1C,MAAM,OAAO,GAAG,qBAAqB,CACnC,GAAG,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,gBAAgB,EAAE,MAAM,EAAE,CAAC,CAClD,CAAC;QACF,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,EAAE,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACtD,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC","sourcesContent":["import type { Request } from 'express';\nimport { describe, expect, it } from 'vitest';\nimport type { ServerConfig } from '../../src/config.js';\nimport {\n makeWorkspaceResolver,\n WorkspaceAuthError\n} from '../../src/http/workspace-auth.js';\n\nfunction cfg(partial: Partial<ServerConfig['auth']>): ServerConfig {\n return {\n cesdkLicense: 'x',\n buildId: 'test',\n port: 8080,\n maxEngines: 8,\n idleTimeoutMs: 900_000,\n logLevel: 'info',\n metricsEnabled: false,\n workspace: { backend: 'local', dir: '/data' },\n auth: { mode: 'header', header: 'X-Workspace-Id', ...partial },\n gateway: {}\n };\n}\n\nfunction fakeReq(headers: Record<string, string>): Request {\n return {\n header: (name: string) => headers[name.toLowerCase()]\n } as unknown as Request;\n}\n\ndescribe('makeWorkspaceResolver', () => {\n it('header mode returns the trusted header value (opaque, unmodified)', () => {\n const resolve = makeWorkspaceResolver(\n cfg({ mode: 'header', header: 'X-Workspace-Id' })\n );\n const req = fakeReq({ 'x-workspace-id': 'u/usr_8842' });\n expect(resolve(req, 'sess-1')).toBe('u/usr_8842');\n });\n\n it('header mode throws WorkspaceAuthError when the header is absent', () => {\n const resolve = makeWorkspaceResolver(cfg({ mode: 'header' }));\n expect(() => resolve(fakeReq({}), 'sess-1')).toThrow(WorkspaceAuthError);\n });\n\n it('session mode derives the ephemeral s/<sessionId> id', () => {\n const resolve = makeWorkspaceResolver(cfg({ mode: 'session' }));\n expect(resolve(fakeReq({}), 'sess-42')).toBe('s/sess-42');\n });\n\n it('single mode returns the fixed id', () => {\n const resolve = makeWorkspaceResolver(\n cfg({ mode: 'single', fixedWorkspaceId: 'acme' })\n );\n expect(resolve(fakeReq({}), 'sess-1')).toBe('acme');\n });\n});\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mount.test.d.ts","sourceRoot":"","sources":["../../../../test/metrics/mount.test.ts"],"names":[],"mappings":""}
|