Add video upload, extraction progress, and reusable analysis sources

This commit is contained in:
Olive Vaughn 2026-09-28 09:38:49 -04:00
parent 690de21fa4
commit 686f897401
24 changed files with 927 additions and 137 deletions

View file

@ -24,7 +24,7 @@
// Writes a PNG per sampled frame into test/browser/out/ so that "it drew
// something" can be checked by eye as well as by pixel count.
import { spawn } from 'node:child_process';
import { spawn, spawnSync } from 'node:child_process';
import { mkdirSync, writeFileSync, rmSync } from 'node:fs';
import { mkdtempSync } from 'node:fs';
import { tmpdir } from 'node:os';
@ -356,9 +356,15 @@ async function main() {
check(await page.eval(CLICK('save')), 'save is clickable');
const saved = await statusMatching(/saved r\d+/);
check(saved !== null, 'the document saves', saved ?? (await page.eval(STATUS)));
// Eleven blocks the first time. The COUNT is not asserted — that is a fact
// about the freeze, not about saving — but that some went up is.
check(/· [1-9]\d* blocks?/.test(saved ?? ''), 'and its tier 2 went with it', saved ?? '');
// Upload count may be zero when the content-addressed blocks already exist
// on this server. The saved clip must still reference them.
const savedBlocks = await page.eval(`(async () => {
const listed = await fetch('/api/projects').then(r => r.json());
const project = await fetch('/api/projects/' + listed.projects[0].id).then(r => r.json());
return project.clips[0].blocks.length;
})()`);
check(savedBlocks > 0, 'the saved clip references its tier 2 blocks',
`${savedBlocks} blocks`);
// Again, unchanged. Content addressing means the second save uploads nothing
// and rewrites nothing: this is the assertion that the keys are stable across
@ -387,6 +393,33 @@ async function main() {
check(!back[0].selectedClip.includes('take'), 'the picture is the reopened document',
JSON.stringify(back[0].selectedClip));
// Exercise the actual file input and FormData path, then wait for the
// extraction job's footage to appear in the server list.
const uploadDir = mkdtempSync(join(tmpdir(), 'arthur-upload-'));
try {
const video = join(uploadDir, 'browser-upload.mp4');
const encoded = spawnSync('ffmpeg', [
'-hide_banner', '-loglevel', 'error', '-y', '-f', 'lavfi',
'-i', 'color=c=blue:s=64x48:r=4:d=1', '-c:v', 'mpeg4', video,
]);
if (encoded.status !== 0) throw new Error(`ffmpeg failed: ${encoded.stderr}`);
const { root } = await page.send('DOM.getDocument');
const { nodeId } = await page.send('DOM.querySelector', {
nodeId: root.nodeId, selector: 'input[type=file]',
});
await page.send('DOM.setFileInputFiles', { files: [video], nodeId });
const extracted = await statusMatching(/video extracted/, 160);
check(extracted !== null, 'the page uploads and extracts a video',
extracted ?? (await page.eval(STATUS)));
const uploadedFootage = await page.eval(`(async () => {
const listed = await fetch('/api/footage').then(r => r.json());
return listed.footage.some(f => f.label === 'browser-upload.mp4' && f.frames === 4);
})()`);
check(uploadedFootage, 'the uploaded footage is selectable');
} finally {
rmSync(uploadDir, { recursive: true, force: true });
}
check(page.logs.length === 0, 'no errors on the console',
page.logs.slice(0, 3).join(' | '));
} finally {