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.
163 lines
7.5 KiB
JavaScript
163 lines
7.5 KiB
JavaScript
// Row 323 (#1819): .53 factory business document; all execution stays in scratch fixtures.
|
|
import { test } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { spawnSync } from "node:child_process";
|
|
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
import { dirname, join } from "node:path";
|
|
import { loadBusiness } from "../src/index.mjs";
|
|
import { REPO, REPO_ROLES, systemConfig, tmp, writeJson } from "./helpers.mjs";
|
|
import { bootConfig } from "../../cli/src/config.mjs";
|
|
import { startBroker } from "../../bus/src/runtime.mjs";
|
|
import { Client } from "../../bus/src/client.mjs";
|
|
|
|
const DOCUMENT = join(REPO, "docs/guides/case1-business.mosaic-stack.json");
|
|
const COMMIT = "c".repeat(40);
|
|
const document = () => JSON.parse(readFileSync(DOCUMENT, "utf8"));
|
|
|
|
function world(t) {
|
|
const root = tmp();
|
|
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
const home = join(root, "home");
|
|
const configDir = join(home, ".config/mosaic-dev");
|
|
mkdirSync(configDir, { recursive: true, mode: 0o700 });
|
|
const doc = document();
|
|
// Replace the approved host project path only in the scratch execution input.
|
|
// The exact shipped document is separately loaded below without any substitution.
|
|
for (const project of Object.values(doc.projects)) {
|
|
project.root = join(root, "fixture-project");
|
|
mkdirSync(project.root, { recursive: true });
|
|
}
|
|
const input = writeJson(join(root, "input/mosaic-stack.json"), doc);
|
|
const config = join(configDir, "config.json");
|
|
const business = join(configDir, "businesses/mosaic-stack.json");
|
|
const copy = join(root, "fake-stack-copy");
|
|
writeFileSync(copy, '#!/usr/bin/env bash\n[ "$1" = copy ] && [ "$3" = --copies ] || exit 4\nmkdir -p "$4/$2/scripts" && : >"$4/$2/scripts/mosaic"\n', { mode: 0o755 });
|
|
const env = {
|
|
PATH: `${dirname(process.execPath)}:/usr/bin:/bin`, HOME: home,
|
|
MOSAIC_CONFIG: config, MOSAIC_ROLES_DIR: REPO_ROLES, CASE2_STACK_COPY: copy,
|
|
};
|
|
const install = () => spawnSync(join(REPO, "scripts/case2-install.sh"), [
|
|
"--business", "mosaic-stack", "--business-file", input, "--commit", COMMIT, "--profile", "factory",
|
|
], { env, encoding: "utf8", timeout: 30000 });
|
|
return { root, home, doc, input, config, configDir, business, env, install };
|
|
}
|
|
|
|
const output = (r) => r.stdout + r.stderr;
|
|
|
|
function configure(x) {
|
|
// Reuse the existing scratch system-config fixture, never a real install config.
|
|
const fixture = JSON.parse(readFileSync(systemConfig(x.root), "utf8"));
|
|
fixture.dataRoot = join(x.root, "data");
|
|
mkdirSync(fixture.dataRoot, { recursive: true, mode: 0o700 });
|
|
writeJson(x.config, fixture);
|
|
writeJson(x.business, x.doc);
|
|
return fixture;
|
|
}
|
|
|
|
test("row 323: committed Case 1 document loads unchanged through the business loader", (t) => {
|
|
const x = world(t);
|
|
const bytes = readFileSync(DOCUMENT);
|
|
mkdirSync(dirname(x.business), { recursive: true, mode: 0o700 });
|
|
writeFileSync(x.business, bytes, { mode: 0o600 });
|
|
const b = loadBusiness("mosaic-stack", { dir: x.configDir, rolesDir: REPO_ROLES });
|
|
assert.deepEqual(readFileSync(x.business), bytes);
|
|
assert.equal(b.id, "mosaic-stack");
|
|
assert.equal(b.human, "jason");
|
|
assert.equal(b.vars["bus.tester"], true);
|
|
assert.equal(b.tracker, null);
|
|
assert.equal(Object.hasOwn(document(), "tracker"), false);
|
|
assert.equal(Object.hasOwn(document(), "library"), false);
|
|
assert.deepEqual(b.projects, { "stack": { root: "/srv/mosaic/src/mosaic-stack" } });
|
|
assert.deepEqual(Object.keys(b.roles), ["chief", "assistant"]);
|
|
assert.deepEqual(b.plugins.harness, ["claude-code"]);
|
|
assert.deepEqual(document().plugins.channel, ["cli", "webui"]);
|
|
assert.deepEqual(b.plugins.channel, ["cli", "webui"]);
|
|
assert.equal(b.launch.by, "chief");
|
|
assert.deepEqual(b.launch.instances, ["assistant"]);
|
|
assert.deepEqual(b.arbiters, { delivery: "chief", technical: "chief" });
|
|
// #416: the .53 factory reaches Anthropic through the proxy, under the factory account.
|
|
assert.deepEqual(b.launch.models["claude-opus-5-5"], { provider: "proxy", model: "claude-opus-5-5", route: "anthropic" });
|
|
assert.deepEqual(b.launch.account, { name: "factory", credential: "proxy" });
|
|
for (const role of Object.values(b.roles)) {
|
|
assert.equal(role.vars.harness, "claude-code");
|
|
assert.equal(role.vars.model, "claude-opus-5-5");
|
|
assert.equal(role.vars.thinking, "high");
|
|
assert.deepEqual(role.credentials, {});
|
|
assert.equal(role.tracker, null);
|
|
}
|
|
});
|
|
|
|
// #416 changed the shipped route; the direct-provider input it replaced still loads.
|
|
test("row 323: a direct Anthropic launch without an account still loads", (t) => {
|
|
const x = world(t);
|
|
x.doc.launch.models["claude-opus-5-5"] = { provider: "anthropic", model: "claude-opus-5-5" };
|
|
delete x.doc.launch.account;
|
|
writeJson(x.business, x.doc);
|
|
const b = loadBusiness("mosaic-stack", { dir: x.configDir, rolesDir: REPO_ROLES });
|
|
assert.deepEqual(b.launch.models["claude-opus-5-5"], { provider: "anthropic", model: "claude-opus-5-5" });
|
|
assert.equal(b.launch.account, undefined);
|
|
});
|
|
|
|
test("row 323: real business validate accepts the Case 1 fixture without tracker credentials", (t) => {
|
|
const x = world(t);
|
|
configure(x);
|
|
const r = spawnSync(process.execPath, [join(REPO, "packages/organization/src/cli.mjs"), "validate", "mosaic-stack"],
|
|
{ env: x.env, encoding: "utf8", timeout: 30000 });
|
|
assert.equal(r.status, 0, output(r));
|
|
assert.equal(JSON.parse(r.stdout).business, "mosaic-stack");
|
|
});
|
|
|
|
test("row 323: real factory install accepts the document's scratch input and writes it unchanged", (t) => {
|
|
const x = world(t);
|
|
const r = x.install();
|
|
assert.equal(r.status, 0, output(r));
|
|
assert.deepEqual(JSON.parse(readFileSync(x.business, "utf8")), x.doc);
|
|
assert.match(output(r), /business mosaic-stack validates/);
|
|
assert.equal(existsSync(join(x.configDir, "personas")), false);
|
|
assert.equal(existsSync(join(x.configDir, "secrets/mosaic-stack")), false);
|
|
});
|
|
|
|
test("row 323: the Case 1 business boots without a tracker or @sync credential", async (t) => {
|
|
const x = world(t);
|
|
const system = configure(x);
|
|
const boot = bootConfig({ system, businessId: "mosaic-stack", env: x.env });
|
|
assert.equal(Object.hasOwn(boot, "trackers"), false);
|
|
const runtime = await startBroker(boot);
|
|
t.after(() => runtime.close());
|
|
assert.deepEqual(runtime.credentials.status(), []);
|
|
const reader = new Client({ path: runtime.path, cap: runtime.readers[0].cap });
|
|
assert.deepEqual(await reader.call("credentials"), []);
|
|
});
|
|
|
|
test("row 323: factory install refuses bus.tester false before writing", (t) => {
|
|
const x = world(t);
|
|
x.doc.vars["bus.tester"] = false;
|
|
writeJson(x.input, x.doc);
|
|
const r = x.install();
|
|
assert.equal(r.status, 1, output(r));
|
|
assert.match(output(r), /refused: bus-tester-off:/);
|
|
assert.equal(existsSync(x.business), false);
|
|
assert.equal(existsSync(x.config), false);
|
|
});
|
|
|
|
test("row 323: literal library key refuses through final business validation", (t) => {
|
|
const x = world(t);
|
|
x.doc.library = {};
|
|
writeJson(x.input, x.doc);
|
|
const r = x.install();
|
|
assert.equal(r.status, 1, output(r));
|
|
assert.match(output(r), /refused: validate: .*unsupported business file key: "library"/);
|
|
assert.equal(existsSync(x.business), false);
|
|
});
|
|
|
|
test("row 323: a library project refuses through the factory profile plan", (t) => {
|
|
const x = world(t);
|
|
x.doc.projects.books = { kind: "library" };
|
|
writeJson(x.input, x.doc);
|
|
const r = x.install();
|
|
assert.equal(r.status, 1, output(r));
|
|
assert.match(output(r), /refused: profile-library-business:/);
|
|
assert.equal(existsSync(x.business), false);
|
|
assert.equal(existsSync(x.config), false);
|
|
});
|