Ctrl K
ComunidadGuías…

scripts/verify-team-backup.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// One owned fake-engine fixture. This command accepts no live server URL.
import assert from "node:assert/strict";
import { pathToFileURL } from "node:url";
import { launchVerificationServer, runControlOmb } from "./control-omb.ts";
import { request } from "./mcp-server.ts";
import { parseTeamBackup } from "../shared/team-backup.ts";

export async function verifyTeamBackup(report: (event: unknown) => void = () => {}) {
  const fixture = await launchVerificationServer();
  report({ fixture: fixture.info });
  const url = fixture.info.url;
  const command = async (...args: string[]) => {
    const result = await runControlOmb([...args, "--url", url]);
    report({ command: [...args, "--url", url], result });
    return result;
  };
  const post = (path: string, body: unknown) => request(path, { method: "POST", body: JSON.stringify(body) }, url);
  try {
    const doctor = await command("doctor") as { ok: boolean };
    assert.equal(doctor.ok, true);
    const { bot } = await command("new-bot", "--name", "Backup probe", "--section", "Original team") as { bot: { id: string } };
    await request(`/api/bots/${bot.id}`, { method: "PATCH", body: JSON.stringify({ chiefOfStaff: true }) }, url);
    await post("/api/groups", { name: "Existing room", memberIds: [bot.id], section: "Original team" });
    const soul = "Keep the fixture's standing instructions. 🐭\n";
    await request(`/api/bots/${bot.id}/profile`, { method: "PATCH", body: JSON.stringify({ soul }) }, url);
    report({ command: "save standing instructions", botId: bot.id, soul });
    await command("send", "--bot", bot.id, "--text", "Remember the backup verification conversation. Reply once.");
    const wait = await command("wait", "--bot", bot.id, "--timeout", "30") as { status: string };
    assert.equal(wait.status, "settled");
    const transcript = await command("messages", "--bot", bot.id, "--limit", "10");
    const before = await request("/api/bots", {}, url);
    const backup = parseTeamBackup(await post("/api/teams/export", { format: "backup", name: "Fixture backup" }));
    report({ command: "export backup", bots: backup.bots.length, conversations: backup.bots.reduce((n, bot) => n + bot.tasks.length, 0) });
    const imported = await post("/api/teams/import?mode=add", JSON.parse(JSON.stringify(backup)));
    assert.equal(imported.bots.length, before.bots.length);
    const added = imported.bots.find((candidate: { name: string }) => candidate.name === "Backup probe 2");
    assert.ok(added);
    assert.equal(added.section, "Original team 2");
    assert.equal(added.soul, soul);
    const after = await request("/api/bots", {}, url);
    for (const existing of before.bots) assert.deepEqual(after.bots.find((candidate: { id: string }) => candidate.id === existing.id), existing);
    for (const existing of before.groups) assert.deepEqual(after.groups.find((candidate: { id: string }) => candidate.id === existing.id), existing);
    assert.deepEqual(await command("messages", "--bot", bot.id, "--limit", "10"), transcript);
    const old = before.bots.find((candidate: { id: string }) => candidate.id === bot.id);
    assert.ok(old.messages.some((message: { role: string; kind: string; text?: string }) => message.role === "bot" && message.kind === "text" && message.text?.includes("hello from fake claude")));
    for (const message of old.messages.filter((message: { kind: string }) => message.kind === "text")) {
      assert.ok(added.messages.some((copy: { text: string; id: string }) => copy.text === message.text && copy.id !== message.id));
    }
    await command("messages", "--bot", added.id, "--limit", "10");
    await assert.rejects(post("/api/teams/import?mode=replace", backup), /Replacing your team/);
    await assert.rejects(post("/api/teams/import", { ...backup, version: 999 }), /Invalid backup/);
    assert.deepEqual(await request("/api/bots", {}, url), after);
    // Setup-only files must preserve the same persona, without importing
    // conversations or permissions. Exercise the real manifest import too.
    const manifest = await post("/api/teams/export", { name: "Fixture manifest" });
    assert.equal(manifest.team.members.find((member: { name: string }) => member.name === "Backup probe").soul, soul);
    const fromManifest = await post("/api/teams/import?mode=add", manifest);
    assert.equal(fromManifest.bots.find((candidate: { name: string }) => candidate.name === "Backup probe 3").soul, soul);
    assert.ok(fromManifest.bots.every((candidate: { section?: string }) => candidate.section === "Fixture manifest"));
    const packageExport = await post("/api/teams/export", { format: "package", name: "Fixture manifest" });
    // Older library files, repeated imports, project rooms and Markdown all
    // use the same additive path. Compare EVERY existing record each time,
    // including previous imports, section Chiefs and room membership.
    for (const [index, example] of [
      { format: "legacy v1", path: "/api/teams/import?mode=add", body: { ...manifest, version: 1, team: { ...manifest.team, room: { name: "Ignored legacy room", defaultResponder: { kind: "everyone" } } } } },
      { format: "legacy v2 project", path: "/api/teams/import?mode=project&room=Template%20room", body: manifest },
      { format: "Markdown package", path: "/api/teams/import?mode=add", body: packageExport.markdown },
      { format: "repeated Markdown package", path: "/api/teams/import?mode=add", body: packageExport.markdown },
    ].entries()) {
      const existing = await request("/api/bots", {}, url);
      const result = await post(example.path, example.body);
      const section = `Fixture manifest ${index + 2}`;
      assert.ok(result.bots.length > 0);
      assert.ok(result.bots.every((candidate: { section?: string; messages: unknown[] }) => candidate.section === section && candidate.messages.length === 0));
      assert.ok(result.groups.every((candidate: { section?: string }) => candidate.section === section));
      if (example.format.includes("project")) assert.equal(result.group.section, section);
      const current = await request("/api/bots", {}, url);
      assert.equal(current.bots.length, existing.bots.length + result.bots.length);
      assert.equal(current.groups.length, existing.groups.length + result.groups.length);
      for (const value of existing.bots) assert.deepEqual(current.bots.find((candidate: { id: string }) => candidate.id === value.id), value);
      for (const value of existing.groups) assert.deepEqual(current.groups.find((candidate: { id: string }) => candidate.id === value.id), value);
      assert.deepEqual(await command("messages", "--bot", bot.id, "--limit", "10"), transcript);
      report({ command: "import template", format: example.format, section, existingBotsKept: existing.bots.length, existingRoomsKept: existing.groups.length });
    }
    await command("send", "--bot", fromManifest.bots[0].id, "--text", "Confirm the imported template is ready. Reply once.");
    assert.equal((await command("wait", "--bot", fromManifest.bots[0].id, "--timeout", "30") as { status: string }).status, "settled");
    assert.ok(JSON.stringify(await command("messages", "--bot", fromManifest.bots[0].id, "--limit", "10")).includes("hello from fake claude"));
    // The copied history is usable for a fresh turn without resuming the
    // original provider session or altering the original bot's transcript.
    await command("send", "--bot", added.id, "--text", "Continue the imported conversation. Reply once.");
    const resumed = await command("wait", "--bot", added.id, "--timeout", "30") as { status: string };
    assert.equal(resumed.status, "settled");
    await command("messages", "--bot", added.id, "--limit", "10");
    assert.deepEqual(await command("messages", "--bot", bot.id, "--limit", "10"), transcript);
    const result = { ok: true, existingBotsKept: before.bots.length, importedBots: imported.bots.length, originalConversationUnchanged: true, importedConversationContinued: true, standingInstructionsPreserved: true, templateSectionsIsolated: true, logPath: fixture.info.logPath };
    report(result);
    return result;
  } finally {
    await fixture.close();
  }
}

if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
  verifyTeamBackup((event) => console.log(JSON.stringify(event))).catch((error) => {
    console.error(error);
    process.exitCode = 1;
  });
}