Files
jason.woltje a8d9eecf65 organization: move packages/business to packages/organization (rename r2 stage B1)
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.
2026-11-09 11:47:48 -06:00

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"] });
});