forked from Crown-Studio/roblox-ts-template
194 lines
8.0 KiB
JavaScript
194 lines
8.0 KiB
JavaScript
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);
|
|
});
|