electron/included-x.test.mjs
Rama main · réplica de lectura del repositorio original.
import { describe, expect, it, vi } from "vitest";
import { createDesktopIncludedX, createIncludedXSync } from "./included-x.mjs";
const access = { url: "https://cloud.openmausbot.com/api/cloud/services/x", token: `omb_xd_${"a".repeat(43)}` };
const paid = (deviceId = "device-1") => ({ status: "connected", deviceId, entitlement: { plan: "pro", status: "active", tier: "pro" } });
function sync(fetchAccess = vi.fn(async () => access)) {
const sent = [], notes = [];
return { sent, notes, fetchAccess, x: createIncludedXSync({ fetchAccess, send: (value, note) => { sent.push(value); notes.push(note ?? null); } }) };
}
describe("included X research access on the desktop", () => {
it("fetches this sign-in's token once the plan is paid, and hands it to the server", async () => {
const { x, sent, fetchAccess } = sync();
await x.onState(paid());
expect(fetchAccess).toHaveBeenCalledTimes(1);
expect(sent).toEqual([access]);
});
it("does not fetch again on every refresh of the same paid sign-in (a new token would replace the one in use)", async () => {
const { x, sent, fetchAccess } = sync();
await x.onState(paid());
await x.onState(paid());
await x.onState({ ...paid(), checking: true });
expect(fetchAccess).toHaveBeenCalledTimes(1);
expect(sent).toEqual([access]);
});
it("clears the server's token when the plan stops being active, the app signs out or must sign in again", async () => {
for (const next of [{ ...paid(), entitlement: { plan: "pro", status: "inactive", tier: "pro" } }, { status: "signed-out" }, { status: "reauth-required", message: "expired" },
{ status: "connected", deviceId: "device-1", entitlement: { plan: "free", status: "inactive", tier: null } }]) {
const { x, sent } = sync();
await x.onState(paid());
await x.onState(next);
expect(sent, JSON.stringify(next)).toEqual([access, null]);
}
});
it("fetches a new token for a new sign-in on this computer", async () => {
const { x, sent, fetchAccess } = sync();
await x.onState(paid("device-1"));
await x.onState(paid("device-2"));
expect(fetchAccess).toHaveBeenCalledTimes(2);
expect(sent).toEqual([access, access]);
});
it("a change of account with the Admin down: the account before's token doesn't stay with the server", async () => {
const fetchAccess = vi.fn(async () => access);
const { x, sent } = sync(fetchAccess);
await x.onState(paid("device-a"));
fetchAccess.mockRejectedValueOnce(new Error("offline"));
await x.onState(paid("device-b"));
expect(sent).toEqual([access, null]);
await x.onState(paid("device-b"));
expect(sent).toEqual([access, null, access]);
});
it("sends nothing for a free account, and null when the Admin says the plan is not active", async () => {
const free = sync();
await free.x.onState({ status: "connected", deviceId: "d", entitlement: { plan: "free", status: "inactive", tier: null } });
expect(free.sent).toEqual([]);
expect(free.fetchAccess).not.toHaveBeenCalled();
const refused = sync(vi.fn(async () => null));
await refused.x.onState(paid());
expect(refused.sent).toEqual([null]);
expect(refused.notes).toEqual([{ offered: false }]);
});
it("keeps trying on later state changes after a failed fetch, without sending anything for it", async () => {
const fetchAccess = vi.fn().mockRejectedValueOnce(new Error("offline")).mockResolvedValueOnce(access);
const { x, sent } = sync(fetchAccess);
await x.onState(paid());
expect(sent).toEqual([]);
await x.onState(paid());
expect(sent).toEqual([access]);
});
it("re-sends the token it holds when the server starts again, and nothing when it holds none", async () => {
const { x, sent } = sync();
x.serverStarted();
expect(sent).toEqual([]);
await x.onState(paid());
x.serverStarted();
expect(sent).toEqual([access, access]);
});
it("asks once while a fetch is already under way", async () => {
let finish;
const fetchAccess = vi.fn(() => new Promise((resolve) => { finish = resolve; }));
const { x, sent } = sync(fetchAccess);
const first = x.onState(paid()), second = x.onState(paid());
finish(access);
await Promise.all([first, second]);
expect(fetchAccess).toHaveBeenCalledTimes(1);
expect(sent).toEqual([access]);
});
});
describe("when the Admin says no to a paid sign-in", () => {
const versioned = (version) => ({ ...paid(), entitlement: { plan: "pro", status: "active", tier: "pro", version } });
it("asks once for that sign-in and plan version, not on every refresh, and asks again when the plan changes", async () => {
const { x, sent, fetchAccess } = sync(vi.fn(async () => null));
await x.onState(versioned(1));
await x.onState(versioned(1));
await x.onState({ ...versioned(1), checking: true });
expect(fetchAccess).toHaveBeenCalledTimes(1);
expect(sent).toEqual([null]);
fetchAccess.mockResolvedValueOnce(access);
await x.onState(versioned(2));
expect(fetchAccess).toHaveBeenCalledTimes(2);
expect(sent).toEqual([null, access]);
});
it("clears the server's not-offered note once the plan stops or the app signs out, so the card offers the way back", async () => {
const { x, sent, notes } = sync(vi.fn(async () => null));
await x.onState(versioned(1));
await x.onState({ status: "signed-out" });
expect(sent).toEqual([null, null]);
expect(notes).toEqual([{ offered: false }, null]);
});
it("sends nothing for a fetch that ends after the plan stopped, or after another sign-in took over", async () => {
let finish;
const fetchAccess = vi.fn(() => new Promise((resolve) => { finish = resolve; }));
const lapsed = sync(fetchAccess);
const asking = lapsed.x.onState(paid());
await lapsed.x.onState({ status: "signed-out" });
finish(access);
await asking;
expect(lapsed.sent).toEqual([]);
const other = sync(vi.fn().mockImplementationOnce(() => new Promise((resolve) => { finish = resolve; })).mockResolvedValueOnce(access));
const first = other.x.onState(paid("device-1"));
void other.x.onState(paid("device-2"));
finish(access);
await first;
await vi.waitFor(() => expect(other.fetchAccess).toHaveBeenCalledTimes(2));
await vi.waitFor(() => expect(other.sent).toEqual([access]));
});
});
describe("main.mjs's wiring of included X research access", () => {
it("asks the current sign-in and posts its answer to whichever server is running, re-sending to a restarted one", async () => {
const posted = [], logged = [];
let account = null, proc = null;
const x = createDesktopIncludedX({ account: () => account, server: () => proc, log: (line) => logged.push(line) });
await x.onState(paid());
expect(posted).toEqual([]);
account = { xResearchAccess: vi.fn(async () => null) };
proc = { postMessage: (message) => posted.push(message) };
await x.onState(paid("device-2"));
expect(posted).toEqual([{ type: "openmausbot:included-x", access: null, offered: false }]);
account.xResearchAccess.mockResolvedValue(access);
await x.onState(paid("device-3"));
proc = { postMessage: (message) => posted.push({ restarted: message }) };
x.serverStarted();
expect(posted.slice(1)).toEqual([{ type: "openmausbot:included-x", access }, { restarted: { type: "openmausbot:included-x", access } }]);
proc = { postMessage: () => { throw new Error("port closed"); } };
x.serverStarted();
expect(logged).toEqual(["X research access sync failed: port closed"]);
});
});