import assert from "node:assert/strict"; import { EventEmitter } from "node:events"; import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; import os from "node:os"; import path from "node:path"; import { PassThrough } from "node:stream"; import test from "node:test"; import { balanceLabCommand, createBalanceLabJobService, inspectBalanceLabRuntime, normalizeBalanceLabJobSpec, } from "./balance-lab-jobs.mjs"; test("normalizes and bounds server Balance Lab requests", () => { const spec = normalizeBalanceLabJobSpec({ source: "ALL", level: 40, partyConfigurations: 2, simulationsPerParty: 3, durationSeconds: 45, partySeed: 17, bossCategory: "all", bossKeys: ["gundrak/entry-29932", "gundrak/entry-29932"], }); assert.equal(spec.source, "all"); assert.deepEqual(spec.bossKeys, ["gundrak/entry-29932"]); assert.throws( () => normalizeBalanceLabJobSpec({ source: "wow", level: 81 }), /Level must be a whole number/, ); assert.throws( () => normalizeBalanceLabJobSpec({ source: "wow", bossKeys: ["boss; shutdown"] }), /boss key is invalid/, ); }); test("builds an argument-array command without a shell", () => { const spec = normalizeBalanceLabJobSpec({ source: "rom", level: 60, partyConfigurations: 4, simulationsPerParty: 2, durationSeconds: 30, partySeed: 9, bossCategory: "rom", bossKeys: ["abbey/boss-1"], }); const command = balanceLabCommand(spec, "C:/reports/job", { projectRoot: "C:/app", pythonExecutable: "python-custom", }); assert.equal(command.executable, "python-custom"); assert.equal(command.args.includes("--party-mode"), true); assert.equal(command.args.includes("automatic"), true); assert.equal(command.args.includes("abbey/boss-1"), true); assert.equal(command.cwd, path.resolve("C:/app")); }); test("detects available and missing Python 3 runtimes", () => { const available = inspectBalanceLabRuntime("python3", () => ({ status: 0, stdout: "Python 3.12.8\n", stderr: "", })); assert.equal(available.available, true); assert.equal(available.version, "Python 3.12.8"); const missing = inspectBalanceLabRuntime("python3", () => ({ status: 127, stdout: "", stderr: "python3: not found", })); assert.equal(missing.available, false); assert.match(missing.error, /could not execute python3/i); }); test("rejects a job before queueing when the server runtime is unavailable", () => { const directory = mkdtempSync(path.join(os.tmpdir(), "healer-man-balance-runtime-")); try { const service = createBalanceLabJobService({ dataDir: directory, projectRoot: directory, runtimeProbe: () => ({ available: false, executable: "python3", version: null, error: "python3 is missing." }), }); assert.throws(() => service.start({ source: "wow", level: 40, partyConfigurations: 1, simulationsPerParty: 1, durationSeconds: 5, partySeed: 1, bossCategory: "all", bossKeys: ["gundrak/entry-29932"], }, { username: "Phenom" }), /Install Python 3 in the server container/); assert.equal(service.list().length, 0); assert.equal(service.runtime().available, false); service.close(); } finally { rmSync(directory, { recursive: true, force: true }); } }); test("queues a job, reports progress, and exposes only allow-listed artifacts", async () => { const directory = mkdtempSync(path.join(os.tmpdir(), "healer-man-balance-job-")); try { const spawnProcess = (_executable, args) => { const child = new EventEmitter(); child.stdout = new PassThrough(); child.stderr = new PassThrough(); child.kill = () => child.emit("close", null); const outputIndex = args.indexOf("--output-dir"); const outputDir = args[outputIndex + 1]; setImmediate(() => { mkdirSync(outputDir, { recursive: true }); writeFileSync(path.join(outputDir, "suite-report.json"), "{}\n"); child.stdout.write("Party 1/1: wow-party-001\n"); child.stderr.write("[balance-lab] 1/1 Eck the Ferocious seed 1\n"); child.stdout.end(); child.stderr.end(); child.emit("close", 0); }); return child; }; const service = createBalanceLabJobService({ dataDir: directory, projectRoot: directory, spawnProcess, pythonExecutable: "python-test", }); const started = service.start({ source: "wow", level: 40, partyConfigurations: 1, simulationsPerParty: 1, durationSeconds: 5, partySeed: 1, bossCategory: "all", bossKeys: ["gundrak/entry-29932"], }, { username: "Phenom" }); assert.equal(started.status, "running"); await new Promise((resolve) => setImmediate(resolve)); const completed = service.get(started.id); assert.equal(completed.status, "complete"); assert.equal(completed.progress.partiesCompleted, 1); assert.deepEqual(completed.artifacts, ["suite-report.json"]); assert.equal(service.artifact(started.id, "suite-report.json").size, 3); assert.throws(() => service.artifact(started.id, "../../game.db"), /artifact was not found/); service.close(); } finally { rmSync(directory, { recursive: true, force: true }); } }); test("restores completed reports as previous runs after a server restart", async () => { const directory = mkdtempSync(path.join(os.tmpdir(), "healer-man-balance-history-")); try { let launches = 0; const spawnProcess = (_executable, args) => { launches += 1; const child = new EventEmitter(); child.stdout = new PassThrough(); child.stderr = new PassThrough(); child.kill = () => child.emit("close", null); const outputDir = args[args.indexOf("--output-dir") + 1]; setImmediate(() => { mkdirSync(outputDir, { recursive: true }); writeFileSync(path.join(outputDir, "suite-report.json"), JSON.stringify({ summary: { partyConfigurations: 2, bosses: 3, runs: 6, classSpecs: 5, bossPressureMedianDtps: 123.5, }, })); child.emit("close", 0); }); return child; }; const firstService = createBalanceLabJobService({ dataDir: directory, projectRoot: directory, spawnProcess, pythonExecutable: "python-test", }); const started = firstService.start({ source: "wow", level: 40, partyConfigurations: 2, simulationsPerParty: 1, durationSeconds: 5, partySeed: 1, bossCategory: "all", bossKeys: ["gundrak/entry-29932"], }, { username: "Phenom" }); await new Promise((resolve) => setImmediate(resolve)); assert.equal(firstService.get(started.id).status, "complete"); firstService.close(); const secondService = createBalanceLabJobService({ dataDir: directory, projectRoot: directory, spawnProcess: () => { throw new Error("restored jobs must not relaunch"); }, pythonExecutable: "python-test", }); const restored = secondService.list(); assert.equal(restored.length, 1); assert.equal(restored[0].id, started.id); assert.equal(restored[0].status, "complete"); assert.equal(restored[0].summary?.runs, 6); assert.deepEqual(restored[0].artifacts, ["suite-report.json"]); assert.equal(secondService.artifact(started.id, "suite-report.json").size > 0, true); assert.equal(launches, 1); secondService.close(); } finally { rmSync(directory, { recursive: true, force: true }); } }); test("marks an interrupted persisted run as failed when the service returns", () => { const directory = mkdtempSync(path.join(os.tmpdir(), "healer-man-balance-interrupted-")); try { const jobsDir = path.join(directory, "balance-lab", "jobs"); const id = "11111111-1111-4111-8111-111111111111"; const outputDir = path.join(jobsDir, id); mkdirSync(outputDir, { recursive: true }); writeFileSync(path.join(outputDir, "job.json"), JSON.stringify({ id, status: "running", createdAt: "2026-09-15T12:00:00.000Z", startedAt: "2026-09-15T12:00:01.000Z", requestedBy: "Phenom", spec: { source: "rom", level: 55, partyConfigurations: 1, simulationsPerParty: 1, durationSeconds: 30, partySeed: 3, bossCategory: "rom", bossKeys: [], }, progress: { party: 1, partiesCompleted: 0, partiesTotal: 1 }, artifacts: [], error: null, logTail: ["Party 1/1: rom-party-001"], })); const service = createBalanceLabJobService({ dataDir: directory, projectRoot: directory }); const restored = service.get(id); assert.equal(restored.status, "failed"); assert.match(restored.error, /server restarted/i); service.close(); } finally { rmSync(directory, { recursive: true, force: true }); } }); test("imports completed report directories created before history metadata existed", () => { const directory = mkdtempSync(path.join(os.tmpdir(), "healer-man-balance-legacy-")); try { const id = "22222222-2222-4222-8222-222222222222"; const outputDir = path.join(directory, "balance-lab", "jobs", id); mkdirSync(outputDir, { recursive: true }); writeFileSync(path.join(outputDir, "suite-report.json"), JSON.stringify({ generatedAt: "2026-09-14T10:30:00.000Z", summary: { partyConfigurations: 3, bosses: 2, runs: 12, classSpecs: 8, bossPressureMedianDtps: 50 }, })); writeFileSync(path.join(outputDir, "suite-config.json"), JSON.stringify({ source: "coa", level: 35, partySeed: 12, partyConfigurationsPerSource: 3, simulationsPerParty: 2, durationSeconds: 45, bossCategory: "wow", bossKeys: ["gundrak/entry-29932", "gundrak/entry-29304"], })); const service = createBalanceLabJobService({ dataDir: directory, projectRoot: directory }); const restored = service.get(id); assert.equal(restored.status, "complete"); assert.equal(restored.spec.source, "coa"); assert.equal(restored.summary?.runs, 12); assert.equal(restored.requestedBy, "unknown (legacy run)"); assert.equal(existsSync(path.join(outputDir, "job.json")), true); service.close(); } finally { rmSync(directory, { recursive: true, force: true }); } }); test("bounds the number of pending server jobs", () => { const directory = mkdtempSync(path.join(os.tmpdir(), "healer-man-balance-queue-")); try { const spawnProcess = () => { const child = new EventEmitter(); child.stdout = new PassThrough(); child.stderr = new PassThrough(); child.kill = () => child.emit("close", null); return child; }; const service = createBalanceLabJobService({ dataDir: directory, projectRoot: directory, spawnProcess, pythonExecutable: "python-test", maxQueued: 1, }); const request = { source: "wow", level: 40, partyConfigurations: 1, simulationsPerParty: 1, durationSeconds: 5, partySeed: 1, bossCategory: "all", bossKeys: ["gundrak/entry-29932"], }; service.start(request, { username: "Phenom" }); assert.throws(() => service.start(request, { username: "Phenom" }), /server queue is full/); service.close(); } finally { rmSync(directory, { recursive: true, force: true }); } });