import assert from "node:assert/strict"; import { spawnSync } from "node:child_process"; import { existsSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import process from "node:process"; import test from "node:test"; import { fileURLToPath, pathToFileURL, URL } from "node:url"; import { buildStateBackend, loadProjectAsync } from "@bedrock-rbx/core"; import { createLocalStatePorts } from "../scripts/bedrock-local-state.mjs"; const pluginUrl = new URL("../scripts/bedrock-local-state.mjs", import.meta.url); const cli = fileURLToPath( new URL("../node_modules/@bedrock-rbx/core/dist/cli/run.mjs", import.meta.url), ); const example = JSON.parse( readFileSync(new URL("../bedrock.config.example.json", import.meta.url)), ); const state = (environment = "production") => ({ version: 1, environment, resources: [] }); function fixture(t) { const root = mkdtempSync(join(tmpdir(), "bedrock-local-state-")); t.after(() => rmSync(root, { recursive: true, force: true })); const directory = join(root, "state"); return { root, directory, ...createLocalStatePorts(directory) }; } test("first reads are empty without creating state directories", async (t) => { const { statePort, stateLockPort, directory } = fixture(t); assert.deepEqual(await statePort.read("production"), { success: true, data: {} }); assert.deepEqual(await stateLockPort.inspect("production"), { success: true, data: undefined }); assert.equal(existsSync(directory), false); }); test("state persists across adapter instances with Bedrock bookkeeping and environment isolation", async (t) => { const { directory, statePort } = fixture(t); const snapshot = { ...state(), pendingRebuild: new Set(["start"]), codegenHash: "a".repeat(64) }; assert.equal((await statePort.write(snapshot)).success, true); const fresh = createLocalStatePorts(directory).statePort; assert.deepEqual((await fresh.read("production")).data.state, snapshot); assert.equal((await fresh.write(state("staging"))).success, true); assert.deepEqual((await fresh.read("production")).data.state, snapshot); assert.deepEqual((await fresh.read("staging")).data.state, state("staging")); assert.deepEqual(JSON.parse(readFileSync(join(directory, "production.json"))).$bedrock, { version: 1, pendingRebuild: ["start"], codegenHash: "a".repeat(64), }); assert.deepEqual(readdirSync(directory).sort(), ["production.json", "staging.json"]); }); test("atomic replacement leaves valid state and no temporary files", async (t) => { const { directory, statePort } = fixture(t); await statePort.write(state()); for (let index = 0; index < 5; index++) { assert.equal( (await statePort.write({ ...state(), codegenHash: String(index).repeat(64) })).success, true, ); assert.equal( (await statePort.read("production")).data.state.codegenHash, String(index).repeat(64), ); } assert.deepEqual(readdirSync(directory), ["production.json"]); }); test("corrupt, unsupported and mismatched state never becomes a first deployment", async (t) => { const { statePort, directory } = fixture(t); await statePort.write(state()); const file = join(directory, "production.json"); for (const raw of [ "{broken", JSON.stringify({ $bedrock: { version: 999 }, environment: "production", resources: [] }), JSON.stringify({ $bedrock: { version: 1 }, environment: "staging", resources: [] }), ]) { writeFileSync(file, raw); const result = await statePort.read("production"); assert.equal(result.success, false); assert.equal(result.err.kind, "stateError"); assert.equal(readFileSync(file, "utf8"), raw); } }); test("invalid state cannot replace the last valid snapshot", async (t) => { const { statePort, directory } = fixture(t); await statePort.write(state()); const before = readFileSync(join(directory, "production.json"), "utf8"); assert.equal((await statePort.write({ ...state(), version: 999 })).success, false); assert.equal(readFileSync(join(directory, "production.json"), "utf8"), before); }); test("unsafe environment names cannot escape the state directory", async (t) => { const { statePort, stateLockPort, directory } = fixture(t); for (const environment of ["../outside", "..", "a/b", "a\\b", "CON", ""]) { assert.equal((await statePort.read(environment)).success, false); assert.equal((await statePort.write(state(environment))).success, false); assert.equal((await stateLockPort.acquire(environment)).success, false); } assert.equal(existsSync(directory), false); }); test("deployment locks exclude independent Node processes and release normally", async (t) => { const { directory, stateLockPort } = fixture(t); const first = await stateLockPort.acquire("production", { operation: "deploy" }); assert.equal(first.success, true); assert.equal((await stateLockPort.inspect("production")).data.operation, "deploy"); const probe = () => spawnSync( process.execPath, [ "--input-type=module", "-e", ` import { createLocalStatePorts } from ${JSON.stringify(pluginUrl.href)}; const lock = await createLocalStatePorts(${JSON.stringify(directory)}).stateLockPort.acquire("production"); if (!lock.success) process.exit(7); await lock.data.release(); `, ], { encoding: "utf8" }, ); assert.equal(probe().status, 7); assert.equal((await first.data.release()).success, true); const next = probe(); assert.equal(next.status, 0, next.stderr); assert.equal((await stateLockPort.inspect("production")).data, undefined); }); test("explicit unlock recovers an abandoned lock without allowing its old holder to release a new one", async (t) => { const { directory, stateLockPort } = fixture(t); const old = await stateLockPort.acquire("production"); const fresh = createLocalStatePorts(directory).stateLockPort; assert.equal((await fresh.forceRelease("production")).success, true); const next = await fresh.acquire("production"); assert.equal(next.success, true); assert.equal((await old.data.release()).success, false); assert.ok((await fresh.inspect("production")).data); assert.equal((await next.data.release()).success, true); }); test("the real Bedrock loader and CLI diff work without GitHub credentials or network calls", async (t) => { const { root, directory } = fixture(t); const config = globalThis.structuredClone(example); config.plugins = [pluginUrl.href]; config.environments.production.universe.universeId = "123456789"; config.environments.production.places.start.placeId = "987654321"; config.places.start.filePath = join(root, "game.rbxl"); config.state.directory = directory; const configFile = join(root, "bedrock.config.json"); writeFileSync(configFile, JSON.stringify(config)); writeFileSync(config.places.start.filePath, "local test artifact"); const loaded = await loadProjectAsync({ cwd: root }); assert.equal(loaded.success, true, JSON.stringify(loaded)); const built = buildStateBackend({ stateConfig: loaded.data.config.state, plugins: loaded.data.plugins, getEnv: () => undefined, }); assert.equal(built.success, true, JSON.stringify(built)); assert.equal(built.data.locking, "exclusive"); await built.data.statePort.write(state()); assert.deepEqual((await built.data.statePort.read("production")).data.state, state()); rmSync(directory, { recursive: true }); const guard = join(root, "offline.mjs"); writeFileSync( guard, 'globalThis.fetch = () => { throw new Error("Unexpected network request"); };', ); const env = { ...process.env }; delete env.BEDROCK_GITHUB_TOKEN; delete env.BEDROCK_API_KEY; const result = spawnSync( process.execPath, [ "--import", pathToFileURL(guard).href, cli, "diff", "--config", configFile, "--env", "production", ], { cwd: root, env, encoding: "utf8", }, ); assert.equal(result.status, 0, result.stdout + result.stderr); assert.equal(existsSync(directory), false); });