返回 DeepSeek-Reasonix
firebase_migration.test.ts
根目录 / workers / crash-report / src / firebase_migration.test.ts
1 import { describe, expect, it } from "vitest";
2 import {
3 assessMigrationCapacity,
4 buildFirebaseGroups,
5 canonicalJSONString,
6 classifyMigrationGroup,
7 contentDigest,
8 firebaseOAuthGrantType,
9 runWrangler,
10 wranglerD1MaxBufferBytes,
11 } from "../scripts/migrate-firebase-data.mjs";
12
13 describe("Firebase retained-sample migration", () => {
14 it("uses the OAuth JWT bearer grant expected by Google's token endpoint", () => {
15 expect(firebaseOAuthGrantType).toBe("urn:ietf:params:oauth:grant-type:jwt-bearer");
16 });
17
18 it("explains active reservations without treating stale open groups as automatically eligible", () => {
19 const now = new Date("2026-08-25T00:00:00Z");
20 const assessment = assessMigrationCapacity([
21 { status: "open", last_seen: "2026-08-20T00:00:00Z", fingerprint: "private", message: "private" },
22 { status: "open", last_seen: "2026-07-10T00:00:00Z" },
23 { status: "open", last_seen: "2026-06-01T00:00:00Z" },
24 { status: "resolved", last_seen: "2026-08-20T00:00:00Z" },
25 { status: "resolved", last_seen: "2026-07-10T00:00:00Z" },
26 { status: "ignored", last_seen: "2026-06-01T00:00:00Z" },
27 { status: "resolved", last_seen: "invalid" },
28 { status: "future-status", last_seen: "2026-06-01T00:00:00Z" },
29 ], now);
30
31 expect(assessment.statusTotals).toEqual({ open: 3, resolved: 3, ignored: 1, other: 1 });
32 expect(assessment.activeReasons).toEqual({
33 open: 3,
34 otherStatus: 1,
35 recentResolvedOrIgnored: 1,
36 invalidResolvedOrIgnored: 1,
37 });
38 expect(assessment.automaticRetention).toEqual({ compacted: 1, archived: 1 });
39 expect(assessment.manualReview).toEqual({ open30to59d: 1, open60dPlus: 1, otherStatus30dPlus: 1 });
40 expect(JSON.stringify(assessment)).not.toContain("private");
41 });
42
43 it("captures a full retained-report page without using Node's default child-process buffer", () => {
44 let configuredMaxBuffer = 0;
45 const rows = runWrangler("/tmp/crash-worker", "reasonix-crash", "SELECT 1", (
46 _command: string,
47 _args: string[],
48 options: { maxBuffer?: number },
49 ) => {
50 configuredMaxBuffer = options.maxBuffer ?? 0;
51 return { status: 0, stdout: '[{"results":[{"value":1}]}]' };
52 });
53
54 expect(rows).toEqual([{ value: 1 }]);
55 expect(configuredMaxBuffer).toBe(wranglerD1MaxBufferBytes);
56 expect(configuredMaxBuffer).toBeGreaterThan(200 * 6 * 96 * 1024);
57 });
58
59 it("keeps the first sample and maps the newest five into absolute ring slots", () => {
60 const fingerprint = "a".repeat(64);
61 const groups = buildFirebaseGroups([{
62 fingerprint,
63 kind: "crash",
64 count: 8,
65 first_seen: "2026-08-01T00:00:00Z",
66 last_seen: "2026-08-08T00:00:00Z",
67 first_version: "v1",
68 last_version: "v2",
69 status: "open",
70 severity: "high",
71 }], [1, 4, 5, 6, 7, 8].map((id) => ({
72 id,
73 fingerprint,
74 kind: "crash",
75 version: id === 1 ? "v1" : "v2",
76 os: "linux",
77 arch: "amd64",
78 message: `sample-${id}`,
79 created_at: `2026-08-0${id}T00:00:00Z`,
80 device: "{}",
81 breadcrumbs: "[]",
82 })));
83 const entry = groups.get(fingerprint) as unknown as { state: string; value: {
84 meta: { count: number };
85 samples: { first: { message: string; groupCount: number }; latest: Record<string, { message: string; eventId: string; sampleEpoch: number }> };
86 } };
87 const value = entry.value;
88 expect(entry.state).toBe("active");
89 expect(value.meta.count).toBe(8);
90 expect(value.samples.first.message).toBe("sample-1");
91 expect(value.samples.first.groupCount).toBe(1);
92 expect(Object.values(value.samples.latest).map((sample) => sample.message).sort()).toEqual([
93 "sample-4", "sample-5", "sample-6", "sample-7", "sample-8",
94 ]);
95 expect(value.samples.latest[2].message).toBe("sample-8");
96 expect(value.samples.latest[2].eventId).toMatch(/^[0-9a-f]{32}$/);
97 expect(value.samples.latest[2]).toMatchObject({ groupCount: 8, sampleEpoch: 1 });
98 expect(JSON.stringify(value)).not.toContain("installId");
99 });
100
101 it("classifies resolved retention windows and canonicalizes readback digests", () => {
102 const now = new Date("2026-08-25T00:00:00Z");
103 expect(classifyMigrationGroup({ status: "open", last_seen: "2020-01-01T00:00:00Z" }, now)).toBe("active");
104 expect(classifyMigrationGroup({ status: "resolved", last_seen: "2026-07-10T00:00:00Z" }, now)).toBe("compacted");
105 expect(classifyMigrationGroup({ status: "ignored", last_seen: "2026-06-01T00:00:00Z" }, now)).toBe("archived");
106 expect(canonicalJSONString({ b: 2, a: { d: 4, c: 3 } })).toBe('{"a":{"c":3,"d":4},"b":2}');
107 expect(contentDigest({ b: 2, a: 1 })).toBe(contentDigest({ a: 1, b: 2 }));
108 });
109 });
110
110 lines TYPESCRIPT