| 1 | import assert from "node:assert/strict"; |
| 2 | import { |
| 3 | analyzeFrontendDiagnosticAnomalies, |
| 4 | createFrontendDiagnostics, |
| 5 | isFrontendDiagnosticsBuild, |
| 6 | isSupportedFrontendDiagnosticSchemaVersion, |
| 7 | } from "../lib/frontendDiagnostics"; |
| 8 | |
| 9 | let now = 10_000; |
| 10 | const diagnostics = createFrontendDiagnostics({ |
| 11 | maxEvents: 32, |
| 12 | now: () => now, |
| 13 | randomID: () => "0123456789abcdef0123456789abcdef", |
| 14 | environment: () => ({ |
| 15 | buildCommit: "test-commit", |
| 16 | buildChannel: "canary", |
| 17 | platform: "windows", |
| 18 | userAgent: "Reasonix test", |
| 19 | devicePixelRatio: 1.25, |
| 20 | viewportWidth: 1440, |
| 21 | viewportHeight: 900, |
| 22 | language: "zh-CN", |
| 23 | reducedMotion: false, |
| 24 | }), |
| 25 | }); |
| 26 | |
| 27 | diagnostics.record("app", "app.surface", { tabId: "private-tab", transcriptText: "PRIVATE_TEXT" }); |
| 28 | assert.equal(diagnostics.getSnapshot().eventCount, 0, "idle recorder ignores events"); |
| 29 | |
| 30 | diagnostics.start(); |
| 31 | now += 10; |
| 32 | diagnostics.record("browser", "click", { |
| 33 | target: "button", |
| 34 | path: "C:\\Users\\private-user\\secret", |
| 35 | token: "PRIVATE_TOKEN", |
| 36 | text: "PRIVATE_TEXT", |
| 37 | }); |
| 38 | now += 10; |
| 39 | diagnostics.record("transcript", "transcript.row-measure", { |
| 40 | rowIndex: 44, |
| 41 | rowKind: "answer", |
| 42 | layoutVersion: "1:42", |
| 43 | layoutVariant: "text-flow", |
| 44 | estimateSource: "static", |
| 45 | estimatedSize: 1_800, |
| 46 | measuredSize: 420, |
| 47 | sizeDelta: -1_380, |
| 48 | contentRevision: 3, |
| 49 | }); |
| 50 | diagnostics.record("history", "history.items-patch", { |
| 51 | patchCount: 2, |
| 52 | contentRevision: 4, |
| 53 | }); |
| 54 | diagnostics.record("transcript", "transcript.scroll-write", { |
| 55 | source: "recovery-end", |
| 56 | owner: "recovery", |
| 57 | writeKind: "scrollToIndex", |
| 58 | targetIndex: "LAST", |
| 59 | targetTop: 720, |
| 60 | previousMode: "manual", |
| 61 | canClaimTail: false, |
| 62 | tailCommand: true, |
| 63 | }); |
| 64 | diagnostics.mark(); |
| 65 | for (let index = 0; index < 40; index += 1) { |
| 66 | now += 1; |
| 67 | diagnostics.record("browser", "scroll", { scrollTop: index * 10, scrollHeight: 2_400, clientHeight: 800 }); |
| 68 | } |
| 69 | diagnostics.record("transcript", "transcript.scroll-write", { |
| 70 | source: "recovery-end", |
| 71 | owner: "recovery", |
| 72 | writeKind: "scrollToIndex", |
| 73 | targetIndex: "LAST", |
| 74 | targetTop: 720, |
| 75 | }); |
| 76 | diagnostics.record("transcript", "transcript.row-measure", { |
| 77 | layoutVersion: "1:42", |
| 78 | layoutVariant: "text-flow", |
| 79 | estimateSource: "static", |
| 80 | measuredSize: 420, |
| 81 | }); |
| 82 | diagnostics.record("history", "history.items-patch", { patchCount: 2, contentRevision: 4 }); |
| 83 | diagnostics.record("workspace", "workspace.session-directory", { |
| 84 | workspaceSessions: 7, |
| 85 | visibleSessions: 5, |
| 86 | hiddenSessions: 2, |
| 87 | hiddenByCollapsed: 1, |
| 88 | hiddenByTruncation: 1, |
| 89 | runtimeOnlySessions: 1, |
| 90 | recoveryCopies: 2, |
| 91 | changeReason: "recovery-copy", |
| 92 | workspaceRoot: "C:\\Users\\private-user\\workspace", |
| 93 | topicId: "private-topic-id", |
| 94 | label: "PRIVATE_TITLE", |
| 95 | }); |
| 96 | |
| 97 | const active = diagnostics.getSnapshot(); |
| 98 | assert.equal(active.status, "recording"); |
| 99 | assert.equal(active.eventCount, 32, "ring buffer remains bounded"); |
| 100 | assert.ok(active.droppedEventCount > 0, "overflow is visible in the summary"); |
| 101 | assert.equal(active.markerCount, 0, "evicted markers are not counted as retained"); |
| 102 | |
| 103 | const payload = diagnostics.stop(); |
| 104 | assert.equal(payload.schemaVersion, 2); |
| 105 | assert.equal(isSupportedFrontendDiagnosticSchemaVersion(1), true, "schema v1 remains readable by replay tools"); |
| 106 | assert.equal(isSupportedFrontendDiagnosticSchemaVersion(2), true, "schema v2 is the current capture format"); |
| 107 | assert.equal(isSupportedFrontendDiagnosticSchemaVersion(3), false, "unknown future schemas are rejected explicitly"); |
| 108 | assert.equal(payload.manifest.platform, "windows"); |
| 109 | assert.equal(payload.events[payload.events.length - 1]?.type, "stop"); |
| 110 | const serialized = JSON.stringify(payload); |
| 111 | assert.equal(serialized.includes("scrollToIndex"), true, "scroll writer details remain available for analysis"); |
| 112 | assert.equal(serialized.includes("targetIndex"), true, "scroll target remains available for analysis"); |
| 113 | assert.equal(serialized.includes("recovery-end"), true, "scroll transition source remains available for analysis"); |
| 114 | assert.equal(serialized.includes("history.items-patch"), true, "history patch event remains available for analysis"); |
| 115 | assert.equal(serialized.includes("layoutVersion"), true, "row layout version remains available for analysis"); |
| 116 | assert.equal(serialized.includes("layoutVariant"), true, "row layout variant remains available for analysis"); |
| 117 | assert.equal(serialized.includes("estimateSource"), true, "height estimate source remains available for analysis"); |
| 118 | assert.equal(serialized.includes("workspace.session-directory"), true, "sidebar directory event remains available for analysis"); |
| 119 | assert.equal(serialized.includes("workspaceSessions"), true, "sidebar session counts remain available for analysis"); |
| 120 | assert.ok(Array.isArray(payload.summary.anomalies), "payload includes automatic anomaly analysis"); |
| 121 | for (const forbidden of ["PRIVATE_TEXT", "PRIVATE_TOKEN", "private-user", "private-tab", "path", "token", "tabId"]) { |
| 122 | assert.equal(serialized.includes(forbidden), false, `payload excludes ${forbidden}`); |
| 123 | } |
| 124 | |
| 125 | const frozenCount = diagnostics.getSnapshot().eventCount; |
| 126 | diagnostics.record("browser", "scroll", { scrollTop: 999 }); |
| 127 | assert.equal(diagnostics.getSnapshot().eventCount, frozenCount, "stopped recorder remains frozen"); |
| 128 | |
| 129 | assert.equal(isFrontendDiagnosticsBuild("test", false), true); |
| 130 | assert.equal(isFrontendDiagnosticsBuild("canary", false), true); |
| 131 | assert.equal(isFrontendDiagnosticsBuild("preview", false), true); |
| 132 | assert.equal(isFrontendDiagnosticsBuild("stable", true), true); |
| 133 | assert.equal(isFrontendDiagnosticsBuild("stable", false), false); |
| 134 | |
| 135 | assert.deepEqual(analyzeFrontendDiagnosticAnomalies([ |
| 136 | { t: 0, type: "navigation.begin", intent: 7 }, |
| 137 | { t: 1, type: "workspace.session-directory", workspaceSessions: 8 }, |
| 138 | { t: 2, type: "workspace.session-directory", workspaceSessions: 7 }, |
| 139 | { t: 3, type: "navigation.settle", intent: 7 }, |
| 140 | { t: 4, type: "history.older-request", trigger: "viewport-user" }, |
| 141 | { t: 5, type: "transcript.scroll-write", owner: "unknown-writer" }, |
| 142 | ]), [ |
| 143 | { code: "settle-before-paint-ready", intent: 7 }, |
| 144 | { code: "navigation-session-count-changed", intent: 7, count: 2 }, |
| 145 | { code: "viewport-older-without-user-input", count: 1 }, |
| 146 | { code: "unknown-scroll-writer", count: 1 }, |
| 147 | ], "automatic analysis reports ordering, paging, catalog, and writer anomalies"); |
| 148 | |
| 149 | assert.deepEqual(analyzeFrontendDiagnosticAnomalies([ |
| 150 | { t: 0, type: "history.viewport-permit" }, |
| 151 | { t: 1, type: "history.older-request", trigger: "viewport-user" }, |
| 152 | ]), [], "a viewport page request consumes one explicit user permit without a false anomaly"); |
| 153 | assert.deepEqual(analyzeFrontendDiagnosticAnomalies([ |
| 154 | { t: 0, type: "navigation.begin", intent: 9 }, |
| 155 | { t: 1, type: "navigation.settle", intent: 9, outcome: "failed" }, |
| 156 | ]), [], "a failed data terminal may release its mask without a paint-ready false positive"); |
| 157 | |
| 158 | console.log("frontend diagnostics tests passed"); |
| 159 |