| 1 | // Run: node --import tsx src/__tests__/transcript-scroll-diagnostics.test.ts |
| 2 | |
| 3 | import assert from "node:assert/strict"; |
| 4 | import { |
| 5 | createTranscriptScrollDiagnostics, |
| 6 | isTranscriptScrollDiagnosticsBuild, |
| 7 | } from "../lib/transcriptScrollDiagnostics"; |
| 8 | |
| 9 | let now = 1_000; |
| 10 | const diagnostics = createTranscriptScrollDiagnostics({ |
| 11 | maxEvents: 4, |
| 12 | now: () => now, |
| 13 | randomID: () => "0123456789abcdef0123456789abcdef", |
| 14 | environment: () => ({ |
| 15 | buildCommit: "test-commit", |
| 16 | buildChannel: "test", |
| 17 | platform: "windows", |
| 18 | userAgent: "Reasonix diagnostic test", |
| 19 | devicePixelRatio: 1.25, |
| 20 | viewportWidth: 1440, |
| 21 | viewportHeight: 900, |
| 22 | reducedMotion: false, |
| 23 | transcriptWidth: 1180, |
| 24 | contentWidth: 960, |
| 25 | fontSize: 14, |
| 26 | lineHeight: 23.52, |
| 27 | processFoldPreference: "auto", |
| 28 | reasoningDisplayMode: "summary", |
| 29 | }), |
| 30 | }); |
| 31 | |
| 32 | diagnostics.record("scroll", { scrollTop: 10 }); |
| 33 | assert.equal(diagnostics.getSnapshot().eventCount, 0, "idle recorder ignores events"); |
| 34 | |
| 35 | diagnostics.start(); |
| 36 | now += 10; |
| 37 | diagnostics.record("wheel", { deltaY: -120 }); |
| 38 | now += 10; |
| 39 | diagnostics.record("scroll", { |
| 40 | scrollTop: 120, |
| 41 | scrollHeight: 2_400, |
| 42 | clientHeight: 800, |
| 43 | bottomDistance: 1_480, |
| 44 | transcriptText: "PRIVATE_TRANSCRIPT_CANARY", |
| 45 | rowKey: "raw-session-row-key", |
| 46 | path: "C:\\Users\\private-user\\secret", |
| 47 | token: "TOKEN_CANARY", |
| 48 | } as never); |
| 49 | now += 10; |
| 50 | diagnostics.mark(); |
| 51 | now += 10; |
| 52 | diagnostics.record("items-rendered", { mountedRows: 12, totalRows: 100, firstVisibleIndex: 44 }); |
| 53 | now += 10; |
| 54 | diagnostics.record("scroll-write", { owner: "jump", writeKind: "scrollToIndex", targetIndex: 45 }); |
| 55 | |
| 56 | const active = diagnostics.getSnapshot(); |
| 57 | assert.equal(active.status, "recording"); |
| 58 | assert.equal(active.eventCount, 4, "ring buffer remains bounded"); |
| 59 | assert.equal(active.droppedEventCount, 2, "ring buffer reports dropped events"); |
| 60 | assert.equal(active.markerCount, 1, "issue marker is retained in summary"); |
| 61 | |
| 62 | const payload = diagnostics.stop(); |
| 63 | assert.equal(payload.schemaVersion, 2); |
| 64 | assert.equal(payload.summary.eventCount, 4); |
| 65 | assert.equal(payload.summary.droppedEventCount, 3); |
| 66 | assert.equal(payload.summary.markerCount, 1); |
| 67 | assert.equal(payload.manifest.platform, "windows"); |
| 68 | assert.equal(payload.events[payload.events.length - 1]?.type, "stop"); |
| 69 | |
| 70 | const serialized = JSON.stringify(payload); |
| 71 | for (const forbidden of [ |
| 72 | "PRIVATE_TRANSCRIPT_CANARY", |
| 73 | "raw-session-row-key", |
| 74 | "private-user", |
| 75 | "TOKEN_CANARY", |
| 76 | "transcriptText", |
| 77 | "rowKey", |
| 78 | "path", |
| 79 | "token", |
| 80 | ]) { |
| 81 | assert.equal(serialized.includes(forbidden), false, `payload excludes ${forbidden}`); |
| 82 | } |
| 83 | |
| 84 | const frozenCount = diagnostics.getSnapshot().eventCount; |
| 85 | now += 10; |
| 86 | diagnostics.record("scroll", { scrollTop: 999 }); |
| 87 | assert.equal(diagnostics.getSnapshot().eventCount, frozenCount, "stopped recorder remains frozen"); |
| 88 | |
| 89 | diagnostics.start(); |
| 90 | assert.equal(diagnostics.getSnapshot().eventCount, 1, "new recording clears the old trace"); |
| 91 | diagnostics.reset(); |
| 92 | assert.equal(diagnostics.getSnapshot().status, "idle"); |
| 93 | assert.equal(diagnostics.getSnapshot().eventCount, 0); |
| 94 | |
| 95 | const markerOverflow = createTranscriptScrollDiagnostics({ |
| 96 | maxEvents: 4, |
| 97 | now: () => now, |
| 98 | randomID: () => "fedcba9876543210fedcba9876543210", |
| 99 | environment: () => ({ |
| 100 | buildCommit: "test-commit", |
| 101 | buildChannel: "test", |
| 102 | platform: "windows", |
| 103 | userAgent: "Reasonix diagnostic test", |
| 104 | devicePixelRatio: 1, |
| 105 | viewportWidth: 1280, |
| 106 | viewportHeight: 720, |
| 107 | reducedMotion: false, |
| 108 | transcriptWidth: 1040, |
| 109 | contentWidth: 960, |
| 110 | fontSize: 14, |
| 111 | lineHeight: 23.52, |
| 112 | processFoldPreference: "expanded", |
| 113 | reasoningDisplayMode: "expanded", |
| 114 | }), |
| 115 | }); |
| 116 | markerOverflow.start(); |
| 117 | markerOverflow.mark(); |
| 118 | for (let i = 0; i < 4; i += 1) markerOverflow.record("scroll", { scrollTop: i }); |
| 119 | const overflowPayload = markerOverflow.stop(); |
| 120 | assert.equal(overflowPayload.summary.markerCount, 0, "summary only counts markers retained in the bounded trace"); |
| 121 | assert.equal(overflowPayload.events.some((event) => event.type === "mark"), false, "evicted markers do not survive serialization"); |
| 122 | |
| 123 | const detailDiagnostics = createTranscriptScrollDiagnostics({ |
| 124 | now: () => now, |
| 125 | randomID: () => "abcdef0123456789abcdef0123456789", |
| 126 | environment: () => ({ |
| 127 | buildCommit: "detail-test", |
| 128 | buildChannel: "test", |
| 129 | platform: "windows", |
| 130 | userAgent: "Reasonix detail diagnostic test", |
| 131 | devicePixelRatio: 1.38, |
| 132 | viewportWidth: 1920, |
| 133 | viewportHeight: 1080, |
| 134 | reducedMotion: true, |
| 135 | transcriptWidth: 1600, |
| 136 | contentWidth: 960, |
| 137 | fontSize: 15, |
| 138 | lineHeight: 25.2, |
| 139 | processFoldPreference: "auto", |
| 140 | reasoningDisplayMode: "legacy-collapsed", |
| 141 | }), |
| 142 | }); |
| 143 | detailDiagnostics.start(); |
| 144 | detailDiagnostics.record("row-measure", { |
| 145 | rowIndex: 44, |
| 146 | rowKind: "reasoning", |
| 147 | layoutVariant: "reasoning-summary", |
| 148 | estimateSource: "static", |
| 149 | estimatedSize: 64, |
| 150 | previousSize: 64, |
| 151 | measuredSize: 64, |
| 152 | sizeDelta: 0, |
| 153 | contentRevision: 3, |
| 154 | foldState: "closed", |
| 155 | disclosureCount: 1, |
| 156 | rowKey: "PRIVATE_ROW_KEY", |
| 157 | transcriptText: "PRIVATE_TRANSCRIPT_CANARY", |
| 158 | }); |
| 159 | detailDiagnostics.record("geometry-contract-violation", { |
| 160 | rowIndex: 45, |
| 161 | rowKind: "tool", |
| 162 | layoutVariant: "tool-collapsed", |
| 163 | estimateSource: "static", |
| 164 | estimatedSize: 37, |
| 165 | measuredSize: 55, |
| 166 | sizeDelta: 18, |
| 167 | relativeError: 18 / 37, |
| 168 | rowKey: "PRIVATE_ROW_KEY_2", |
| 169 | }); |
| 170 | detailDiagnostics.record("scroll-state", { |
| 171 | source: "jump-bottom", |
| 172 | previousMode: "manual", |
| 173 | mode: "tail-follow", |
| 174 | atBottom: true, |
| 175 | scrollable: true, |
| 176 | readerIntent: false, |
| 177 | canClaimTail: false, |
| 178 | tailCommand: true, |
| 179 | scrollTop: 4_000, |
| 180 | scrollHeight: 5_000, |
| 181 | clientHeight: 800, |
| 182 | bottomDistance: 200, |
| 183 | }); |
| 184 | detailDiagnostics.record("scroll-write", { |
| 185 | owner: "tail-follow", |
| 186 | writeKind: "scrollTo", |
| 187 | source: "jump-bottom", |
| 188 | phase: "settle", |
| 189 | targetTop: 5_000, |
| 190 | settleFrame: 2, |
| 191 | offBottomFrames: 2, |
| 192 | stagnantFrames: 0, |
| 193 | }); |
| 194 | detailDiagnostics.record("geometry-revision", { |
| 195 | geometryRevision: 12, |
| 196 | sources: ["row-measure", "items-rendered", "private-source"], |
| 197 | scrollHeight: 5_400, |
| 198 | footerHeight: 220, |
| 199 | viewport: 800, |
| 200 | mounted: 40, |
| 201 | total: 420, |
| 202 | transient: true, |
| 203 | }); |
| 204 | detailDiagnostics.record("scroll-write", { |
| 205 | owner: "anchor-compensation", |
| 206 | writeKind: "scrollBy", |
| 207 | source: "reader-stability", |
| 208 | phase: "correct-offset", |
| 209 | sequence: 13, |
| 210 | generation: 4, |
| 211 | ownershipEpoch: 8, |
| 212 | geometryRevision: 12, |
| 213 | transactionId: 3, |
| 214 | rejectedReason: "duplicate-revision-phase", |
| 215 | }); |
| 216 | const detailPayload = detailDiagnostics.stop(); |
| 217 | const rowMeasurement = detailPayload.events.find((event) => event.type === "row-measure"); |
| 218 | assert.deepEqual(rowMeasurement, { |
| 219 | t: 0, |
| 220 | type: "row-measure", |
| 221 | rowIndex: 44, |
| 222 | estimatedSize: 64, |
| 223 | previousSize: 64, |
| 224 | measuredSize: 64, |
| 225 | sizeDelta: 0, |
| 226 | contentRevision: 3, |
| 227 | disclosureCount: 1, |
| 228 | rowKind: "reasoning", |
| 229 | layoutVariant: "reasoning-summary", |
| 230 | estimateSource: "static", |
| 231 | foldState: "closed", |
| 232 | }); |
| 233 | const geometryViolation = detailPayload.events.find((event) => event.type === "geometry-contract-violation"); |
| 234 | assert.deepEqual(geometryViolation, { |
| 235 | t: 0, |
| 236 | type: "geometry-contract-violation", |
| 237 | rowIndex: 45, |
| 238 | estimatedSize: 37, |
| 239 | measuredSize: 55, |
| 240 | sizeDelta: 18, |
| 241 | relativeError: 0.49, |
| 242 | rowKind: "tool", |
| 243 | layoutVariant: "tool-collapsed", |
| 244 | estimateSource: "static", |
| 245 | }); |
| 246 | const geometryRevision = detailPayload.events.find((event) => event.type === "geometry-revision"); |
| 247 | assert.deepEqual(geometryRevision?.sources, ["row-measure", "items-rendered"], "geometry sources keep only fixed, content-free enums"); |
| 248 | const anchorWrite = detailPayload.events.find((event) => event.owner === "anchor-compensation"); |
| 249 | assert.equal(anchorWrite?.owner, "anchor-compensation", "known anchor writers do not degrade to other"); |
| 250 | assert.equal(anchorWrite?.ownershipEpoch, 8, "writer ownership epochs survive sanitization"); |
| 251 | assert.equal(anchorWrite?.rejectedReason, "duplicate-revision-phase", "writer rejection reasons survive sanitization"); |
| 252 | const detailSerialized = JSON.stringify(detailPayload); |
| 253 | assert.equal(detailSerialized.includes("PRIVATE_ROW_KEY"), false, "row measurements exclude stable row keys"); |
| 254 | assert.equal(detailSerialized.includes("PRIVATE_TRANSCRIPT_CANARY"), false, "row measurements exclude transcript text"); |
| 255 | assert.equal(detailSerialized.includes("PRIVATE_ROW_KEY_2"), false, "geometry violations exclude stable row keys"); |
| 256 | |
| 257 | assert.equal(isTranscriptScrollDiagnosticsBuild("test", false), true, "test builds expose diagnostics"); |
| 258 | assert.equal(isTranscriptScrollDiagnosticsBuild("preview", false), true, "preview builds expose diagnostics"); |
| 259 | assert.equal(isTranscriptScrollDiagnosticsBuild("canary", false), true, "canary builds expose diagnostics"); |
| 260 | assert.equal(isTranscriptScrollDiagnosticsBuild("stable", true), true, "development server exposes diagnostics"); |
| 261 | assert.equal(isTranscriptScrollDiagnosticsBuild("stable", false), false, "stable production hides diagnostics"); |
| 262 | |
| 263 | console.log("transcript scroll diagnostics tests passed"); |
| 264 |