pi-mega-compact 0.8.7 → 0.8.10

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 (110) hide show
  1. package/dist/extensions/dashboard-server/api-contracts/core.js +7 -0
  2. package/dist/extensions/dashboard-server/api-contracts/endpoints.js +104 -0
  3. package/dist/extensions/dashboard-server/api-contracts/game.js +9 -0
  4. package/dist/extensions/dashboard-server/api-contracts/index.js +9 -0
  5. package/dist/extensions/dashboard-server/api-contracts/infrastructure.js +10 -0
  6. package/dist/extensions/dashboard-server/api-contracts/multi-repo.js +9 -0
  7. package/dist/extensions/dashboard-server/api-contracts/snapshot.js +8 -0
  8. package/dist/extensions/dashboard-server/api-contracts.js +8 -0
  9. package/dist/extensions/dashboard-server/api-contracts.test.js +869 -0
  10. package/dist/extensions/dashboard-server/auth.js +18 -0
  11. package/dist/extensions/dashboard-server/helpers.js +37 -0
  12. package/dist/extensions/dashboard-server/html/all-repos-tab.js +26 -0
  13. package/dist/extensions/dashboard-server/html/body-open.js +23 -0
  14. package/dist/extensions/dashboard-server/html/current-repo-tab.js +130 -0
  15. package/dist/extensions/dashboard-server/html/head-open.js +16 -0
  16. package/dist/extensions/dashboard-server/html/high-score-tab.js +25 -0
  17. package/dist/extensions/dashboard-server/html/repo-detail-modal.js +26 -0
  18. package/dist/extensions/dashboard-server/html/script.js +259 -0
  19. package/dist/extensions/dashboard-server/html/styles.js +103 -0
  20. package/dist/extensions/dashboard-server/html/summary-tab.js +19 -0
  21. package/dist/extensions/dashboard-server/html-template.js +41 -0
  22. package/dist/extensions/dashboard-server/html.js +46 -1
  23. package/dist/extensions/dashboard-server/perf-server.test.js +80 -0
  24. package/dist/extensions/dashboard-server/server.js +269 -30
  25. package/dist/extensions/dashboard-server/tailscale.js +8 -0
  26. package/dist/extensions/dashboard-server/types.js +4 -0
  27. package/dist/extensions/mega-dashboard.js +9 -0
  28. package/dist/extensions/mega-events/perf-handler.js +71 -0
  29. package/dist/extensions/mega-events/register.js +2 -0
  30. package/dist/extensions/mega-events.js +1 -0
  31. package/dist/extensions/mega-runtime/state.js +59 -1
  32. package/dist/src/store/sqlite/connection.js +35 -0
  33. package/dist/src/store/sqlite/index-store.js +167 -0
  34. package/dist/src/store/sqlite/memory.js +54 -0
  35. package/dist/src/store/sqlite/minhash-lsh.js +47 -0
  36. package/dist/src/store/sqlite/perf-samples.js +81 -0
  37. package/dist/src/store/sqlite/perf-samples.test.js +54 -0
  38. package/dist/src/store/sqlite/schema.js +14 -0
  39. package/dist/src/store/sqlite/sessions.js +39 -0
  40. package/dist/src/store/sqlite/transaction.js +19 -0
  41. package/dist/src/store/sqlite.js +1 -0
  42. package/dist/src/vectorStore/add.js +260 -0
  43. package/dist/src/vectorStore/dedup.js +52 -0
  44. package/dist/src/vectorStore/index.js +10 -0
  45. package/dist/src/vectorStore/queries.js +83 -0
  46. package/dist/src/vectorStore/search.js +95 -0
  47. package/dist/src/vectorStore/session.js +19 -0
  48. package/dist/src/vectorStore/store.js +105 -0
  49. package/dist/src/vectorStore/types.js +6 -0
  50. package/dist/src/vectorStore/utils.js +23 -0
  51. package/extensions/dashboard-client/package-lock.json +1771 -0
  52. package/extensions/dashboard-client/package.json +27 -0
  53. package/extensions/dashboard-client/src/App.tsx +82 -0
  54. package/extensions/dashboard-client/src/api/client.ts +145 -0
  55. package/extensions/dashboard-client/src/components/CacheStatusPerModel.tsx +44 -0
  56. package/extensions/dashboard-client/src/components/CompressionCard.tsx +61 -0
  57. package/extensions/dashboard-client/src/components/ContextGauge.tsx +59 -0
  58. package/extensions/dashboard-client/src/components/DataSafetyCard.tsx +49 -0
  59. package/extensions/dashboard-client/src/components/ErrorBoundary.tsx +53 -0
  60. package/extensions/dashboard-client/src/components/EventCategoryFilter.tsx +16 -0
  61. package/extensions/dashboard-client/src/components/EventStream.tsx +152 -0
  62. package/extensions/dashboard-client/src/components/LoadingSpinner.tsx +7 -0
  63. package/extensions/dashboard-client/src/components/MemoryStatusCard.tsx +24 -0
  64. package/extensions/dashboard-client/src/components/ModelBadge.tsx +41 -0
  65. package/extensions/dashboard-client/src/components/PerfChart.tsx +160 -0
  66. package/extensions/dashboard-client/src/components/RepoDetailModal.tsx +126 -0
  67. package/extensions/dashboard-client/src/components/RepoTable.tsx +150 -0
  68. package/extensions/dashboard-client/src/components/SessionInfo.tsx +65 -0
  69. package/extensions/dashboard-client/src/components/SummaryTiles.tsx +52 -0
  70. package/extensions/dashboard-client/src/components/TabBar.tsx +34 -0
  71. package/extensions/dashboard-client/src/components/TriggerStatus.tsx +62 -0
  72. package/extensions/dashboard-client/src/hooks/useApi.ts +77 -0
  73. package/extensions/dashboard-client/src/hooks/useSSE.ts +87 -0
  74. package/extensions/dashboard-client/src/index.html +12 -0
  75. package/extensions/dashboard-client/src/main.tsx +23 -0
  76. package/extensions/dashboard-client/src/styles/base.css +121 -0
  77. package/extensions/dashboard-client/src/styles/overview-events.css +318 -0
  78. package/extensions/dashboard-client/src/styles/repos-metrics.css +307 -0
  79. package/extensions/dashboard-client/src/tabs/ConfigTab.tsx +16 -0
  80. package/extensions/dashboard-client/src/tabs/EventsTab.tsx +37 -0
  81. package/extensions/dashboard-client/src/tabs/MetricsTab.tsx +49 -0
  82. package/extensions/dashboard-client/src/tabs/OverviewTab.tsx +80 -0
  83. package/extensions/dashboard-client/src/tabs/ReposTab.tsx +59 -0
  84. package/extensions/dashboard-client/tsconfig.json +28 -0
  85. package/extensions/dashboard-client/vite.config.ts +38 -0
  86. package/extensions/dashboard-server/api-contracts/core.ts +302 -0
  87. package/extensions/dashboard-server/api-contracts/endpoints.ts +476 -0
  88. package/extensions/dashboard-server/api-contracts/game.ts +145 -0
  89. package/extensions/dashboard-server/api-contracts/index.ts +159 -0
  90. package/extensions/dashboard-server/api-contracts/infrastructure.ts +465 -0
  91. package/extensions/dashboard-server/api-contracts/multi-repo.ts +328 -0
  92. package/extensions/dashboard-server/api-contracts/snapshot.ts +386 -0
  93. package/extensions/dashboard-server/api-contracts.test.ts +1050 -0
  94. package/extensions/dashboard-server/api-contracts.ts +96 -0
  95. package/extensions/dashboard-server/auth.ts +20 -0
  96. package/extensions/dashboard-server/html.ts +46 -1
  97. package/extensions/dashboard-server/perf-server.test.ts +101 -0
  98. package/extensions/dashboard-server/server.ts +862 -503
  99. package/extensions/dashboard-server/tailscale.ts +7 -0
  100. package/extensions/dashboard-server/types.ts +23 -114
  101. package/extensions/mega-dashboard.ts +17 -0
  102. package/extensions/mega-events/perf-handler.ts +113 -0
  103. package/extensions/mega-events/register.ts +2 -0
  104. package/extensions/mega-events.ts +1 -0
  105. package/extensions/mega-runtime/state.ts +57 -0
  106. package/package.json +2 -1
  107. package/src/store/sqlite/perf-samples.test.ts +65 -0
  108. package/src/store/sqlite/perf-samples.ts +125 -0
  109. package/src/store/sqlite/schema.ts +14 -0
  110. package/src/store/sqlite.ts +1 -0
@@ -0,0 +1,96 @@
1
+ /**
2
+ * @deprecated Import from `./api-contracts/index.js` (barrel) instead.
3
+ * This file is retained for backward compatibility. All types are re-exported
4
+ * from domain modules under `api-contracts/`.
5
+ *
6
+ * See: api-contracts/core.ts, snapshot.ts, multi-repo.ts, game.ts, infrastructure.ts
7
+ */
8
+
9
+ // Re-export everything from the barrel
10
+ export type {
11
+ // Core
12
+ HttpMethod,
13
+ EndpointDef,
14
+ SseCompactStart,
15
+ SseCompactEnd,
16
+ SseCompactTrigger,
17
+ SseCompactSkip,
18
+ SseTierChanged,
19
+ SseModelChanged,
20
+ SsePressureLifted,
21
+ SseCheckpointPersisted,
22
+ SseRecallInject,
23
+ SseAnchorsUpdated,
24
+ SseConfigUpdated,
25
+ SseConfigPreset,
26
+ SseCrewPresenceChanged,
27
+ SseCrewTurnChanged,
28
+ SseCrewBanditChosen,
29
+ // Snapshot
30
+ SnapshotResponse,
31
+ TriggerResponse,
32
+ CompressionTotalsResponse,
33
+ CompactHistoryEntry,
34
+ CompactionRequest,
35
+ CompactionResponse,
36
+ // Multi-repo
37
+ RepoListItem,
38
+ RepoSnapshotEntry,
39
+ RepoSnapshotMap,
40
+ IndexesIndexRow,
41
+ IndexesSummaryResponse,
42
+ IndexesDiffEntry,
43
+ DiffRequest,
44
+ SnapshotLike,
45
+ DiffResponse,
46
+ UpdateRepoConfigRequest,
47
+ // Game
48
+ GameConfig,
49
+ GameStateResponse,
50
+ GameRitualStage,
51
+ SseGameRitualStart,
52
+ SseGameRitualStage,
53
+ SseGameRitualEnd,
54
+ SseGameModeChanged,
55
+ SseGameRender,
56
+ // Infrastructure
57
+ InfraHealthResponse,
58
+ InfraPerfSampleResponse,
59
+ InfraRateLimitStatus,
60
+ InfraRateLimitResponse,
61
+ ContextLevelState,
62
+ TierOverrideState,
63
+ FallbackState,
64
+ RepeatInjectionState,
65
+ SupersedeGatingState,
66
+ MinHashBandState,
67
+ // Composite
68
+ SseEvent,
69
+ // Endpoints registry (Sprint A1)
70
+ VersionResponse,
71
+ IndexSummary,
72
+ IndexFallbackResponse,
73
+ ReposQuery,
74
+ ReposResponse,
75
+ SummaryResponse,
76
+ DriftSeverity,
77
+ DriftSignal,
78
+ RepoDrift,
79
+ DriftReportResponse,
80
+ ServerEntry,
81
+ ServersResponse,
82
+ PerfPercentile,
83
+ PerfAverage,
84
+ PerfLatest,
85
+ PerfCacheHit,
86
+ PerfDiag,
87
+ PerfQuery,
88
+ PerfResponse,
89
+ GameScoreRow,
90
+ GameScoresQuery,
91
+ AchievementRow,
92
+ GameStatePatch,
93
+ SseEndpointDef,
94
+ } from './api-contracts/index.js';
95
+
96
+ export { ENDPOINTS } from './api-contracts/index.js';
@@ -0,0 +1,20 @@
1
+ import { randomUUID } from "node:crypto";
2
+
3
+ const CSRF_TOKENS = new Map<string, number>();
4
+ export const CSRF_MAX_AGE_MS = 60 * 60 * 1000; // 60 min rotation
5
+
6
+ export function generateCsrfToken(): string {
7
+ const token = randomUUID();
8
+ CSRF_TOKENS.set(token, Date.now());
9
+ return token;
10
+ }
11
+
12
+ export function isCsrfValid(token: string): boolean {
13
+ if (!CSRF_TOKENS.has(token)) return false;
14
+ const ts = CSRF_TOKENS.get(token) ?? 0;
15
+ if (Date.now() - ts > CSRF_MAX_AGE_MS) {
16
+ CSRF_TOKENS.delete(token);
17
+ return false;
18
+ }
19
+ return true;
20
+ }
@@ -189,6 +189,7 @@ export function dashboardHtml(tierName: string): string {
189
189
  <button class="tab" data-tab="active">Active Repos</button>
190
190
  <button class="tab" data-tab="summary">Summary</button>
191
191
  <button class="tab" data-tab="game">Game Mode</button>
192
+ <button class="tab" data-tab="perf">Perf</button>
192
193
  </nav>
193
194
 
194
195
  <!-- Current repo (existing single-repo view) -->
@@ -443,6 +444,18 @@ export function dashboardHtml(tierName: string): string {
443
444
  <div id="game-empty">No scores yet — run a session with game mode on.</div>
444
445
  </div>
445
446
 
447
+ <!-- Perf (v0.8.8) — live local instrumentation -->
448
+ <div class="tab-panel" id="panel-perf">
449
+ <div class="grid">
450
+ <div class="card"><h2>Model latency</h2><div class="stat-grid"><span class="label">Turn p50</span><span class="value" id="pf-turn-p50">—</span><span class="label">Turn p95</span><span class="value" id="pf-turn-p95">—</span><span class="label">Provider p50</span><span class="value" id="pf-prov-p50">—</span><span class="label">Provider p95</span><span class="value" id="pf-prov-p95">—</span></div></div>
451
+ <div class="card"><h2>Throughput</h2><div class="stat-grid"><span class="label">TPS (avg)</span><span class="value" id="pf-tps">—</span><span class="label">Cache hit %</span><span class="value" id="pf-cache">—</span></div></div>
452
+ <div class="card"><h2>Process</h2><div class="stat-grid"><span class="label">RSS</span><span class="value" id="pf-rss">—</span><span class="label">Heap</span><span class="value" id="pf-heap">—</span><span class="label">CPU user/sys</span><span class="value" id="pf-cpu">—</span></div></div>
453
+ <div class="card"><h2>Snapshot cost</h2><div class="stat-grid"><span class="label">DB recompute p50</span><span class="value" id="pf-db-p50">—</span><span class="label">DB recompute p95</span><span class="value" id="pf-db-p95">—</span><span class="label">Disk write p50</span><span class="value" id="pf-disk">—</span></div></div>
454
+ <div class="card"><h2>TUI lag proxy</h2><div class="stat-grid"><span class="label">Live-trim fires</span><span class="value" id="pf-recompute">—</span><span class="label">Cache replays</span><span class="value" id="pf-replays">—</span><span class="label">Fast-gate skips</span><span class="value" id="pf-skips">—</span></div><div class="meter-sub">skip vs recompute vs replay cadence</div></div>
455
+ </div>
456
+ <div class="updated" id="perf-updated">waiting for data</div>
457
+ </div>
458
+
446
459
  <script>
447
460
  (function() {
448
461
  var evBox = document.getElementById('events');
@@ -1000,9 +1013,40 @@ export function dashboardHtml(tierName: string): string {
1000
1013
  }).catch(function() {});
1001
1014
  }
1002
1015
 
1016
+ // --- Perf tab (v0.8.8) — live local instrumentation ----------------------
1017
+ var perfPollTimer = null;
1018
+ function pollPerf() {
1019
+ fetch('/api/perf?minutes=30').then(function(r) { return r.ok ? r.json() : null; }).then(function(d) { // guardrails-allow PREVENT-PI-004: browser-side fetch in dashboard HTML template (not Node runtime)
1020
+ if (!d) return;
1021
+ var el = document.getElementById('perf-updated');
1022
+ if (el) el.textContent = d.sampleCount + ' samples \u00b7 updated ' + (d.updatedAt || '');
1023
+ function setText(id, txt) { var e = document.getElementById(id); if (e) e.textContent = txt; }
1024
+ function fmtMs(v) { return v == null ? '\u2014' : (v >= 100 ? Math.round(v) + 'ms' : v.toFixed(1) + 'ms'); }
1025
+ function fmtNum(v, dec) { return v == null ? '\u2014' : (typeof v === 'number' ? v.toFixed(dec) : '\u2014'); }
1026
+ setText('pf-turn-p50', fmtMs(d.turn_latency_ms && d.turn_latency_ms.p50));
1027
+ setText('pf-turn-p95', fmtMs(d.turn_latency_ms && d.turn_latency_ms.p95));
1028
+ setText('pf-prov-p50', fmtMs(d.provider_latency_ms && d.provider_latency_ms.p50));
1029
+ setText('pf-prov-p95', fmtMs(d.provider_latency_ms && d.provider_latency_ms.p95));
1030
+ setText('pf-tps', fmtNum(d.tps && d.tps.avg, 1));
1031
+ setText('pf-cache', (d.cache_hit_pct && typeof d.cache_hit_pct.avg === 'number') ? fmtNum(d.cache_hit_pct.avg, 1) + '%' : '\u2014');
1032
+ setText('pf-rss', (d.rss_mb && typeof d.rss_mb.latest === 'number') ? fmtNum(d.rss_mb.latest, 1) + ' MB' : '\u2014');
1033
+ setText('pf-heap', (d.heap_mb && typeof d.heap_mb.latest === 'number') ? fmtNum(d.heap_mb.latest, 1) + ' MB' : '\u2014');
1034
+ setText('pf-cpu', (d.cpu_user_ms && d.cpu_sys_ms) ? (fmtNum(d.cpu_user_ms.latest,1) + ' / ' + fmtNum(d.cpu_sys_ms.latest,1) + ' ms') : '\u2014');
1035
+ setText('pf-db-p50', fmtMs(d.db_recompute_ms && d.db_recompute_ms.p50));
1036
+ setText('pf-db-p95', fmtMs(d.db_recompute_ms && d.db_recompute_ms.p95));
1037
+ setText('pf-disk', fmtMs(d.disk_write_ms && d.disk_write_ms.p50));
1038
+ var diag = d.diag || {};
1039
+ setText('pf-recompute', diag.liveTrimFires != null ? String(diag.liveTrimFires) : '\u2014');
1040
+ setText('pf-replays', diag.liveTrimReplays != null ? String(diag.liveTrimReplays) : '\u2014');
1041
+ setText('pf-skips', diag.ctxFastGate != null ? String(diag.ctxFastGate) : '\u2014');
1042
+ }).catch(function() {});
1043
+ }
1044
+ function startPerfPoll() { if (perfPollTimer) return; pollPerf(); perfPollTimer = setInterval(pollPerf, 2000); }
1045
+ function stopPerfPoll() { if (perfPollTimer) { clearInterval(perfPollTimer); perfPollTimer = null; } }
1046
+
1003
1047
  // --- Tab switching ------------------------------------------------------
1004
1048
  var tabs = document.querySelectorAll('.tab');
1005
- var panels = { current: 'panel-current', all: 'panel-all', active: 'panel-active', summary: 'panel-summary', game: 'panel-game' };
1049
+ var panels = { current: 'panel-current', all: 'panel-all', active: 'panel-active', summary: 'panel-summary', game: 'panel-game', perf: 'panel-perf' };
1006
1050
  for (var i = 0; i < tabs.length; i++) {
1007
1051
  tabs[i].addEventListener('click', function() {
1008
1052
  var name = this.getAttribute('data-tab');
@@ -1017,6 +1061,7 @@ export function dashboardHtml(tierName: string): string {
1017
1061
  if (name === 'all' || name === 'summary') pollIndex();
1018
1062
  if (name === 'active') pollServers();
1019
1063
  if (name === 'game') { renderGameScores(); renderAchievements(); }
1064
+ if (name === 'perf') startPerfPoll(); else stopPerfPoll();
1020
1065
  });
1021
1066
  }
1022
1067
  })();
@@ -0,0 +1,101 @@
1
+ /**
2
+ * perf-server.test.ts — v0.8.8 /api/perf endpoint (GET aggregates + 405).
3
+ * Mirrors the server.test.ts spawn-and-fetch harness (self-contained so the
4
+ * dashboard HTTP-port lane stays isolated).
5
+ */
6
+ import { test, describe } from "node:test";
7
+ import assert from "node:assert/strict";
8
+ import { mkdtempSync, rmSync, readFileSync } from "node:fs";
9
+ import { tmpdir } from "node:os";
10
+ import { join } from "node:path";
11
+ import { spawn } from "node:child_process";
12
+ import { recordPerfSample } from "../../src/store/sqlite.js";
13
+
14
+ const SERVER_ENTRY = new URL("./server.js", import.meta.url).pathname;
15
+
16
+ function waitFor(cond: () => boolean | Promise<boolean>, timeoutMs = 6000): Promise<void> {
17
+ const start = Date.now();
18
+ return new Promise((resolve, reject) => {
19
+ const tick = async () => {
20
+ if (await cond()) return resolve();
21
+ if (Date.now() - start > timeoutMs) return reject(new Error("timeout"));
22
+ setTimeout(tick, 50);
23
+ };
24
+ tick();
25
+ });
26
+ }
27
+
28
+ function freshDir(prefix: string): string {
29
+ return mkdtempSync(join(tmpdir(), prefix));
30
+ }
31
+
32
+ async function withServer<T>(
33
+ port: string,
34
+ dir: string,
35
+ fn: (port: number) => Promise<T>,
36
+ ): Promise<T> {
37
+ process.env.MEGACOMPACT_DASHBOARD_PORT = port;
38
+ const child = spawn(process.execPath, [SERVER_ENTRY, dir], { stdio: "ignore" });
39
+ try {
40
+ await waitFor(async () => {
41
+ try {
42
+ const raw = JSON.parse(readFileSync(join(dir, "port.pid"), "utf-8"));
43
+ const res = await fetch(`http://localhost:${raw.port}/api/version`);
44
+ return res.ok;
45
+ } catch {
46
+ return false;
47
+ }
48
+ });
49
+ const raw = JSON.parse(readFileSync(join(dir, "port.pid"), "utf-8"));
50
+ return await fn(raw.port);
51
+ } finally {
52
+ child.kill("SIGTERM");
53
+ delete process.env.MEGACOMPACT_DASHBOARD_PORT;
54
+ rmSync(dir, { recursive: true, force: true });
55
+ }
56
+ }
57
+
58
+ interface PerfResp {
59
+ sampleCount: number;
60
+ turn_latency_ms: { p50: number; p95: number; n: number };
61
+ provider_latency_ms: { p50: number; p95: number; n: number };
62
+ tps: { avg: number; n: number };
63
+ cache_hit_pct: { avg: number; latest: number; n: number };
64
+ db_recompute_ms: { p50: number; p95: number; n: number };
65
+ disk_write_ms: { p50: number; p95: number; n: number };
66
+ rss_mb: { latest: number; n: number };
67
+ heap_mb: { latest: number; n: number };
68
+ cpu_user_ms: { latest: number; n: number };
69
+ cpu_sys_ms: { latest: number; n: number };
70
+ diag: { ctxFastGate: number; liveTrimFires: number; liveTrimReplays: number } | null;
71
+ }
72
+
73
+ describe("v0.8.8 /api/perf", () => {
74
+ test("GET returns aggregates over recorded perf_samples", async () => {
75
+ const dir = freshDir("dash-perf-agg-");
76
+ recordPerfSample(dir, "turn_latency_ms", 100, { turnIndex: 1 });
77
+ recordPerfSample(dir, "turn_latency_ms", 200);
78
+ recordPerfSample(dir, "tps", 50);
79
+ recordPerfSample(dir, "rss_mb", 256);
80
+ await withServer("19440", dir, async (port) => {
81
+ const res = await fetch(`http://localhost:${port}/api/perf?minutes=30`);
82
+ assert.equal(res.status, 200);
83
+ const d = (await res.json()) as PerfResp;
84
+ assert.equal(d.sampleCount, 4);
85
+ assert.equal(d.turn_latency_ms.n, 2);
86
+ assert.equal(d.turn_latency_ms.p50, 100); // nearest-rank p50 of [100,200]
87
+ assert.equal(d.turn_latency_ms.p95, 200); // nearest-rank p95 of [100,200]
88
+ assert.equal(d.tps.avg, 50);
89
+ assert.equal(d.rss_mb.latest, 256);
90
+ assert.equal(d.diag, null); // no runtime wrote dashboard.json in this dir
91
+ });
92
+ });
93
+
94
+ test("non-GET (POST) -> 405", async () => {
95
+ const dir = freshDir("dash-perf-meth-");
96
+ await withServer("19441", dir, async (port) => {
97
+ const res = await fetch(`http://localhost:${port}/api/perf`, { method: "POST" });
98
+ assert.equal(res.status, 405);
99
+ });
100
+ });
101
+ });