99 lines
3.7 KiB
JavaScript
99 lines
3.7 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { readFile } from "node:fs/promises";
|
|
import test from "node:test";
|
|
import vm from "node:vm";
|
|
import { DEFAULT_GRID_LOD_PROFILES } from "../apps/catalog/src/mapGridPolicy.mjs";
|
|
|
|
const seed = JSON.parse(await readFile(new URL("../runtime-seed/page-layouts/map.json", import.meta.url), "utf8"));
|
|
const serverSource = await readFile(new URL("./catalog-server.mjs", import.meta.url), "utf8");
|
|
const mcpSource = await readFile(new URL("./foundry-mcp.mjs", import.meta.url), "utf8");
|
|
const extensionKeys = [
|
|
"majorLinesEnabled",
|
|
"majorLabelsEnabled",
|
|
"majorLineWidthMultiplier",
|
|
"volumeEnabled",
|
|
"volumeMinimumHeightMeters",
|
|
"volumeMaximumHeightMeters",
|
|
"volumeBandHeightMeters",
|
|
];
|
|
|
|
function applicationError(code, statusCode = 400) {
|
|
const error = new Error(code);
|
|
error.statusCode = statusCode;
|
|
return error;
|
|
}
|
|
|
|
const validators = {
|
|
applicationError,
|
|
isObject: (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value),
|
|
requireNumber(value, minimum, maximum, code) {
|
|
if (typeof value !== "number" || !Number.isFinite(value) || value < minimum || value > maximum) {
|
|
throw applicationError(code);
|
|
}
|
|
return value;
|
|
},
|
|
requireBoolean(value, code) {
|
|
if (typeof value !== "boolean") throw applicationError(code);
|
|
return value;
|
|
},
|
|
requireHex(value, code) {
|
|
const normalized = String(value || "");
|
|
if (!/^#[0-9a-f]{6}$/i.test(normalized)) throw applicationError(code);
|
|
return normalized.toLowerCase();
|
|
},
|
|
};
|
|
|
|
function loadServerGridContract() {
|
|
const start = serverSource.indexOf("const GRID_LOD_PROFILE_KEYS");
|
|
const end = serverSource.indexOf("function validateMapPageSettingsPatch");
|
|
assert.ok(start >= 0 && end > start, "server grid contract source boundaries");
|
|
const context = vm.createContext({ ...validators });
|
|
const source = `${serverSource.slice(start, end)}\n;globalThis.gridContract = { DEFAULT_GRID_LOD_PROFILES, promoteLegacyGridLodProfiles, validateGridLodProfiles };`;
|
|
new vm.Script(source, { filename: "catalog-server.grid-contract.mjs" }).runInContext(context);
|
|
return context.gridContract;
|
|
}
|
|
|
|
const serverGrid = loadServerGridContract();
|
|
|
|
test("seed, client defaults and server promotion keep the sector-v2 profile contract identical", () => {
|
|
assert.deepEqual(seed.settings.gridLodProfiles, DEFAULT_GRID_LOD_PROFILES);
|
|
const legacyProfiles = seed.settings.gridLodProfiles.map((profile) => {
|
|
const legacy = { ...profile };
|
|
for (const key of extensionKeys) delete legacy[key];
|
|
return legacy;
|
|
});
|
|
assert.deepEqual(
|
|
structuredClone(serverGrid.validateGridLodProfiles(legacyProfiles)),
|
|
DEFAULT_GRID_LOD_PROFILES,
|
|
);
|
|
});
|
|
|
|
test("flat layouts migrate mode-aware extensions while explicit invalid hierarchy is rejected", () => {
|
|
const flatSettings = {
|
|
...seed.settings,
|
|
gridLodProfiles: undefined,
|
|
gridLod1StepKm: 3,
|
|
gridTileSizeKm: 10,
|
|
gridLod2Mode: "graticule",
|
|
};
|
|
const migrated = structuredClone(serverGrid.validateGridLodProfiles(
|
|
serverGrid.promoteLegacyGridLodProfiles(flatSettings),
|
|
));
|
|
assert.equal(migrated[0].majorLinesEnabled, false);
|
|
assert.equal(migrated[0].majorLabelsEnabled, false);
|
|
assert.equal(migrated[1].mode, "graticule");
|
|
assert.equal(migrated[1].volumeEnabled, false);
|
|
|
|
const invalidExplicit = structuredClone(DEFAULT_GRID_LOD_PROFILES);
|
|
invalidExplicit[0].stepKm = 3;
|
|
invalidExplicit[0].tileSizeKm = 10;
|
|
assert.throws(
|
|
() => serverGrid.validateGridLodProfiles(invalidExplicit),
|
|
/invalid_map_grid_lod_profile_1_tileSizeKm_step_ratio/,
|
|
);
|
|
});
|
|
|
|
test("Foundry MCP exposes every persisted sector-v2 profile field", () => {
|
|
for (const key of extensionKeys) assert.match(mcpSource, new RegExp(`\\b${key}: \\{`));
|
|
});
|