proteum 2.2.8 → 2.3.0
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/AGENTS.md +5 -3
- package/README.md +50 -12
- package/agents/project/AGENTS.md +47 -10
- package/agents/project/CODING_STYLE.md +5 -1
- package/agents/project/client/AGENTS.md +2 -0
- package/agents/project/diagnostics.md +8 -5
- package/agents/project/optimizations.md +1 -0
- package/agents/project/root/AGENTS.md +18 -10
- package/agents/project/tests/AGENTS.md +6 -1
- package/agents/project/tests/e2e/AGENTS.md +13 -0
- package/agents/project/tests/e2e/REAL_WORLD_JOURNEY_TESTS.md +192 -0
- package/cli/commands/check.ts +21 -3
- package/cli/commands/configure.ts +1 -0
- package/cli/commands/connect.ts +40 -4
- package/cli/commands/diagnose.ts +136 -5
- package/cli/commands/doctor.ts +24 -4
- package/cli/commands/explain.ts +105 -6
- package/cli/commands/mcp.ts +16 -0
- package/cli/commands/orient.ts +66 -3
- package/cli/commands/perf.ts +118 -13
- package/cli/commands/runtime.ts +151 -0
- package/cli/commands/trace.ts +116 -21
- package/cli/mcp/provider.ts +365 -0
- package/cli/mcp/stdio.ts +16 -0
- package/cli/presentation/commands.ts +79 -22
- package/cli/presentation/devSession.ts +2 -0
- package/cli/runtime/commands.ts +95 -12
- package/cli/utils/agentOutput.ts +46 -0
- package/cli/utils/agents.ts +225 -48
- package/common/dev/inspection.ts +30 -9
- package/common/dev/mcpPayloads.ts +736 -0
- package/common/dev/mcpServer.ts +254 -0
- package/docs/agent-routing.md +126 -0
- package/docs/dev-commands.md +2 -0
- package/docs/dev-sessions.md +2 -1
- package/docs/diagnostics.md +68 -23
- package/docs/mcp.md +149 -0
- package/docs/migrate-from-2.1.3.md +15 -5
- package/docs/request-tracing.md +12 -6
- package/eslint.js +220 -0
- package/package.json +2 -1
- package/server/app/devMcp.ts +159 -0
- package/server/services/router/http/cache.ts +116 -0
- package/server/services/router/http/index.ts +94 -35
- package/server/services/router/index.ts +8 -11
- package/tests/agents-utils.test.cjs +89 -11
- package/tests/dev-transpile-watch.test.cjs +117 -8
- package/tests/eslint-rules.test.cjs +110 -0
- package/tests/inspection.test.cjs +67 -0
- package/tests/mcp.test.cjs +127 -0
- package/tests/router-cache-config.test.cjs +74 -0
|
@@ -65,6 +65,33 @@ const findAssetContaining = (appRoot, extension, marker) => {
|
|
|
65
65
|
return candidates.find((filepath) => fs.readFileSync(filepath, 'utf8').includes(marker));
|
|
66
66
|
};
|
|
67
67
|
|
|
68
|
+
const toPublicAssetUrl = (appRoot, filepath) => {
|
|
69
|
+
const publicRoot = path.join(appRoot, 'dev', 'public');
|
|
70
|
+
const relativePath = path.relative(publicRoot, filepath).split(path.sep).join('/');
|
|
71
|
+
|
|
72
|
+
return `/public/${relativePath}`;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const request = async (port, urlPath, headers = {}) => {
|
|
76
|
+
const response = await fetch(`http://127.0.0.1:${port}${urlPath}`, { headers });
|
|
77
|
+
const body = await response.text();
|
|
78
|
+
|
|
79
|
+
return { response, body };
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const waitForHeader = async (port, urlPath, headerName, expectedValue, timeoutMs = 60000) => {
|
|
83
|
+
const deadline = Date.now() + timeoutMs;
|
|
84
|
+
|
|
85
|
+
while (Date.now() < deadline) {
|
|
86
|
+
const { response } = await request(port, urlPath, { Accept: 'text/html' });
|
|
87
|
+
if (response.headers.get(headerName) === expectedValue) return response;
|
|
88
|
+
|
|
89
|
+
await sleep(250);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
throw new Error(`Timed out waiting for ${urlPath} header ${headerName}=${expectedValue}.`);
|
|
93
|
+
};
|
|
94
|
+
|
|
68
95
|
const waitForAssetContaining = async (appRoot, extension, marker, timeoutMs = 60000) => {
|
|
69
96
|
const deadline = Date.now() + timeoutMs;
|
|
70
97
|
|
|
@@ -172,9 +199,10 @@ const createSharedStyleSource = (marker) => `.shared-style-marker {
|
|
|
172
199
|
}
|
|
173
200
|
`;
|
|
174
201
|
|
|
175
|
-
const createFixture = (root, port) => {
|
|
202
|
+
const createFixture = (root, port, options = {}) => {
|
|
176
203
|
const appRoot = path.join(root, 'app');
|
|
177
204
|
const sharedRoot = path.join(root, 'shared');
|
|
205
|
+
const cacheConfigSource = options.routerCache ? ` cache: ${options.routerCache},\n` : '';
|
|
178
206
|
|
|
179
207
|
fs.mkdirSync(path.join(appRoot, 'public'), { recursive: true });
|
|
180
208
|
fs.mkdirSync(path.join(appRoot, 'client', 'assets', 'identity'), { recursive: true });
|
|
@@ -332,6 +360,7 @@ export const routerBaseConfig = {
|
|
|
332
360
|
upload: {
|
|
333
361
|
maxSize: '10mb',
|
|
334
362
|
},
|
|
363
|
+
${cacheConfigSource}
|
|
335
364
|
csp: {
|
|
336
365
|
scripts: [],
|
|
337
366
|
},
|
|
@@ -403,6 +432,31 @@ Router.page(
|
|
|
403
432
|
);
|
|
404
433
|
`,
|
|
405
434
|
);
|
|
435
|
+
if (options.staticPage) {
|
|
436
|
+
writeFile(
|
|
437
|
+
path.join(appRoot, 'client', 'pages', 'static-cache.tsx'),
|
|
438
|
+
`import Router from '@/client/router';
|
|
439
|
+
import { SharedMarker } from '@test/shared';
|
|
440
|
+
|
|
441
|
+
Router.page(
|
|
442
|
+
'/static-cache',
|
|
443
|
+
{
|
|
444
|
+
auth: false,
|
|
445
|
+
layout: false,
|
|
446
|
+
static: { urls: ['/static-cache'] },
|
|
447
|
+
},
|
|
448
|
+
null,
|
|
449
|
+
() => {
|
|
450
|
+
return (
|
|
451
|
+
<main>
|
|
452
|
+
<SharedMarker />
|
|
453
|
+
</main>
|
|
454
|
+
);
|
|
455
|
+
},
|
|
456
|
+
);
|
|
457
|
+
`,
|
|
458
|
+
);
|
|
459
|
+
}
|
|
406
460
|
|
|
407
461
|
writeFile(
|
|
408
462
|
path.join(sharedRoot, 'package.json'),
|
|
@@ -448,13 +502,8 @@ const stopDevServer = async (child) => {
|
|
|
448
502
|
});
|
|
449
503
|
};
|
|
450
504
|
|
|
451
|
-
|
|
452
|
-
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'proteum-transpile-watch-'));
|
|
453
|
-
const port = await resolvePortPair();
|
|
454
|
-
const { appRoot, sharedRoot } = createFixture(root, port);
|
|
455
|
-
const sessionFile = path.join(appRoot, 'var', 'run', 'proteum', 'dev', 'transpile-watch-test.json');
|
|
505
|
+
const startDevServer = (appRoot, port, sessionFile) => {
|
|
456
506
|
let output = '';
|
|
457
|
-
|
|
458
507
|
const child = spawn(
|
|
459
508
|
process.execPath,
|
|
460
509
|
[cliBin, 'dev', '--cwd', appRoot, '--port', String(port), '--session-file', sessionFile, '--no-cache', '--verbose'],
|
|
@@ -476,8 +525,21 @@ test('proteum dev invalidates client assets and reloads for transpiled package s
|
|
|
476
525
|
output += chunk.toString();
|
|
477
526
|
});
|
|
478
527
|
|
|
528
|
+
return {
|
|
529
|
+
child,
|
|
530
|
+
getOutput: () => output,
|
|
531
|
+
};
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
test('proteum dev invalidates client assets and reloads for transpiled package scripts and styles', { timeout: 180000 }, async () => {
|
|
535
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'proteum-transpile-watch-'));
|
|
536
|
+
const port = await resolvePortPair();
|
|
537
|
+
const { appRoot, sharedRoot } = createFixture(root, port);
|
|
538
|
+
const sessionFile = path.join(appRoot, 'var', 'run', 'proteum', 'dev', 'transpile-watch-test.json');
|
|
539
|
+
const { child, getOutput } = startDevServer(appRoot, port, sessionFile);
|
|
540
|
+
|
|
479
541
|
try {
|
|
480
|
-
await waitForSessionReady(sessionFile, child,
|
|
542
|
+
await waitForSessionReady(sessionFile, child, getOutput);
|
|
481
543
|
|
|
482
544
|
const initialScriptAsset = await waitForAssetContaining(appRoot, '.js', 'SCRIPT_MARKER_INITIAL');
|
|
483
545
|
const initialScriptContent = fs.readFileSync(initialScriptAsset, 'utf8');
|
|
@@ -511,3 +573,50 @@ test('proteum dev invalidates client assets and reloads for transpiled package s
|
|
|
511
573
|
fs.rmSync(root, { recursive: true, force: true });
|
|
512
574
|
}
|
|
513
575
|
});
|
|
576
|
+
|
|
577
|
+
test('proteum dev applies router HTTP cache config to HTML and public assets', { timeout: 180000 }, async () => {
|
|
578
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'proteum-router-cache-'));
|
|
579
|
+
const port = await resolvePortPair();
|
|
580
|
+
const dynamicHtmlCacheControl = 'private, max-age=7';
|
|
581
|
+
const staticHtmlCacheControl = 'private, max-age=13';
|
|
582
|
+
const publicAssetCacheControl = 'private, max-age=17';
|
|
583
|
+
const { appRoot } = createFixture(root, port, {
|
|
584
|
+
staticPage: true,
|
|
585
|
+
routerCache: `{
|
|
586
|
+
html: {
|
|
587
|
+
dynamic: { cacheControl: '${dynamicHtmlCacheControl}', surrogateControl: 'dynamic-surrogate' },
|
|
588
|
+
static: { cacheControl: '${staticHtmlCacheControl}', surrogateControl: 'static-surrogate' },
|
|
589
|
+
},
|
|
590
|
+
publicAssets: {
|
|
591
|
+
dev: '${publicAssetCacheControl}',
|
|
592
|
+
versioned: '${publicAssetCacheControl}',
|
|
593
|
+
unversioned: '${publicAssetCacheControl}',
|
|
594
|
+
etag: false,
|
|
595
|
+
lastModified: false,
|
|
596
|
+
},
|
|
597
|
+
}`,
|
|
598
|
+
});
|
|
599
|
+
const sessionFile = path.join(appRoot, 'var', 'run', 'proteum', 'dev', 'router-cache-test.json');
|
|
600
|
+
const { child, getOutput } = startDevServer(appRoot, port, sessionFile);
|
|
601
|
+
|
|
602
|
+
try {
|
|
603
|
+
await waitForSessionReady(sessionFile, child, getOutput);
|
|
604
|
+
|
|
605
|
+
const { response: dynamicResponse } = await request(port, '/', { Accept: 'text/html' });
|
|
606
|
+
assert.equal(dynamicResponse.headers.get('cache-control'), dynamicHtmlCacheControl);
|
|
607
|
+
assert.equal(dynamicResponse.headers.get('surrogate-control'), 'dynamic-surrogate');
|
|
608
|
+
|
|
609
|
+
const staticResponse = await waitForHeader(port, '/static-cache', 'cache-control', staticHtmlCacheControl);
|
|
610
|
+
assert.equal(staticResponse.headers.get('surrogate-control'), 'static-surrogate');
|
|
611
|
+
|
|
612
|
+
const asset = await waitForAssetContaining(appRoot, '.js', 'SCRIPT_MARKER_INITIAL');
|
|
613
|
+
const { response: assetResponse } = await request(port, toPublicAssetUrl(appRoot, asset));
|
|
614
|
+
|
|
615
|
+
assert.equal(assetResponse.headers.get('cache-control'), publicAssetCacheControl);
|
|
616
|
+
assert.equal(assetResponse.headers.get('etag'), null);
|
|
617
|
+
assert.equal(assetResponse.headers.get('last-modified'), null);
|
|
618
|
+
} finally {
|
|
619
|
+
await stopDevServer(child);
|
|
620
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
621
|
+
}
|
|
622
|
+
});
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
const assert = require('node:assert/strict');
|
|
2
|
+
const test = require('node:test');
|
|
3
|
+
const { Linter } = require('eslint');
|
|
4
|
+
|
|
5
|
+
const { createProteumEslintConfig } = require('../eslint.js');
|
|
6
|
+
|
|
7
|
+
const lint = (code) => {
|
|
8
|
+
const linter = new Linter({ configType: 'flat' });
|
|
9
|
+
return linter.verify(code, createProteumEslintConfig(), {
|
|
10
|
+
filename: 'client/example.tsx',
|
|
11
|
+
});
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const swallowedErrorRuleId = 'proteum/no-swallowed-caught-error';
|
|
15
|
+
|
|
16
|
+
test('proteum lint rejects empty catch blocks', () => {
|
|
17
|
+
const messages = lint(`
|
|
18
|
+
export const run = () => {
|
|
19
|
+
try {
|
|
20
|
+
risky();
|
|
21
|
+
} catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
`);
|
|
26
|
+
|
|
27
|
+
assert.equal(messages.some((message) => message.ruleId === swallowedErrorRuleId), true);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('proteum lint rejects promise catches that discard the error', () => {
|
|
31
|
+
const messages = lint(`
|
|
32
|
+
export const run = () => {
|
|
33
|
+
api.load().catch(() => toast.error('Could not load'));
|
|
34
|
+
};
|
|
35
|
+
`);
|
|
36
|
+
|
|
37
|
+
assert.equal(messages.some((message) => message.ruleId === swallowedErrorRuleId), true);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('proteum lint rejects generic catch feedback that drops original error details', () => {
|
|
41
|
+
const messages = lint(`
|
|
42
|
+
export const run = async () => {
|
|
43
|
+
try {
|
|
44
|
+
await Investor.api.getDashboard();
|
|
45
|
+
} catch (error) {
|
|
46
|
+
toast.error('Could not load API dashboard');
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
`);
|
|
50
|
+
|
|
51
|
+
assert.equal(messages.some((message) => message.ruleId === swallowedErrorRuleId), true);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('proteum lint allows rethrowing the caught error', () => {
|
|
55
|
+
const messages = lint(`
|
|
56
|
+
export const run = async () => {
|
|
57
|
+
try {
|
|
58
|
+
await Investor.api.getDashboard();
|
|
59
|
+
} catch (error) {
|
|
60
|
+
toast.error('Could not load API dashboard');
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
`);
|
|
65
|
+
|
|
66
|
+
assert.equal(messages.filter((message) => message.ruleId === swallowedErrorRuleId).length, 0);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('proteum lint allows surfacing original error details', () => {
|
|
70
|
+
const messages = lint(`
|
|
71
|
+
export const run = async () => {
|
|
72
|
+
try {
|
|
73
|
+
await Investor.api.getDashboard();
|
|
74
|
+
} catch (error) {
|
|
75
|
+
toast.error('Could not load API dashboard', {
|
|
76
|
+
description: error instanceof Error ? error.message : String(error),
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
`);
|
|
81
|
+
|
|
82
|
+
assert.equal(messages.filter((message) => message.ruleId === swallowedErrorRuleId).length, 0);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('proteum lint allows surfacing a message derived from the caught error', () => {
|
|
86
|
+
const messages = lint(`
|
|
87
|
+
export const run = async () => {
|
|
88
|
+
try {
|
|
89
|
+
await Investor.api.getDashboard();
|
|
90
|
+
} catch (error) {
|
|
91
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
92
|
+
setError(message);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
`);
|
|
96
|
+
|
|
97
|
+
assert.equal(messages.filter((message) => message.ruleId === swallowedErrorRuleId).length, 0);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('proteum lint allows routing promise failures to app error handling', () => {
|
|
101
|
+
const messages = lint(`
|
|
102
|
+
export const run = () => {
|
|
103
|
+
Investor.api.ensureApiKey().catch((error) => {
|
|
104
|
+
Router.app.handleError(error);
|
|
105
|
+
});
|
|
106
|
+
};
|
|
107
|
+
`);
|
|
108
|
+
|
|
109
|
+
assert.equal(messages.filter((message) => message.ruleId === swallowedErrorRuleId).length, 0);
|
|
110
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
const assert = require('node:assert/strict');
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
const test = require('node:test');
|
|
4
|
+
|
|
5
|
+
const coreRoot = path.resolve(__dirname, '..');
|
|
6
|
+
process.env.TS_NODE_PROJECT = path.join(coreRoot, 'cli', 'tsconfig.json');
|
|
7
|
+
process.env.TS_NODE_TRANSPILE_ONLY = '1';
|
|
8
|
+
require('ts-node/register/transpile-only');
|
|
9
|
+
|
|
10
|
+
const { explainOwner } = require('../common/dev/inspection.ts');
|
|
11
|
+
|
|
12
|
+
const createRoute = (routePath, filepath) => ({
|
|
13
|
+
chunkFilepath: filepath,
|
|
14
|
+
chunkId: filepath.replace(/[^A-Za-z0-9]+/g, '_'),
|
|
15
|
+
codeRaw: routePath,
|
|
16
|
+
filepath,
|
|
17
|
+
hasData: false,
|
|
18
|
+
kind: 'page',
|
|
19
|
+
methodName: 'page',
|
|
20
|
+
normalizedOptionKeys: [],
|
|
21
|
+
path: routePath,
|
|
22
|
+
pathRaw: routePath,
|
|
23
|
+
scope: 'app',
|
|
24
|
+
sourceLocation: { line: 1, column: 1 },
|
|
25
|
+
targetResolution: 'literal',
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const createManifest = (routes) => ({
|
|
29
|
+
app: {
|
|
30
|
+
coreRoot,
|
|
31
|
+
root: '/tmp/proteum-app',
|
|
32
|
+
},
|
|
33
|
+
commands: [],
|
|
34
|
+
connectedProjects: [],
|
|
35
|
+
controllers: [],
|
|
36
|
+
diagnostics: [],
|
|
37
|
+
layouts: [],
|
|
38
|
+
routes: {
|
|
39
|
+
client: routes,
|
|
40
|
+
server: [],
|
|
41
|
+
},
|
|
42
|
+
services: {
|
|
43
|
+
app: [],
|
|
44
|
+
routerPlugins: [],
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('root owner lookup does not match every dynamic route containing a slash', () => {
|
|
49
|
+
const manifest = createManifest([
|
|
50
|
+
createRoute('/domains/:slug((?!tlds$|tld$|sector$)[^/]+)', '/tmp/proteum-app/client/pages/domains/slug.tsx'),
|
|
51
|
+
createRoute('/admin/data/:tab([^/]+)', '/tmp/proteum-app/client/pages/admin/data.tsx'),
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
assert.deepEqual(explainOwner(manifest, '/').matches, []);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('root owner lookup returns only the literal root route when present', () => {
|
|
58
|
+
const manifest = createManifest([
|
|
59
|
+
createRoute('/domains/:slug((?!tlds$|tld$|sector$)[^/]+)', '/tmp/proteum-app/client/pages/domains/slug.tsx'),
|
|
60
|
+
createRoute('/', '/tmp/proteum-app/client/pages/home.tsx'),
|
|
61
|
+
]);
|
|
62
|
+
|
|
63
|
+
const matches = explainOwner(manifest, '/').matches;
|
|
64
|
+
|
|
65
|
+
assert.equal(matches.length, 1);
|
|
66
|
+
assert.equal(matches[0].label, '/');
|
|
67
|
+
});
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
const assert = require('node:assert/strict');
|
|
2
|
+
const fs = require('node:fs');
|
|
3
|
+
const os = require('node:os');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const test = require('node:test');
|
|
6
|
+
|
|
7
|
+
const coreRoot = path.resolve(__dirname, '..');
|
|
8
|
+
process.env.TS_NODE_PROJECT = path.join(coreRoot, 'cli', 'tsconfig.json');
|
|
9
|
+
process.env.TS_NODE_TRANSPILE_ONLY = '1';
|
|
10
|
+
require('ts-node/register/transpile-only');
|
|
11
|
+
require('../cli/context.ts');
|
|
12
|
+
|
|
13
|
+
const { Client } = require('@modelcontextprotocol/sdk/client/index.js');
|
|
14
|
+
const { InMemoryTransport } = require('@modelcontextprotocol/sdk/inMemory.js');
|
|
15
|
+
const { createMcpPayload, compactTraceResponse, resolveInstructionRouting } = require('../common/dev/mcpPayloads.ts');
|
|
16
|
+
const { createProteumMcpServer } = require('../common/dev/mcpServer.ts');
|
|
17
|
+
|
|
18
|
+
const writeFile = (filepath, content) => {
|
|
19
|
+
fs.mkdirSync(path.dirname(filepath), { recursive: true });
|
|
20
|
+
fs.writeFileSync(filepath, content);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
test('instruction routing returns compact selected files for a page query', () => {
|
|
24
|
+
const appRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'proteum-mcp-app-'));
|
|
25
|
+
|
|
26
|
+
writeFile(path.join(appRoot, 'AGENTS.md'), '# App Agents\n\n- root\n');
|
|
27
|
+
writeFile(path.join(appRoot, 'client', 'AGENTS.md'), '# Client Agents\n\n- client\n');
|
|
28
|
+
writeFile(path.join(appRoot, 'client', 'pages', 'AGENTS.md'), '# Page Agents\n\n- pages\n');
|
|
29
|
+
writeFile(path.join(appRoot, 'diagnostics.md'), '# Diagnostics\n\n- diagnose\n');
|
|
30
|
+
|
|
31
|
+
const payload = resolveInstructionRouting({ appRoot, query: '/domains/:slug client/pages/domain.tsx' });
|
|
32
|
+
|
|
33
|
+
assert.equal(payload.ok, true);
|
|
34
|
+
assert.equal(payload.format, 'proteum-mcp-v1');
|
|
35
|
+
assert.deepEqual(
|
|
36
|
+
payload.data.selected.map((entry) => path.relative(appRoot, entry.file)).sort(),
|
|
37
|
+
['AGENTS.md', 'client/AGENTS.md', 'client/pages/AGENTS.md'],
|
|
38
|
+
);
|
|
39
|
+
assert.equal(payload.data.readWhen.some((entry) => entry.file && entry.file.endsWith('diagnostics.md')), true);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('trace payload keeps default output compact and paginates full details', () => {
|
|
43
|
+
const request = {
|
|
44
|
+
id: 'req_1',
|
|
45
|
+
method: 'GET',
|
|
46
|
+
path: '/domains',
|
|
47
|
+
url: 'http://localhost:3000/domains',
|
|
48
|
+
capture: 'deep',
|
|
49
|
+
startedAt: new Date().toISOString(),
|
|
50
|
+
finishedAt: new Date().toISOString(),
|
|
51
|
+
durationMs: 42,
|
|
52
|
+
statusCode: 200,
|
|
53
|
+
droppedEvents: 0,
|
|
54
|
+
calls: Array.from({ length: 3 }, (_, index) => ({
|
|
55
|
+
id: `call_${index}`,
|
|
56
|
+
origin: 'server',
|
|
57
|
+
label: `Call ${index}`,
|
|
58
|
+
method: 'POST',
|
|
59
|
+
path: `/api/${index}`,
|
|
60
|
+
statusCode: 200,
|
|
61
|
+
durationMs: 10 + index,
|
|
62
|
+
requestDataKeys: [],
|
|
63
|
+
resultKeys: [],
|
|
64
|
+
})),
|
|
65
|
+
events: Array.from({ length: 12 }, (_, index) => ({
|
|
66
|
+
index,
|
|
67
|
+
elapsedMs: index,
|
|
68
|
+
type: index === 2 ? 'error' : 'mark',
|
|
69
|
+
details: { index, long: 'x'.repeat(300) },
|
|
70
|
+
})),
|
|
71
|
+
sqlQueries: Array.from({ length: 4 }, (_, index) => ({
|
|
72
|
+
id: `sql_${index}`,
|
|
73
|
+
callerLabel: 'Service.query',
|
|
74
|
+
callerMethod: 'query',
|
|
75
|
+
callerPath: 'server/services/Domain/index.ts',
|
|
76
|
+
kind: 'query',
|
|
77
|
+
operation: 'findMany',
|
|
78
|
+
model: 'Domain',
|
|
79
|
+
durationMs: index + 1,
|
|
80
|
+
fingerprint: `fp_${index}`,
|
|
81
|
+
query: 'select * from Domain where id = ?',
|
|
82
|
+
})),
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const compact = compactTraceResponse({ request });
|
|
86
|
+
const full = compactTraceResponse({ detail: 'full', limit: 5, offset: 4, request });
|
|
87
|
+
|
|
88
|
+
assert.equal(compact.data.page, undefined);
|
|
89
|
+
assert.equal(compact.omitted.length, 1);
|
|
90
|
+
assert.equal(full.data.page.events.length, 5);
|
|
91
|
+
assert.equal(full.data.page.hasMore, true);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('MCP server registers the Proteum read-only tool contract', async () => {
|
|
95
|
+
const payload = createMcpPayload({ summary: 'ok', data: { value: 1 } });
|
|
96
|
+
const provider = {
|
|
97
|
+
diagnose: async () => payload,
|
|
98
|
+
doctor: async () => payload,
|
|
99
|
+
explainSummary: async () => payload,
|
|
100
|
+
instructionsResolve: async () => payload,
|
|
101
|
+
logsTail: async () => payload,
|
|
102
|
+
orient: async () => payload,
|
|
103
|
+
perfRequest: async () => payload,
|
|
104
|
+
perfTop: async () => payload,
|
|
105
|
+
readResource: async () => payload,
|
|
106
|
+
runtimeStatus: async () => payload,
|
|
107
|
+
traceLatest: async () => payload,
|
|
108
|
+
traceShow: async () => payload,
|
|
109
|
+
};
|
|
110
|
+
const server = createProteumMcpServer({ provider, version: 'test' });
|
|
111
|
+
const client = new Client({ name: 'mcp-test', version: '1.0.0' });
|
|
112
|
+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
|
113
|
+
|
|
114
|
+
await server.connect(serverTransport);
|
|
115
|
+
await client.connect(clientTransport);
|
|
116
|
+
|
|
117
|
+
const tools = await client.listTools();
|
|
118
|
+
const result = await client.callTool({ name: 'runtime_status', arguments: {} });
|
|
119
|
+
const resource = await client.readResource({ uri: 'proteum://runtime/status' });
|
|
120
|
+
|
|
121
|
+
assert.equal(tools.tools.some((tool) => tool.name === 'runtime_status'), true);
|
|
122
|
+
assert.match(result.content[0].text, /proteum-mcp-v1/);
|
|
123
|
+
assert.match(resource.contents[0].text, /proteum-mcp-v1/);
|
|
124
|
+
|
|
125
|
+
await client.close();
|
|
126
|
+
await server.close();
|
|
127
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
const assert = require('node:assert/strict');
|
|
2
|
+
const test = require('node:test');
|
|
3
|
+
|
|
4
|
+
require('ts-node/register/transpile-only');
|
|
5
|
+
|
|
6
|
+
const {
|
|
7
|
+
resolveHttpCacheConfig,
|
|
8
|
+
resolvePublicAssetCacheControl,
|
|
9
|
+
} = require('../server/services/router/http/cache');
|
|
10
|
+
|
|
11
|
+
test('router cache config preserves current defaults', () => {
|
|
12
|
+
const cache = resolveHttpCacheConfig();
|
|
13
|
+
|
|
14
|
+
assert.equal(cache.html.dynamic.cacheControl, 'no-store, no-cache, must-revalidate, proxy-revalidate');
|
|
15
|
+
assert.equal(cache.html.dynamic.surrogateControl, 'no-store');
|
|
16
|
+
assert.equal(cache.html.static.cacheControl, 'public, max-age=0, must-revalidate');
|
|
17
|
+
assert.equal(cache.html.static.surrogateControl, false);
|
|
18
|
+
assert.equal(cache.publicAssets.dev, 'no-store');
|
|
19
|
+
assert.equal(cache.publicAssets.versioned, 'public, max-age=31536000, immutable');
|
|
20
|
+
assert.equal(cache.publicAssets.unversioned, 'public, max-age=0, must-revalidate');
|
|
21
|
+
assert.equal(cache.publicAssets.etag, undefined);
|
|
22
|
+
assert.equal(cache.publicAssets.lastModified, undefined);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('router cache config resolves granular public asset policies', () => {
|
|
26
|
+
const cache = resolveHttpCacheConfig({
|
|
27
|
+
publicAssets: {
|
|
28
|
+
dev: 'dev-cache',
|
|
29
|
+
versioned: 'versioned-cache',
|
|
30
|
+
unversioned: 'unversioned-cache',
|
|
31
|
+
etag: false,
|
|
32
|
+
lastModified: false,
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
assert.equal(cache.publicAssets.etag, false);
|
|
37
|
+
assert.equal(cache.publicAssets.lastModified, false);
|
|
38
|
+
assert.equal(
|
|
39
|
+
resolvePublicAssetCacheControl({
|
|
40
|
+
res: undefined,
|
|
41
|
+
filePath: '/app/public/client.abc123.js',
|
|
42
|
+
profile: 'prod',
|
|
43
|
+
cache: cache.publicAssets,
|
|
44
|
+
}),
|
|
45
|
+
'versioned-cache',
|
|
46
|
+
);
|
|
47
|
+
assert.equal(
|
|
48
|
+
resolvePublicAssetCacheControl({
|
|
49
|
+
res: { req: { originalUrl: '/public/client.js?v=123' } },
|
|
50
|
+
filePath: '/app/public/client.js',
|
|
51
|
+
profile: 'prod',
|
|
52
|
+
cache: cache.publicAssets,
|
|
53
|
+
}),
|
|
54
|
+
'versioned-cache',
|
|
55
|
+
);
|
|
56
|
+
assert.equal(
|
|
57
|
+
resolvePublicAssetCacheControl({
|
|
58
|
+
res: { req: { originalUrl: '/public/client.js' } },
|
|
59
|
+
filePath: '/app/public/client.js',
|
|
60
|
+
profile: 'prod',
|
|
61
|
+
cache: cache.publicAssets,
|
|
62
|
+
}),
|
|
63
|
+
'unversioned-cache',
|
|
64
|
+
);
|
|
65
|
+
assert.equal(
|
|
66
|
+
resolvePublicAssetCacheControl({
|
|
67
|
+
res: { req: { originalUrl: '/public/client.abc123.js' } },
|
|
68
|
+
filePath: '/app/public/client.abc123.js',
|
|
69
|
+
profile: 'dev',
|
|
70
|
+
cache: cache.publicAssets,
|
|
71
|
+
}),
|
|
72
|
+
'dev-cache',
|
|
73
|
+
);
|
|
74
|
+
});
|