import { beforeEach, describe, expect, it } from 'vitest' import { useDownloadsStore } from '@/lib/stores/downloadsStore' import { useJobsStore } from '@/lib/stores/jobsStore' // The SSE dispatcher is private to `lib/events.ts`. We exercise it by // sending bytes through a mocked EventSource-style mechanism, which is // overkill for unit coverage of the switch arms. Instead we use the public // store APIs with the same payload shapes the dispatcher would forward, // which gives us tight, deterministic checks on the store reducers that // the dispatcher delegates to. // // The dispatcher itself is nine lines of `switch` glue; its only failure // mode is routing an event to the wrong store method. We cover that by // asserting every event → store transition produces the expected state. describe('jobs store — SSE-driven reducer', () => { beforeEach(() => useJobsStore.getState().clear()) it('jobStarted inserts a running entry', () => { useJobsStore.getState().started('job-1', 'pipeline') expect(useJobsStore.getState().jobs['job-1']).toMatchObject({ id: 'job-1', kind: 'pipeline', status: 'running', }) }) it('jobProgress attaches progress to an existing entry', () => { useJobsStore.getState().started('job-1', 'pipeline') useJobsStore.getState().progress({ jobId: 'job-1', status: { status: 'running' }, step: 'detect', currentPage: 0, totalPages: 3, currentStepIndex: 0, totalSteps: 4, overallPercent: 10, }) expect(useJobsStore.getState().jobs['job-1'].progress?.overallPercent).toBe(10) }) it('jobFinished flips status and stores error', () => { useJobsStore.getState().started('job-1', 'pipeline') useJobsStore.getState().finished('job-1', 'failed', 'boom') const entry = useJobsStore.getState().jobs['job-1'] expect(entry.status).toBe('failed') expect(entry.error).toBe('boom') }) it('snapshot replaces the entire registry', () => { useJobsStore.getState().started('stale', 'pipeline') useJobsStore.getState().setSnapshot([{ id: 'fresh', kind: 'pipeline', status: 'running' }]) expect(useJobsStore.getState().jobs).toEqual({ fresh: { id: 'fresh', kind: 'pipeline', status: 'running' }, }) }) it('byStatus filters the registry', () => { useJobsStore.getState().setSnapshot([ { id: 'a', kind: 'pipeline', status: 'running' }, { id: 'b', kind: 'pipeline', status: 'completed' }, ]) expect( useJobsStore .getState() .byStatus('running') .map((j) => j.id), ).toEqual(['a']) }) }) describe('downloads store — SSE-driven reducer', () => { beforeEach(() => useDownloadsStore.getState().clear()) it('progress upserts per id', () => { useDownloadsStore.getState().progress({ id: 'pkg-1', filename: 'model.bin', downloaded: 10, total: 100, status: { status: 'downloading' }, }) useDownloadsStore.getState().progress({ id: 'pkg-1', filename: 'model.bin', downloaded: 50, total: 100, status: { status: 'downloading' }, }) expect(useDownloadsStore.getState().downloads['pkg-1'].downloaded).toBe(50) }) it('snapshot replaces the entire registry', () => { useDownloadsStore.getState().progress({ id: 'stale', filename: 'x', downloaded: 0, status: { status: 'started' }, }) useDownloadsStore.getState().setSnapshot([ { id: 'fresh', filename: 'y', downloaded: 0, status: { status: 'started' }, }, ]) const ids = Object.keys(useDownloadsStore.getState().downloads) expect(ids).toEqual(['fresh']) }) })