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.
Files changed (51) hide show
  1. package/AGENTS.md +5 -3
  2. package/README.md +50 -12
  3. package/agents/project/AGENTS.md +47 -10
  4. package/agents/project/CODING_STYLE.md +5 -1
  5. package/agents/project/client/AGENTS.md +2 -0
  6. package/agents/project/diagnostics.md +8 -5
  7. package/agents/project/optimizations.md +1 -0
  8. package/agents/project/root/AGENTS.md +18 -10
  9. package/agents/project/tests/AGENTS.md +6 -1
  10. package/agents/project/tests/e2e/AGENTS.md +13 -0
  11. package/agents/project/tests/e2e/REAL_WORLD_JOURNEY_TESTS.md +192 -0
  12. package/cli/commands/check.ts +21 -3
  13. package/cli/commands/configure.ts +1 -0
  14. package/cli/commands/connect.ts +40 -4
  15. package/cli/commands/diagnose.ts +136 -5
  16. package/cli/commands/doctor.ts +24 -4
  17. package/cli/commands/explain.ts +105 -6
  18. package/cli/commands/mcp.ts +16 -0
  19. package/cli/commands/orient.ts +66 -3
  20. package/cli/commands/perf.ts +118 -13
  21. package/cli/commands/runtime.ts +151 -0
  22. package/cli/commands/trace.ts +116 -21
  23. package/cli/mcp/provider.ts +365 -0
  24. package/cli/mcp/stdio.ts +16 -0
  25. package/cli/presentation/commands.ts +79 -22
  26. package/cli/presentation/devSession.ts +2 -0
  27. package/cli/runtime/commands.ts +95 -12
  28. package/cli/utils/agentOutput.ts +46 -0
  29. package/cli/utils/agents.ts +225 -48
  30. package/common/dev/inspection.ts +30 -9
  31. package/common/dev/mcpPayloads.ts +736 -0
  32. package/common/dev/mcpServer.ts +254 -0
  33. package/docs/agent-routing.md +126 -0
  34. package/docs/dev-commands.md +2 -0
  35. package/docs/dev-sessions.md +2 -1
  36. package/docs/diagnostics.md +68 -23
  37. package/docs/mcp.md +149 -0
  38. package/docs/migrate-from-2.1.3.md +15 -5
  39. package/docs/request-tracing.md +12 -6
  40. package/eslint.js +220 -0
  41. package/package.json +2 -1
  42. package/server/app/devMcp.ts +159 -0
  43. package/server/services/router/http/cache.ts +116 -0
  44. package/server/services/router/http/index.ts +94 -35
  45. package/server/services/router/index.ts +8 -11
  46. package/tests/agents-utils.test.cjs +89 -11
  47. package/tests/dev-transpile-watch.test.cjs +117 -8
  48. package/tests/eslint-rules.test.cjs +110 -0
  49. package/tests/inspection.test.cjs +67 -0
  50. package/tests/mcp.test.cjs +127 -0
  51. 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
- test('proteum dev invalidates client assets and reloads for transpiled package scripts and styles', { timeout: 180000 }, async () => {
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, () => output);
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
+ });