返回 DeepSeek-Reasonix
index.test.ts
根目录 / workers / crash-report / src / index.test.ts
1 import { describe, expect, it } from "vitest";
2 import {
3 groupFingerprintFromPath,
4 isDevelopmentReport,
5 effectiveGroupSeverity,
6 isDevelopmentGroup,
7 isKnownNonCrashDiagnostic,
8 namespaceReportFingerprint,
9 newestReleaseVersion,
10 diagnosticWindowWhere,
11 normalizeForFingerprint,
12 Ping,
13 Metrics,
14 CLI_TELEMETRY_SCHEMA_SQL,
15 ensureCLITelemetrySchema,
16 refreshMetricUserRollup,
17 severityForReport,
18 telemetryTableNames,
19 } from "./index";
20 import { renderStats } from "./stats";
21 import clientSurfaceMigrationSQL from "../migrate-client-surface.sql?raw";
22
23 const base = {
24 kind: "crash",
25 source: "frontend.global",
26 label: "window.error",
27 errorType: "Error",
28 errorMessage: "boom",
29 topFrame: "at render (assets/index.js:1:2)",
30 };
31
32 describe("metrics compatibility", () => {
33 it("defaults old ping and metrics payloads to desktop", () => {
34 const ping = Ping.parse({
35 installId: "a".repeat(32),
36 version: "v1.20.0",
37 os: "darwin",
38 arch: "arm64",
39 });
40 const metrics = Metrics.parse({
41 version: "v1.20.0",
42 os: "darwin",
43 counters: [{ signal: "turns", bucket: "count", count: 1 }],
44 });
45 expect(ping.surface).toBe("desktop");
46 expect(metrics.surface).toBe("desktop");
47 });
48
49 it("accepts CLI surface and fixed CLI signals", () => {
50 const parsed = Metrics.safeParse({
51 surface: "cli",
52 version: "v1.20.0",
53 os: "linux",
54 counters: [
55 { signal: "cli_mode", bucket: "run", count: 1 },
56 { signal: "cli_profile", bucket: "delivery", count: 1 },
57 { signal: "cli_turn_latency", bucket: "s_5_15", count: 1 },
58 { signal: "cli_exit", bucket: "success", count: 1 },
59 ],
60 });
61 expect(parsed.success).toBe(true);
62 if (parsed.success) expect(parsed.data.surface).toBe("cli");
63 });
64
65 it("rejects invalid client surfaces", () => {
66 expect(
67 Metrics.safeParse({
68 surface: "server",
69 version: "v1.20.0",
70 os: "linux",
71 counters: [{ signal: "turns", bucket: "count", count: 1 }],
72 }).success,
73 ).toBe(false);
74 });
75
76 it("drops unknown signals without rejecting known counters in the batch", () => {
77 const payload = {
78 version: "v1.17.16",
79 os: "darwin",
80 counters: [
81 { signal: "settings_auto_plan", bucket: "off", count: 1 },
82 { signal: "cache_hit", bucket: "90_100", count: 1 },
83 ],
84 };
85
86 const parsed = Metrics.safeParse(payload);
87 expect(parsed.success).toBe(true);
88 if (!parsed.success) return;
89 expect(parsed.data.counters).toEqual([{ signal: "cache_hit", bucket: "90_100", count: 1 }]);
90 });
91
92 it("accepts an all-unknown batch as an empty no-op", () => {
93 const parsed = Metrics.safeParse({
94 version: "v1.18.0",
95 os: "darwin",
96 counters: [{ signal: "future_signal", arbitrary: "future payload" }],
97 });
98
99 expect(parsed.success).toBe(true);
100 if (!parsed.success) return;
101 expect(parsed.data.counters).toEqual([]);
102 });
103
104 it("still rejects malformed counters for known signals", () => {
105 expect(
106 Metrics.safeParse({
107 version: "v1.18.0",
108 os: "darwin",
109 counters: [{ signal: "cache_hit", bucket: "not allowed", count: 1 }],
110 }).success,
111 ).toBe(false);
112 });
113
114 it("accepts desktop lifecycle and Windows diagnostic counters", () => {
115 const parsed = Metrics.safeParse({
116 version: "v1.19.0",
117 os: "windows",
118 counters: [
119 { signal: "desktop_exit_phase", bucket: "healthy", count: 2 },
120 { signal: "desktop_uptime", bucket: "m_2_10", count: 2 },
121 { signal: "desktop_webview2_failure", bucket: "gpu_process_exited", count: 1 },
122 { signal: "desktop_restore", bucket: "timeout", count: 1 },
123 ],
124 });
125
126 expect(parsed.success).toBe(true);
127 if (!parsed.success) return;
128 expect(parsed.data.counters).toHaveLength(4);
129 });
130 });
131
132 describe("stats window and release baseline", () => {
133 it("uses an inclusive calendar window for diagnostic groups", () => {
134 expect(diagnosticWindowWhere(7)).toBe("date(last_seen) >= date('now', '-6 day')");
135 expect(diagnosticWindowWhere(30)).toBe("date(last_seen) >= date('now', '-29 day')");
136 });
137
138 it("does not promote prerelease or synthetic non-semver labels", () => {
139 expect(newestReleaseVersion(["v1.19.4", "v1.20.0-beta.1", "dev", "v9.9.9-test"])).toBe("v1.19.4");
140 });
141 });
142
143 describe("telemetry deployment order compatibility", () => {
144 it("keeps the released Desktop tables unchanged and isolates CLI rows", () => {
145 expect(telemetryTableNames("desktop")).toEqual({
146 pings: "pings",
147 metrics: "metrics",
148 metricUsers: "metric_users",
149 });
150 expect(telemetryTableNames("cli")).toEqual({
151 pings: "cli_pings",
152 metrics: "cli_metrics",
153 metricUsers: "cli_metric_users",
154 });
155 });
156
157 it("keeps the migration additive when it runs before the released Worker", () => {
158 expect(clientSurfaceMigrationSQL).not.toMatch(/\b(?:DROP|ALTER)\b/);
159 expect(clientSurfaceMigrationSQL).not.toMatch(
160 /CREATE TABLE(?: IF NOT EXISTS)?\s+(?:pings|metrics|metric_users)\b/,
161 );
162 for (const table of ["cli_pings", "cli_metrics", "cli_metric_users"]) {
163 expect(clientSurfaceMigrationSQL).toMatch(
164 new RegExp(`CREATE TABLE IF NOT EXISTS\\s+${table}\\b`),
165 );
166 }
167 });
168
169 it("keeps the migration and Worker bootstrap schema identical", () => {
170 const normalize = (sql: string) => sql.replace(/\s+/g, " ").trim().replace(/;$/, "");
171 const migration = normalize(clientSurfaceMigrationSQL);
172 for (const statement of CLI_TELEMETRY_SCHEMA_SQL) {
173 expect(migration).toContain(normalize(statement));
174 }
175 });
176
177 it("uses additive idempotent DDL when the Worker deploys before the migration", async () => {
178 const prepared: string[] = [];
179 let batches = 0;
180 const db = {
181 prepare(sql: string) {
182 prepared.push(sql);
183 return { sql };
184 },
185 async batch() {
186 batches++;
187 return [];
188 },
189 } as unknown as D1Database;
190
191 await Promise.all([
192 ensureCLITelemetrySchema({ DB: db }),
193 ensureCLITelemetrySchema({ DB: db }),
194 ]);
195
196 expect(batches).toBe(1);
197 expect(prepared).toEqual([...CLI_TELEMETRY_SCHEMA_SQL]);
198 expect(prepared.every((sql) => /CREATE (?:TABLE|INDEX) IF NOT EXISTS/.test(sql))).toBe(true);
199 expect(prepared.join("\n")).not.toMatch(/\b(?:DROP|ALTER)\b/);
200 });
201
202 it("retries schema initialization after a transient D1 failure", async () => {
203 let batches = 0;
204 const db = {
205 prepare(sql: string) {
206 return { sql };
207 },
208 async batch() {
209 batches++;
210 if (batches === 1) throw new Error("temporary D1 failure");
211 return [];
212 },
213 } as unknown as D1Database;
214
215 await expect(ensureCLITelemetrySchema({ DB: db })).rejects.toThrow("temporary D1 failure");
216 await expect(ensureCLITelemetrySchema({ DB: db })).resolves.toBeUndefined();
217 expect(batches).toBe(2);
218 });
219 });
220
221 function fakeRollupDB(options: { failAtQuery?: number } = {}) {
222 const cursor = { next_signal: 0 };
223 const queried: string[] = [];
224 const batches: string[][] = [];
225 const db = {
226 prepare(sql: string) {
227 const stmt = {
228 sql,
229 binds: [] as unknown[],
230 bind(...args: unknown[]) {
231 stmt.binds = args;
232 return stmt;
233 },
234 async first() {
235 return sql.includes("FROM metric_user_rollup_state") ? { next_signal: cursor.next_signal } : null;
236 },
237 async all() {
238 if (!sql.includes("COUNT(DISTINCT install_id)")) return { results: [] };
239 const nth = queried.length;
240 queried.push(String(stmt.binds[0]));
241 if (options.failAtQuery === nth) throw new Error("D1 DB exceeded its CPU time limit and was reset");
242 return { results: [{ bucket: "dark", total: 7 }] };
243 },
244 async run() {
245 if (sql.includes("INSERT INTO metric_user_rollup_state")) cursor.next_signal = Number(stmt.binds[0]);
246 return {};
247 },
248 };
249 return stmt;
250 },
251 async batch(stmts: { sql: string }[]) {
252 batches.push(stmts.map((s) => s.sql));
253 return [];
254 },
255 } as unknown as D1Database;
256 return { env: { DB: db } as unknown as Parameters<typeof refreshMetricUserRollup>[0], cursor, queried, batches };
257 }
258
259 describe("metric_user rollup", () => {
260 it("walks the whole signal list across runs without repeating one", async () => {
261 const { env, cursor, queried } = fakeRollupDB();
262
263 await refreshMetricUserRollup(env, 3);
264 expect(cursor.next_signal).toBe(3);
265 await refreshMetricUserRollup(env, 3);
266 expect(cursor.next_signal).toBe(6);
267
268 expect(queried).toHaveLength(6);
269 expect(new Set(queried).size).toBe(6);
270 });
271
272 it("wraps the cursor back to the start after a full pass", async () => {
273 const { env, cursor, queried } = fakeRollupDB();
274 await refreshMetricUserRollup(env, 10_000);
275 expect(queried.length).toBeGreaterThan(50);
276 expect(new Set(queried).size).toBe(queried.length);
277 expect(cursor.next_signal).toBe(0);
278 });
279
280 it("moves past a signal whose query is abandoned instead of retrying it forever", async () => {
281 const { env, cursor, queried } = fakeRollupDB({ failAtQuery: 1 });
282 await refreshMetricUserRollup(env, 3);
283 expect(queried).toHaveLength(3);
284 expect(cursor.next_signal).toBe(3);
285 });
286
287 it("replaces a signal's rows in one batch so no reader sees it half-written", async () => {
288 const { env, batches } = fakeRollupDB();
289 await refreshMetricUserRollup(env, 2);
290
291 const writes = batches.filter((b) => b.some((sql) => /DELETE FROM metric_user_rollup\b/.test(sql)));
292 expect(writes).toHaveLength(2);
293 for (const batch of writes) {
294 expect(batch[0]).toMatch(/DELETE FROM metric_user_rollup\b/);
295 expect(batch.slice(1).every((sql) => /INSERT INTO metric_user_rollup\b/.test(sql))).toBe(true);
296 }
297 });
298 });
299
300 describe("diagnostic classification", () => {
301 it("keeps development reports out of release crash priority", () => {
302 expect(isDevelopmentReport({ ...base, version: "dev-32bit" })).toBe(true);
303 expect(isDevelopmentReport({ ...base, version: "v1.40.0", channel: "dev" })).toBe(true);
304 expect(severityForReport({ ...base, version: "dev" })).toBe("low");
305 });
306
307 it("downranks browser notices and recovered React renders", () => {
308 expect(
309 isKnownNonCrashDiagnostic({ ...base, errorMessage: "ResizeObserver loop limit exceeded" }),
310 ).toBe(true);
311 expect(
312 isKnownNonCrashDiagnostic({ ...base, errorMessage: "Minified React error #520; recovered" }),
313 ).toBe(true);
314 expect(
315 severityForReport({ ...base, errorMessage: "additional File object is not a file on the disk" }),
316 ).toBe("low");
317 });
318
319 it("keeps actionable release crashes high", () => {
320 expect(severityForReport({ ...base, version: "v1.40.0", channel: "stable" })).toBe("high");
321 });
322
323 it("reclassifies historical groups before dashboard prioritization", () => {
324 expect(
325 effectiveGroupSeverity({
326 fingerprint: "a".repeat(64),
327 severity: "high",
328 title: "[window.error] ResizeObserver loop limit exceeded",
329 }),
330 ).toBe("low");
331 expect(
332 effectiveGroupSeverity({
333 fingerprint: `dev:${"b".repeat(64)}`,
334 severity: "critical",
335 title: "[window.error] ResizeObserver loop limit exceeded",
336 }),
337 ).toBe("critical");
338 });
339
340 it("keeps ambiguous legacy history out of the development-only lane", () => {
341 const fingerprint = "c".repeat(64);
342 const stableThenDevelopment = {
343 fingerprint,
344 severity: "high",
345 title: "[window.error] actionable release crash",
346 first_version: "v1.17.15",
347 last_version: "dev-32bit",
348 last_channel: "dev",
349 };
350 const developmentThenStable = {
351 ...stableThenDevelopment,
352 first_version: "dev-32bit",
353 last_version: "v1.17.15",
354 last_channel: "stable",
355 };
356 // A retained first/last summary cannot distinguish a dev-only group from
357 // dev -> stable -> dev once the middle release sample has been pruned.
358 const developmentAroundStable = {
359 ...stableThenDevelopment,
360 first_version: "dev-32bit",
361 last_version: "dev-32bit",
362 last_channel: "dev",
363 };
364 expect(isDevelopmentGroup(stableThenDevelopment)).toBe(false);
365 expect(effectiveGroupSeverity(stableThenDevelopment)).toBe("high");
366 expect(isDevelopmentGroup(developmentThenStable)).toBe(false);
367 expect(effectiveGroupSeverity(developmentThenStable)).toBe("high");
368 expect(isDevelopmentGroup(developmentAroundStable)).toBe(false);
369 expect(effectiveGroupSeverity(developmentAroundStable)).toBe("high");
370 });
371 });
372
373 describe("development fingerprint namespace", () => {
374 const hash = "d".repeat(64);
375
376 it("preserves stable fingerprints and isolates development reports", () => {
377 expect(namespaceReportFingerprint(hash, false)).toBe(hash);
378 expect(namespaceReportFingerprint(hash, true)).toBe(`dev:${hash}`);
379 });
380
381 it("recognizes namespaced development groups independently of version labels", () => {
382 expect(
383 isDevelopmentGroup({
384 fingerprint: `dev:${hash}`,
385 }),
386 ).toBe(true);
387 });
388
389 it("keeps namespaced fingerprints reachable from dashboard links", () => {
390 expect(groupFingerprintFromPath(`/stats/group/dev:${hash}`)).toBe(`dev:${hash}`);
391 expect(groupFingerprintFromPath(`/stats/group/${hash}`)).toBe(hash);
392 expect(groupFingerprintFromPath("/stats/group/dev:not-a-hash")).toBe(null);
393 });
394 });
395
396 describe("opaque crash fingerprints", () => {
397 const opaque = {
398 kind: "crash",
399 source: "frontend.global",
400 label: "window.error",
401 errorType: "string",
402 errorMessage: "Script error.",
403 message: "[window.error]\n\nScript error.",
404 topFrame: "",
405 };
406
407 it("splits locationless Script error reports by safe context hint", () => {
408 const startup = normalizeForFingerprint({ ...opaque, fingerprintHint: "build:abc|view:app://reasonix/|cats:startup>tabs" });
409 const markdown = normalizeForFingerprint({ ...opaque, fingerprintHint: "build:abc|view:app://reasonix/|cats:render>markdown" });
410 expect(startup).not.toBe(markdown);
411 });
412
413 it("preserves grouping when old clients omit the optional hint", () => {
414 expect(normalizeForFingerprint(opaque)).toBe(normalizeForFingerprint({ ...opaque, fingerprintHint: "" }));
415 expect(normalizeForFingerprint(opaque)).toBe(
416 "crash\nfrontend.global\nwindow.error\nstring\n\nScript error.",
417 );
418 });
419 });
420
421 describe("diagnostics dashboard lanes", () => {
422 it("keeps release, performance, development, and notices out of one another's priority lists", () => {
423 type StatsData = Parameters<typeof renderStats>[0];
424 const row = {
425 fingerprint: "fingerprint",
426 kind: "crash",
427 count: 1,
428 first_version: "v1.40.0",
429 last_version: "v1.40.0",
430 seen: "2026-07-19",
431 status: "open",
432 title: "release-actionable",
433 source: "frontend.global",
434 label: "window.error",
435 error_type: "Error",
436 top_frame: "at render",
437 severity: "high",
438 last_os: "windows",
439 last_arch: "amd64",
440 last_channel: "stable",
441 regressed_at: "",
442 };
443 const data: StatsData = {
444 daily: [],
445 versions: [],
446 platforms: [],
447 crashes: [
448 row,
449 { ...row, fingerprint: "perf", kind: "performance", title: "performance-only", severity: "medium" },
450 {
451 ...row,
452 fingerprint: `dev:${"e".repeat(64)}`,
453 title: "development-only",
454 last_version: "dev-32bit",
455 last_channel: "DEV",
456 severity: "low",
457 development: true,
458 },
459 { ...row, fingerprint: "notice", title: "browser-notice-only", severity: "low" },
460 ],
461 metrics: [],
462 previousMetrics: [],
463 metricUsers: [],
464 metricUsersUnavailable: false,
465 metricUsersComputedAt: "",
466 sources: [],
467 overview: { latestAdoptionPct: null, openReports: 4, newLatestReports: 0, regressedReports: 0, criticalOpenReports: 1 },
468 latestVersion: "v1.40.0",
469 filters: {
470 surface: "desktop",
471 status: "",
472 source: "",
473 version: "",
474 os: "",
475 platform: "",
476 newLatest: false,
477 regressed: false,
478 windowDays: 30,
479 preferenceMode: "users",
480 },
481 };
482
483 const html = renderStats(
484 data,
485 { id: 1, email: "admin@example.com", role: "admin", created_at: "", approved_at: "" },
486 "diagnostics",
487 );
488 const releaseLane = html.slice(html.indexOf("Needs attention"), html.indexOf("Performance signals"));
489 const performanceLane = html.slice(html.indexOf("Performance signals"), html.indexOf("Development diagnostics"));
490 const developmentLane = html.slice(html.indexOf("Development diagnostics"), html.indexOf("Report filters"));
491
492 expect(releaseLane).toContain("release-actionable");
493 expect(releaseLane).not.toContain("performance-only");
494 expect(releaseLane).not.toContain("development-only");
495 expect(releaseLane).not.toContain("browser-notice-only");
496 expect(performanceLane).toContain("performance-only");
497 expect(performanceLane).not.toContain("development-only");
498 expect(developmentLane).toContain("development-only");
499 });
500
501 it("preserves the CLI surface in dashboard navigation and filters", () => {
502 type StatsData = Parameters<typeof renderStats>[0];
503 const data: StatsData = {
504 daily: [],
505 versions: [],
506 platforms: [],
507 crashes: [],
508 metrics: [],
509 previousMetrics: [],
510 metricUsers: [],
511 metricUsersUnavailable: false,
512 metricUsersComputedAt: "",
513 sources: [],
514 overview: { latestAdoptionPct: null, openReports: 0, newLatestReports: 0, regressedReports: 0, criticalOpenReports: 0 },
515 latestVersion: "",
516 filters: {
517 surface: "cli",
518 status: "",
519 source: "",
520 version: "",
521 os: "",
522 platform: "",
523 newLatest: false,
524 regressed: false,
525 windowDays: 30,
526 preferenceMode: "users",
527 },
528 };
529 const html = renderStats(
530 data,
531 { id: 1, email: "viewer@example.com", role: "viewer", created_at: "", approved_at: "" },
532 "usage",
533 );
534 expect(html).toContain("surface=cli");
535 expect(html).toContain('aria-label="Client surface"');
536 expect(html).toContain('href="/stats"');
537 });
538
539 it("says the deduplication did not finish instead of showing an empty dashboard", () => {
540 type StatsData = Parameters<typeof renderStats>[0];
541 const data: StatsData = {
542 daily: [],
543 versions: [],
544 platforms: [],
545 crashes: [],
546 metrics: [],
547 previousMetrics: [],
548 metricUsers: [],
549 metricUsersUnavailable: true,
550 metricUsersComputedAt: "",
551 sources: [],
552 overview: { latestAdoptionPct: null, openReports: 0, newLatestReports: 0, regressedReports: 0, criticalOpenReports: 0 },
553 latestVersion: "",
554 filters: {
555 surface: "desktop",
556 status: "",
557 source: "",
558 version: "",
559 os: "",
560 platform: "",
561 newLatest: false,
562 regressed: false,
563 windowDays: 30,
564 preferenceMode: "users",
565 },
566 };
567 const html = renderStats(
568 data,
569 { id: 1, email: "viewer@example.com", role: "viewer", created_at: "", approved_at: "" },
570 "preferences",
571 );
572 const installs = html.slice(html.indexOf("Deduplicated installs"), html.indexOf("Launch/open snapshots"));
573 expect(installs).toContain("deduplication");
574 expect(installs).toContain("window=7d");
575 expect(installs).not.toContain("No settings preference metrics yet");
576 });
577
578 it("shows how old the precomputed window is", () => {
579 type StatsData = Parameters<typeof renderStats>[0];
580 const data: StatsData = {
581 daily: [],
582 versions: [],
583 platforms: [],
584 crashes: [],
585 metrics: [],
586 previousMetrics: [],
587 metricUsers: [{ signal: "settings_theme", bucket: "dark", total: 12 }],
588 metricUsersUnavailable: false,
589 metricUsersComputedAt: "2026-08-03T07:28:41.019Z",
590 sources: [],
591 overview: { latestAdoptionPct: null, openReports: 0, newLatestReports: 0, regressedReports: 0, criticalOpenReports: 0 },
592 latestVersion: "",
593 filters: {
594 surface: "desktop",
595 status: "",
596 source: "",
597 version: "",
598 os: "",
599 platform: "",
600 newLatest: false,
601 regressed: false,
602 windowDays: 30,
603 preferenceMode: "users",
604 },
605 };
606 const user = { id: 1, email: "viewer@example.com", role: "viewer", created_at: "", approved_at: "" } as const;
607 expect(renderStats(data, user, "preferences")).toContain("2026-08-03 07:28Z");
608 });
609
610 it("shows deduplicated affected installs on agent health", () => {
611 type StatsData = Parameters<typeof renderStats>[0];
612 const data: StatsData = {
613 daily: [],
614 versions: [],
615 platforms: [],
616 crashes: [],
617 metrics: [{ signal: "desktop_hang", bucket: "windows_ui_thread", total: 12 }],
618 previousMetrics: [],
619 metricUsers: [{ signal: "desktop_hang", bucket: "windows_ui_thread", total: 3 }],
620 metricUsersUnavailable: false,
621 metricUsersComputedAt: "",
622 sources: [],
623 overview: { latestAdoptionPct: null, openReports: 0, newLatestReports: 0, regressedReports: 0, criticalOpenReports: 0 },
624 latestVersion: "v1.19.4",
625 filters: {
626 surface: "desktop",
627 status: "",
628 source: "",
629 version: "",
630 os: "",
631 platform: "",
632 newLatest: false,
633 regressed: false,
634 windowDays: 7,
635 preferenceMode: "users",
636 },
637 };
638 const html = renderStats(
639 data,
640 { id: 1, email: "viewer@example.com", role: "viewer", created_at: "", approved_at: "" },
641 "health",
642 );
643 const installs = html.slice(html.indexOf("Affected installs"), html.indexOf("Signal distributions"));
644 expect(installs).toContain("Desktop hangs");
645 expect(installs).toContain(">3<");
646 expect(installs).not.toContain(">12<");
647 });
648 });
649
649 lines TYPESCRIPT