Ctrl K
ComunidadGuías…

electron/computer-asks.node-test.mjs

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

Archivo original ↗Preguntar a KiBot
// While this app is open, each of the person's own Clouds hears how this computer is and asks for it when a bot there
// needs it: the app's own prompt comes up once per ask, and Turn on, there or on the card, does here what it takes.
import assert from "node:assert/strict";
import { randomUUID } from "node:crypto";
import { mkdir, mkdtemp, realpath, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import { test } from "node:test";
import { setTimeout as delay } from "node:timers/promises";

import { ownCloud } from "./cloud-list.mjs";
import { createComputerAsks, QUIET_AFTER_NOT_NOW_MS } from "./computer-asks.mjs";
import { createComputerSharing } from "./computer-sharing.mjs";
import { createLendingTarget } from "./lending-target.mjs";
import { createAskPrompt } from "./ask-prompt.mjs";

const HOME = "https://omb-u-fixture.fly.dev", TEAM = "https://omb-t-acme.fly.dev";
const myCloud = { id: "my-cloud", name: "My Cloud", origin: HOME, home: true };
const acme = { id: "acme", name: "Acme team", origin: TEAM, team: { id: "t-acme", role: "owner" } };
const READY = { ready: true, lent: true, reason: null, fix: null };
const BLOCKED = { ready: false, lent: false, reason: "blocked", fix: "start" };

const until = async (check, what, ms = 5000) => {
  const deadline = Date.now() + ms;
  while (Date.now() < deadline) { if (await check()) return; await delay(10); }
  assert.fail(`timed out waiting for ${what}`);
};

/** A Cloud that takes asks: what this computer said each poll, and the asks it answers with. */
function cloudHome() {
  const json = value => ({ ok: true, status: 200, headers: { get: () => null }, body: (async function* () { yield Buffer.from(JSON.stringify(value)); })() });
  const state = { environmentId: randomUUID(), sessionId: randomUUID(), connects: [], polls: [], asks: [], version: 0 };
  state.say = asks => { state.asks = asks; state.version++; };
  state.fetch = async (url, init) => {
    const route = new URL(url).pathname;
    if (route === "/api/auth/session") return json({ kind: "session", id: state.sessionId, scopes: ["admin", "client"], cloudHome: true });
    if (route === "/.well-known/openmausbot/environment") {
      return json({ environmentId: state.environmentId, capabilities: { sharedComputers: true, lendingDetails: true, deviceLink: true, myChromeLink: true, computerAsks: true } });
    }
    const body = init?.body ? JSON.parse(init.body) : {};
    if (route === "/api/shared-computers/connect") { state.connects.push(body); return json({ ok: true }); }
    if (route === "/api/shared-computers/asks") {
      state.polls.push(body);
      if (body.heard === state.version) await delay(40, undefined, { signal: init?.signal }).catch(() => {});
      init?.signal?.throwIfAborted();
      return json({ version: state.version, asks: state.asks });
    }
    if (route.endsWith("/poll")) { await delay(40, undefined, { signal: init?.signal }).catch(() => {}); init?.signal?.throwIfAborted(); return json({ job: null }); }
    return json({ ok: true });
  };
  return state;
}

function session() {
  const state = { status: "connected", account: { id: "acct_1" }, machine: { status: "ready", origin: HOME }, clouds: [
    { id: "c-home", origin: HOME, teamId: null, role: "owner", primary: true },
    { id: "c-team", origin: TEAM, teamId: "t-acme", role: "owner" },
  ] };
  return { state: () => state, isOwnCloud: origin => state.machine?.origin === origin || state.clouds.some(cloud => ownCloud(cloud) && cloud.origin === origin) };
}

async function computer(t, { homes, places = [myCloud], screen = () => READY, prompt = async () => null, startScreen = async () => READY, confirmTerminal = async () => false, file }) {
  const dir = await realpath(await mkdtemp(path.join(tmpdir(), "omb-computer-asks-")));
  t.after(() => rm(dir, { recursive: true, force: true, maxRetries: 10, retryDelay: 200 }));
  const account = session();
  const target = createLendingTarget({ account: () => account, environments: () => places });
  const sharing = createComputerSharing({
    file: file ?? path.join(dir, "profile", "computer-sharing.json"), environments: () => places, enabled: async () => false, home: path.join(dir, "home"),
    fetch: (url, init) => homes[new URL(url).origin].fetch(url, init),
    cloud: target.cloud, myCloud: target.environment, ownClouds: target.places, screen, prompt, startScreen, confirmTerminal,
    hostControl: async () => ({ renew: async () => {}, release: async () => {} }),
    WebSocket: class { close() {} send() {} },
  });
  t.after(() => sharing.close());
  return sharing;
}

const ask = (fields = {}) => ({ id: "ask-1", need: "screen", reason: "paused", bot: "Scout", why: "Open the invoice in Excel", state: "asking", turnOn: 0, ...fields });

test("paused, this computer still tells its own Cloud so, and the card's Turn on resumes it", async t => {
  const homes = { [HOME]: cloudHome() };
  const prompts = [];
  const sharing = await computer(t, { homes, prompt: async (env, entry) => { prompts.push([env.id, entry.id]); return null; } });
  sharing.start();
  await until(() => homes[HOME].connects.length === 1, "lending to start");
  sharing.pause();
  await until(() => homes[HOME].polls.some(poll => poll.paused === true), "My Cloud to hear that this computer is paused");
  assert.deepEqual(homes[HOME].polls.at(-1).screen, { ready: true, reason: null });
  homes[HOME].say([ask()]);
  await until(() => prompts.length === 1, "the app's own prompt");
  homes[HOME].say([ask({ state: "turning-on", turnOn: 1 })]);
  await until(() => !sharing.paused(), "Turn on to resume this computer");
  await until(() => homes[HOME].connects.length === 2, "lending to come back");
  assert.equal(homes[HOME].connects[1].computer, true);
  await delay(150);
  assert.equal(prompts.length, 1, "one prompt per ask");
});

test("a Resume save that fails answers the ask failed at once, still paused", async t => {
  const homes = { [HOME]: cloudHome() };
  const dir = await realpath(await mkdtemp(path.join(tmpdir(), "omb-computer-asks-blocked-")));
  t.after(() => rm(dir, { recursive: true, force: true, maxRetries: 10, retryDelay: 200 }));
  const file = path.join(dir, "computer-sharing.json");
  const sharing = await computer(t, { homes, file });
  sharing.start();
  await until(() => homes[HOME].connects.length === 1, "lending to start");
  sharing.pause();
  await until(() => homes[HOME].polls.some(poll => poll.paused === true), "My Cloud to hear that this computer is paused");
  await mkdir(`${file}.tmp`);
  homes[HOME].say([ask({ state: "turning-on", turnOn: 1 })]);
  await until(() => homes[HOME].polls.some(poll => poll.answers.some(answer => answer.outcome === "failed")), "the failure to reach the Cloud at once");
  assert.deepEqual(homes[HOME].polls.find(poll => poll.answers.length).answers, [{ id: "ask-1", outcome: "failed", reason: "paused" }]);
  assert.equal(sharing.paused(), true);
});

test("screen control isn't running: Turn on starts it, and a start that fails tells the Cloud why", async t => {
  const homes = { [HOME]: cloudHome() };
  let status = BLOCKED, starts = 0, fixed = false;
  const sharing = await computer(t, { homes, screen: () => status, startScreen: async () => { starts++; if (fixed) status = READY; return status; } });
  sharing.start();
  await until(() => homes[HOME].polls.some(poll => poll.screen.reason === "blocked"), "My Cloud to hear why apps and screen aren't lent");
  homes[HOME].say([ask({ reason: "blocked", state: "turning-on", turnOn: 1 })]);
  await until(() => homes[HOME].polls.some(poll => poll.answers.some(answer => answer.outcome === "failed")), "the failure to reach the Cloud");
  assert.deepEqual(homes[HOME].polls.find(poll => poll.answers.length).answers, [{ id: "ask-1", outcome: "failed", reason: "blocked" }]);
  assert.equal(starts, 1);
  fixed = true;
  homes[HOME].say([ask({ reason: "blocked", state: "turning-on", turnOn: 2 })]);
  await until(() => starts === 2, "Try again to start it again");
  await sharing.screenChanged();
  await until(() => homes[HOME].connects.some(connect => connect.computer === true), "apps and screen to be lent");
  await until(() => homes[HOME].polls.at(-1).screen.ready === true, "the Cloud to hear it works");
});

test("the app's own prompt: Turn on there acts at once, Not now keeps it quiet for that Cloud and need", async () => {
  const answers = [], acted = [];
  let choice = "turn-on";
  const prompts = [];
  const asks = createComputerAsks({ prompt: async (_env, entry) => { prompts.push(entry.id); return choice; }, turnOn: async (_env, entry) => { acted.push(entry.id); return { ok: true }; },
    answered: () => {}, now: () => 1_000 });
  const env = { id: "my-cloud", name: "My Cloud" };
  asks.heard(env, [ask()]);
  await until(() => acted.length === 1, "Turn on to act");
  answers.push(...asks.pending(env.id));
  assert.deepEqual(answers, [{ id: "ask-1", outcome: "turning-on" }]);
  asks.sent(env.id, answers);
  asks.heard(env, [ask({ state: "turning-on" })]);
  await delay(10);
  assert.deepEqual([prompts, acted], [["ask-1"], ["ask-1"]], "nothing comes up or runs twice");

  choice = "not-now";
  asks.heard(env, [ask({ id: "ask-2" })]);
  await until(() => asks.pending(env.id).length === 1, "Not now to go back");
  assert.deepEqual(asks.pending(env.id), [{ id: "ask-2", outcome: "declined" }]);
  asks.heard(env, [ask({ id: "ask-3" })]);
  asks.heard(env, [ask({ id: "ask-4", need: "terminal", reason: "terminal-off" })]);
  await delay(10);
  assert.deepEqual(prompts, ["ask-1", "ask-2", "ask-4"], `quiet for ${QUIET_AFTER_NOT_NOW_MS / 60_000} minutes for the same need only`);
  asks.heard(env, [ask({ id: "ask-5", reason: "starting" })]);
  await delay(10);
  assert.equal(prompts.includes("ask-5"), false, "nothing to turn on while it starts by itself");
});

test("a prompt closes when the ask was answered elsewhere, and doesn't come up while the person looks at that Cloud", async () => {
  const closed = [];
  let shownTimes = 0;
  const asks = createComputerAsks({ prompt: (_env, _entry, signal) => { shownTimes++; return new Promise(resolve => signal.addEventListener("abort", () => { closed.push(true); resolve(null); })); },
    turnOn: async () => ({ ok: true }) });
  const env = { id: "my-cloud", name: "My Cloud" };
  asks.heard(env, [ask()]);
  await delay(5);
  asks.heard(env, [ask()]);
  asks.heard(env, [ask()]);
  await delay(5);
  assert.equal(shownTimes, 1, "the same ask heard again while its prompt is up never shows another");
  asks.heard(env, [ask(), ask({ id: "ask-other", need: "folders" })]);
  await delay(5);
  assert.equal(shownTimes, 1, "one prompt at a time for a Cloud: another ask waits until it closes");
  asks.heard(env, []);
  await until(() => closed.length === 1, "the prompt to close");

  const shown = [];
  const dialog = { showMessageBox: async (...args) => { shown.push(args); return { response: 0 }; } };
  const focused = { isDestroyed: () => false, isFocused: () => true, isVisible: () => true, show: () => {}, webContents: { getURL: () => `${HOME}/chat` } };
  const prompt = createAskPrompt({ dialog, window: () => focused, platform: "win32" });
  assert.equal(await prompt({ id: "my-cloud", name: "My Cloud", origin: HOME }, ask(), new AbortController().signal), null, "the card is right there");
  assert.equal(await prompt({ id: "client", name: "Client A", origin: "https://omb-c-a.fly.dev" }, ask(), new AbortController().signal), "turn-on");
  assert.equal(shown.at(-1).length, 1, "a PC's prompt has no parent window, so it shows even with the app in the tray");
  const mac = createAskPrompt({ dialog, window: () => null, platform: "darwin" });
  assert.equal(await mac({ id: "client", name: "Client A", origin: "https://omb-c-a.fly.dev" }, ask(), new AbortController().signal), null,
    "on a Mac only a window's sheet can be closed again, so no window, no prompt");
});

test("a Team Cloud never hears this computer", async t => {
  const homes = { [HOME]: cloudHome(), [TEAM]: cloudHome() };
  const sharing = await computer(t, { homes, places: [myCloud, acme] });
  sharing.start();
  await until(() => homes[HOME].polls.length > 0, "My Cloud to hear this computer");
  await delay(100);
  assert.equal(homes[TEAM].polls.length, 0);
});