import Database from 'better-sqlite3'; import { randomUUID } from 'node:crypto'; import { afterEach, beforeEach, expect, it, vi } from 'vitest'; import { moderationCaseOutcomes, moderationCaseStatuses } from '../../src/lib/moderation.js'; import { newFixture, type Fixture } from '../helpers/fixture.js'; let fixture: Fixture; beforeEach(async () => { vi.resetModules(); fixture = await newFixture([]); }); afterEach(async () => fixture.cleanup()); it('migrates every legacy state while preserving assignments, decisions, evidence, actions and relationships', async () => { const { migrations } = await import('../../src/lib/server/db/migrations.js'); const { runMigrations } = await import('../../src/lib/server/db/migration-runner.js'); const handle = new Database(':memory:'); handle.pragma('foreign_keys = ON'); try { runMigrations(handle, migrations.slice(0, 35)); handle.exec(`INSERT INTO auth_users(id,username,display_name,password_hash,role,created_at,updated_at) VALUES('staff','staff','Staff','unused','moderator',0,0)`); const statuses = ['open', 'in_review', 'escalated', ...moderationCaseOutcomes]; for (const status of statuses) { handle .prepare( `INSERT INTO moderation_cases(id,game,guid,title,status,actor_id,actor,created_at,updated_at,revision,assignee_id,review_at) VALUES(?,'t5','123','Existing case',?,'staff','Staff','2026-10-17','2026-10-17',7,'staff','2026-11-01')` ) .run(status, status); } handle.exec(` INSERT INTO moderation_events(game,guid,actor,action,subject_id,detail,created_at) VALUES('t5','123','Staff','case.warning_issued','warning_issued','Original decision','2026-10-17'); INSERT INTO moderation_evidence VALUES('e','warning_issued','Original excerpt','Archived chat','{"entry":"unchanged"}','staff','Staff','2026-10-17'); INSERT INTO moderation_sanctions(id,game,guid,kind,scope,reason,note,actor_id,actor,created_at,case_id) VALUES('s','t5','123','mute','game','Original action','','staff','Staff','2026-10-17','warning_issued'); UPDATE moderation_cases SET review_sanction_id='s' WHERE id='warning_issued'; INSERT INTO moderation_case_action_links VALUES('warning_issued','restriction','s','Staff','2026-10-17'); INSERT INTO moderation_case_relationships VALUES('r','open','warning_issued','same_incident','staff','Staff','2026-10-17',NULL); INSERT INTO moderation_case_assessments VALUES('warning_issued',6,'{"summary":"Existing finding"}','staff','Staff','2026-10-17'); INSERT INTO moderation_case_notifications(user_id,case_id,kind,created_at) VALUES('staff','warning_issued','evidence','2026-10-17'); `); const children = [ 'moderation_evidence', 'moderation_sanctions', 'moderation_case_action_links', 'moderation_case_relationships', 'moderation_case_assessments', 'moderation_case_notifications' ]; const before = children.map((table) => handle.prepare(`SELECT * FROM ${table}`).all()); runMigrations(handle, migrations); runMigrations(handle, migrations); expect(children.map((table) => handle.prepare(`SELECT * FROM ${table}`).all())).toEqual( before ); for (const old of statuses) { const expected = old === 'open' ? 'open' : ['in_review', 'escalated'].includes(old) ? 'in_review' : 'closed'; expect( handle .prepare( 'SELECT status,revision,assignee_id,review_at FROM moderation_cases WHERE id=?' ) .get(old) ).toEqual({ status: expected, revision: old === expected ? 7 : 8, assignee_id: 'staff', review_at: '2026-11-01' }); } const events = handle .prepare("SELECT detail FROM moderation_events WHERE action='case.status_migrated'") .all() as { detail: string }[]; expect(events.map((event) => JSON.parse(event.detail).from).sort()).toEqual( ['escalated', ...moderationCaseOutcomes].sort() ); expect( handle .prepare("SELECT detail FROM moderation_events WHERE action='case.warning_issued'") .get() ).toEqual({ detail: 'Original decision' }); expect(() => handle.exec("UPDATE moderation_cases SET status='warning_issued' WHERE id='open'") ).toThrow('CHECK'); expect(handle.pragma('foreign_key_check')).toEqual([]); } finally { handle.close(); } }); it('changes lifecycle independently of ownership, findings and enforcement with revision and retry protection', async () => { const { db } = await import('../../src/lib/server/db/index.js'); const { mutateModeration } = await import('../../src/lib/server/moderation/actions.js'); const { moderationCaseDetail } = await import('../../src/lib/server/moderation/cases.js'); const handle = db(); for (const role of ['moderator', 'super_viewer']) handle .prepare( `INSERT INTO auth_users(id,username,display_name,password_hash,role,created_at,updated_at) VALUES(?,?,?,'unused',?,0,0)` ) .run(role, role, role, role); const mutate = (input: Record, actor = 'moderator') => mutateModeration(actor, { idempotencyKey: randomUUID(), ...input }); const { id } = mutate({ op: 'create-case', game: 't5', guid: '123', title: 'Reported conduct', assigneeId: 'moderator' }); mutate({ op: 'assess-case', id, revision: 1, summary: 'Ambiguous context', rules: [], findings: [{ category: 'threats', finding: 'inconclusive', detail: 'Need more context' }] }); const original = moderationCaseDetail('moderator', id); let revision = 2; for (const status of ['in_review', 'closed', 'open', 'closed', 'in_review']) { const request = { op: 'update-case', id, revision, status, reason: 'Workflow changed', idempotencyKey: randomUUID() }; expect(() => mutate({ ...request, reason: '' })).toThrow('Status change reason'); expect(() => mutate({ ...request, revision: revision - 1 })).toThrow('changed'); expect(() => mutate(request, 'super_viewer')).toThrow('Moderation access'); mutate(request); mutate(request); revision++; const data = moderationCaseDetail('moderator', id); expect(data.case).toMatchObject({ status, revision, assigneeId: 'moderator' }); expect(data.assessmentHistory).toEqual(original.assessmentHistory); expect(data.case.evidence).toEqual(original.case.evidence); expect(data.sanctions).toEqual(original.sanctions); expect(data.events[0]).toMatchObject({ action: 'case.updated' }); } for (const status of [...moderationCaseOutcomes, 'assigned', 'escalated']) expect(() => mutate({ op: 'update-case', id, revision, status, reason: 'Invalid state' }) ).toThrow('Choose Open'); expect(moderationCaseStatuses).toEqual(['open', 'in_review', 'closed']); expect(handle.prepare('SELECT COUNT(*) AS n FROM moderation_actions').get()).toEqual({ n: 0 }); });