Files

234 lines
12 KiB
JavaScript

// Row 164: channel selection stays independent of human proof and administration.
import test from "node:test";
import assert from "node:assert/strict";
import { mkdirSync, renameSync, symlinkSync, unlinkSync } from "node:fs";
import { join } from "node:path";
import { createLoader } from "../src/loader.mjs";
import { KINDS } from "../src/kinds.mjs";
import { tree } from "./helpers.mjs";
const code = expected => e => e.code === expected;
const fixtureModule = tag => `export const interfaceVersion = 1;
export async function dispatch(request, {call}) { return call(request.verb, {...request.args, fixture:${JSON.stringify(tag)}}); }
`;
const settings = channel => ({ business: "demo", revision: "a".repeat(64), plugins: channel === undefined ? {} : { channel } });
const helper = () => import("../src/channel.mjs");
test("channel registry requires dispatch at interface 1 and switches a list", () => {
assert.deepEqual(KINDS.channel, { kind: "channel", interfaceVersion: 1, exports: ["dispatch"], switch: "list" });
assert.ok(Object.isFrozen(KINDS.channel));
assert.ok(Object.isFrozen(KINDS.channel.exports));
});
test("channel shipped modules load and preserve every supported operation and bus refusal", async () => {
const loader = createLoader({ log: () => {} });
assert.deepEqual((await loader.load({ channel: ["cli", "webui"] })).refused, []);
assert.deepEqual(loader.loaded().channel, ["cli", "webui"]);
const modules = loader.modules("channel");
for (const [index, verbs] of [
[0, ["project.list", "project.create", "thread.list", "thread.create", "thread.messages", "thread.send", "thread.open", "session.list", "session.create", "session.clear", "session.cancel", "session.operation", "message.receive", "message.read"]],
[1, ["project.list", "project.create", "thread.list", "thread.create", "thread.messages", "thread.send", "thread.open", "session.list", "session.create", "session.clear", "session.cancel", "session.operation"]],
]) {
for (const verb of verbs) {
const seen = [];
const result = await modules[index].dispatch({ verb, args: { thread: "thread-one", body: "literal", class: "REQUEST" } }, {
call: async (v, args) => { seen.push([v, args]); return { receipt: "preserved" }; },
});
assert.deepEqual(result, { receipt: "preserved" });
assert.deepEqual(seen, [[verb, { thread: "thread-one", body: "literal", class: "REQUEST" }]]);
}
const refusal = Object.assign(new Error("thread-not-owned"), { code: "thread-not-owned" });
await assert.rejects(modules[index].dispatch({ verb: "thread.list", args: {} }, { call: async () => { throw refusal; } }), e => e === refusal);
let calls = 0;
await assert.rejects(modules[index].dispatch({ verb: "plugins.set", args: {} }, { call: async () => calls++ }), code("channel-operation"));
assert.equal(calls, 0);
}
await assert.rejects(modules[1].dispatch({ verb: "message.receive", args: {} }, { call: async () => assert.fail("not a web chat operation") }), code("channel-operation"));
});
test("channel snapshot is a frozen matched pair and a captured array survives reload", async t => {
const s = tree(t);
s.write("channel", "one", fixtureModule("one"));
s.write("channel", "two", fixtureModule("two"));
const loader = await s.loader();
await loader.load({ channel: ["two", "one"] });
assert.equal(typeof loader.snapshot, "function");
const captured = loader.snapshot("channel");
assert.deepEqual(captured.map(x => x.name), ["two", "one"]);
assert.deepEqual(captured.map(x => x.name), loader.loaded().channel);
assert.deepEqual(captured.map(x => x.module), loader.modules("channel"));
assert.ok(Object.isFrozen(captured));
assert.ok(captured.every(Object.isFrozen));
assert.throws(() => captured.push(captured[0]), TypeError);
await loader.load({ channel: ["one"] });
assert.deepEqual(loader.snapshot("channel").map(x => x.name), ["one"]);
assert.deepEqual(captured.map(x => x.name), ["two", "one"]);
assert.deepEqual(await captured[0].module.dispatch({ verb: "thread.list", args: {} }, { call: async (verb, args) => args }), { fixture: "two" });
assert.throws(() => loader.snapshot("no-such-kind"), code("unknown-kind"));
});
test("channel guards refuse bad names, missing paths, exports and versions while retaining last good", async t => {
const s = tree(t);
s.write("channel", "good", fixtureModule("good"));
mkdirSync(join(s.pkg, "channel", "empty"));
mkdirSync(join(s.pkg, "channel", "dir-index", "index.mjs"), { recursive: true });
s.write("channel", "missing-export", "export const interfaceVersion = 1;\n");
s.write("channel", "missing-version", "export function dispatch() {}\n");
s.write("channel", "bad-type", "export const interfaceVersion = 1; export const dispatch = 1;\n");
s.write("channel", "wrong-version", "export const interfaceVersion = 2; export function dispatch() {}\n");
const loader = await s.loader();
assert.deepEqual((await loader.load({ channel: ["good"] })).refused, []);
for (const [names, expected] of [
[["../good"], "bad-name"], [["absent"], "missing"], [["empty"], "missing"], [["dir-index"], "missing"],
[["missing-export"], "missing-export"], [["missing-version"], "missing-export"], [["bad-type"], "missing-export"], [["wrong-version"], "interface-version"],
]) {
const result = await loader.load({ channel: names });
assert.equal(result.refused.length, 1);
assert.equal(result.refused[0].code, expected);
assert.deepEqual(result.loaded.channel, ["good"]);
assert.deepEqual(result.refused[0].kept, ["good"]);
}
});
test("channel guards refuse symlinked module, index and kind directories", async t => {
const s = tree(t);
const good = s.write("channel", "good", fixtureModule("good"));
const loader = await s.loader();
await loader.load({ channel: ["good"] });
symlinkSync(good, join(s.pkg, "channel", "linked"));
const index = s.write("channel", "linked-index", fixtureModule("unused"));
unlinkSync(join(index, "index.mjs"));
symlinkSync(join(good, "index.mjs"), join(index, "index.mjs"));
for (const name of ["linked", "linked-index"]) {
const result = await loader.load({ channel: [name] });
assert.equal(result.refused[0].code, "symlink");
const refusedPath = `/channel/${name}${name === "linked-index" ? "/index.mjs" : ""} is a symlink`;
assert.ok(result.refused[0].message.endsWith(refusedPath));
assert.deepEqual(result.loaded.channel, ["good"]);
}
renameSync(join(s.pkg, "channel"), join(s.pkg, "channel-real"));
symlinkSync(join(s.pkg, "channel-real"), join(s.pkg, "channel"));
assert.equal((await loader.load({ channel: ["good"] })).refused[0].code, "symlink");
});
test("channel caller re-reads settings and keeps off switches independent by module name", async () => {
const { createChannelCaller } = await helper();
let selected = ["webui"];
const calls = [], logged = [];
const call = async (verb, args) => {
calls.push([verb, args]);
return verb === "plugins" ? settings(selected) : { ok: true };
};
const cli = createChannelCaller({ name: "cli", call, log: entry => logged.push(entry) });
const webui = createChannelCaller({ name: "webui", call, log: entry => logged.push(entry) });
await assert.rejects(cli("thread.list", {}), e => e.code === "channel-disabled" && /mosaic plugins.*mosaic plugins set/s.test(e.message));
assert.deepEqual(calls.splice(0), [["plugins", {}]]);
assert.deepEqual(await webui("thread.list", {}), { ok: true });
assert.deepEqual(calls.splice(0), [["plugins", {}], ["thread.list", {}]]);
selected = ["cli"];
assert.deepEqual(await cli("project.list", {}), { ok: true });
calls.length = 0;
await assert.rejects(webui("thread.send", { body: "private" }), code("channel-disabled"));
assert.deepEqual(calls, [["plugins", {}]]);
for (selected of [[], undefined]) {
calls.length = 0;
await assert.rejects(cli("message.receive", {}), code("channel-disabled"));
assert.deepEqual(calls, [["plugins", {}]]);
}
assert.deepEqual(logged, []);
});
test("channel caller malformed settings refuse without a chat operation and no unrelated kind loads", async () => {
const { createChannelCaller } = await helper();
let answer = null;
let ops = 0;
const logged = [];
const call = async verb => verb === "plugins" ? answer : ++ops;
const caller = createChannelCaller({ name: "cli", call, log: e => logged.push(e) });
for (answer of [null, [], {}, { plugins: null }, { plugins: [] }]) {
await assert.rejects(caller("project.list", {}), code("channel-config"));
assert.equal(ops, 0);
}
answer = { ...settings(["cli"]), plugins: { channel: ["cli"], "usage-source": ["missing-unrelated"] } };
assert.equal(await caller("project.list", {}), 1);
assert.deepEqual(logged, []);
});
test("channel caller uses last good after loader refusal and fails closed on first load", async () => {
const { createChannelCaller } = await helper();
let selected = ["cli"];
const logged = [];
const call = async verb => verb === "plugins" ? settings(selected) : "kept";
const caller = createChannelCaller({ name: "cli", call, log: e => logged.push(e) });
assert.equal(await caller("project.list", {}), "kept");
selected = ["missing-module"];
assert.equal(await caller("project.list", {}), "kept");
assert.equal(logged[0].code, "missing");
assert.deepEqual(logged[0].kept, ["cli"]);
const fresh = createChannelCaller({ name: "cli", call, log: () => {} });
await assert.rejects(fresh("project.list", {}), code("channel-disabled"));
});
test("channel dispatch holds no selection lock while a slow send is awaiting", async () => {
const { createChannelCaller } = await helper();
let release, started;
const entered = new Promise(r => started = r);
const held = new Promise(r => release = r);
const caller = createChannelCaller({ name: "cli", log: () => {}, call: async verb => {
if (verb === "plugins") return settings(["cli"]);
if (verb === "thread.send") { started(); await held; return "sent"; }
return "listed";
} });
const send = caller("thread.send", {});
await entered;
let timer;
try {
assert.equal(await Promise.race([caller("project.list", {}), new Promise((_, reject) => timer = setTimeout(() => reject(new Error("dispatch blocked list")), 1000))]), "listed");
} finally { clearTimeout(timer); release(); }
assert.equal(await send, "sent");
});
test("channel last realpath check refuses an index swapped after component checks", async t => {
const s = tree(t);
const good = s.write("channel", "good", fixtureModule("good"));
const swapped = s.write("channel", "swapped", fixtureModule("old"));
let swap = false;
const loader = await s.loader({ barrier: (point, { name }) => {
if (swap && point === "checked" && name === "swapped") {
unlinkSync(join(swapped, "index.mjs"));
symlinkSync(join(good, "index.mjs"), join(swapped, "index.mjs"));
}
} });
await loader.load({ channel: ["good"] });
swap = true;
const result = await loader.load({ channel: ["swapped"] });
assert.equal(result.refused[0].code, "symlink");
assert.deepEqual(result.loaded.channel, ["good"]);
});
test("channel caller refuses invalid core construction", async () => {
const { createChannelCaller } = await helper();
for (const name of ["../cli", "Cli", null])
assert.throws(() => createChannelCaller({ name, call: async () => ({}) }), code("bad-name"));
assert.throws(() => createChannelCaller({ name: "cli", call: null }), TypeError);
});
test("channel caller invokes the selected loaded module instead of bypassing dispatch", async t => {
const s = tree(t);
s.write("channel", "cli", fixtureModule("cli-fixture"));
s.write("channel", "webui", fixtureModule("webui-fixture"));
const { createChannelCaller } = await import(new URL("./channel.mjs", s.loaderUrl));
const caller = createChannelCaller({ name: "webui", log: () => {}, call: async (verb, args) =>
verb === "plugins" ? settings(["cli", "webui"]) : args });
assert.deepEqual(await caller("thread.list", { project: "one" }), { project: "one", fixture: "webui-fixture" });
});
test("channel caller keeps the selected module operation filter in front of the core transport", async () => {
const { createChannelCaller } = await helper();
let writes = 0;
const caller = createChannelCaller({ name: "cli", log: () => {}, call: async verb =>
verb === "plugins" ? settings(["cli"]) : ++writes });
await assert.rejects(caller("plugins.set", {}), code("channel-operation"));
assert.equal(writes, 0);
});