Ctrl K
ComunidadGuías…

scripts/testing/skill-approval-ui-smoke.cjs

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

Archivo original ↗Preguntar a KiBot
const { BrowserWindow } = require("electron");
const assert = require("node:assert/strict");
const { mkdirSync, writeFileSync, readFileSync } = require("node:fs");
const { join } = require("node:path");
const { pathToFileURL } = require("node:url");

module.exports = async function verifySkillApprovalUi({ root, home, url, api, until, capability }) {
  const { mountPreview } = await import(pathToFileURL(join(root, "scripts/testing/preview-fixture.ts")).href);
  const bot = (await api("/api/bots", "POST", { name: "Sprout fixture", modelSelection: { instanceId: "claude", model: "claude-sonnet-5" } })).body.bot;
  const savedThreadId = bot.threadId;
  const readBot = async () => (await api("/api/bots")).body.bots.find(candidate => candidate.id === bot.id);
  const send = async (threadId, text) => {
    assert.equal((await api(`/api/bots/${bot.id}/messages`, "POST", { threadId, text })).status, 202);
    await until(async () => !(await readBot()).busy);
  };
  await send(savedThreadId, "Keep this conversation");
  const keptMessageIds = (await readBot()).messages.map(message => message.id);
  const pendingTask = (await api(`/api/bots/${bot.id}/tasks`, "POST", { title: "Pending skill" })).body.task;
  const stage = async () => {
    const minted = await capability(bot.id, pendingTask.threadId);
    assert.equal(minted.status, 201);
    const result = await api("/api/internal/skills/stage", "POST", {
      fromBotId: bot.id, fromThreadId: pendingTask.threadId, action: "create", source: "conversation",
      skill_md: "---\nname: fixture-review\ndescription: Review the fixture checklist.\n---\n\n# Fixture review\n\nRead the synthetic checklist and report what passed.\n",
    }, { authorization: `Bearer ${minted.body.token}` });
    assert.equal(result.status, 201, JSON.stringify(result.body));
    return (await readBot()).messages.find(message => message.card?.skillRequest?.stagedId === result.body.stagedId);
  };
  const first = await stage();
  const preview = await mountPreview({ info: { url } }, {
    entry: "/scripts/testing/skill-approval-preview.tsx", route: "/__skill-approval.html", title: "Isolated skill approvals", logLevel: "silent",
  });
  const windows = [0, 1].map(() => new BrowserWindow({ show: false, width: 1280, height: 900, webPreferences: { contextIsolation: true, sandbox: true } }));
  const evaluate = (window, js) => window.webContents.executeJavaScript(js);
  const pending = window => evaluate(window, "document.querySelector('[aria-label=\"Pending skill confirmation\"]') !== null");
  const click = (window, label) => evaluate(window, `(() => {
    const button = [...document.querySelectorAll('button')].find(b => (b.getAttribute('aria-label') || b.textContent.trim()) === ${JSON.stringify(label)});
    if (!button || button.disabled) throw new Error('Missing enabled button: ' + ${JSON.stringify(label)});
    button.click(); return true;
  })()`);
  const state = window => evaluate(window, "JSON.parse(document.querySelector('#fixture-state').textContent)");
  const evidence = join(root, ".omb-scratch/verify-evidence/skill-approval");
  mkdirSync(evidence, { recursive: true });
  try {
    for (const window of windows) await window.loadURL(`${preview.previewUrl}?bot=${bot.id}`);
    for (const window of windows) await until(() => pending(window));
    writeFileSync(join(evidence, "pending.png"), (await windows[0].webContents.capturePage()).toPNG());
    await click(windows[0], "Deny");
    for (const window of windows) await until(async () => !await pending(window));
    assert.equal((await readBot()).messages.find(message => message.id === first.id).card.answered, "deny");

    // A missing stage must also remain dismissible; no reviewed code is run.
    await stage();
    await until(() => pending(windows[0]));
    writeFileSync(join(home, "skill-state", bot.id, "staged.json"), JSON.stringify({ writes: {} }));
    await click(windows[0], "Deny");
    for (const window of windows) await until(async () => !await pending(window));

    // Reusing the name proves Deny did not leave an invisible reservation.
    const deletedCard = await stage();
    for (const window of windows) await until(() => pending(window));
    await click(windows[0], "All threads");
    await evaluate(windows[0], `(() => {
      const title = [...document.querySelectorAll('button')].find(button => button.querySelector('div')?.textContent === 'Pending skill');
      const remove = title?.parentElement.querySelector('button[aria-label="Delete thread"]');
      if (!remove || remove.disabled) throw new Error('Missing enabled thread deletion control');
      remove.click(); return true;
    })()`);
    await until(async () => !(await readBot()).tasks.some(task => task.threadId === pendingTask.threadId));
    // The second client receives only SSE, not the DELETE response.
    for (const window of windows) {
      await until(async () => (await state(window)).threadId === savedThreadId && !await pending(window));
      await until(async () => (await state(window)).messageIds.includes(keptMessageIds.at(-1)));
      assert.equal((await state(window)).messageIds.includes(deletedCard.id), false);
      assert.equal(await evaluate(window, "document.querySelector('textarea').disabled"), false);
    }
    assert.deepEqual(JSON.parse(readFileSync(join(home, "skill-state", bot.id, "staged.json"), "utf8")).writes, {});
    assert.deepEqual((await readBot()).messages.map(message => message.id), keptMessageIds);
    await click(windows[0], "All threads");
    await evaluate(windows[0], "document.querySelector('textarea').focus(); true");
    windows[0].webContents.insertText("Still here after deleting the skill thread");
    windows[0].webContents.sendInputEvent({ type: "keyDown", keyCode: "Return" });
    windows[0].webContents.sendInputEvent({ type: "keyUp", keyCode: "Return" });
    await until(async () => (await readBot()).messages.some(message => message.role === "user" && message.text === "Still here after deleting the skill thread"));
    const reply = await until(async () => {
      const current = await readBot();
      return !current.busy && current.messages.find(message => !keptMessageIds.includes(message.id) && message.role === "bot" && message.kind === "text" && message.text === "hello from fake claude");
    });
    for (const window of windows) await until(async () => (await state(window)).messageIds.includes(reply.id));
    writeFileSync(join(evidence, "after-delete.png"), (await windows[0].webContents.capturePage()).toPNG());
    const result = { deny: true, missingStageDeny: true, activeThreadDeletion: true, secondClient: true, savedConversationIntact: true, sentAfterDeletion: true, botRetained: true };
    writeFileSync(join(evidence, "results.json"), JSON.stringify(result, null, 2));
    console.log(JSON.stringify({ ...result, evidence }));
  } catch (error) {
    writeFileSync(join(evidence, "failure.json"), JSON.stringify({ bot: await readBot(), ui: await state(windows[0]) }, null, 2));
    writeFileSync(join(evidence, "failure.png"), (await windows[0].webContents.capturePage()).toPNG());
    throw error;
  } finally {
    for (const window of windows) window.destroy();
    await preview.close();
  }
};