server/bot-wire-privacy.e2e.test.ts
Rama main · réplica de lectura del repositorio original.
// BotWirePrivateKeys enforces the projection at compile time, but a `...rest` spread carrying a private key out does not
// fail typecheck, so this pins it at runtime against the real server with the private field live on the stored record.
import { spawn, type ChildProcess } from "node:child_process";
import { chmodSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
import { freePortBlock } from "./testing/ports.ts";
import { removeTempDir, waitForExit } from "./testing/cleanup.ts";
// Library store import is root-explicit, so the spawned server’s DATA_DIR never has to match this process.
import { installLibrarySkill } from "./skill-library.ts";
const SERVER_DIR = dirname(fileURLToPath(import.meta.url));
const ROOT = join(SERVER_DIR, "..");
const FAKE_CLI = join(SERVER_DIR, "testing", "fake-acp-cli.ts");
describe("bot wire privacy against the real server", () => {
let home = "";
let data = "";
let output = "";
let child: ChildProcess | null = null;
let base = "";
async function until<T>(read: () => T | Promise<T>, accept: (value: T) => boolean): Promise<T> {
const end = Date.now() + 20_000;
for (;;) {
const value = await read();
if (accept(value)) return value;
if (Date.now() >= end) throw new Error(`Fixture wait expired: ${JSON.stringify(value)}`);
await new Promise((resolve) => setTimeout(resolve, 40));
}
}
async function start(skillsLibrary: boolean) {
const port = await freePortBlock([0, 1]);
base = `http://127.0.0.1:${port}`;
output = "";
writeFileSync(join(data, "config.json"), JSON.stringify({
features: { skillsLibrary },
instances: { grok: { driver: "grokAgent", config: { cli: FAKE_CLI, fullAuto: false } } },
}));
const proc = spawn(process.execPath, [join(SERVER_DIR, "index.ts")], {
cwd: ROOT,
env: {
PATH: dirname(process.execPath), ...(process.env.SystemRoot ? { SystemRoot: process.env.SystemRoot } : {}),
HOME: home, USERPROFILE: home, OMB_DATA_DIR: data,
APPDATA: join(home, "appdata"), LOCALAPPDATA: join(home, "localappdata"),
OMB_PORT: String(port), OMB_WEBHOOK_PORT: String(port + 1),
},
stdio: ["ignore", "pipe", "pipe"],
});
child = proc;
proc.stdout!.on("data", (chunk) => { output += chunk; });
proc.stderr!.on("data", (chunk) => { output += chunk; });
await until(async () => {
if (proc.exitCode !== null) throw new Error(`server exited during boot:\n${output}`);
try { return (await fetch(base + "/api/health")).ok; } catch { return false; }
}, Boolean);
}
async function stop() {
if (!child) return;
const proc = child;
child = null;
await waitForExit(proc, { signal: "SIGTERM" });
}
const api = async (method: string, path: string, body?: unknown): Promise<{ status: number; body: any }> => {
const response = await fetch(base + path, {
method, headers: { "content-type": "application/json" },
...(body === undefined ? {} : { body: JSON.stringify(body) }),
});
return { status: response.status, body: await response.json().catch(() => ({})) };
};
beforeAll(() => {
chmodSync(FAKE_CLI, 0o755);
home = mkdtempSync(join(tmpdir(), "omb-bot-wire-privacy-"));
data = join(home, "data");
mkdirSync(data, { recursive: true });
});
afterAll(async () => {
await stop();
if (home) await removeTempDir(home);
});
afterEach(async () => { await stop(); });
it("never serializes assignedSkills, flag off or on, even when the record carries assignments", async () => {
await start(false);
const created = await api("POST", "/api/bots", { name: "Wire Privacy Bot" });
expect(created.status, JSON.stringify(created.body)).toBe(201);
const botId = created.body.bot.id as string;
await stop();
// Seed the server-private field the way a flag-off-on-flag-on cycle or
// an applied pending-assignments sweep leaves it behind on disk.
const bots = JSON.parse(readFileSync(join(data, "bots.json"), "utf8")) as any[];
bots.find((bot) => bot.id === botId)!.assignedSkills = ["some-library-skill"];
writeFileSync(join(data, "bots.json"), JSON.stringify(bots, null, 2));
for (const flag of [false, true]) {
await start(flag);
const listed = await api("GET", "/api/bots?messages=0");
expect(listed.status, JSON.stringify(listed.body)).toBe(200);
expect(listed.body.bots.some((bot: any) => bot.id === botId)).toBe(true);
expect(JSON.stringify(listed.body)).not.toContain("assignedSkills");
await stop();
}
// The field was live on the record in the server that produced those
// payloads — the assertion above was not checking an empty bot.
const stored = (JSON.parse(readFileSync(join(data, "bots.json"), "utf8")) as any[]).find((bot) => bot.id === botId)!;
expect(stored.assignedSkills).toEqual(["some-library-skill"]);
});
it("serves an assigned library-only skill on the single-skill GET route only while the library is on", async () => {
// The library store is plain files under DATA_DIR/skills-library, so the spawned server sees the entry on its next boot.
const instructions = "---\nname: route-library-skill\ndescription: Serves through the assigned-skill route.\n---\n\n# route-library-skill\n\nReads through the library.\n";
const installed = installLibrarySkill({
name: "route-library-skill",
instructions,
source: "test",
reviewState: "approved",
root: join(data, "skills-library"),
});
expect("error" in installed).toBe(false);
await start(false);
const created = await api("POST", "/api/bots", { name: "Library Route Bot" });
expect(created.status, JSON.stringify(created.body)).toBe(201);
const botId = created.body.bot.id as string;
await stop();
const bots = JSON.parse(readFileSync(join(data, "bots.json"), "utf8")) as any[];
bots.find((bot) => bot.id === botId)!.assignedSkills = ["route-library-skill"];
writeFileSync(join(data, "bots.json"), JSON.stringify(bots, null, 2));
await start(false);
expect(await api("GET", `/api/bots/${botId}/skills/route-library-skill`)).toMatchObject({ status: 404 });
await stop();
await start(true);
expect(await api("GET", `/api/bots/${botId}/skills/route-library-skill`))
.toMatchObject({ status: 200, body: { text: instructions } });
await stop();
});
});