import { randomUUID } from 'node:crypto'; import type Database from 'better-sqlite3'; import { afterEach, beforeEach, expect, it, vi } from 'vitest'; import { newFixture, type Fixture } from '../helpers/fixture.js'; let fixture: Fixture; let handle: Database.Database; const clock = '2026-10-17T12:00:00.000Z'; const server = 't5_zombie_theater_1'; beforeEach(async () => { vi.resetModules(); vi.useFakeTimers({ toFake: ['Date'] }); vi.setSystemTime(new Date(clock)); fixture = await newFixture([server]); const { seedServerRegistry } = await import('../../src/lib/server/fleet/servers.js'); await seedServerRegistry(); const { db } = await import('../../src/lib/server/db/index.js'); handle = db(); handle .prepare( `INSERT INTO auth_users(id,username,display_name,password_hash,role,created_at,updated_at) VALUES('moderator','moderator','moderator','unused','moderator',0,0)` ) .run(); }); afterEach(async () => { vi.useRealTimers(); await fixture.cleanup(); }); async function mutate(input: Record) { const { mutateModeration } = await import('../../src/lib/server/moderation/actions.js'); return mutateModeration('moderator', { idempotencyKey: randomUUID(), ...input }); } async function detail(id: string) { const { moderationCaseDetail } = await import('../../src/lib/server/moderation/cases.js'); return moderationCaseDetail('moderator', id); } async function create(overrides: Record = {}) { return mutate({ op: 'create-case', game: 't5', guid: '123', title: 'Review archived conversation', ...overrides }); } async function archive(messages: Array<{ guid?: string; message: string }>) { const { saveTelemetry } = await import('../../src/lib/server/fleet/telemetry.js'); const { matchChatTimeline } = await import('../../src/lib/server/fleet/chat.js'); saveTelemetry({ server, map: 'zombie_theater', event: 'game_start', players: [{ guid: '123', name: 'Subject' }], receivedAt: clock }); const { id: matchId } = handle.prepare('SELECT id FROM leaderboard_matches').get() as { id: number; }; const insert = handle.prepare('INSERT INTO match_chat VALUES(?,?,?,?)'); for (const [index, message] of messages.entries()) insert.run( matchId, `message-${index}`, 1, JSON.stringify({ type: 'message', context: { realTime: null, matchElapsedMs: null, round: null }, message: { time: '1:01', guid: message.guid ?? '123', slot: message.guid ? 2 : 1, player: message.guid ? 'Witness' : 'Subject', message: message.message } }) ); return { matchId, entries: matchChatTimeline(matchId) }; } it('rejects a witness-only selection, foreign archive IDs and another game without changing a case', async () => { const { matchId, entries } = await archive([ { message: 'Subject statement' }, { guid: '456', message: 'Witness reply' } ]); const { id } = await create(); const before = await detail(id); for (const messageIds of [ [entries[1].messageId], [`${matchId + 1}:message-0:1`], [entries[0].messageId, `${matchId + 1}:message-1:1`] ]) await expect( mutate({ op: 'attach-evidence', id, revision: 1, evidence: { matchId, messageIds } }) ).rejects.toMatchObject({ status: expect.any(Number) }); expect(await detail(id)).toEqual(before); const other = await create({ game: 't6' }); await expect( mutate({ op: 'attach-evidence', id: other.id, revision: 1, evidence: { matchId, messageIds: [entries[0].messageId] } }) ).rejects.toMatchObject({ status: 404 }); expect((await detail(other.id)).case).toMatchObject({ revision: 1, evidence: [] }); }); it('allows a new witness reply alongside an attached subject and deduplicates later selections', async () => { const { matchId, entries } = await archive([ { message: 'Subject statement' }, { guid: '456', message: 'Witness reply' } ]); const [subjectId, witnessId] = entries.map((entry) => entry.messageId!); const { id } = await create(); await mutate({ op: 'attach-evidence', id, revision: 1, evidence: { matchId, messageIds: [subjectId], contextBefore: 0, contextAfter: 0 } }); const request = { op: 'attach-evidence', id, revision: 2, evidence: { matchId, messageIds: [witnessId, subjectId, witnessId], contextBefore: 0, contextAfter: 0 }, idempotencyKey: randomUUID() }; await mutate(request); await mutate(request); const attached = await detail(id); expect(attached.case.revision).toBe(3); expect(attached.case.evidence).toHaveLength(2); const snapshot = JSON.parse(attached.case.evidence[1].snapshot!); expect(snapshot.selectedMessageIds).toEqual([witnessId]); expect(snapshot.highlightedMessageIds).toEqual([subjectId]); expect(snapshot.entries.map((entry: { messageId: string }) => entry.messageId)).toEqual([ subjectId, witnessId ]); expect(snapshot.entries[1].message.guid).toBe('456'); await mutate({ ...request, revision: 3, idempotencyKey: randomUUID() }); expect(await detail(id)).toEqual(attached); }); it('validates selection and context bounds and rolls back opening a case with invalid evidence', async () => { const { matchId, entries } = await archive([{ message: 'Subject statement' }]); const messageId = entries[0].messageId!; const invalid = [ { messageIds: [] }, { messageIds: Array.from({ length: 101 }, () => messageId) }, { messageIds: [messageId], contextBefore: -1 }, { messageIds: [messageId], contextAfter: 21 }, { messageIds: [messageId], contextBefore: 1.5 }, { messageIds: [messageId], contextAfter: '3' } ]; for (const evidence of invalid) await expect(create({ evidence: { matchId, ...evidence } })).rejects.toMatchObject({ status: 400 }); expect(handle.prepare('SELECT COUNT(*) AS count FROM moderation_cases').get()).toEqual({ count: 0 }); expect(handle.prepare('SELECT COUNT(*) AS count FROM moderation_events').get()).toEqual({ count: 0 }); expect(handle.prepare('SELECT COUNT(*) AS count FROM moderation_requests').get()).toEqual({ count: 0 }); }); it('rolls back a legacy bulk attachment if the case reaches its evidence limit partway through', async () => { const { matchId } = await archive([ { message: 'Repeated statement' }, { message: 'Repeated statement' }, { message: 'Repeated statement' } ]); const { id } = await create(); const insert = handle.prepare('INSERT INTO moderation_evidence VALUES(?,?,?,?,?,?,?,?)'); for (let index = 0; index < 99; index++) insert.run( randomUUID(), id, `Note ${index}`, 'Staff note', null, 'moderator', 'moderator', clock ); const before = await detail(id); const idempotencyKey = randomUUID(); await expect( mutate({ op: 'attach-evidence', id, revision: 1, evidence: { matchId, message: 'Repeated statement' }, idempotencyKey }) ).rejects.toMatchObject({ status: 409 }); expect(await detail(id)).toEqual(before); expect( handle .prepare('SELECT COUNT(*) AS count FROM moderation_requests WHERE request_key=?') .get(idempotencyKey) ).toEqual({ count: 0 }); });