234 lines
12 KiB
JavaScript
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);
|
|
});
|