git mv packages/business -> packages/organization (@mosaic/organization). The old package stays as one-line re-export shims of each moved module, with src/business.mjs re-exporting src/organization-config.mjs and src/cli.mjs forwarding to the moved executable. BusinessError is OrganizationConfigError (one constructor, name kept BusinessError); OrganizationError stays distinct. The bus adapter gains organization-context.mjs (busOrganization), with business.mjs (busBusiness) as the same function. Import consumers, manifests, the lockfile, scripts/mosaic, stack-copy.sh COPY_PATHS (both roots), seat sandbox RUNNER_FILES and the copy closure walker (organization-imports.mjs, both roots required) move to the new path. v1 serialization, v1 writers and the stage A fixtures are unchanged; the stage A contract tests stay where they were and pass as before.
77 lines
3.9 KiB
JavaScript
77 lines
3.9 KiB
JavaScript
// Stream S4 (PLAN C5): the organization file's `appearance: { theme, mode }`.
|
|
import test, { after } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { readFileSync, rmSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
import { businessDoc, rolesCopy, tmp as makeRoot, writeJson } from "./helpers.mjs";
|
|
import { validateBusinessDocument } from "../src/organization-config.mjs";
|
|
import { APPEARANCE_MODES, checkAppearance } from "../src/appearance.mjs";
|
|
import { pluginSettings } from "../src/plugin-settings.mjs";
|
|
import { BusinessError } from "../src/errors.mjs";
|
|
|
|
const roots = [];
|
|
after(() => roots.forEach(root => rmSync(root, { recursive: true, force: true })));
|
|
|
|
function fixture() {
|
|
const root = makeRoot();
|
|
roots.push(root);
|
|
const rolesDir = rolesCopy(root);
|
|
const doc = businessDoc(root, "acme");
|
|
const file = writeJson(join(root, "businesses", "acme.json"), doc);
|
|
return { root, rolesDir, doc, file, check: d => validateBusinessDocument(d, file, { rolesDir }) };
|
|
}
|
|
const withAppearance = (doc, appearance) => ({ ...structuredClone(doc), appearance });
|
|
|
|
test("S4 appearance is optional: a file without it is valid and the validated business has no appearance key", () => {
|
|
const f = fixture();
|
|
const business = f.check(f.doc);
|
|
assert.equal(Object.hasOwn(business, "appearance"), false);
|
|
assert.equal(checkAppearance(undefined, "x"), undefined);
|
|
});
|
|
|
|
test("S4 appearance accepts theme and mode, either or both, and the result is frozen", () => {
|
|
const f = fixture();
|
|
for (const appearance of [{}, { theme: "harbor" }, { mode: "dim" }, { theme: "grove", mode: "system" }]) {
|
|
const business = f.check(withAppearance(f.doc, appearance));
|
|
assert.deepEqual({ ...business.appearance }, appearance);
|
|
assert.ok(Object.isFrozen(business.appearance));
|
|
}
|
|
for (const mode of APPEARANCE_MODES) assert.equal(f.check(withAppearance(f.doc, { mode })).appearance.mode, mode);
|
|
assert.deepEqual([...APPEARANCE_MODES], ["system", "light", "dim", "dark"]);
|
|
});
|
|
|
|
test("S4 appearance refuses a bad shape, an unknown key, a bad mode and a theme name that isn't a plugin name", () => {
|
|
const f = fixture();
|
|
const refused = [
|
|
null, [], "harbor", 3,
|
|
{ colour: "red" }, { theme: "harbor", extra: 1 },
|
|
{ mode: "night" }, { mode: "Dark" }, { mode: "" }, { mode: null }, { mode: 1 },
|
|
{ theme: "Harbor" }, { theme: "har bor" }, { theme: "../harbor" }, { theme: "a/b" }, { theme: "" }, { theme: 7 }, { theme: null },
|
|
{ theme: `a${"b".repeat(64)}` }, { theme: "<script>" }, { theme: "x;}body{display:none" },
|
|
];
|
|
for (const appearance of refused) {
|
|
assert.throws(() => f.check(withAppearance(f.doc, appearance)), BusinessError, JSON.stringify(appearance));
|
|
}
|
|
// The message names the field, so a person editing the file can find it.
|
|
assert.throws(() => f.check(withAppearance(f.doc, { mode: "night" })), /appearance\.mode/);
|
|
assert.throws(() => f.check(withAppearance(f.doc, { theme: "Harbor" })), /appearance\.theme/);
|
|
});
|
|
|
|
test("S4 appearance checks a theme's shape only: a name nothing installed is valid and keeps its value", () => {
|
|
const f = fixture();
|
|
const business = f.check(withAppearance(f.doc, { theme: "no-such-theme", mode: "dark" }));
|
|
assert.deepEqual({ ...business.appearance }, { theme: "no-such-theme", mode: "dark" });
|
|
});
|
|
|
|
test("S4 the organization writers keep appearance when they rewrite the file", () => {
|
|
const f = fixture();
|
|
const doc = withAppearance(f.doc, { theme: "grove", mode: "dim" });
|
|
const file = writeJson(join(f.root, "businesses", "acme.json"), doc);
|
|
const settings = pluginSettings({ dir: f.root, rolesDir: f.rolesDir });
|
|
const view = settings.read("acme");
|
|
settings.set("acme", { revision: view.revision, plugins: { "usage-source": ["meter"] } }, "jason");
|
|
const written = JSON.parse(readFileSync(file, "utf8"));
|
|
assert.deepEqual(written.appearance, { theme: "grove", mode: "dim" });
|
|
assert.deepEqual(written.plugins, { "usage-source": ["meter"] });
|
|
});
|