Something went wrong. Try again.
Monorepo for the nibling.run platform
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462import assert from "node:assert/strict";import { after, before, test } from "node:test";import { configure, type Session } from "@nibling/sdk";import { openHref, Platform, type Auth, type KeyValue, type SavedProject } from "../src/platform/state.ts";import { createFakePds, fakeNetwork } from "../../sdk/tests/fake-pds.ts";import { startGitServer, type GitServer } from "../../sdk/tests/git-server.ts";
const pds = createFakePds({ url: "https://pds.test" });const alice = pds.createAccount("alice");const bob = pds.createAccount("bob");configure(fakeNetwork([pds]));
const bytes = (text: string) => new TextEncoder().encode(text);const PASSWORD = "push-token";const REPO_DID = "did:plc:hellorepo";
/** OAuth stand-in: "signing in" as a handle records the DID that resume and complete hand back. */function fakeAuth(): Auth { let pending: Session["did"] | undefined; return { signIn: async (handle) => void (pending = handle === "bob.test" ? bob : alice), complete: async () => pds.session(pending!), resume: async (did) => (pds.repos.has(did) ? pds.session(did) : undefined), signOut: async () => {}, };}
function memoryStorage(): KeyValue { const map = new Map<string, string>(); return { getItem: (k) => map.get(k) ?? null, setItem: (k, v) => void map.set(k, v), removeItem: (k) => void map.delete(k) };}
async function signedIn(storage = memoryStorage(), handle = "alice.test") { const platform = new Platform({ auth: fakeAuth(), storage, starter: () => new Map([["index.html", bytes("<h1>starter</h1>")]]), resolveRepo: async (ref) => { assert.equal(ref, "alice.test/hello"); return { owner: alice, name: "hello", repoDid: REPO_DID, knot: knot(), cloneUrl: `${knot()}/${REPO_DID}` }; }, pushToken: async () => PASSWORD, }); await platform.signIn(handle); await platform.completeSignIn(new URLSearchParams()); return platform;}
/** The open project, which must be a saved one. */function saved(platform: Platform): SavedProject { const open = platform.state.project; assert.equal(open?.kind, "project"); return open as SavedProject;}
let server: GitServer;const knot = () => new URL(server.url).origin;
before(async () => { server = await startGitServer({ name: REPO_DID, password: PASSWORD, seed: { "README.md": "hello\n", "src/main.ts": "console.log(1);\n" }, branches: { feature: { "src/main.ts": "console.log(2);\n", "src/feature.ts": "export const x = 1;\n" } }, }); pds.docs.set(REPO_DID, { "@context": ["https://www.w3.org/ns/did/v1"], id: REPO_DID, service: [{ id: "#tangled_knot", type: "TangledKnot", serviceEndpoint: knot() }], } as never);});
after(() => server.close());
test("a new project saves an edit that a fresh boot reopens by its address", async () => { const storage = memoryStorage(); const platform = await signedIn(storage); assert.equal(platform.state.account?.handle, "alice.test");
await platform.newProject("demo"); const open = saved(platform); assert.equal(open.project.record.name, "demo"); assert.equal(open.project.record.slug, "demo"); assert.equal(open.actor, "alice.test"); assert.deepEqual(open.files.list(), ["index.html"]);
open.files.write("index.html", "<h1>edited</h1>"); assert.deepEqual(platform.state.project!.files.dirty(), ["index.html"]); await platform.saveProject(); assert.deepEqual(platform.state.notice, { tone: "info", text: "Saved" }); assert.deepEqual(platform.state.project!.files.dirty(), []);
const rebooted = new Platform({ auth: fakeAuth(), storage }); await rebooted.boot({ project: { actor: "alice.test", name: "demo" } }); assert.equal(rebooted.state.account?.session.did, alice); assert.equal(saved(rebooted).project.uri, open.project.uri); assert.equal(saved(rebooted).actor, "alice.test"); assert.equal(rebooted.state.project?.files.read("index.html"), "<h1>edited</h1>");});
test("a save that loses the swap keeps the edits and says why", async () => { const first = await signedIn(); await first.newProject("contested"); const uri = saved(first).project.uri; const second = await signedIn(); await second.openProject(uri);
second.state.project!.files.write("index.html", "second"); await second.saveProject(); first.state.project!.files.write("index.html", "first"); await first.saveProject();
assert.equal(first.state.notice?.tone, "error"); assert.match(first.state.notice!.text, /^Someone else saved this project after you opened it/); assert.deepEqual(first.state.project!.files.dirty(), ["index.html"]); assert.equal(first.state.busy, undefined);});
test("another account's project only saves after a fork", async () => { const owner = await signedIn(); await owner.newProject("shared"); const uri = saved(owner).project.uri;
const visitor = await signedIn(memoryStorage(), "bob.test"); await visitor.openProject(uri); visitor.state.project!.files.write("index.html", "bob's"); await visitor.saveProject(); assert.equal(visitor.state.notice?.text, "This project belongs to someone else. Fork it to save your changes.");
await visitor.forkProject(); const fork = saved(visitor); assert.equal(fork.project.did, bob); assert.deepEqual(fork.project.record.forkOf, { uri, cid: saved(owner).project.cid }); assert.deepEqual(fork.files.dirty(), ["index.html"]); await visitor.saveProject(); assert.equal(visitor.state.notice?.text, "Saved");});
test("an opened repo writes no record, and its owner pushes edits back to it", async () => { const seed = server.git("rev-parse", "main"); const platform = await signedIn(); pds.resetCounts(); await platform.openRepo("alice.test/hello"); const open = platform.state.project!; assert.equal(platform.state.notice, undefined); assert.equal(open.kind, "repo"); assert.deepEqual(open.kind === "repo" && open.repo, { owner: "alice.test", ownerDid: alice, name: "hello" }); assert.deepEqual(open.base?.ref, { repo: REPO_DID, ref: "main", commit: seed }); assert.deepEqual(open.files.list(), ["README.md", "src/main.ts"]);
open.files.write("README.md", "hello from nibling\n"); await platform.pushToTangled("Edit the readme"); const head = server.git("rev-parse", "main"); assert.deepEqual(platform.state.notice, { tone: "info", text: `Pushed ${head.slice(0, 7)} to main` }); assert.equal(server.git("log", "-1", "--format=%s %an", "main"), "Edit the readme alice.test"); assert.equal(server.git("show", "main:README.md"), "hello from nibling"); assert.equal(server.git("rev-parse", "main^"), seed); assert.deepEqual(platform.state.project!.base?.ref, { repo: REPO_DID, ref: "main", commit: head }); assert.deepEqual(platform.state.project!.files.dirty(), []); assert.equal(pds.count("com.atproto.repo.createRecord") + pds.count("com.atproto.repo.putRecord"), 0);});
test("saving an opened repo makes it the saver's project on the repo's commit, and a push rebases it", async () => { const platform = await signedIn(); await platform.openRepo("alice.test/hello"); const seed = platform.state.project!.base!.ref.commit; platform.state.project!.files.write("README.md", "saved first\n"); await platform.saveProject(); assert.deepEqual(platform.state.notice, { tone: "info", text: "Saved" }); const open = saved(platform); assert.equal(open.actor, "alice.test"); assert.equal(open.project.record.name, "hello"); assert.deepEqual(open.project.record.base, { repo: REPO_DID, ref: "main", commit: seed }); assert.deepEqual(open.files.dirty(), []);
await platform.pushToTangled("Push the saved project"); const head = server.git("rev-parse", "main"); const pushed = saved(platform); assert.deepEqual(pushed.project.record.base, { repo: REPO_DID, ref: "main", commit: head }); assert.deepEqual(pushed.project.record.files, []);
const reopened = await signedIn(); await reopened.openProject(pushed.project.uri); assert.equal(reopened.state.project?.files.read("README.md"), "saved first\n");});
test("a repo opens signed out, and someone else's repo saves as the viewer's project but does not push", async () => { const anonymous = new Platform({ auth: fakeAuth(), resolveRepo: async () => ({ owner: alice, name: "hello", repoDid: REPO_DID, knot: knot(), cloneUrl: `${knot()}/${REPO_DID}` }) }); await anonymous.openRepo("alice.test/hello"); assert.equal(anonymous.state.project?.kind, "repo"); await anonymous.saveProject(); assert.deepEqual(anonymous.state.notice, { tone: "error", text: "Sign in first." });
const visitor = await signedIn(memoryStorage(), "bob.test"); await visitor.openRepo("alice.test/hello"); await visitor.pushToTangled("not mine"); assert.deepEqual(visitor.state.notice, { tone: "error", text: "This repo belongs to someone else. Save it as your project to keep your changes." }); await visitor.forkProject(); assert.equal(saved(visitor).project.did, bob); assert.equal(saved(visitor).project.record.base?.repo, REPO_DID);});
test("openRepo at a branch opens that branch's files, and a push there leaves main alone", async () => { const platform = await signedIn(); const seed = server.git("rev-parse", "feature"); const mainBefore = server.git("rev-parse", "main"); await platform.openRepo("alice.test/hello", { ref: "feature" }); const open = platform.state.project!; assert.equal(open.kind, "repo"); assert.deepEqual(open.kind === "repo" && open.repo.ref, "feature"); assert.deepEqual(open.files.list(), ["README.md", "src/feature.ts", "src/main.ts"]); assert.equal(open.files.read("src/main.ts"), "console.log(2);\n"); assert.deepEqual(open.base?.ref, { repo: REPO_DID, ref: "feature", commit: seed }); // The route's own address, not the repo's resolved default, is what names the open branch. assert.equal(openHref(open), "/tangled.org/alice.test/hello/tree/feature");
open.files.write("src/main.ts", "console.log(4);\n"); await platform.pushToTangled("Edit on feature"); assert.equal(server.git("show", "feature:src/main.ts"), "console.log(4);"); assert.equal(server.git("rev-parse", "main"), mainBefore); assert.deepEqual(platform.state.project!.base?.ref, { repo: REPO_DID, ref: "feature", commit: server.git("rev-parse", "feature") });});
test("openRepo scoped to a path re-roots the project, and a push there touches only that subtree", async () => { const platform = await signedIn(); const seed = server.git("rev-parse", "main"); const readmeBefore = server.git("show", "main:README.md"); await platform.openRepo("alice.test/hello", { path: "src" }); const open = platform.state.project!; assert.equal(open.kind, "repo"); assert.deepEqual(open.kind === "repo" && open.repo.path, "src"); assert.deepEqual(open.files.list(), ["main.ts"]); assert.equal(open.files.read("main.ts"), "console.log(1);\n"); assert.deepEqual(open.base?.ref, { repo: REPO_DID, ref: "main", commit: seed, path: "src" }); assert.equal(openHref(open), "/tangled.org/alice.test/hello/tree/main/src");
open.files.write("main.ts", "console.log('edited');\n"); await platform.pushToTangled("Edit scoped to src"); const head = server.git("rev-parse", "main"); assert.equal(server.git("show", "main:src/main.ts"), "console.log('edited');"); assert.equal(server.git("show", "main:README.md"), readmeBefore); assert.deepEqual(platform.state.project!.base?.ref, { repo: REPO_DID, ref: "main", commit: head, path: "src" });
await platform.saveProject(); const savedProject = saved(platform); assert.deepEqual(savedProject.project.record.base, { repo: REPO_DID, ref: "main", commit: head, path: "src" });
const reopened = await signedIn(); await reopened.openProject(savedProject.project.uri); assert.deepEqual(reopened.state.project?.files.list(), ["main.ts"]); assert.equal(reopened.state.project?.files.read("main.ts"), "console.log('edited');\n");});
test("two concurrent saves perform exactly one putRecord", async () => { const platform = await signedIn(); await platform.newProject("concurrent"); platform.state.project!.files.write("index.html", "one");
pds.resetCounts(); await Promise.all([platform.saveProject(), platform.saveProject()]);
assert.equal(pds.count("com.atproto.repo.putRecord"), 1); assert.deepEqual(platform.state.notice, { tone: "info", text: "Saved" }); assert.deepEqual(platform.state.project!.files.dirty(), []);});
test("a non-owner's push is refused with the exact message", async () => { const owner = await signedIn(); await owner.openRepo("alice.test/hello"); await owner.saveProject(); const uri = saved(owner).project.uri;
const visitor = await signedIn(memoryStorage(), "bob.test"); await visitor.openProject(uri); await visitor.pushToTangled("not mine");
assert.deepEqual(visitor.state.notice, { tone: "error", text: "This project belongs to someone else. Fork it to push your changes." });});
test("a push that lands but fails to save still records the pushed commit, and a retry Save succeeds", async () => { const platform = await signedIn(); await platform.openRepo("alice.test/hello"); await platform.saveProject(); platform.state.project!.files.write("README.md", "pushed but not saved\n");
const session = platform.state.account!.session; const realHandler = session.client.handler; let failed = false; session.client.handler = (pathname, init) => { if (!failed && pathname.includes("com.atproto.repo.putRecord")) { failed = true; return Promise.reject(new Error("network blip")); } return realHandler(pathname, init); };
await platform.pushToTangled("Edit the readme"); const head = server.git("rev-parse", "main"); assert.deepEqual(platform.state.notice, { tone: "error", text: `Pushed ${head.slice(0, 7)} to main, but saving the project failed; Save again.` }); assert.deepEqual(platform.state.project!.base?.ref, { repo: REPO_DID, ref: "main", commit: head }); assert.deepEqual(platform.state.project!.files.dirty(), ["README.md"]);
session.client.handler = realHandler; await platform.saveProject(); assert.deepEqual(platform.state.notice, { tone: "info", text: "Saved" }); assert.deepEqual(saved(platform).project.record.base, { repo: REPO_DID, ref: "main", commit: head }); assert.deepEqual(platform.state.project!.files.dirty(), []);
const reopened = await signedIn(); await reopened.openProject(saved(platform).project.uri); assert.equal(reopened.state.project?.base?.ref.commit, head);});
test("operations that need an account or a project say so", async () => { const platform = new Platform({ auth: fakeAuth() }); await platform.newProject("x"); assert.deepEqual(platform.state.notice, { tone: "error", text: "Sign in first." }); await platform.signIn("alice.test"); await platform.completeSignIn(new URLSearchParams()); await platform.pushToTangled("m"); assert.deepEqual(platform.state.notice, { tone: "error", text: "Open a project first." });});
test("opening a project no one has flags the notice notFound; opening a name that does not resolve at all does not", async () => { const platform = await signedIn(); await platform.openProject({ actor: "alice.test", name: "no-such-project" }); assert.equal(platform.state.notice?.tone, "error"); assert.equal(platform.state.notice?.notFound, true); assert.equal(platform.state.project, undefined);
await platform.openProject({ actor: "ghost.test", name: "anything" }); assert.equal(platform.state.notice?.tone, "error"); assert.equal(platform.state.notice?.notFound, undefined);});
/** OAuth as one browser keeps it: completed sign-ins store a session per DID until that DID signs out. */function browserAuth(stored = new Set<Session["did"]>()): Auth & { stored: Set<Session["did"]> } { let pending: Session["did"] | undefined; return { stored, signIn: async (handle) => void (pending = handle === "bob.test" ? bob : alice), complete: async () => (stored.add(pending!), pds.session(pending!)), resume: async (did) => (stored.has(did) ? pds.session(did) : undefined), signOut: async (did) => void stored.delete(did), };}
async function signInAs(platform: Platform, handle: string) { await platform.signIn(handle); await platform.completeSignIn(new URLSearchParams());}
const handles = (platform: Platform) => platform.state.accounts.map((a) => a.handle);
test("a second sign-in adds an account, and switching resumes the stored session without signing in again", async () => { const auth = browserAuth(); const platform = new Platform({ auth, storage: memoryStorage() }); await signInAs(platform, "alice.test"); await signInAs(platform, "bob.test"); assert.equal(platform.state.account?.handle, "bob.test"); assert.deepEqual(handles(platform), ["alice.test", "bob.test"]);
let redirected = false; const signIn = auth.signIn; auth.signIn = async (handle) => ((redirected = true), signIn(handle)); await platform.switchAccount(alice); assert.equal(platform.state.account?.session.did, alice); assert.equal(platform.state.account?.handle, "alice.test"); assert.equal(redirected, false); assert.deepEqual(handles(platform), ["alice.test", "bob.test"]);});
test("finishing a sign-in on a fresh page, as after the OAuth redirect, keeps the accounts already signed in", async () => { const storage = memoryStorage(); const auth = browserAuth(); await signInAs(new Platform({ auth, storage }), "alice.test"); const returned = new Platform({ auth, storage }); await returned.signIn("bob.test"); await returned.completeSignIn(new URLSearchParams()); assert.equal(returned.state.account?.session.did, bob); assert.deepEqual(handles(returned), ["alice.test", "bob.test"]); await returned.switchAccount(alice); assert.equal(returned.state.account?.session.did, alice);});
test("switching to an account whose session lapsed sends it through sign-in", async () => { const auth = browserAuth(); const platform = new Platform({ auth, storage: memoryStorage() }); await signInAs(platform, "alice.test"); await signInAs(platform, "bob.test"); auth.stored.delete(alice); const asked: string[] = []; auth.signIn = async (handle) => void asked.push(handle); await platform.switchAccount(alice); assert.deepEqual(asked, ["alice.test"]); assert.equal(platform.state.account?.session.did, bob);});
test("signing out one account keeps the other signed in, and the project stays open", async () => { const auth = browserAuth(); const platform = new Platform({ auth, storage: memoryStorage(), starter: () => new Map([["a.txt", bytes("a")]]) }); await signInAs(platform, "alice.test"); await signInAs(platform, "bob.test");
await platform.signOut(alice); assert.equal(platform.state.account?.session.did, bob); assert.deepEqual(handles(platform), ["bob.test"]); assert.deepEqual([...auth.stored], [bob]);
await signInAs(platform, "alice.test"); await platform.newProject("kept"); await platform.signOut(); assert.equal(platform.state.account?.session.did, bob); assert.equal(saved(platform).project.record.name, "kept");
await platform.signOut(); assert.equal(platform.state.account, undefined); assert.deepEqual(platform.state.accounts, []); assert.equal(platform.state.project, undefined);});
test("the remembered accounts survive a fresh boot, with the current one resumed", async () => { const storage = memoryStorage(); const auth = browserAuth(); const first = new Platform({ auth, storage }); await signInAs(first, "alice.test"); await signInAs(first, "bob.test"); await first.switchAccount(alice);
const rebooted = new Platform({ auth: browserAuth(auth.stored), storage }); await rebooted.boot(); assert.equal(rebooted.state.account?.session.did, alice); assert.deepEqual(handles(rebooted), ["alice.test", "bob.test"]); await rebooted.switchAccount(bob); assert.equal(rebooted.state.account?.session.did, bob);});
test("unreadable or throwing storage only costs the remembered accounts", async () => { const garbage = memoryStorage(); garbage.setItem("nib.platform.accounts", "{not json"); const booted = new Platform({ auth: browserAuth(), storage: garbage }); await booted.boot(); assert.deepEqual(booted.state.accounts, []);
const throwing: KeyValue = { getItem: () => { throw new Error("denied"); }, setItem: () => { throw new Error("denied"); }, removeItem: () => { throw new Error("denied"); }, }; const platform = new Platform({ auth: browserAuth(), storage: throwing }); await platform.boot(); await signInAs(platform, "alice.test"); assert.deepEqual(handles(platform), ["alice.test"]); assert.equal(platform.state.notice, undefined);});