import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { describe, expect, it } from "vitest"; import { createEngine, seedRandom } from "@nengine/core"; import { createEngineNamespaces, McpGateway } from "../src/index.js"; const TIME_YAML = [ "id: default", "dayLengthTicks: 480", "seasons:", " - { name: spring, dayCount: 28 }", "start: { day: 1, season: spring }", "", ].join("\n"); const FARM_YAML = [ "id: farm", "width: 2", "height: 2", 'tilesetRef: "tiles"', "spawn: { x: 0, y: 0 }", "layers:", " - name: ground", " data:", " - [a, b]", " - [b, a]", "", ].join("\n"); interface Setup { engine: ReturnType; gateway: McpGateway; tmp: string; close: () => Promise; } async function setup(seed?: number): Promise { const tmp = await mkdtemp(join(tmpdir(), "farm-rng-ns-")); await mkdir(join(tmp, "maps"), { recursive: true }); await writeFile(join(tmp, "time.yaml"), TIME_YAML); await writeFile(join(tmp, "maps", "farm.yaml"), FARM_YAML); const engine = createEngine({ contentDir: tmp, seed }); const gateway = new McpGateway(); for (const namespace of createEngineNamespaces(engine, { gateway })) { gateway.register(namespace); } await engine.start(); return { engine, gateway, tmp, close: async () => { await engine.stop(); await rm(tmp, { recursive: true, force: true }); }, }; } async function callText(gateway: McpGateway, name: string, args: Record = {}): Promise<{ ok: boolean; text: string; }> { const result = await gateway.callTool(name, args); return { ok: result.isError !== true, text: result.content[0]?.text ?? "" }; } describe("rng namespace", () => { it("rng.get reports {seed, calls} anchored to the engine seed", async () => { const { gateway, close } = await setup(42); try { const got = await callText(gateway, "rng.get"); expect(got.ok).toBe(true); expect(JSON.parse(got.text)).toEqual({ seed: 42, calls: 0 }); } finally { await close(); } }); it("rng.set resets the RNG to a seed and makes subsequent world randomness deterministic", async () => { const { engine, gateway, close } = await setup(); try { engine.rng.next(); const set = await callText(gateway, "rng.set", { seed: 123 }); expect(set.ok).toBe(true); expect(JSON.parse(set.text)).toEqual({ seed: 123, calls: 0 }); const expected = seedRandom(123); expect(engine.rng.next()).toBe(expected.next()); expect(engine.rng.nextInt(10)).toBe(expected.nextInt(10)); expect(JSON.parse((await callText(gateway, "rng.get")).text)).toEqual({ seed: 123, calls: 2 }); } finally { await close(); } }); it("rng.set rejects non-finite and non-number seeds", async () => { const { gateway, close } = await setup(); try { const nan = await callText(gateway, "rng.set", { seed: Number.NaN }); expect(nan.ok).toBe(false); expect(nan.text).toMatch(/seed must be a finite number/); const inf = await callText(gateway, "rng.set", { seed: Infinity }); expect(inf.ok).toBe(false); expect(inf.text).toMatch(/seed must be a finite number/); const string = await callText(gateway, "rng.set", { seed: "123" }); expect(string.ok).toBe(false); expect(string.text).toMatch(/seed must be a finite number/); const missing = await callText(gateway, "rng.set", {}); expect(missing.ok).toBe(false); expect(missing.text).toMatch(/missing required arguments: seed/); } finally { await close(); } }); it("rng://state resource reflects the live RNG state", async () => { const { engine, gateway, close } = await setup(7); try { const state = await gateway.readResource("rng://state"); expect(JSON.parse(state.contents[0]?.text ?? "{}")).toEqual({ seed: 7, calls: 0 }); engine.rng.next(); const after = await gateway.readResource("rng://state"); expect(JSON.parse(after.contents[0]?.text ?? "{}")).toEqual({ seed: 7, calls: 1 }); } finally { await close(); } }); it("rng namespace has no prompts", async () => { const { gateway, close } = await setup(); try { const tools = gateway.listTools().map((tool) => tool.name); expect(tools).toContain("rng.get"); expect(tools).toContain("rng.set"); const prompts = gateway.listPrompts().map((prompt) => prompt.name); expect(prompts.some((name) => name.startsWith("rng."))).toBe(false); } finally { await close(); } }); });