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.
109 lines
4.2 KiB
JavaScript
109 lines
4.2 KiB
JavaScript
// Shared fixtures. Every test works in its own temporary directory and
|
|
// points MOSAIC_CONFIG there; nothing reads the real ~/.config.
|
|
|
|
import { chmodSync, cpSync, mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { dirname, join, resolve } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
export const REPO = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..", "..");
|
|
export const REPO_ROLES = join(REPO, "roles");
|
|
export const FAR_DATE = "2099-01-01";
|
|
|
|
export function tmp() {
|
|
return mkdtempSync(join(tmpdir(), "mosaic-business-"));
|
|
}
|
|
|
|
export function writeJson(file, value, mode = 0o600) {
|
|
mkdirSync(dirname(file), { recursive: true });
|
|
writeFileSync(file, typeof value === "string" ? value : `${JSON.stringify(value, null, 2)}\n`);
|
|
chmodSync(file, mode);
|
|
return file;
|
|
}
|
|
|
|
// A token file with a placeholder value; the package never reads it.
|
|
export function tokenFile(dir, name) {
|
|
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
const file = join(dir, name);
|
|
writeFileSync(file, "placeholder-not-a-token");
|
|
chmodSync(file, 0o600);
|
|
return file;
|
|
}
|
|
|
|
// A copy of the shipped role files in a scratch roles directory.
|
|
export function rolesCopy(root) {
|
|
const dir = join(root, "roles");
|
|
cpSync(REPO_ROLES, dir, { recursive: true });
|
|
return dir;
|
|
}
|
|
|
|
// A complete valid business document for the shipped roles, with
|
|
// token files under `<root>/secrets`, and a project root under `<root>`.
|
|
export function businessDoc(root, id = "acme") {
|
|
const secrets = join(root, "secrets");
|
|
const ref = (role, service) => service === "gitea"
|
|
? { file: tokenFile(secrets, `${role}-gitea.token`), rotateBy: FAR_DATE }
|
|
: { file: tokenFile(secrets, `${role}-vikunja.token`), expires: FAR_DATE };
|
|
const role = (definition, botId, extra = {}) => ({
|
|
definition,
|
|
tracker: { bot: `bot-${id}-${definition}`, botId },
|
|
credentials: { gitea: ref(definition, "gitea"), vikunja: ref(definition, "vikunja") },
|
|
...extra,
|
|
});
|
|
const projectRoot = join(root, "project");
|
|
mkdirSync(projectRoot, { recursive: true });
|
|
return {
|
|
businessVersion: 1,
|
|
id,
|
|
human: "jason",
|
|
arbiters: { delivery: "pm", technical: "cto" },
|
|
projects: { stack: { root: projectRoot } },
|
|
vars: { "tracker.baseUrl": "http://127.0.0.1:3456", "gitea.baseUrl": "https://git.example", "tracker.pollSeconds": 60 },
|
|
tracker: {
|
|
sync: { bot: `bot-${id}-sync`, botId: 9, credentials: { vikunja: ref("sync", "vikunja") } },
|
|
labels: {},
|
|
},
|
|
roles: {
|
|
pm: role("pm", 3, { holder: "sage", vars: { harness: "pi", model: "claude-opus-5-5" } }),
|
|
cto: role("cto", 4, { holder: "darkwing", vars: { harness: "claude-code" } }),
|
|
coder: role("coder", 5, { vars: { harness: "pi", "limits.network": "none" } }),
|
|
reviewer: role("reviewer", 6),
|
|
// The chief of staff launches (REQ-LAUNCH-1, decision 139); it holds no
|
|
// credential and no tracker bot.
|
|
chief: { definition: "chief-of-staff" },
|
|
},
|
|
launch: launchDoc(),
|
|
};
|
|
}
|
|
|
|
// The system config file mosaic-config.mjs validates, under `root`.
|
|
export function systemConfig(root) {
|
|
const file = join(root, "config", "config.json");
|
|
writeJson(file, {
|
|
configVersion: 1,
|
|
environment: "development",
|
|
dataRoot: join(root, "data"),
|
|
execution: { backend: "docker", provider: "zai", model: "glm-5.3-flash", adapter: "mock" },
|
|
});
|
|
return file;
|
|
}
|
|
|
|
// The checked system layer, as systemVars() returns it, without spawning.
|
|
export function systemFor(root) {
|
|
return {
|
|
environment: "development",
|
|
dataRoot: join(root, "data"),
|
|
"execution.backend": "docker",
|
|
"execution.provider": "zai",
|
|
"execution.model": "glm-5.3-flash",
|
|
"execution.adapter": "mock",
|
|
};
|
|
}
|
|
|
|
// REQ-LIMIT-1 fixture policy: exact entries; these values are data, not ceilings.
|
|
export function launchDoc(by='chief',instances=['coder','reviewer'],counts={opus:2,sonnet:4}){
|
|
const models=Object.fromEntries(['claude-opus-5-5','claude-sonnet-5-5','glm-5.3-flash'].map(model=>[model,{provider:'zai',model}]));
|
|
const values={};if(counts.opus!==undefined)values['claude-opus-5-5']=counts.opus;if(counts.sonnet!==undefined)values['claude-sonnet-5-5']=counts.sonnet;
|
|
return {by,instances,models,limits:{models:{zai:values}}};
|
|
}
|