server/bot-moves.test.ts
Rama main · réplica de lectura del repositorio original.
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { assertBotHere, botMoveRefusal, createBotMoves, MY_CLOUD_TEAM } from "./bot-moves.ts";
import type { BotRecord } from "./store.ts";
import { WaitManager } from "./waits.ts";
const dirs: string[] = [];
afterEach(() => { for (const dir of dirs.splice(0)) rmSync(dir, { recursive: true, force: true }); });
const ACME = { id: "team-acme", name: "Acme" };
function waitsFor(settled: string[][]) {
const dir = mkdtempSync(join(tmpdir(), "omb-bot-moves-"));
dirs.push(dir);
return new WaitManager({
file: join(dir, "waits.json"), probes: { stamp: async () => null }, sourceAlive: () => true, botBusy: () => false,
post: () => true, followUp: () => undefined, showCard: (wait) => `card-${wait.label}`, settleCard: (wait, how) => { settled.push([wait.label, how]); },
});
}
function setup({ busy = false, waits = null as WaitManager | null } = {}) {
const bots = new Map<string, Record<string, unknown>>([["scout", { id: "scout", name: "Scout" }], ["pip", { id: "pip", name: "Pip" }]]);
const routines = [
{ id: "brief", botId: "scout", enabled: true }, { id: "weekly", botId: "scout", enabled: false }, { id: "pips", botId: "pip", enabled: true },
];
const hooks = [{ id: "hook", botId: "scout", enabled: true }];
const events = [{ id: "mail", botId: "scout", enabled: true }];
const removed: string[] = [];
const moves = createBotMoves({
store: {
bot: (id) => (bots.get(id) as unknown as BotRecord) ?? null,
patchBot: (id, patch) => { const bot = bots.get(id)!; Object.assign(bot, patch); return bot as unknown as BotRecord; },
},
routines: () => ({ listRoutines: () => routines as never, update: (id: string, patch: { enabled?: boolean }) => Object.assign(routines.find((routine) => routine.id === id)!, patch) as never,
removeForBot: (botId: string) => { removed.push(botId); for (let i = routines.length - 1; i >= 0; i--) if (routines[i]!.botId === botId) routines.splice(i, 1); } }),
webhooks: () => ({ list: () => hooks as never, update: (id: string, patch: unknown) => Object.assign(hooks.find((hook) => hook.id === id)!, patch as object) as never }),
appEvents: () => ({ list: () => events as never, update: async (id: string, patch: unknown) => Object.assign(events.find((event) => event.id === id)!, patch as object) as never }),
waits: () => waits,
busy: () => busy,
now: () => 1_000,
});
return { moves, bots, routines, hooks, events, removed };
}
const on = (list: Array<{ id: string; enabled: boolean }>) => Object.fromEntries(list.map((entry) => [entry.id, entry.enabled]));
describe("My Cloud holding a bot while it moves into a team", () => {
it("takes no turns and switches off its automations, keeping which were on; holding it again changes nothing", async () => {
const { moves, bots, routines, hooks, events } = setup();
expect(await moves.hold("scout", ACME)).toEqual({ movedTo: { team: ACME, state: "moving", at: 1_000, held: ["routine:brief", "webhook:hook", "app-event:mail"] } });
expect([on(routines), on(hooks), on(events)]).toEqual([{ brief: false, weekly: false, pips: true }, { hook: false }, { mail: false }]);
expect(() => assertBotHere(bots.get("scout") as never)).toThrow("Scout is moving to Acme. It's ready there in a moment.");
expect(() => assertBotHere(bots.get("pip") as never)).not.toThrow();
expect((await moves.hold("scout", ACME) as { movedTo: { held: string[] } }).movedTo.held).toEqual(["routine:brief", "webhook:hook", "app-event:mail"]);
expect(await moves.hold("scout", { id: "team-beta", name: "Beta" })).toEqual({ refused: "Scout is moving to Acme. It's ready there in a moment." });
});
it("waits while the bot is answering, and refuses a bot that is gone", async () => {
expect(await setup({ busy: true }).moves.hold("scout", ACME)).toEqual({ busy: "Scout" });
expect(await setup().moves.hold("nobody", ACME)).toEqual({ refused: "This bot is no longer here." });
});
it("lets the bot go on as it was when the move stops: what it switched off comes back on, nothing else", async () => {
const { moves, bots, routines, hooks, events } = setup();
await moves.hold("scout", ACME);
expect(await moves.release("scout")).toEqual({ released: true });
expect([on(routines), on(hooks), on(events)]).toEqual([{ brief: true, weekly: false, pips: true }, { hook: true }, { mail: true }]);
expect(bots.get("scout")!.movedTo).toBeUndefined();
expect(() => assertBotHere(bots.get("scout") as never)).not.toThrow();
});
it("keeps the bot as the line that opens it in the team once it arrived: archived, its routines gone, never let go of after", async () => {
const { moves, bots, routines, removed } = setup();
await moves.hold("scout", ACME);
expect(moves.moved("scout", { id: "team-beta", name: "Beta" })).toEqual({ refused: "This bot is not moving to that team." });
expect(moves.moved("scout", ACME)).toMatchObject({ movedTo: { team: ACME, state: "moved" } });
expect(bots.get("scout")).toMatchObject({ hidden: true, movedTo: { state: "moved" } });
expect([removed, routines.map((routine) => routine.id)]).toEqual([["scout"], ["pips"]]);
expect(botMoveRefusal(bots.get("scout") as never)).toBe("Scout lives in Acme now.");
expect(await moves.release("scout")).toEqual({ refused: "Scout lives in Acme now." });
expect(moves.moved("scout", ACME)).toMatchObject({ movedTo: { state: "moved" } });
expect(removed, "once").toEqual(["scout"]);
expect(moves.moved("pip", ACME)).toEqual({ refused: "This bot is not moving to that team." });
});
it("cancels the moving bot's waits, card and all, so none fires for a bot that lives in the team; other bots keep theirs", async () => {
const settled: string[][] = [];
const waits = waitsFor(settled);
const made = (botId: string, label: string) => waits.create({ botId, threadId: `t-${botId}`, label, condition: { kind: "file_exists", path: `/${label}` }, followUp: "Tell them." });
await made("scout", "the report"); await made("pip", "the build");
const { moves } = setup({ waits });
await moves.hold("scout", ACME);
expect(waits.all().map((wait) => wait.botId)).toEqual(["pip"]);
expect(settled).toEqual([["the report", "cancelled"]]);
await made("scout", "the invoice");
moves.moved("scout", ACME);
expect(waits.all().map((wait) => wait.botId)).toEqual(["pip"]);
expect(settled).toEqual([["the report", "cancelled"], ["the invoice", "cancelled"]]);
});
});
describe("a bot moving from this computer to My Cloud", () => {
const MY_CLOUD = { id: MY_CLOUD_TEAM, name: "My Cloud" };
function here() {
const bots = new Map<string, Record<string, unknown>>([["scout", { id: "scout", name: "Scout" }], ["pip", { id: "pip", name: "Pip" }], ["maus", { id: "maus", name: "Maus", defaultBot: { v: 1 } }]]);
const groups = [{ id: "both", memberIds: ["scout", "pip"], dm: false }, { id: "alone", memberIds: ["scout", "maus"], dm: false }];
const routines = [{ id: "brief", botId: "scout", enabled: true }];
const removed: string[] = [];
const moves = createBotMoves({
store: { bot: (id) => (bots.get(id) as unknown as BotRecord) ?? null, patchBot: (id, patch) => Object.assign(bots.get(id)!, patch) as unknown as BotRecord, groups,
patchGroup: (id, patch) => Object.assign(groups.find((group) => group.id === id)!, patch) },
routines: () => ({ listRoutines: () => routines as never, update: (id: string, patch: { enabled?: boolean }) => Object.assign(routines.find((routine) => routine.id === id)!, patch) as never, removeForBot: (botId: string) => { removed.push(botId); } }),
webhooks: () => null, appEvents: () => null, waits: () => null, busy: () => false,
});
return { moves, bots, groups, routines, removed };
}
it("keeps its routines here, off, leaves a room some of whose bots stay, and keeps a room all of whose bots left", async () => {
const { moves, bots, groups, routines, removed } = here();
await moves.hold("scout", MY_CLOUD);
moves.moved("scout", MY_CLOUD);
expect(removed, "My Cloud ending brings it back with them").toEqual([]);
expect(routines[0]!.enabled).toBe(false);
expect(bots.get("scout")).toMatchObject({ hidden: true, movedTo: { state: "moved" } });
expect(groups.find((group) => group.id === "both")!.memberIds, "the room goes on here without it").toEqual(["pip"]);
expect(groups.find((group) => group.id === "alone")!.memberIds, "only the helper every place has is left").toEqual(["scout", "maus"]);
});
it("is here again as it went if My Cloud ends, its automations left off", async () => {
const { moves, bots, routines } = here();
await moves.hold("scout", MY_CLOUD);
moves.moved("scout", MY_CLOUD);
expect(moves.back("scout")).toEqual({ released: true });
expect(bots.get("scout")).toMatchObject({ hidden: false, movedTo: undefined });
expect(routines[0]!.enabled).toBe(false);
expect(moves.back("pip"), "a bot that never left has nothing to come back from").toEqual({ released: true });
});
it("a bot that went into a team never comes back this way", async () => {
const { moves } = here();
await moves.hold("pip", ACME);
moves.moved("pip", ACME);
expect("refused" in moves.back("pip")).toBe(true);
});
});