import assert from "node:assert/strict"; import { EventEmitter } from "node:events"; import { existsSync, mkdirSync, mkdtempSync, readFileSync, 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 { resolveConfig } from "vite"; import { balanceLabCommand, createBalanceLabJobService, inspectBalanceLabRuntime, normalizeBalanceLabJobBatch, normalizeBalanceLabJobSpec, } from "./balance-lab-jobs.mjs"; test("disables public assets and filesystem watching in Balance Lab Vite mode", async () => { const config = await resolveConfig({ server: { watch: null } }, "serve", "balance-lab"); assert.equal(config.publicDir, ""); assert.deepEqual(config.server.watch?.ignored, ["**/*"]); }); test("enables the local Balance Lab GUI without watching its generated reports", async () => { const config = await resolveConfig({}, "serve", "balance-lab-gui"); assert.equal(config.define["import.meta.env.VITE_LOCAL_BALANCE_LAB_GUI"], '"true"'); assert.ok(config.server.watch?.ignored?.includes("**/artifacts/balance-lab-gui-runtime/**")); }); test("normalizes and bounds server Balance Lab requests", () => { const spec = normalizeBalanceLabJobSpec({ source: "ALL", specSelection: "specific", selectedBuildIds: ["balance-wow-priest-healer-holy-level-40", "balance-wow-priest-healer-holy-level-40"], level: 40, partyConfigurations: 2, simulationsPerParty: 3, durationSeconds: 45, partySeed: 17, seatSweep: true, includeBetaSpecs: true, bossCategory: "all", bossKeys: ["gundrak/entry-29932", "gundrak/entry-29932"], }); assert.equal(spec.source, "all"); assert.equal(spec.specSelection, "specific"); assert.deepEqual(spec.selectedBuildIds, ["balance-wow-priest-healer-holy-level-40"]); assert.deepEqual(spec.bossKeys, ["gundrak/entry-29932"]); assert.equal(spec.seatSweep, true); assert.equal(spec.includeBetaSpecs, true); 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/, ); assert.throws( () => normalizeBalanceLabJobSpec({ source: "wow", specSelection: "specific", selectedBuildIds: [] }), /Select at least one class\/spec build/, ); }); test("normalizes complete level batches before any jobs are queued", () => { const batch = normalizeBalanceLabJobBatch({ specs: [10, 20, 30].map((level) => ({ source: "wow", level, partyConfigurations: 1, simulationsPerParty: 1, durationSeconds: 5, partySeed: 1, bossCategory: "all", bossKeys: ["gundrak/entry-29932"], })), }); assert.deepEqual(batch.map((spec) => spec.level), [10, 20, 30]); assert.throws(() => normalizeBalanceLabJobBatch({ specs: [] }), /must contain 1 through 80/); assert.throws(() => normalizeBalanceLabJobBatch({ specs: [{ source: "wow", level: 10 }, { source: "wow", level: 81 }], }), /Level must be a whole number/); }); 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"], seatSweep: true, includeBetaSpecs: true, specSelection: "specific", selectedBuildIds: ["balance-rom-mage-damage-primary-level-60"], }); 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("specific"), true); assert.equal(command.args.includes("balance-rom-mage-damage-primary-level-60"), true); assert.equal(command.args.includes("abbey/boss-1"), true); assert.equal(command.args.includes("--seat-sweep"), true); assert.equal(command.args.includes("--include-beta-specs"), 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("Running 1 simulations (1 parties x 1 bosses x 1 runs per party/boss)...\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("corrects queued party progress when the eligible pool caps unique compositions", async () => { const directory = mkdtempSync(path.join(os.tmpdir(), "healer-man-balance-capacity-")); try { const spawnProcess = (_executable, args) => { 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"), "{}\n"); child.stdout.write("Running 1 simulations (1 parties x 1 bosses x 1 runs per party/boss)...\n"); child.stdout.write("Party 1/1: wow-party-001\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: 10, simulationsPerParty: 1, durationSeconds: 5, partySeed: 1, bossCategory: "all", bossKeys: ["gundrak/entry-29932"], }, { username: "Phenom" }); assert.equal(started.progress.partiesTotal, 10); await new Promise((resolve) => setImmediate(resolve)); const completed = service.get(started.id); assert.equal(completed.status, "complete"); assert.equal(completed.progress.partiesTotal, 1); assert.equal(completed.progress.partiesCompleted, 1); service.close(); } finally { rmSync(directory, { recursive: true, force: true }); } }); test("snapshots the published companion readiness policy for each server job", async () => { const directory = mkdtempSync(path.join(os.tmpdir(), "healer-man-balance-readiness-")); try { let launchedArgs = []; const spawnProcess = (_executable, args) => { launchedArgs = args; const child = new EventEmitter(); child.stdout = new PassThrough(); child.stderr = new PassThrough(); child.kill = () => child.emit("close", null); return child; }; const policy = { revision: 7, updatedAt: 123, updatedBy: "Phenom", config: { schemaVersion: 1, statuses: { "warrior:protection": "ready" } }, }; const service = createBalanceLabJobService({ dataDir: directory, projectRoot: directory, spawnProcess, pythonExecutable: "python-test", readinessPolicyProvider: () => policy, }); const job = service.start({ source: "wow", level: 40, partyConfigurations: 1, simulationsPerParty: 1, durationSeconds: 5, partySeed: 1, bossCategory: "all", bossKeys: ["gundrak/entry-29932"], includeBetaSpecs: true, }, { username: "Phenom" }); const policyPath = path.join(directory, "balance-lab", "jobs", job.id, "companion-spec-readiness.json"); assert.equal(JSON.parse(readFileSync(policyPath, "utf8")).revision, 7); assert.equal(launchedArgs.includes("--companion-readiness-policy"), true); assert.equal(launchedArgs.includes(policyPath), true); assert.equal(launchedArgs.includes("--include-beta-specs"), true); service.close(); } finally { rmSync(directory, { recursive: true, force: true }); } }); test("reports exhausted file watchers as a specific startup failure", async () => { const directory = mkdtempSync(path.join(os.tmpdir(), "healer-man-balance-watchers-")); try { const spawnProcess = () => { const child = new EventEmitter(); child.stdout = new PassThrough(); child.stderr = new PassThrough(); child.kill = () => child.emit("close", null); setImmediate(() => { child.stderr.write("Error: ENOSPC: System limit for number of file watchers reached, watch '/app/public/assets/example'\n"); child.stdout.end(); child.stderr.end(); child.emit("close", 1); }); 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" }); await new Promise((resolve) => setImmediate(resolve)); const failed = service.get(started.id); assert.equal(failed.status, "failed"); assert.match(failed.error, /file-watcher limit was exhausted/i); 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 }); } }); test("runs separate level jobs concurrently and reports worker utilization", () => { const directory = mkdtempSync(path.join(os.tmpdir(), "healer-man-balance-parallel-")); try { let launches = 0; const spawnProcess = () => { launches += 1; 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", maxConcurrent: 3, maxQueued: 10, }); const requestForLevel = (level) => ({ source: "wow", level, partyConfigurations: 1, simulationsPerParty: 1, durationSeconds: 5, partySeed: 1, bossCategory: "all", bossKeys: ["gundrak/entry-29932"], }); const jobs = service.startMany({ specs: [10, 20, 30].map(requestForLevel) }, { username: "Phenom" }); assert.equal(launches, 3); assert.deepEqual(jobs.map((job) => job.status), ["running", "running", "running"]); assert.deepEqual(jobs.map((job) => job.spec.level), [10, 20, 30]); assert.deepEqual(service.runtime(), { available: true, executable: "python-test", version: "test process", error: null, maxConcurrent: 3, running: 3, queued: 0, maxQueued: 10, }); service.close(); } finally { rmSync(directory, { recursive: true, force: true }); } }); test("rejects an oversized batch without queueing a partial sweep", () => { const directory = mkdtempSync(path.join(os.tmpdir(), "healer-man-balance-batch-capacity-")); try { const service = createBalanceLabJobService({ dataDir: directory, projectRoot: directory, spawnProcess: () => { throw new Error("an over-capacity batch must not launch"); }, pythonExecutable: "python-test", maxQueued: 2, }); const specs = [10, 20, 30].map((level) => ({ source: "wow", level, partyConfigurations: 1, simulationsPerParty: 1, durationSeconds: 5, partySeed: 1, bossCategory: "all", bossKeys: ["gundrak/entry-29932"], })); assert.throws(() => service.startMany({ specs }, { username: "Phenom" }), /server queue is full/); assert.equal(service.list().length, 0); service.close(); } finally { rmSync(directory, { recursive: true, force: true }); } });