import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { newFixture, type Fixture } from '../helpers/fixture.js'; import { betaFlow, campaignFlow } from '../../src/lib/operation-flow.js'; let fixture: Fixture; beforeEach(async () => { vi.resetModules(); fixture = await newFixture(); }); afterEach(async () => { await fixture.cleanup(); }); describe('persistent activity', () => { it('keeps beta failures and retries across reopening without duplicating polls', async () => { const { db, closeDb } = await import('../../src/lib/server/db/index.js'); const handle = db(); handle.prepare("INSERT INTO servers (name, komodo_name) VALUES ('server', 'server')").run(); handle.prepare("INSERT INTO scripts (id, name, target_rel_path) VALUES (1, 'test', 'test.gsc')").run(); handle.prepare("INSERT INTO script_versions (id, script_id, version, sha256, size, content) VALUES (1, 1, 1, 'hash', 0, X'')").run(); handle.prepare("INSERT INTO betas (id, script_id, script_version_id, trigger_kind, state) VALUES (1, 1, 1, 'ui', 'running')").run(); handle.prepare("INSERT INTO beta_servers (beta_id, server, had_original, stage) VALUES (1, 'server', 0, 'deploying')").run(); handle.prepare("UPDATE beta_servers SET stage = 'failed', error = 'write failed' WHERE beta_id = 1").run(); handle.prepare("UPDATE beta_servers SET stage = 'failed', error = 'write failed', updated_at = 'later' WHERE beta_id = 1").run(); handle.prepare("UPDATE beta_servers SET stage = 'awaiting-empty', error = NULL WHERE beta_id = 1").run(); handle.prepare("UPDATE betas SET state = 'rolled-back' WHERE id = 1").run(); const before = handle.prepare('SELECT * FROM beta_events ORDER BY id').all(); expect(before).toHaveLength(5); expect(before).toEqual(expect.arrayContaining([expect.objectContaining({ message: 'server: Failed — write failed' })])); closeDb(); expect(db().prepare('SELECT * FROM beta_events ORDER BY id').all()).toEqual(before); }); it('records campaign files, restart and health milestones, and cascades cleanup', async () => { const { db } = await import('../../src/lib/server/db/index.js'); const handle = db(); handle.prepare("INSERT INTO servers (name, komodo_name) VALUES ('server', 'server')").run(); handle.prepare("INSERT INTO deployments (id, action, started_at) VALUES ('campaign', 'promote', '2026-09-17T00:00:00Z')").run(); handle.prepare("INSERT INTO deployment_files (deployment_id, server, verb, rel_path) VALUES ('campaign', 'server', 'updated', 'test.gsc')").run(); handle.prepare("INSERT INTO deployment_servers (deployment_id, server, files_changed, restart_state) VALUES ('campaign', 'server', 1, 'pending')").run(); handle.prepare("UPDATE deployment_servers SET attempts = attempts + 1, checked_at = 'later'").run(); expect(handle.prepare('SELECT * FROM campaign_events').all()).toHaveLength(3); handle.prepare("UPDATE deployment_servers SET restarted_at = '2026-09-17T00:01:00Z'").run(); handle.prepare("UPDATE deployment_servers SET restart_state = 'done'").run(); handle.prepare("UPDATE deployments SET finished_at = '2026-09-17T00:02:00Z'").run(); const messages = (handle.prepare('SELECT message FROM campaign_events ORDER BY id').all() as { message: string }[]).map(e => e.message); expect(messages).toHaveLength(6); expect(messages).toContain('server: Restart requested; verifying health'); expect(messages).toContain('server: Server complete'); handle.prepare("DELETE FROM deployments WHERE id = 'campaign'").run(); expect(handle.prepare('SELECT * FROM campaign_events').all()).toEqual([]); }); }); describe('operation progress', () => { const server = { stage: 'awaiting-empty', deployed_at: 'now', restarted_at: null, healthy_at: null, match_seen_at: null }; it('uses the slowest beta server and omits disabled gates', () => { const flow = betaFlow({ state: 'running', wait_for_health: 0, wait_for_match: 0 }, [server, { ...server, stage: 'ready' }]); expect(flow.map(s => s.label)).toEqual(['Deploy artifacts', 'Wait for empty servers', 'Restart servers', 'Review and promote']); expect(flow.map(s => s.state)).toEqual(['done', 'current', 'pending', 'pending']); }); it('does not mark a failed deployment as completed', () => { expect(betaFlow({ state: 'running', wait_for_health: 1, wait_for_match: 1 }, [{ ...server, stage: 'failed', deployed_at: null }])[0].state).toBe('failed'); expect(campaignFlow([{ restart_state: 'failed', files_changed: 0, restarted_at: null }], false)[0].state).toBe('failed'); }); it('requires health verification after campaign restart and preserves manual work', () => { expect(campaignFlow([{ restart_state: 'pending', files_changed: 1, restarted_at: 'now' }], false).map(s => s.state)).toEqual(['done', 'done', 'done', 'current']); expect(campaignFlow([{ restart_state: 'manual', files_changed: 1, restarted_at: null }], true).some(s => s.state === 'current')).toBe(false); expect(campaignFlow([{ restart_state: 'done', files_changed: 1, restarted_at: 'now' }], true).every(s => s.state === 'done')).toBe(true); }); });