Skip to content

Commit 1b9174f

Browse files
committed
test(recording-viewer): Vitest infrastructure with fixture-based parser tests
- Install vitest 4.1.5, add test/test:watch scripts, vitest.config.ts (node env) - Add resolveSchemaVersion() to parseSession.ts with precedence: metadata.schemaVersion > sessionStart.schemaVersion > 1 (legacy) - Extend LoadedSession with required schemaVersion: number field - Update App.tsx API load path to call resolveSchemaVersion() and pass it into setSession() - Improve parseEventsFile() malformed-line handling: console.warn with 1-based line number - Sync generated types: add schemaVersion to SessionStartEvent/SessionMetadata, recorderVersion to SessionMetadata (aligns with extension source) - Add 4 fixture recordings: v1-basic, v2-basic, v2-no-metadata, malformed-last-line - Add tsconfig.test.json for test type-checking scope - 23 parser tests covering version resolution, new event types, malformed lines, legacy buildResult shape
1 parent bd05235 commit 1b9174f

15 files changed

Lines changed: 689 additions & 9 deletions

File tree

recording-viewer/package-lock.json

Lines changed: 363 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

recording-viewer/package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88
"build": "tsc -b && vite build",
99
"lint": "eslint .",
1010
"preview": "vite preview",
11-
"sync-types": "node scripts/sync-types.mjs"
11+
"sync-types": "node scripts/sync-types.mjs",
12+
"test": "vitest run",
13+
"test:watch": "vitest"
1214
},
1315
"dependencies": {
1416
"react": "^19.2.4",
@@ -27,6 +29,7 @@
2729
"globals": "^17.4.0",
2830
"typescript": "~5.9.3",
2931
"typescript-eslint": "^8.56.1",
30-
"vite": "^8.0.0"
32+
"vite": "^8.0.0",
33+
"vitest": "4.1.5"
3134
}
3235
}

recording-viewer/src/App.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { useState, useCallback, useRef, useMemo } from 'react';
2-
import type { Annotation, AnnotationLabel, LoadedSession, RecordedEvent, SessionMetadata, ReplayEqSnapshot, VideoSyncConfig } from './types';
2+
import type { Annotation, AnnotationLabel, LoadedSession, RecordedEvent, SessionMetadata, SessionStartEvent, ReplayEqSnapshot, VideoSyncConfig } from './types';
3+
import { resolveSchemaVersion } from './parseSession';
34
import { FileDropZone } from './components/FileDropZone';
45
import { RecordingInfo } from './components/RecordingInfo';
56
import { SessionList } from './components/SessionList';
@@ -102,7 +103,9 @@ function App() {
102103
setIsVideoPlaying(false);
103104
videoTimeRef.current = 0;
104105
setZoomedXDomain(null);
105-
setSession({ metadata, events, fileName: sessionId, replayEq, annotations: loadedAnnotations });
106+
const firstSessionStart = events.find(e => e.type === 'sessionStart') as SessionStartEvent | undefined;
107+
const schemaVersion = resolveSchemaVersion(metadata, firstSessionStart);
108+
setSession({ metadata, events, fileName: sessionId, schemaVersion, replayEq, annotations: loadedAnnotations });
106109
} catch (err) {
107110
console.error('Failed to load session:', err);
108111
} finally {

recording-viewer/src/generated/recordingTypes.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ export interface SessionStartEvent {
8080
exerciseId: number;
8181
participantId: string | undefined;
8282
exerciseRoot?: string;
83+
/** Schema version for forward-compat parsing. Introduced in Block AB (version 2). */
84+
schemaVersion?: number;
8385
}
8486

8587
export interface SessionEndEvent {
@@ -277,4 +279,8 @@ export interface SessionMetadata {
277279
startTime: number;
278280
endTime: number | undefined;
279281
eventCount: number;
282+
/** Schema version for forward-compat parsing. Introduced in Block D (version 2). */
283+
schemaVersion?: number;
284+
/** Recorder version string, set by storageWriter at write time. */
285+
recorderVersion?: string;
280286
}

recording-viewer/src/parseSession.ts

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,33 @@
1-
import type { RecordedEvent, SessionMetadata, LoadedSession } from './types';
1+
import type { RecordedEvent, SessionMetadata, SessionStartEvent } from './types';
2+
import type { LoadedSession } from './types';
3+
4+
/**
5+
* Resolve the schema version for a session.
6+
*
7+
* Precedence (highest to lowest):
8+
* 1. metadata.schemaVersion — written by storageWriter at flush time
9+
* 2. firstSessionStartEvent.schemaVersion — written inline by recorder
10+
* 3. 1 — legacy recordings that pre-date versioning
11+
*/
12+
export function resolveSchemaVersion(
13+
metadata: SessionMetadata | undefined | null,
14+
firstSessionStartEvent: SessionStartEvent | undefined,
15+
): number {
16+
if (metadata?.schemaVersion != null) return metadata.schemaVersion;
17+
if (firstSessionStartEvent?.schemaVersion != null) return firstSessionStartEvent.schemaVersion;
18+
return 1;
19+
}
220

321
export function parseEventsFile(text: string): RecordedEvent[] {
422
const events: RecordedEvent[] = [];
5-
for (const line of text.split('\n')) {
23+
const lines = text.split('\n');
24+
for (let i = 0; i < lines.length; i++) {
25+
const line = lines[i];
626
if (line.trim().length === 0) continue;
727
try {
828
events.push(JSON.parse(line) as RecordedEvent);
929
} catch {
10-
// Skip malformed JSONL lines
30+
console.warn(`[parseSession] Skipping malformed JSONL line ${i + 1}: ${line.slice(0, 80)}`);
1131
}
1232
}
1333
return events;
@@ -49,5 +69,8 @@ export async function parseDroppedFiles(files: FileList): Promise<LoadedSession>
4969
fileName = files[0].name;
5070
}
5171

52-
return { metadata, events, fileName };
72+
const firstSessionStart = events.find((e): e is SessionStartEvent => e.type === 'sessionStart');
73+
const schemaVersion = resolveSchemaVersion(metadata, firstSessionStart);
74+
75+
return { metadata, events, fileName, schemaVersion };
5376
}

recording-viewer/src/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ export interface LoadedSession {
5555
metadata: SessionMetadata | null;
5656
events: RecordedEvent[];
5757
fileName: string;
58+
/** Resolved schema version: metadata.schemaVersion > sessionStart.schemaVersion > 1 */
59+
schemaVersion: number;
5860
replayEq?: ReplayEqSnapshot[];
5961
annotations?: Annotation[];
6062
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{"type":"sessionStart","timestamp":1700300000000,"exerciseId":55,"participantId":"student-4"}
2+
{"type":"textChange","timestamp":1700300001000,"uri":"file:///workspace/src/Broken.java","changes":[{"range":{"startLine":0,"startCharacter":0,"endLine":0,"endCharacter":0},"rangeOffset":0,"rangeLength":0,"text":"class Broken {"}]}
3+
{"type":"save","timestamp":1700300002000,"uri":"file:///workspace/src/Broken.java"}
4+
{"type":"textChange","ti
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{"type":"sessionStart","timestamp":1700000000000,"exerciseId":42,"participantId":"student-1"}
2+
{"type":"textChange","timestamp":1700000001000,"uri":"file:///workspace/src/Main.java","changes":[{"range":{"startLine":0,"startCharacter":0,"endLine":0,"endCharacter":0},"rangeOffset":0,"rangeLength":0,"text":"public class Main {"}]}
3+
{"type":"save","timestamp":1700000002000,"uri":"file:///workspace/src/Main.java"}
4+
{"type":"textChange","timestamp":1700000003000,"uri":"file:///workspace/src/Main.java","changes":[{"range":{"startLine":1,"startCharacter":0,"endLine":1,"endCharacter":0},"rangeOffset":19,"rangeLength":0,"text":" public static void main(String[] args) {}"}]}
5+
{"type":"save","timestamp":1700000004000,"uri":"file:///workspace/src/Main.java"}
6+
{"type":"sessionEnd","timestamp":1700000005000,"exerciseId":42}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"sessionId": "v1-basic-session",
3+
"exerciseId": 42,
4+
"participantId": "student-1",
5+
"startTime": 1700000000000,
6+
"endTime": 1700000005000,
7+
"eventCount": 6
8+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{"type":"sessionStart","timestamp":1700100000000,"exerciseId":99,"participantId":"student-2","schemaVersion":2}
2+
{"type":"textChange","timestamp":1700100001000,"uri":"file:///workspace/src/Solution.java","changes":[{"range":{"startLine":0,"startCharacter":0,"endLine":0,"endCharacter":0},"rangeOffset":0,"rangeLength":0,"text":"class Solution {"}]}
3+
{"type":"irisChatSendAttempt","timestamp":1700100002000,"content":"How do I fix this?","status":"sent"}
4+
{"type":"intervention","timestamp":1700100003000,"action":"blocked","level":"notification","shouldIntervene":false,"eq":0.1,"confidence":"insufficient"}
5+
{"type":"fileSnapshotError","timestamp":1700100004000,"uri":"file:///workspace/src/Solution.java","reason":"File not found after 3 retries"}
6+
{"type":"fileCreate","timestamp":1700100005000,"uri":"file:///workspace/src/Helper.java"}
7+
{"type":"textDocumentOpen","timestamp":1700100006000,"uri":"file:///workspace/src/Helper.java"}
8+
{"type":"textDocumentClose","timestamp":1700100007000,"uri":"file:///workspace/src/Solution.java"}
9+
{"type":"fileDelete","timestamp":1700100008000,"uri":"file:///workspace/src/Helper.java"}
10+
{"type":"fileRename","timestamp":1700100009000,"oldUri":"file:///workspace/src/Helper.java","newUri":"file:///workspace/src/Util.java"}
11+
{"type":"sessionEnd","timestamp":1700100010000,"exerciseId":99}

0 commit comments

Comments
 (0)