// node --test web/socialweb-computer-ui/socialweb-cubit.test.mjs (after `cargo wasm`) // The page cubit over the real AtircCubit + WebrtcCubit and the real atirc and requester wasm; // the OAuth client, the session's PDS (atirc lines through it) and the bidder are stubbed. import assert from "node:assert/strict"; import { readFile } from "node:fs/promises"; import { registerHooks } from "node:module"; import { test } from "node:test"; import { load } from "../../../atlibs/rust/src/harness.mjs"; registerHooks({ resolve(specifier, context, next) { // atlibs/webrtc/ is the atlibs-webrtc sibling checkout's js/; the rest of atlibs/ is core's javascript/src const lib = specifier.startsWith("atlibs/webrtc/") ? new URL(`../../../atlibs-webrtc/js/${specifier.slice("atlibs/webrtc/".length)}`, import.meta.url).href : specifier.startsWith("atlibs/") && new URL(`../../../atlibs/javascript/src/${specifier.slice("atlibs/".length)}`, import.meta.url).href; return next(lib || specifier, context); }, }); const { SocialwebCubit, channelView, classify, sharedSessions } = await import("./socialweb-cubit.js"); const { requestScope, grantedSessions, enabledFeatures } = await import("atlibs/oauth/index.js"); const { AtircCubit } = await import("../../../atlibs-agents/web/atirc/atirc-cubit.js"); const { WebrtcCubit } = await import("../webrtc/webrtc-cubit.js"); const T = "com.publicdomainrelay.temp"; const ALICE = "did:plc:lpfuqerea3deuoyrn7ojser4"; const OTHER = "did:plc:someoneelse0000000000000"; const CHANNEL = "#alice_test"; // atirc is the atlibs-agents sibling checkout's module (its own `cargo wasm`); the requester is this repo's const repo = (name) => (name === "atirc" ? "../../../atlibs-agents/" : "../../"); const release = (name) => new URL(`${repo(name)}target/wasm32-unknown-unknown/release/${name}.wasm`, import.meta.url); const manifest = async (name) => JSON.parse(await readFile(new URL(`${repo(name)}modules/${name}/module.json`, import.meta.url), "utf8")); const wasm = async (name) => ({ module: new Uint8Array(await readFile(release(name))), calls: (await manifest(name)).calls }); const context = async (name, fetch) => { const { run } = await load(release(name)); const store = new Map(); return { wasm: { run: async ({ entry, input }) => run(entry, input) }, http: { fetch }, settings: { get: async (k) => store.get(k), set: async (k, v) => store.set(k, v) } }; }; const POLICY = { sessions: { requester: { scope: `repo:${T}.compute.vm?action=create&action=delete rpc:${T}.market.submitRfp?aud=*` }, atirc: { scope: "rpc:chat.atirc.list?aud=did:web:irc.fedfork.com%23atirc rpc:chat.atirc.post?aud=did:web:irc.fedfork.com%23atirc" }, }, features: { agents: { requires: ["requester", "atirc"] } }, }; const line = (seq, text, { did = ALICE, nick = "bob-hat-9", identity = `${nick}.alice.test` } = {}) => ({ seq, channel: CHANNEL, did, nick, identity, text, ts: `2026-11-08T20:00:${String(seq).padStart(2, "0")}Z`, source: "xrpc" }); // One page: the stubbed OAuth client granting `scope`, the session's PDS (repo writes, // service-auth, chat.atirc.* behind atproto-proxy), the bidder, and the three cubits. async function page(scope, lines = []) { const xrpc = []; const channel = [...lines]; const agent = { did: ALICE, com: { atproto: { server: { getSession: async () => ({ success: true, data: { handle: "alice.test" } }) } } }, async fetchHandler(path, init) { const [nsid, query] = path.replace("/xrpc/", "").split("?"); const input = init.body && JSON.parse(init.body); xrpc.push({ nsid, query, input, headers: init.headers }); if (nsid === "chat.atirc.list") { const cursor = Number(new URLSearchParams(query).get("cursor")); const messages = channel.filter((m) => m.seq > cursor); return Response.json({ messages, cursor: messages.at(-1)?.seq ?? cursor }); } if (nsid === "chat.atirc.post") { const posted = line(channel.length ? channel.at(-1).seq + 1 : 1, input.text, { nick: "alice" }); channel.push(posted); return Response.json(posted); } if (nsid === "com.atproto.server.getServiceAuth") return Response.json({ token: "svc-jwt" }); if (nsid === "com.atproto.repo.createRecord") return Response.json({ uri: `at://${ALICE}/${input.collection}/r${xrpc.length}`, cid: `bafy${xrpc.length}` }); return new Response("{}", { status: 400 }); }, }; const signIns = []; const revoked = []; const client = Promise.resolve({ init: async () => scope && { session: { did: ALICE, getTokenInfo: async () => ({ scope }) } }, signIn: async (identifier, options) => signIns.push({ identifier, scope: options.scope }), revoke: async (did) => revoked.push(did), }); const sessions = sharedSessions(client, POLICY); const bidder = []; const http = async (url, init) => (bidder.push({ url, init }), Response.json({ ok: true, name: "bid-1" })); const secp256k1 = { utils: { randomSecretKey: () => new Uint8Array(32).fill(1) }, getPublicKey: () => new Uint8Array(33).fill(2), sign: () => new Uint8Array(64) }; const compute = new WebrtcCubit({ client: Promise.resolve(sessions.client("requester")), makeAgent: () => agent, secp256k1, module: wasm("requester"), ctx: await context("requester", http) }); const atirc = new AtircCubit({ client: Promise.resolve(sessions.client("atirc")), makeAgent: () => agent, module: wasm("atirc"), ctx: await context("atirc", http) }); const cubit = new SocialwebCubit({ sessions, policy: POLICY, atirc, compute }); await cubit.init(); return { cubit, atirc, compute, xrpc, channel, signIns, revoked, bidder }; } test("policy: request scope, granted sessions, features", () => { assert.equal(requestScope(POLICY, []), "atproto"); const all = requestScope(POLICY, ["requester", "atirc"]); assert.deepEqual(grantedSessions(POLICY, all), ["requester", "atirc"]); assert.deepEqual(grantedSessions(POLICY, requestScope(POLICY, ["atirc"])), ["atirc"]); assert.deepEqual(grantedSessions(POLICY, "atproto rpc:chat.atirc.list?aud=did:web:irc.fedfork.com%23atirc"), [], "half of a session is none of it"); assert.deepEqual(enabledFeatures(POLICY, ["atirc"]), []); assert.deepEqual(enabledFeatures(POLICY, ["atirc", "requester"]), ["agents"]); }); test("classify and channelView: scrobbles, owner questions, agents, VM requests from your agents only", () => { const Q = "0cd72e00-7953-42d4-8306-49c1b6102ba8"; assert.equal(classify({ text: "🚀 shipped it" }).kind, "scrobble"); assert.equal(classify({ text: "✅ done" }).kind, "scrobble"); assert.equal(classify({ text: "1. not an emoji" }).kind, "chat"); assert.equal(classify({ text: "plain line 🚀" }).kind, "chat"); assert.deepEqual(classify({ text: 'vm-request {"bidders":["did:plc:x"],"rm":"-rf"}' }), { kind: "vm-request", input: { bidders: ["did:plc:x"] } }); assert.equal(classify({ text: "vm-request {not json" }).kind, "chat"); const view = channelView([ line(1, `[ask ${Q}] push the branch?`, { nick: "atlibs" }), line(2, "🚀 shipped atlibs ssh"), line(3, 'vm-request {"user_data":"#cloud-config\\n"}'), line(4, 'vm-request {"user_data":"x"}', { did: OTHER, nick: "mallory" }), line(5, `[decided ${Q}] keep it local`, { nick: "atlibs" }), line(6, "vm-request 3: rfp at://x", { nick: "alice" }), line(7, 'vm-request {"bidders":["did:plc:y"]}'), ], { did: ALICE, handle: "alice.test", channel: CHANNEL }); assert.deepEqual(view.lines.map((l) => l.kind), ["ask", "scrobble", "vm-request", "vm-request", "decided", "vm-reply", "vm-request"]); assert.deepEqual(view.questions, [{ id: Q, question: "push the branch?", askedBy: "atlibs.alice.test", status: "decided", outcome: "keep it local", by: "atlibs.alice.test" }]); assert.deepEqual(view.requests.map((r) => [r.seq, r.replied]), [[7, false], [3, true]], "mallory's request (another DID) is not listed"); const bob = view.agents.find((a) => a.nick === "bob-hat-9"); assert.deepEqual([bob.lines, bob.scrobbles], [3, 1]); }); // Anyone may post "[answer ] ..." text: only the channel owner's answer, the asker's (or // owner's) decision and asks from the owner's account count; the rest are unverified chat. test("channelView: forged owner-question lines are unverified and change nothing", () => { const Q = "11111111-2222-3333-4444-555555555555"; const R = "66666666-7777-8888-9999-aaaaaaaaaaaa"; const mallory = { did: OTHER, nick: "mallory", identity: "mallory.example" }; const owner = { nick: "alice", identity: "alice.test" }; const view = channelView([ line(1, `[ask ${Q}] deploy?`, { nick: "atlibs" }), line(2, `[answer ${Q}] yes, deploy`, mallory), line(3, `[answer ${Q}] yes, deploy`, { nick: "bob-hat-9" }), line(4, `[decided ${Q}] deployed`, { nick: "bob-hat-9" }), line(5, `[ask ${R}] wire the money?`, mallory), line(6, `[ask ${Q}] overwrite the question`, mallory), line(7, `[answer ${Q}] no, hold`, owner), ], { did: ALICE, handle: "alice.test", channel: CHANNEL }); assert.equal(view.owner, "alice.test"); assert.deepEqual(view.lines.map((l) => [l.kind, Boolean(l.unverified)]), [ ["ask", false], ["chat", true], ["chat", true], ["chat", true], ["chat", true], ["chat", true], ["answer", false], ]); assert.deepEqual(view.questions, [{ id: Q, question: "deploy?", askedBy: "atlibs.alice.test", status: "answered", outcome: "no, hold", by: "alice.test" }]); // Without any owner line or a signed-in owner, no ask verifies. const stranger = channelView([line(1, `[ask ${Q}] deploy?`, { nick: "atlibs" })], { did: OTHER, handle: "mallory.example", channel: CHANNEL }); assert.deepEqual([stranger.questions, stranger.lines[0].unverified], [[], true]); }); test("one session: only atirc granted, so compute is signed out and agents is off", async () => { const { cubit, atirc, compute, signIns } = await page(requestScope(POLICY, ["atirc"]), [line(1, "hello")]); assert.deepEqual([cubit.state.session, cubit.state.granted, cubit.state.features], ["signed-in", ["atirc"], []]); assert.equal(atirc.state.session, "signed-in"); assert.equal(atirc.state.messages.list[0].text, "hello"); assert.equal(compute.state.session, "signed-out"); await cubit.announce(); assert.match(cubit.state.announce.error, /agents needs the requester and atirc sessions/); // Turning compute on: one new consent for both sessions, as the signed-in DID. await cubit.setSessions(["atirc", "requester"]); assert.deepEqual(signIns, [{ identifier: ALICE, scope: requestScope(POLICY, ["atirc", "requester"]) }]); }); test("signed out: login asks for the identity and the ticked sessions in one consent", async () => { const { cubit, atirc, compute, signIns } = await page(undefined); assert.deepEqual([cubit.state.session, atirc.state.session, compute.state.session], ["signed-out", "signed-out", "signed-out"]); await cubit.login("alice.test", ["requester"]); assert.deepEqual(signIns, [{ identifier: "alice.test", scope: requestScope(POLICY, ["requester"]) }]); }); test("both sessions: announce the VM, approve an agent's vm-request, answer it on the channel", async () => { const endpoint = "https://bidder.example"; const { cubit, atirc, compute, xrpc, channel, bidder, revoked } = await page(requestScope(POLICY, ["requester", "atirc"]), [ line(1, `vm-request ${JSON.stringify({ user_data: "#cloud-config\n", bidders: [endpoint] })}`), line(2, 'vm-request {"user_data":"x"}', { did: OTHER, nick: "mallory" }), ]); assert.deepEqual(cubit.state.features, ["agents"]); assert.equal(compute.state.session, "signed-in"); // Announce needs a VM the page found. await cubit.announce(); assert.match(cubit.state.announce.error, /no receipt/); compute.emit({ receipt: { uri: `at://did:plc:hat9/${T}.market.receipt/receipt-1`, cid: "bafyrc", bidder: "did:plc:hat9" }, tag: "ssh", phase: "open" }); await cubit.announce(); assert.equal(cubit.state.announce.status, "done"); const posted = xrpc.filter((c) => c.nsid === "chat.atirc.post"); assert.equal(posted[0].input.text, `vm: at://did:plc:hat9/${T}.market.receipt/receipt-1 tag ssh open (socialweb-computer-ui)`); assert.equal(posted[0].headers["atproto-proxy"], "did:web:irc.fedfork.com#atirc"); // Requests run only once the owner allows them, and only from the account's agents. await cubit.approve(1); assert.match(cubit.state.approvals[1].error, /not allowed/); cubit.allowAgentRequests(true); await cubit.approve(2); assert.match(cubit.state.approvals[2].error, /no vm-request 2 from your agents/); await cubit.approve(1); assert.equal(cubit.state.approvals[1].status, "done", cubit.state.approvals[1].error); const rfp = xrpc.find((c) => c.input?.collection === `${T}.market.rfp`); assert.equal(rfp.input.record.signatures[0].issuer, ALICE); assert.equal(bidder[0].url, `${endpoint}/xrpc/${T}.market.submitRfp`); assert.match(channel.at(-1).text, /^vm-request 1: rfp at:\/\/.*market\.rfp.* submitted to 1 bidder\(s\)$/); await atirc.refresh(); assert.equal(channelView(atirc.state.messages.list, { did: ALICE, handle: "alice.test", channel: CHANNEL }).requests.find((r) => r.seq === 1).replied, true); cubit.emit({ approvals: {} }); await cubit.approve(1); assert.match(cubit.state.approvals[1].error, /already answered/); await cubit.logout(); assert.deepEqual(revoked, [ALICE]); assert.deepEqual([cubit.state.session, atirc.state.session, compute.state.session], ["signed-out", "signed-out", "signed-out"]); }); // A page whose plugin context lacks the rkey ledger has every cleanup delete refused ("not created by // this module"): each page that runs the requester must declare managed_records on that context. test("pages that run the requester declare storage.managed_records on its plugin context", async () => { for (const page of ["socialweb-computer-ui", "webrtc"]) { const main = await readFile(new URL(`../${page}/main.js`, import.meta.url), "utf8"); const contexts = [...main.matchAll(/createPluginContext\(\{([^)]*)\}\)/g)].map((m) => m[1]); const requester = contexts.filter((c) => /attestKeys/.test(c)); assert.ok(requester.length > 0, `${page}: the requester's context (the one with attestKeys)`); for (const c of requester) assert.match(c, /storage:\s*\{\s*managed_records:\s*\{\s*\}\s*\}/, `${page}: ${c}`); } });