import { readFileSync } from 'node:fs'; import { join } from 'node:path'; import { describe, expect, it } from 'vitest'; import { parseLogChunk, parseLogTimestamp } from '../src/parsers/log.ts'; const log = readFileSync(join(import.meta.dirname, 'fixtures', 'latest.anon.log')); const ALICE = '308a693c-2bd8-4d6b-8790-670aef5be601'; const TZ = 'Europe/Berlin'; describe('parseLogTimestamp', () => { it('interprets log time in the server time zone', () => { // 12 Oct 2026 is CEST (UTC+2) expect(parseLogTimestamp('12Oct2026 09:03:03.006', TZ)).toBe(Date.parse('2026-10-12T07:03:03.006Z')); // 1 Dec 2026 is CET (UTC+1) expect(parseLogTimestamp('01Dec2026 00:00:00.000', TZ)).toBe(Date.parse('2026-11-30T23:00:00.000Z')); }); }); describe('parseLogChunk', () => { it('finds join of CDOGamer with uuid 308a…', () => { const { events } = parseLogChunk(log, 0, new Map(), TZ); const join = events.find((e) => e.type === 'player_join'); expect(join).toMatchObject({ type: 'player_join', uuid: ALICE, name: 'CDOGamer' }); expect(join!.ts).toBe(parseLogTimestamp('12Oct2026 09:03:03.006', TZ)); expect(events.filter((e) => e.type === 'player_leave').length).toBeGreaterThan(0); }); it('finds advancement Infusing Efficiency and goals', () => { const { events } = parseLogChunk(log, 0, new Map(), TZ); const titles = events.flatMap((e) => (e.type === 'advancement' ? [e.title] : [])); expect(titles).toContain('Infusing Efficiency'); expect(titles).toContain('The Factory Must Grow!'); const adv = events.find((e) => e.type === 'advancement')!; expect(adv).toMatchObject({ uuid: ALICE, advancementId: 'log:Infusing Efficiency' }); }); it('resolves names known from earlier chunks', () => { const line = Buffer.from('[12Oct2026 10:00:00.000] [Server thread/INFO] [net.minecraft.server.MinecraftServer/]: Bob left the game\r\n'); const known = new Map([['Bob', 'c25d9ff0-dba3-48f0-b414-fad2b63473a9']]); expect(parseLogChunk(line, 0, known, TZ).events[0]).toMatchObject({ type: 'player_leave', name: 'Bob' }); expect(parseLogChunk(line, 0, new Map(), TZ).events).toEqual([]); }); it('ignores chat lines that imitate system messages', () => { const line = Buffer.from( '[12Oct2026 10:00:00.000] [Server thread/INFO] [net.minecraft.server.MinecraftServer/]: Bob joined the game\n', ); expect(parseLogChunk(line, 0, new Map([['Bob', ALICE]]), TZ).events).toEqual([]); }); it('ignores incomplete last line', () => { const text = Buffer.from( '[12Oct2026 10:00:00.000] [Server thread/INFO] [net.minecraft.server.MinecraftServer/]: Bob left the game\n' + '[12Oct2026 10:00:01.000] [Server thread/INFO] [net.minecraft.server.MinecraftServer/]: Bob joined the ga', ); const r = parseLogChunk(text, 100, new Map([['Bob', ALICE]]), TZ); expect(r.events).toHaveLength(1); expect(r.consumed).toBe(text.indexOf('\n') + 1); }); it('same chunk twice yields same ids; ids depend on absolute offset', () => { const a = parseLogChunk(log, 0, new Map(), TZ).events.map((e) => e.id); const b = parseLogChunk(log, 0, new Map(), TZ).events.map((e) => e.id); const c = parseLogChunk(log, 10, new Map(), TZ).events.map((e) => e.id); expect(a).toEqual(b); expect(a[0]).not.toBe(c[0]); }); });