Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141// The web host end to end (Phase C, docs/WEB-APP.md), headless: two// browser-profile stand-ins against the fake network, driving the exact// bridge the tile frames speak — open from the home space, save through// tp-data (with the web durability rule), send ops through tp-sync, and// prove a second profile receives both.// Run: deno run -A src/web/webhost.test.ts
import { MemoryFs, setZiranFs, zfs } from '../lib/zfs.ts';import { passwordSession } from '../sync/spaces/client.ts';import { FakeSpacesNetwork } from '../sync/spaces/fake.ts';import { SpaceClient } from '../sync/spaces/client.ts';import { parseTile, writeTile } from '../server/tilefile.ts';import { importTileToHome, listHomeDocs } from './home.ts';import { WebHost } from './host.ts';
let failures = 0;const check = (label: string, ok: boolean, detail = '') => { console.log(`${ok ? 'PASS' : 'FAIL'} ${label}${detail ? ` — ${detail}` : ''}`); if (!ok) failures++;};const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));const b64 = (s: string) => btoa(s).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');const unb64 = (s: string) => atob(s.replace(/-/g, '+').replace(/_/g, '/'));
const net = new FakeSpacesNetwork();const resolver = { plcDirectory: net.url };const alice = net.account('alice');const auth = await passwordSession(net.url, alice.handle, alice.password);
/* ——— profile A: import, open, save, send ——— */
const aMem = new MemoryFs();setZiranFs(aMem);const client = new SpaceClient({ auth, resolver });const mkPath = '/mk/doc.tile';await writeTile( { name: 'Web Phase C', model: { id: 'com.berjon.ziran.testtile' }, resources: {} }, new Map([ ['/', new TextEncoder().encode('<!doctype html><title>c</title>')], ['/.well-known/web-tiles-storage/state', new TextEncoder().encode('{"v":0}')], ]), mkPath,);const imported = await importTileToHome(client, await zfs.readFile(mkPath), 'doc.tile');
const a = new WebHost(auth, fetch, { resolver, autoPoll: false, publishDelayMs: 50 });await a.init();const aDoc = await a.openHomeDoc(imported.rkey);check('openHomeDoc materializes from the home space', aDoc.name === 'Web Phase C', aDoc.path);check('…verified on the way in', !aDoc.tile.damaged);
const attach = await a.bridge.syncAttach(aDoc.localId);check('the tile attaches live (tp-sync hello)', attach.shared === true);
check( 'tp-data load reads the materialized self-storage', new TextDecoder().decode((await a.bridge.loadData(aDoc.localId, 'state'))!) === '{"v":0}',);await a.bridge.putData(aDoc.localId, 'state', new TextEncoder().encode('{"v":1}'));check( 'tp-data put rewrites it', new TextDecoder().decode((await a.bridge.loadData(aDoc.localId, 'state'))!) === '{"v":1}',);await sleep(150); // the shrunken durability windowawait a.bridge.syncSend(aDoc.localId, b64('op-from-a'));await sleep(400); // flush quiet windowawait a.provider.pollNow(true);
/* ——— profile B: a different browser, nothing local ——— */
const bMem = new MemoryFs();setZiranFs(bMem);const b = new WebHost(auth, fetch, { resolver, autoPoll: false, publishDelayMs: 50 });const bEvents: Array<Record<string, unknown>> = [];b.events.addEventListener('sync-msg', (ev) => bEvents.push((ev as CustomEvent).detail as Record<string, unknown>));await b.init();const bDoc = await b.openHomeDoc(imported.rkey);check( 'profile B materializes the SAVED state (putData survived the tab)', new TextDecoder().decode((await b.bridge.loadData(bDoc.localId, 'state'))!) === '{"v":1}',);await b.provider.pollNow(true);check( 'profile B replays A’s op through tp-sync', bEvents.some((e) => e.docId === bDoc.localId && unb64(String(e.b64)) === 'op-from-a'), JSON.stringify(bEvents.map((e) => unb64(String(e.b64)))),);
// And back the other way: B sends, A (on its own store) receives.await b.bridge.syncSend(bDoc.localId, b64('op-from-b'));await sleep(400);await b.provider.pollNow(true);setZiranFs(aMem);const aEvents: Array<Record<string, unknown>> = [];a.events.addEventListener('sync-msg', (ev) => aEvents.push((ev as CustomEvent).detail as Record<string, unknown>));await a.provider.pollNow(true);check( 'ops flow back to profile A', aEvents.some((e) => e.docId === aDoc.localId && unb64(String(e.b64)) === 'op-from-b'), JSON.stringify(aEvents.map((e) => unb64(String(e.b64)))),);
/* ——— rename: the manifest, the space record, and the list all follow ——— */
setZiranFs(aMem);await a.rename(aDoc.localId, 'Renamed on the web');check('rename updates the open doc', a.doc(aDoc.localId)?.name === 'Renamed on the web');check( 'rename rewrites the materialized manifest', (await parseTile(aDoc.path, { verify: false })).manifest.name === 'Renamed on the web',);check( 'rename reaches the home space record (the document list)', (await listHomeDocs(client)).find((d) => d.rkey === imported.rkey)?.name === 'Renamed on the web',);
/* ——— go live from home: the doc MOVES into a shared space ——— */
const moved = await a.provider.share(aDoc.localId, { newSpaceName: 'Web Crew' });check('sharing a home doc lands it in a real space', !moved.space.includes('/space/space.ziran.home/'), moved.space);const sum = a.provider.summary();const dSum = sum.docs[aDoc.localId];check('…same localId, new address', dSum?.space === moved.space && dSum?.home !== true);check('…now with a share link and invitable roles', Boolean(dSum?.link) && (dSum?.roles ?? []).some((r) => r.invitable));check('…the space carries the renamed title', moved.at.length > 0 && (await client.getRecord(moved.space, auth.did, 'space.ziran.doc', moved.docRkey)).value.name === 'Renamed on the web');const movedCp = await client.getRecord(moved.space, auth.did, 'space.ziran.checkpoint', moved.docRkey) .then(() => true).catch(() => false);check('…with a go-live checkpoint (others can instantiate)', movedCp);check('the home space no longer lists it', !(await listHomeDocs(client)).some((d) => d.rkey === imported.rkey));check('sharing again is idempotent (already live)', (await a.provider.share(aDoc.localId)) === moved);check('the home space itself is flagged, never a go-live target', sum.spaces?.some((s) => s.home === true) === true && sum.spaces?.every((s) => s.home !== true || s.ref !== moved.space) === true);
a.close();b.close();await net.close();console.log(failures ? `\n${failures} FAILED` : '\nall green');Deno.exit(failures ? 1 : 0);