import { test, expect, beforeAll, afterAll } from 'bun:test'; import { mkdtempSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { Hono } from 'hono'; const root = mkdtempSync(join(tmpdir(), 'ytp-piano-')); process.env.DB_PATH = join(root, 'test.db'); const db = await import('./db.js'); const { registerPianoRoutes } = await import('./piano.js'); const ID = '0gfX0dFLaBc', TOKEN = 'piano-worker-test-'.repeat(3); let app, time = 1000; const post = (path, body = {}, token = TOKEN) => app.request(path, { method: 'POST', headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); beforeAll(async () => { await db.initDb(); await db.upsertMedia(ID, { status: 'ready', duration: 60, gen: 1 }); app = new Hono(); await registerPianoRoutes(app, { db, enabled: true, workerToken: TOKEN, now: () => time, adminAuth: async (c, next) => c.req.header('x-admin') === 'yes' ? next() : c.json({ ok: false }, 401), }).ready; }); afterAll(async () => { db.db.close(); rmSync(root, { recursive: true, force: true }); }); test('server piano requires admin requests and dedicated worker authentication', async () => { expect((await post(`/api/media/${ID}/piano`)).status).toBe(401); expect((await post('/api/piano-worker/claim', {}, 'wrong')).status).toBe(401); const result = await app.request(`/api/media/${ID}/piano`, { method: 'POST', headers: { 'x-admin': 'yes' }, }); expect(result.status).toBe(202); expect((await result.json()).job.status).toBe('queued'); }); test('claim is exclusive; validated results persist as compact notes with inferred hands', async () => { const first = await (await post('/api/piano-worker/claim')).json(); expect(first.job.audioPath).toBe(`/api/media/${ID}?a=1`); expect((await (await post('/api/piano-worker/claim')).json()).job).toBeNull(); expect( (await post(`/api/piano-worker/${ID}`, { action: 'complete', lease: 'wrong', notes: [] })) .status, ).toBe(409); expect( ( await post(`/api/piano-worker/${ID}`, { action: 'complete', lease: first.job.lease, notes: [{ pitch: 60, start: 2, end: 1, velocity: 80 }], }) ).status, ).toBe(422); expect( ( await post(`/api/piano-worker/${ID}`, { action: 'complete', lease: first.job.lease, notes: [{ pitch: 48, start: 1.24, end: 1.78, velocity: 85 }], }) ).status, ).toBe(200); const result = await (await app.request(`/api/media/${ID}/piano`)).json(); expect(result.job.notes[0]).toEqual({ pitch: 48, start: 1.24, end: 1.78, velocity: 85, hand: 'left', }); }); test('changed source invalidates notes, lost leases are re-claimed and stale workers cannot publish', async () => { await db.upsertMedia(ID, { gen: 2 }); expect((await (await app.request(`/api/media/${ID}/piano`)).json()).job.status).toBe('stale'); await app.request(`/api/media/${ID}/piano`, { method: 'POST', headers: { 'x-admin': 'yes' } }); const old = (await (await post('/api/piano-worker/claim')).json()).job; time += 120001; const next = (await (await post('/api/piano-worker/claim')).json()).job; expect(next.lease).not.toBe(old.lease); expect( (await post(`/api/piano-worker/${ID}`, { action: 'complete', lease: old.lease, notes: [] })) .status, ).toBe(409); expect( (await post(`/api/piano-worker/${ID}`, { action: 'heartbeat', lease: next.lease })).status, ).toBe(200); }); test('optional server pipeline defaults off and rejects malformed ids', async () => { const disabled = new Hono(); await registerPianoRoutes(disabled, { db, adminAuth: async (c, next) => next() }).ready; expect((await disabled.request(`/api/media/${ID}/piano`, { method: 'POST' })).status).toBe(503); expect((await disabled.request('/api/media/bad/piano')).status).toBe(400); });