electron/bot-move-main.node-test.mjs
Rama main · réplica de lectura del repositorio original.
import assert from "node:assert/strict";
import test from "node:test";
import { choosable, createBotMove, MY_CLOUD } from "./bot-move-main.mjs";
const reply = (status, body = {}) => ({ ok: status < 400, status, body });
const bots = (count) => Array.from({ length: count }, (_, i) => ({ id: `b${i}`, name: `Bot ${i}` }));
/** This computer's server (each bot holds or goes on as it is told), the send, My Cloud's session, and the clock's timers. */
function world({ busy = () => null, checks = [], untouched = [], ready = () => true, lease = null, sleep = async () => {} } = {}) {
let journal = null, clock = 1_000;
const timers = [];
const w = {
holds: new Map(), calls: [], cloud: [], passes: [], scopes: [],
get journal() { return journal; },
set journal(value) { journal = structuredClone(value); },
advance(ms) { clock += ms; },
timers,
async tick() { for (const timer of timers.splice(0)) timer.work(); await w.move.kick(); },
};
const local = {
async call(method, route, body) {
w.calls.push([method, route, body]);
if (route === "/api/cloud-send/scope") { w.scopes.push(body.bots); return reply(200, { v: 1, scope: `scope-${w.scopes.length}` }); }
if (route.startsWith("/api/cloud-send?move=1")) return reply(200, { v: 1, items: w.chosen().filter((id) => !untouched.includes(id)).map((id) => ({ key: `bot:${id}`, kind: "bot", id, parts: {} })) });
if (route === "/api/cloud-connections") return reply(200, { computer: { label: "MacBook Air" } });
if (route === "/api/bots?messages=0") return reply(200, { bots: w.chosen().map((id) => ({ id, cwd: `/work/${id}` })) });
const id = /^\/api\/bot-moves\/([\w-]+)$/u.exec(route)?.[1];
if (id && method === "DELETE") { w.holds.delete(id); return reply(200, { v: 1, released: true }); }
if (id && body.state === "moving") {
const who = busy(id);
if (who) return reply(409, { busy: who });
w.holds.set(id, "moving");
return reply(200, { v: 1, movedTo: { held: [`routine:r-${id}`, `webhook:w-${id}`] } });
}
if (id && body.state === "moved") { w.holds.set(id, "moved"); return reply(200, { v: 1, movedTo: {} }); }
if (id && body.state === "back") { w.holds.delete(id); return reply(200, { v: 1, released: true }); }
return reply(404);
},
};
w.chosen = () => (journal?.moves ?? []).flatMap((move) => move.bots.map((bot) => bot.id));
w.lent = [];
w.lendWith = (lender) => { w.lender = lender; };
w.move = createBotMove({
journal: { read: () => structuredClone(journal), write: (value) => { journal = structuredClone(value); } },
local,
send: {
async move(options) { w.passes.push({ ...options }); },
async check(options) { w.passes.push({ check: true, ...options }); const next = checks.shift(); if (next instanceof Error) throw next; return { ok: next ?? true }; },
},
session: async () => ({ async call(method, route, body) { w.cloud.push([method, route, body]); return reply(200, { v: 1 }); } }),
ready, lease, sleep,
lend: async (projects) => { (w.lender ?? ((seen) => w.lent.push(seen)))(projects); },
now: () => clock,
timers: { set: (work, ms) => { const timer = { work, ms }; timers.push(timer); return timer; }, clear: (timer) => { const at = timers.indexOf(timer); if (at >= 0) timers.splice(at, 1); } },
});
return w;
}
const settle = async (w) => { await w.move.kick(); await new Promise((resolve) => setImmediate(resolve)); await w.move.kick(); };
test("a choice of bots goes as one move: each is held, what was on here goes on there, and each stays here as the line that opens it", async () => {
const w = world();
assert.ok(w.move.start(bots(3)));
await settle(w);
const move = w.journal.moves[0];
assert.equal(move.phase, "done", JSON.stringify(move));
assert.deepEqual(move.team, MY_CLOUD);
assert.deepEqual([...w.holds.values()], ["moved", "moved", "moved"]);
assert.deepEqual(w.passes.map((pass) => pass.check ? "check" : pass.final ? "final" : "copy"), ["copy", "final", "check"], "one copy, one final and one check for all of them");
assert.ok(w.passes.every((pass) => pass.scope === "scope-1"), "every read of this computer's server is kept to the choice");
assert.deepEqual(w.cloud.filter(([method]) => method === "PATCH").map(([, route]) => route).sort(), ["/api/routines/r-b0", "/api/routines/r-b1", "/api/routines/r-b2", "/api/webhooks/w-b0", "/api/webhooks/w-b1", "/api/webhooks/w-b2"]);
assert.deepEqual(w.cloud.find(([, route]) => route === "/api/cloud-send/arrived")[2], { v: 1, computer: "MacBook Air", webhooks: ["w-b0", "w-b1", "w-b2"] });
assert.deepEqual(w.lent, [["/work/b0", "/work/b1", "/work/b2"]], "My Cloud can reach their folders on this computer");
assert.deepEqual(w.move.view(), { bot: { id: move.bot.id, name: "Bot 0" }, team: { id: "my-cloud", name: "My Cloud" }, phase: "done", bots: 3 });
});
test("while lending can't take the bots' folders yet, a later look does, and the move is done meanwhile", async () => {
let refuse = true;
const w = world();
const lentTo = [];
w.lendWith((projects) => { if (refuse) throw new Error("lending is paused"); lentTo.push(projects); });
w.move.start(bots(1));
await settle(w);
assert.equal(w.journal.moves[0].phase, "done");
assert.deepEqual(w.journal.moves[0].lend, ["/work/b0"]);
refuse = false;
await w.move.kick();
assert.deepEqual(lentTo, [["/work/b0"]]);
assert.equal(w.journal.moves[0].lend, undefined);
});
test("any number of bots go in one move, and an untouched bot, which a move has nothing to carry of, stays here", async () => {
const many = world({ untouched: ["b7"] });
many.move.start(bots(1_000));
await settle(many);
assert.equal(many.journal.moves[0].phase, "done");
assert.equal(many.journal.moves[0].bots.length, 999);
assert.equal(many.holds.size, 999);
assert.equal(many.holds.has("b7"), false);
assert.equal(many.passes.length, 3, "still one copy, one final and one check");
const nothing = world({ untouched: ["b0"] });
nothing.move.start(bots(1));
await settle(nothing);
assert.equal(nothing.journal.moves[0].stopped, true);
assert.equal(nothing.holds.size, 0);
});
test("a bot still answering holds the move for it; the others stay held meanwhile, and the rest of this computer goes on", async () => {
let answering = "Bot 1";
const w = world({ busy: (id) => (id === "b1" ? answering : null) });
w.move.start(bots(3));
await settle(w);
assert.equal(w.journal.moves[0].phase, "freeze");
assert.equal(w.holds.has("b2"), false, "held in turn, as each is free");
answering = null;
await w.tick();
assert.equal(w.journal.moves[0].phase, "done");
});
test("a check that fails lets every bot of the choice go on here, and the move tries again later from the copy", async () => {
const w = world({ checks: [false, false, false] });
w.move.start(bots(2));
await settle(w);
const move = w.journal.moves[0];
assert.equal(move.phase, "copy");
assert.equal(move.failing, true);
assert.ok(move.retryAt > 1_000);
assert.equal(w.holds.size, 0, "nothing stays held");
assert.equal(w.move.view().phase, "failed");
});
test("bots already on their way are not moved twice, and My Cloud ending brings the ones that moved back", async () => {
const w = world();
w.move.start(bots(2), "acct-1");
assert.deepEqual(w.move.moving(), ["b0", "b1"]);
assert.equal(w.move.start(bots(2)), null, "no bot left to move");
await settle(w);
assert.deepEqual(w.move.moving(), [], "done");
await w.move.back("acct-2");
assert.equal(w.holds.size, 2, "another account's Cloud ending says nothing of these");
await w.move.back("acct-1");
assert.equal(w.holds.size, 0);
assert.deepEqual(w.journal.moves, []);
});
test("Move everything stops a choice still on its way, and its bots go on here until that move takes them", async () => {
let answering = "Bot 1";
const w = world({ busy: (id) => (id === "b1" ? answering : null) });
w.move.start(bots(2));
await settle(w);
assert.equal(w.holds.get("b0"), "moving");
await w.move.drop();
assert.equal(w.holds.size, 0);
assert.deepEqual(w.journal.moves, []);
answering = null;
});
test("Move everything leaves a choice at its switch to finish: My Cloud already runs its bots", async () => {
const w = world();
w.move.start(bots(2));
await settle(w);
w.journal = { ...w.journal, moves: w.journal.moves.map((move) => ({ ...move, phase: "switch" })) };
w.holds.set("b0", "moving"); w.holds.set("b1", "moving");
await w.move.drop();
assert.equal(w.holds.get("b0"), "moving", "not let go here while My Cloud runs it");
assert.equal(w.journal.moves.length, 1);
assert.equal(w.journal.moves[0].phase, "switch");
});
test("it waits its turn at My Cloud behind another computer's move, and lets go when it is done", async () => {
let asks = 0;
const released = [];
const lease = { take: async () => (++asks < 3 ? { name: "NUC" } : null), release: async () => { released.push(asks); } };
const naps = [];
const w = world({ lease, sleep: async (ms) => { naps.push(ms); } });
w.move.start(bots(1));
await settle(w);
assert.equal(w.journal.moves[0].phase, "done");
assert.deepEqual(naps, [60_000, 60_000]);
assert.ok(released.length >= 1);
});
test("only the bots a person can move are offered: never the helper every place has, an archived one, or one that moved", () => {
assert.deepEqual(choosable([{ id: "a", name: "A" }, { id: "d", name: "D", defaultBot: { v: 1 } }, { id: "h", name: "H", hidden: true }, { id: "m", name: "M", movedTo: { state: "moved" } }, { id: "bad id", name: "X" }, null]).map((bot) => bot.id), ["a"]);
});