231 lines
14 KiB
JavaScript
231 lines
14 KiB
JavaScript
// Row 65 (REQ-PLUG-2, Q156): the two harness modules load through the loader with every export
|
|
// the kind names, answer the questions core used to answer by name, and no core source names a
|
|
// harness outside the exceptions the brief lists.
|
|
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { chmodSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join, relative, resolve } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { spawnSync } from "node:child_process";
|
|
import { createLoader } from "../src/loader.mjs";
|
|
import { KINDS } from "../src/kinds.mjs";
|
|
|
|
const REPO = resolve(fileURLToPath(new URL("../../../", import.meta.url)));
|
|
|
|
async function load(name) {
|
|
const logged = [];
|
|
const loader = createLoader({ log: entry => logged.push(entry) });
|
|
const result = await loader.load({ harness: [name] });
|
|
assert.deepEqual(result.refused, [], name);
|
|
assert.deepEqual(logged, [], name);
|
|
const [module] = loader.modules("harness");
|
|
return module;
|
|
}
|
|
|
|
function scratch(t) {
|
|
const root = mkdtempSync(join(tmpdir(), "mosaic-harness-plugin-"));
|
|
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
return root;
|
|
}
|
|
|
|
test("row 65 both harness modules load through the loader with every export the kind names", async () => {
|
|
for (const name of ["pi", "claude-code"]) {
|
|
const module = await load(name);
|
|
for (const key of KINDS.harness.exports) assert.equal(typeof module[key], "function", `${name} ${key}`);
|
|
assert.equal(module.interfaceVersion, 1);
|
|
}
|
|
});
|
|
|
|
test("row 65 each module answers for its own harness: adapter, levels, account, web, turn end and session files", async () => {
|
|
const pi = await load("pi"), claude = await load("claude-code");
|
|
assert.equal(pi.adapter(), join(REPO, "adapters", "pi", "adapter.sh"));
|
|
assert.equal(claude.adapter(), join(REPO, "adapters", "claude", "adapter.sh"));
|
|
// pi 0.85.1 --thinking and claude 2.1.302 --effort, as probed (BUILD.md); claude has no off or minimal.
|
|
assert.deepEqual([...pi.levels()], ["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
|
|
assert.deepEqual([...claude.levels()], ["low", "medium", "high", "xhigh", "max"]);
|
|
assert.deepEqual([pi.countsAccount(), claude.countsAccount()], [false, true]);
|
|
assert.deepEqual([pi.builtinWeb(), claude.builtinWeb()], [false, true]);
|
|
assert.deepEqual([pi.turnEnd(), claude.turnEnd()], ["marker", "exit"]);
|
|
assert.deepEqual([pi.outputPolicy({ proxy: true }), pi.outputPolicy({ proxy: false }), claude.outputPolicy({ proxy: true }), claude.outputPolicy({ proxy: false })], ["non-newline-stdout", "stdout", "adapter", "adapter"]);
|
|
assert.deepEqual([pi.typedName("list_tasks"), claude.typedName("list_tasks")], ["list_tasks", "mcp__mosaic__list_tasks"]);
|
|
assert.equal(pi.gateTimeout(), null);
|
|
assert.deepEqual(claude.gateTimeout(), { n: 10, k: 2, hook: 20 });
|
|
assert.equal(pi.transcript({ runDir: "/r" }), "/r/session");
|
|
assert.equal(claude.transcript({ runDir: "/r" }), "/r/home/.claude/projects");
|
|
assert.equal(pi.transcript({ runDir: "/r", store: { session: "/s", projects: "/p" } }), "/s");
|
|
assert.equal(claude.transcript({ runDir: "/r", store: { session: "/s", projects: "/p" } }), "/p");
|
|
assert.equal(pi.reliesOn().length, 5);
|
|
assert.equal(claude.reliesOn().length, 5);
|
|
});
|
|
|
|
test("row 65 claude-code preflight: a missing credential, then a missing or relative binary, each refuse with their code", async t => {
|
|
const claude = await load("claude-code");
|
|
const root = scratch(t);
|
|
const bin = join(root, "claude");
|
|
writeFileSync(bin, "#!/bin/sh\necho 2.1.302\n");
|
|
chmodSync(bin, 0o755);
|
|
const token = { CLAUDE_CODE_OAUTH_TOKEN: "fixture" };
|
|
assert.equal(claude.preflight({ env: { MOSAIC_CLAUDE_BIN: bin } }).code, "no-session-credential");
|
|
assert.equal(claude.preflight({ env: { ...token, PATH: join(root, "none") } }).code, "harness-binary-missing");
|
|
assert.equal(claude.preflight({ env: { ...token, MOSAIC_CLAUDE_BIN: "claude" } }).code, "harness-binary-missing");
|
|
assert.equal(claude.preflight({ env: { ...token, MOSAIC_CLAUDE_BIN: bin } }), null);
|
|
assert.equal(claude.preflight({ env: { ...token, PATH: root } }), null);
|
|
assert.equal(claude.version({ env: { MOSAIC_CLAUDE_BIN: bin, PATH: process.env.PATH } }), "2.1.302");
|
|
// The pinned binary outside /usr is mounted where the adapter looks for it, and the unslop hook's
|
|
// files where the hook commands name them (row 66).
|
|
const unslop = ["claude-hook.js", "unslop-check.js", "lists.json"].map(f => [join(REPO, "tools", "unslop-hook", f), undefined]);
|
|
assert.deepEqual(claude.mounts({ env: { MOSAIC_CLAUDE_BIN: bin } }).map(m => [m.src, m.dest]), [[bin, bin], ...unslop, [join(REPO, "adapters/claude/stream.mjs"), undefined]]);
|
|
assert.deepEqual(claude.mounts({ env: {} }).map(m => [m.src, m.dest]), [...unslop, [join(REPO, "adapters/claude/stream.mjs"), undefined]]);
|
|
});
|
|
|
|
// Row 68's claudeVersion precedence, moved here with the version (row 65): the manifest records
|
|
// what the binary sessions run answers.
|
|
test("row 65 claude-code version asks MOSAIC_CLAUDE_BIN when set, else claude on PATH, else null", async t => {
|
|
const claude = await load("claude-code");
|
|
const dir = scratch(t);
|
|
mkdirSync(join(dir, "bin"));
|
|
const stand = (p, v) => {
|
|
writeFileSync(p, `#!/bin/sh\necho "${v} (Claude Code)"\n`);
|
|
chmodSync(p, 0o755);
|
|
return p;
|
|
};
|
|
stand(join(dir, "bin", "claude"), "1.0.0");
|
|
const pinned = stand(join(dir, "pinned"), "9.9.9");
|
|
const PATH = join(dir, "bin");
|
|
assert.equal(claude.version({ env: { PATH, MOSAIC_CLAUDE_BIN: pinned } }), "9.9.9 (Claude Code)");
|
|
assert.equal(claude.version({ env: { PATH } }), "1.0.0 (Claude Code)");
|
|
assert.equal(claude.version({ env: { PATH: join(dir, "none") } }), null);
|
|
});
|
|
|
|
test("row 65 pi preflight refuses missing credentials, and its version is the package pin", async () => {
|
|
const pi = await load("pi");
|
|
assert.equal(pi.preflight({ env: {} }).code, "no-session-credential");
|
|
const pkg = JSON.parse(readFileSync(join(REPO, "package.json"), "utf8"));
|
|
assert.equal(pi.version({ env: {} }), pkg.dependencies?.["@earendil-works/pi-coding-agent"] ?? pkg.devDependencies?.["@earendil-works/pi-coding-agent"]);
|
|
// Row 66: the unslop extension's files, bound read-only at their own paths.
|
|
assert.deepEqual(pi.mounts({ env: {} }).map(m => m.dest ?? m.src), [join(REPO, "packages/plugins/harness/pi/model-stream.mjs"), join(REPO, "node_modules"), ...["extension.ts", "unslop-check.js", "lists.json"].map(f => join(REPO, "tools", "unslop-hook", f))]);
|
|
});
|
|
|
|
test("row 65 env passes the thinking level to both adapters, and claude-code's bundle writes its hook and MCP files", async t => {
|
|
const pi = await load("pi"), claude = await load("claude-code");
|
|
const session = { provider: "fixture", model: "m", thinking: "high", toolSocket: "/run/tools.sock" };
|
|
const piEnv = pi.env({ policy: { tools: ["read"] }, tools: [{ name: "list_tasks" }], session });
|
|
assert.equal(piEnv.PI_OFFLINE, "1", "pi avoids startup network operations");
|
|
assert.deepEqual([piEnv.MOSAIC_THINKING, piEnv.PI_MODEL, piEnv.MOSAIC_TOOLS], ["high", "m", "read,list_tasks"]);
|
|
assert.equal(piEnv.MOSAIC_MODEL_ACTIVITY_SOURCE, undefined, "Pi retains stdout fallback for paths without streaming activity");
|
|
// Row 66: pi loads the unslop check as its second extension.
|
|
assert.deepEqual(piEnv.MOSAIC_EXTENSIONS.split(","), [join(REPO, "packages", "harness", "src", "pi-extension.mjs"), join(REPO, "tools", "unslop-hook", "extension.ts")]);
|
|
const dir = scratch(t);
|
|
const written = [];
|
|
const write = (name, text) => { writeFileSync(join(dir, name), text); written.push(name); return [name, "0".repeat(64)]; };
|
|
const quote = path => { assert.ok(!path.includes("'")); return `'${path}'`; };
|
|
const out = claude.bundle({ dir, files: { policy: join(dir, "policy.json"), tools: join(dir, "tools.json") }, session, write, quote });
|
|
assert.deepEqual(written, ["settings.json", "mcp.json"]);
|
|
const settings = JSON.parse(readFileSync(join(dir, "settings.json"), "utf8"));
|
|
assert.match(settings.hooks.PreToolUse[0].hooks[0].command, /^timeout -k 2 10 .* \|\| exit 2$/);
|
|
assert.equal(settings.hooks.PreToolUse[0].hooks[0].timeout, 20);
|
|
// Row 66: the unslop hooks can't block: each command ends with `; exit 0`.
|
|
assert.match(settings.hooks.Stop[0].hooks[0].command, /^timeout -k 2 10 .*claude-hook\.js' stop; exit 0$/);
|
|
assert.match(settings.hooks.UserPromptSubmit[0].hooks[0].command, /^timeout -k 2 10 .*claude-hook\.js' prompt; exit 0$/);
|
|
assert.equal(JSON.parse(readFileSync(join(dir, "mcp.json"), "utf8")).mcpServers.mosaic.args.at(-1), "/run/tools.sock");
|
|
const claudeEnv = claude.env({ files: out.files, policy: { tools: ["read"], typed: [] }, session });
|
|
assert.equal(claudeEnv.MOSAIC_MODEL_ACTIVITY_SOURCE, "adapter");
|
|
assert.deepEqual([claudeEnv.MOSAIC_THINKING, claudeEnv.MOSAIC_CLAUDE_SETTINGS, claudeEnv.MOSAIC_CLAUDE_MCP_CONFIG], ["high", out.files.settings, out.files.mcp]);
|
|
});
|
|
|
|
// The level a route names reaches the harness binary as its flag: each module's env() feeds the
|
|
// real adapter, and a stand-in pi or claude records the argv it was given (dewey B3).
|
|
test("row 65 a route's thinking reaches pi as --thinking and claude as --effort, through each module's env and adapter", async t => {
|
|
const pi = await load("pi"), claude = await load("claude-code");
|
|
const dir = scratch(t);
|
|
mkdirSync(join(dir, "bin"));
|
|
for (const name of ["pi", "claude"]) {
|
|
writeFileSync(join(dir, "bin", name), '#!/bin/sh\nprintf "%s\\n" "$@" > "$ARGV_OUT"\necho \'{"type":"result","subtype":"success","result":"ok"}\'\n');
|
|
chmodSync(join(dir, "bin", name), 0o755);
|
|
}
|
|
for (const f of ["prompt.md", "settings.json", "mcp.json"]) writeFileSync(join(dir, f), "{}");
|
|
const run = (adapter, moduleEnv) => {
|
|
const out = join(dir, "argv");
|
|
rmSync(out, { force: true });
|
|
const r = spawnSync("/bin/sh", [join(REPO, "adapters", adapter, "adapter.sh")], {
|
|
encoding: "utf8",
|
|
env: {
|
|
PATH: `${join(dir, "bin")}:/usr/bin:/bin`, ARGV_OUT: out, MOSAIC_SYSTEM_PROMPT_FILE: join(dir, "prompt.md"),
|
|
MOSAIC_REQUEST: "x", MOSAIC_WORKSPACE: join(dir, "ws"), MOSAIC_SESSION_DIR: join(dir, "session"), MOSAIC_MODEL: "m",
|
|
...moduleEnv,
|
|
},
|
|
});
|
|
assert.equal(r.status, 0, r.stderr);
|
|
return readFileSync(out, "utf8").split("\n");
|
|
};
|
|
const flag = (argv, name) => {
|
|
assert.equal(argv.filter(a => a === name).length, 1, argv.join(" "));
|
|
return argv[argv.indexOf(name) + 1];
|
|
};
|
|
for (const thinking of ["xhigh", "low"]) {
|
|
const session = { provider: "fixture", model: "m", thinking, toolSocket: "/run/tools.sock" };
|
|
assert.equal(flag(run("pi", pi.env({ policy: { tools: ["read"] }, tools: [], session })), "--thinking"), thinking);
|
|
const files = { settings: join(dir, "settings.json"), mcp: join(dir, "mcp.json") };
|
|
assert.equal(flag(run("claude", claude.env({ files, policy: { tools: ["read"], typed: [] }, session })), "--effort"), thinking);
|
|
}
|
|
});
|
|
|
|
// A harness name in core: a quoted "pi" or "claude-code", or either inside a regex alternation.
|
|
const NAMED = /["'`](?:pi|claude-code)["'`]|[(|](?:pi|claude-code)[|)]/;
|
|
// The brief's Out of scope list: the tool-policy gate, the human-proof check, the engines that
|
|
// start pi directly, and the process scan. Each must still name a harness, so the list stays true.
|
|
const EXCEPT = [
|
|
"packages/harness/src/gate.mjs",
|
|
"packages/harness/src/claude-gate.mjs",
|
|
"packages/harness/src/pi-extension.mjs",
|
|
"packages/bus/src/human.mjs",
|
|
"packages/conversation/src/",
|
|
"packages/discord/src/",
|
|
"packages/control-board/src/scan.mjs",
|
|
];
|
|
function sources(dir, out = []) {
|
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
const path = join(dir, entry.name);
|
|
if (entry.isDirectory()) { if (entry.name !== "node_modules" && entry.name !== "third_party") sources(path, out); }
|
|
else if (/\.(?:mjs|js)$/.test(entry.name)) out.push(path);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
test("row 65 no core source outside packages/plugins names a harness, apart from the listed exceptions", () => {
|
|
const named = [];
|
|
const hit = new Set();
|
|
for (const pkg of readdirSync(join(REPO, "packages"))) {
|
|
if (pkg === "plugins") continue;
|
|
let files;
|
|
try { files = sources(join(REPO, "packages", pkg, "src")); } catch (e) { if (e.code === "ENOENT") continue; throw e; }
|
|
for (const file of files) {
|
|
const rel = relative(REPO, file);
|
|
readFileSync(file, "utf8").split("\n").forEach((line, i) => {
|
|
if (!NAMED.test(line)) return;
|
|
const exception = EXCEPT.find(e => (e.endsWith("/") ? rel.startsWith(e) : rel === e));
|
|
if (exception) hit.add(exception);
|
|
else named.push(`${rel}:${i + 1}: ${line.trim()}`);
|
|
});
|
|
}
|
|
}
|
|
assert.deepEqual(named, []);
|
|
assert.deepEqual([...hit].sort(), [...EXCEPT].sort(), "every exception still names a harness");
|
|
// human.mjs keeps one: the human-proof regex that refuses any agent harness binary.
|
|
const human = readFileSync(join(REPO, "packages", "bus", "src", "human.mjs"), "utf8").split("\n").filter(line => NAMED.test(line));
|
|
assert.deepEqual(human.map(line => line.trim()), ["/^(pi|claude|claude-code|codex)(?:\\.js)?$/.test(command) ||"]);
|
|
});
|
|
|
|
test("row 65 the grep's pattern catches a quoted name and a regex alternation, and not a word in prose", () => {
|
|
for (const line of ['if (harness === "pi")', "x = 'claude-code';", "/^(pi|codex)$/", "/^(codex|claude-code)$/", "if (harness !== 'pi')", "['pi', 'claude-code'].includes(harness)"]) assert.ok(NAMED.test(line), line);
|
|
for (const line of ["// pi runs the bundle", "const piModel = 1;", "pipeline"]) assert.ok(!NAMED.test(line), line);
|
|
});
|
|
|
|
test('pi materializes module-declared compatibility without a provider-specific switch', async t => {
|
|
const pi=await load('pi'),root=scratch(t),dir=join(root,'bundle');mkdirSync(dir);
|
|
const entry={provider:'fixture',model:'fixture-id'},compat={thinkingFormat:'openai',zaiToolStream:false};
|
|
const out=pi.bundle({dir,models:{fixture:entry},proxy:{baseUrl:'http://127.0.0.1:9999',key:'fixture-secret'},transports:{fixture:{piProvider:'fixture',endpoint:'v1',api:'openai-completions',compat}}});
|
|
assert.deepEqual(JSON.parse(readFileSync(out.files.proxyModels,'utf8')).providers.fixture.compat,compat);
|
|
});
|