Add a new "test-jazz-stream" task to flow.toml for testing live stream recording flow, including setup instructions and verification steps.

Update CommentBox component to handle image uploads with validation, preview, and progress tracking, and to manage comments with Jazz container initialization.
This commit is contained in:
Nikita
2025-12-25 05:04:43 -08:00
parent 15432a69b5
commit 9c90b7db8d
9 changed files with 1395 additions and 93 deletions

100
tests/README.md Normal file
View File

@@ -0,0 +1,100 @@
# Jazz Live Stream Recording Test
Tests the end-to-end flow of live stream recording with Jazz FileStream.
## Prerequisites
1. **Start Linsa dev server** (in one terminal):
```bash
cd /Users/nikiv/org/linsa/linsa
f dev
```
2. **Wait for server to be ready** at `http://localhost:3000`
## Running the Test
In a separate terminal:
```bash
cd /Users/nikiv/org/linsa/linsa
f test-jazz-stream
```
Or with shortcuts:
```bash
f test
f tjs
```
## What the Test Does
1. **Simulates stream-guard** uploading video chunks
2. **POSTs to API** `/api/stream-recording`
3. **Creates 5 chunks** of fake video data (256KB each)
4. **Stores chunks** in `/Users/nikiv/fork-i/garden-co/jazz/glide-storage/stream-recordings/`
5. **Verifies** chunk files exist on disk
## Viewing the Results
After the test completes:
1. **Open Linsa streams page**:
```
http://localhost:3000/streams
```
2. **Wait for auto-sync** (happens every 5 seconds)
- The page will fetch chunks from the API
- Convert them to Jazz FileStream
- Display the timeline
3. **Open Glide browser**:
- Build and run Glide
- Timeline will appear on canvas
- Horizontal scrollable timeline showing the 5 chunks
## Test Output
The test will show:
- Stream ID and key
- Chunk upload progress
- Storage location
- Links to view the timeline
## Manual Testing
You can also manually POST chunks:
```bash
curl -X POST http://localhost:3000/api/stream-recording?action=start \
-H "Content-Type: application/json" \
-d '{
"streamId": "manual-test",
"title": "Manual Test Stream",
"startedAt": '$(date +%s000)',
"streamKey": "test-key"
}'
# Upload a chunk
echo "fake video data" | base64 | \
curl -X POST http://localhost:3000/api/stream-recording?action=chunk \
-H "Content-Type: application/json" \
-d @- <<EOF
{
"streamId": "manual-test",
"chunkIndex": 0,
"data": "$(cat -)",
"timestamp": $(date +%s000)
}
EOF
```
## Cleanup
Test recordings are stored in:
```
/Users/nikiv/fork-i/garden-co/jazz/glide-storage/stream-recordings/
```
You can delete test streams manually if needed.

194
tests/jazz-stream-test.ts Normal file
View File

@@ -0,0 +1,194 @@
/**
* Jazz Live Stream Recording Test
*
* This test:
* 1. Starts a local Jazz node server
* 2. Creates a ViewerAccount with Jazz
* 3. Simulates stream-guard POSTing video chunks
* 4. Tests the sync flow: API → Jazz FileStream
* 5. Verifies timeline visualization data
*/
import { co } from "jazz-tools"
import { ViewerAccount, StreamRecording, StreamRecordingList } from "../packages/web/src/lib/jazz/schema"
import { randomBytes } from "crypto"
const API_BASE = "http://localhost:3000"
const JAZZ_CLOUD_URL = "wss://cloud.jazz.tools/?key=jazz_cloud_demo"
async function sleep(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms))
}
async function testStreamRecording() {
console.log("🎷 [Test] Starting Jazz Live Stream Recording Test")
console.log("")
// Step 1: Create Jazz account
console.log("1⃣ Creating Jazz ViewerAccount...")
// Note: In a real test, we'd use jazz-tools to create an actual account
// For this test, we'll simulate the flow
const streamId = `test-stream-${Date.now()}`
const streamKey = `test-key-${randomBytes(8).toString("hex")}`
console.log(` Stream ID: ${streamId}`)
console.log(` Stream Key: ${streamKey}`)
console.log("")
// Step 2: Start recording via API
console.log("2⃣ Starting recording session...")
const startResponse = await fetch(`${API_BASE}/api/stream-recording?action=start`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
streamId,
title: "Test Live Stream",
startedAt: Date.now(),
streamKey,
metadata: {
width: 1920,
height: 1080,
fps: 30,
bitrate: 5000000,
},
}),
})
if (!startResponse.ok) {
throw new Error(`Failed to start recording: ${await startResponse.text()}`)
}
const startData = await startResponse.json()
console.log(` ✓ Recording started: ${JSON.stringify(startData)}`)
console.log("")
// Step 3: Upload video chunks
console.log("3⃣ Uploading video chunks...")
const numChunks = 5
for (let i = 0; i < numChunks; i++) {
// Create fake video chunk (256KB of random data)
const chunkData = randomBytes(256 * 1024)
const base64Data = chunkData.toString("base64")
const chunkResponse = await fetch(`${API_BASE}/api/stream-recording?action=chunk`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
streamId,
chunkIndex: i,
data: base64Data,
timestamp: Date.now() + (i * 1000), // 1 second apart
metadata: {
width: 1920,
height: 1080,
fps: 30,
bitrate: 5000000,
},
}),
})
if (!chunkResponse.ok) {
throw new Error(`Failed to upload chunk ${i}: ${await chunkResponse.text()}`)
}
const chunkData2 = await chunkResponse.json()
console.log(` ✓ Chunk ${i} uploaded (${Math.round(chunkData.length / 1024)}KB)`)
// Wait a bit between chunks to simulate real streaming
await sleep(100)
}
console.log("")
// Step 4: List recordings via API
console.log("4⃣ Fetching recordings from API...")
const listResponse = await fetch(`${API_BASE}/api/stream-recording`)
if (!listResponse.ok) {
throw new Error(`Failed to list recordings: ${await listResponse.text()}`)
}
const listData = await listResponse.json() as { recordings: any[] }
console.log(` ✓ Found ${listData.recordings.length} recording(s)`)
const ourRecording = listData.recordings.find(r => r.streamId === streamId)
if (!ourRecording) {
throw new Error("Our recording not found in list!")
}
console.log(` ✓ Recording found with ${ourRecording.chunks?.length || 0} chunks`)
console.log("")
// Step 5: Verify chunk files exist
console.log("5⃣ Verifying chunk files...")
const fs = await import("fs/promises")
const chunksDir = `/Users/nikiv/fork-i/garden-co/jazz/glide-storage/stream-recordings/${streamId}`
try {
const files = await fs.readdir(chunksDir)
const chunkFiles = files.filter(f => f.startsWith("chunk-"))
console.log(`${chunkFiles.length} chunk files found in ${chunksDir}`)
for (const file of chunkFiles.slice(0, 3)) {
const stats = await fs.stat(`${chunksDir}/${file}`)
console.log(` - ${file}: ${Math.round(stats.size / 1024)}KB`)
}
} catch (err) {
console.error(` ✗ Failed to read chunks directory: ${err}`)
}
console.log("")
// Step 6: End recording
console.log("6⃣ Ending recording session...")
const endResponse = await fetch(`${API_BASE}/api/stream-recording?action=end`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
streamId,
endedAt: Date.now(),
}),
})
if (!endResponse.ok) {
throw new Error(`Failed to end recording: ${await endResponse.text()}`)
}
const endData = await endResponse.json()
console.log(` ✓ Recording ended: ${JSON.stringify(endData)}`)
console.log("")
// Step 7: Summary
console.log("7⃣ Test Summary")
console.log(` Stream ID: ${streamId}`)
console.log(` Chunks uploaded: ${numChunks}`)
console.log(` Total data: ${Math.round((numChunks * 256))}KB`)
console.log(` Storage: /Users/nikiv/fork-i/garden-co/jazz/glide-storage/stream-recordings/${streamId}`)
console.log("")
// Step 8: Next steps
console.log("📝 Next Steps:")
console.log(" 1. Open Linsa at http://localhost:3000/streams")
console.log(" 2. The page will auto-sync this recording to Jazz FileStream")
console.log(" 3. Timeline will appear showing the 5 chunks")
console.log(" 4. Open Glide browser to see timeline on canvas")
console.log("")
console.log("✅ Test completed successfully!")
console.log("")
console.log("🎯 To see the timeline:")
console.log(" - Visit http://localhost:3000/streams")
console.log(" - Wait for auto-sync (5 seconds)")
console.log(" - Timeline will show the test stream")
console.log("")
}
// Run the test
testStreamRecording().catch((error) => {
console.error("")
console.error("❌ Test failed:", error)
console.error("")
process.exit(1)
})