167 lines
6.2 KiB
JavaScript
167 lines
6.2 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",
|
|
"selectionFillColor",
|
|
"selectionFillOpacityPercent",
|
|
"selectionOutlineColor",
|
|
"selectionOutlineWidthPx",
|
|
"selectionOutlineOpacityPercent",
|
|
];
|
|
|
|
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("Robot2B legacy radius migrates per LOD without hiding the application", () => {
|
|
const legacyRobot2BSettings = {
|
|
gridHeightMeters: 500,
|
|
gridLod1MaxHeightKm: 10,
|
|
gridLod1StepKm: 1,
|
|
gridLod2MaxHeightKm: 50,
|
|
gridLod2StepKm: 5,
|
|
gridLod3StepKm: 25,
|
|
gridRadiusKm: 1_000,
|
|
gridLineWidth: 4,
|
|
gridColor: "#f5f5f5",
|
|
gridOpacity: 12,
|
|
gridDotsEnabled: true,
|
|
gridDotsSize: 7,
|
|
gridDotsColor: "#ffffff",
|
|
gridDotsOpacity: 58,
|
|
};
|
|
const migrated = structuredClone(serverGrid.validateGridLodProfiles(
|
|
serverGrid.promoteLegacyGridLodProfiles(legacyRobot2BSettings),
|
|
));
|
|
|
|
assert.deepEqual(migrated.map((profile) => profile.radiusKm), [50, 1_000, 1_000, 1_000, 100]);
|
|
assert.ok(migrated.every((profile) => profile.radiusKm / profile.stepKm <= 512));
|
|
assert.ok(migrated.every((profile) => profile.graticuleLineWidthPx === 3));
|
|
});
|
|
|
|
test("Foundry MCP exposes every persisted sector-v2 profile field", () => {
|
|
for (const key of extensionKeys) assert.match(mcpSource, new RegExp(`\\b${key}: \\{`));
|
|
});
|
|
|
|
test("server persists selected-sector visuals and accepts a 1,000 m address band inside its range", () => {
|
|
const profiles = structuredClone(DEFAULT_GRID_LOD_PROFILES);
|
|
Object.assign(profiles[0], {
|
|
volumeMaximumHeightMeters: 1_000,
|
|
volumeBandHeightMeters: 1_000,
|
|
selectionFillColor: "#A0B1C2",
|
|
selectionFillOpacityPercent: 34,
|
|
selectionOutlineColor: "#C3D4E5",
|
|
selectionOutlineWidthPx: 7.5,
|
|
selectionOutlineOpacityPercent: 81,
|
|
});
|
|
const validated = structuredClone(serverGrid.validateGridLodProfiles(profiles));
|
|
assert.equal(validated[0].volumeBandHeightMeters, 1_000);
|
|
assert.equal(validated[0].selectionFillColor, "#a0b1c2");
|
|
assert.equal(validated[0].selectionFillOpacityPercent, 34);
|
|
assert.equal(validated[0].selectionOutlineColor, "#c3d4e5");
|
|
assert.equal(validated[0].selectionOutlineWidthPx, 7.5);
|
|
assert.equal(validated[0].selectionOutlineOpacityPercent, 81);
|
|
});
|
|
|
|
test("server rejects selected-sector visual values outside the canonical bounds", () => {
|
|
for (const [field, value] of [
|
|
["selectionFillColor", "cyan"],
|
|
["selectionFillOpacityPercent", 101],
|
|
["selectionOutlineColor", "#ffff"],
|
|
["selectionOutlineWidthPx", 12.01],
|
|
["selectionOutlineOpacityPercent", -1],
|
|
]) {
|
|
const profiles = structuredClone(DEFAULT_GRID_LOD_PROFILES);
|
|
profiles[0][field] = value;
|
|
assert.throws(
|
|
() => serverGrid.validateGridLodProfiles(profiles),
|
|
new RegExp(`invalid_map_grid_lod_profile_1_${field}`),
|
|
);
|
|
}
|
|
});
|