// Episode JSONL: append-only files the trainer tails by byte offset.
import test from "node:test";
import assert from "node:assert/strict";
import { mkdtempSync, readdirSync, appendFileSync, readFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { EpisodeWriter, readNewTransitions } from "../dist/episodes.js";
const t1 = { g: "toy", f: [[1, 0], [0, 1]], a: 1, r: 0.5, f2: null, done: true };
const t2 = { g: "toy", s: "k1", a: 0, r: -1, s2: null, done: true };
test("writer buffers, flushes one file per game+month, and clears", () => {
const dir = mkdtempSync(join(tmpdir(), "tk-ep-"));
const w = new EpisodeWriter(dir);
w.add(t1);
w.add(t1);
assert.equal(w.pending, 2);
w.flush();
assert.equal(w.pending, 0);
const files = readdirSync(join(dir, "episodes"));
assert.equal(files.length, 1);
assert.match(files[0], /^toy-\d{4}-\d{2}\.jsonl$/);
const lines = readFileSync(join(dir, "episodes", files[0]), "utf8").trim().split("\n");
assert.deepEqual(lines.map((l) => JSON.parse(l)), [t1, t1]);
w.flush(); // empty flush is a no-op
});
test("readNewTransitions resumes from offsets and skips foreign/torn lines", () => {
const dir = mkdtempSync(join(tmpdir(), "tk-ep-"));
const epDir = join(dir, "episodes");
const w = new EpisodeWriter(dir);
w.add(t1);
w.flush();
const state = { offsets: {} };
const first = readNewTransitions([epDir], "toy", state);
assert.equal(first.length, 1);
assert.deepEqual(readNewTransitions([epDir], "toy", state), [], "nothing new");
// append: one more line, a foreign-game line, and a torn line with no newline
const file = join(epDir, readdirSync(epDir)[0]);
appendFileSync(file, JSON.stringify(t2) + "\n" + JSON.stringify({ ...t1, g: "other" }) + "\n" + '{"g":"toy","half');
const second = readNewTransitions([epDir], "toy", state);
assert.deepEqual(second, [t2], "only the new same-game complete line");
// finish the torn line — it arrives on the next read
appendFileSync(file, '":1}\n');
const third = readNewTransitions([epDir], "toy", state);
assert.equal(third.length, 1);
assert.equal(third[0].half, 1);
});
test("a missing episode dir just yields nothing", () => {
assert.deepEqual(readNewTransitions([join(tmpdir(), "tk-none-xyz")], "toy", { offsets: {} }), []);
});