import { afterEach, beforeEach, expect, it, vi } from 'vitest'; import { newFixture, type Fixture } from '../helpers/fixture.js'; import { campaignOutcome, type CampaignSummary } from '../../src/lib/ui/campaigns.js'; let fixture: Fixture; beforeEach(async () => { vi.resetModules(); fixture = await newFixture([]); }); afterEach(async () => { await fixture.cleanup(); }); async function setup() { const { db } = await import('../../src/lib/server/db/index.js'); const handle = db(); const { GET } = await import('../../src/routes/api/campaigns/+server.js'); const get = async (query = ''): Promise<{ deployments: (CampaignSummary & { id: string })[]; total: number }> => (await GET({ url: new URL(`http://localhost/api/campaigns?${query}`) })).json(); const add = (id: string, states: string[], action = 'promote', deleted = false) => { handle.prepare('INSERT INTO deployments(id, action, started_at) VALUES(?,?,?)').run(id, action, '2026-10-06T12:00:00Z'); for (const [index, state] of states.entries()) { handle.prepare('INSERT INTO deployment_servers(deployment_id, server, files_changed, restart_state, server_deleted_at) VALUES(?,?,1,?,?)') .run(id, `t5_zombie_theater_${index + 1}`, state, deleted ? '2026-10-06T13:00:00Z' : null); } }; return { handle, get, add }; } it('searches script names, campaign IDs and friendly server names across all retained history before pagination', async () => { const { handle, get, add } = await setup(); add('old-multi-script', ['done']); add('old-legacy-script', ['done']); handle.exec(`INSERT INTO scripts(id,name,target_rel_path) VALUES(1,'Double Tap','double.gsc'),(2,'100% power','power.gsc'); INSERT INTO script_versions(id,script_id,version,sha256,size) VALUES(1,1,2,'hash',0); INSERT INTO deployment_scripts(deployment_id,script_id,script_version_id) VALUES('old-multi-script',1,1); UPDATE deployments SET script_id=2 WHERE id='old-legacy-script';`); for (let i = 0; i < 30; i++) add(`newer-${i}`, ['done']); expect((await get('q=DOUBLE%20TAP&limit=1')).deployments.map(row => row.id)).toEqual(['old-multi-script']); expect((await get('q=old-legacy&limit=1')).deployments.map(row => row.id)).toEqual(['old-legacy-script']); expect((await get('q=%25')).deployments.map(row => row.id)).toEqual(['old-legacy-script']); const friendly = await get('q=Kino%20Der%20Toten&limit=1'); expect(friendly.total).toBe(32); expect(friendly.deployments).toHaveLength(1); expect((await get('q=not-found')).total).toBe(0); }); it('keeps API status filters and active/history partitions consistent with displayed outcomes', async () => { const { handle, get, add } = await setup(); add('complete', ['done']); add('deleted-complete', ['done'], 'promote', true); add('pending', ['done', 'pending']); add('held', ['held']); add('manual', ['manual']); add('failed', ['failed', 'pending']); add('deleted-failed', ['failed'], 'promote', true); add('withdrawn', ['skipped', 'cancelled']); add('empty', []); add('rollback', ['done'], 'rollback'); add('reversed', ['done']); handle.prepare("UPDATE deployments SET rolled_back_by='rollback' WHERE id='reversed'").run(); const all = (await get('limit=100')).deployments; for (const status of ['attention', 'progress', 'complete', 'rolled-back', 'inactive']) { const expected = all.filter(row => campaignOutcome(row).status === status).map(row => row.id); const filtered = await get(`status=${status}&limit=100`); expect(filtered.total).toBe(expected.length); expect(filtered.deployments.map(row => row.id)).toEqual(expected); } const active = await get('active=true'); expect(active.deployments.map(row => row.id).sort()).toEqual(['failed', 'held', 'manual', 'pending']); const history = await get('view=history'); expect(history.total + active.total).toBe(all.length); expect(history.deployments.some(row => active.deployments.some(candidate => candidate.id === row.id))).toBe(false); }); it('combines search, type and status filters before counting and paginating history', async () => { const { get, add } = await setup(); for (let i = 0; i < 30; i++) add(`repair-${i}`, ['done'], 'drift'); add('promote', ['done']); add('incomplete-repair', ['pending'], 'drift'); const query = 'q=maintenance%20repair&action=drift&status=complete&view=history&limit=25'; const first = await get(query); const second = await get(`${query}&offset=25`); expect(first.total).toBe(30); expect(first.deployments).toHaveLength(25); expect(second.deployments).toHaveLength(5); expect(new Set([...first.deployments, ...second.deployments].map(row => row.id)).size).toBe(30); expect((await get('action=rollback')).total).toBe(0); });