~ knotless Search/
↑↓ move↵ open⇧↵ all results

johnandersen777.bsky.social/atlibs

0
0
main
184 lines · 9.8 KB Raw History
<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="utf-8">
	<title>plugin-context test</title>
</head>
<body>
	<!-- Serve the repo root (python3 -m http.server) and open /javascript/src/plugin-context/test.html. The title ends PASS or FAIL. -->
	<pre id="out"></pre>
	<script type="module">
		import { createPluginContext } from "./index.js";

		const out = document.getElementById("out");
		const say = (line) => { out.textContent += `${line}\n`; console.log(line); };
		const assert = (ok, message) => { if (!ok) throw new Error(message); };
		const eq = (a, b, message) => assert(JSON.stringify(a) === JSON.stringify(b), `${message}: ${JSON.stringify(a)} !== ${JSON.stringify(b)}`);
		const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

		const dbName = `plugin-context-test-${crypto.randomUUID()}`;
		const ctx = createPluginContext({
			id: "atproto-integrations",
			dbName,
			storage: { clients: { indexes: [] }, pending: { indexes: ["createdAt"] }, sessions: { indexes: ["userId", "did", "updatedAt"] }, replays: { indexes: ["exp"] }, locks: { indexes: [] } },
			settingsSchema: { sessionKey: { type: "secret" }, siteIntegrations: { type: "string" } },
		});
		const rawRow = async (key) => {
			const db = await new Promise((resolve) => (indexedDB.open(dbName).onsuccess = (event) => resolve(event.target.result)));
			const value = await new Promise((resolve) => (db.transaction("rows").objectStore("rows").get(key).onsuccess = (event) => resolve(event.target.result)));
			db.close();
			return value;
		};

		const tests = {
			async "crud and versioned cas"() {
				const { clients } = ctx.storage;
				eq(await clients.get("a"), null, "absent get");
				await clients.put("a", { n: 1, gone: undefined });
				eq(await clients.get("a"), { n: 1 }, "json round trip");
				assert(await clients.exists("a"), "exists");
				const v = await clients.getVersioned("a");
				eq((await clients.compareAndSet("a", null, { n: 2 })).applied, false, "create-only on existing");
				const set = await clients.compareAndSet("a", v.revision, { n: 2 });
				assert(set.applied && set.revision !== v.revision, "cas with revision");
				eq((await clients.compareAndSet("a", v.revision, { n: 3 })).applied, false, "stale revision");
				eq((await clients.compareAndDelete("a", v.revision)).applied, false, "stale delete");
				eq((await clients.compareAndDelete("a", set.revision)).applied, true, "delete");
				eq(await clients.delete("a"), false, "delete absent");
			},
			async "query where, orderBy, limit, cursor"() {
				const { sessions } = ctx.storage;
				await sessions.putMany([1, 2, 3, 4, 5].map((i) => ({ id: `s${i}`, data: { userId: i % 2 ? "u1" : "u2", did: "did:plc:x", updatedAt: i } })));
				const mine = await sessions.query({ where: { userId: "u1" }, limit: 100 });
				eq(mine.items.map((item) => item.id), ["s1", "s3", "s5"], "equality");
				const newest = await sessions.query({ where: { did: "did:plc:x" }, orderBy: { updatedAt: "desc" }, limit: 2 });
				eq([newest.items.map((item) => item.id), newest.hasMore], [["s5", "s4"], true], "orderBy desc + limit");
				const next = await sessions.query({ where: { did: "did:plc:x" }, orderBy: { updatedAt: "desc" }, limit: 2, cursor: newest.cursor });
				eq(next.items.map((item) => item.id), ["s3", "s2"], "cursor");
				eq(await sessions.count({ updatedAt: { gte: 2, lt: 4 } }), 2, "range count");
				eq(await sessions.count({ userId: { in: ["u2"] } }), 2, "in");
				eq((await sessions.getMany(["s1", "nope"])).size, 1, "getMany");
				eq(await sessions.deleteMany(["s1", "s2", "nope"]), 2, "deleteMany");
			},
			async "replay guard (service-auth.ts rememberJti)"() {
				const { replays } = ctx.storage;
				const now = Math.floor(Date.now() / 1000);
				await replays.put("old jti", { exp: now - 10 });
				const remember = async (jti) => {
					const stale = await replays.query({ where: { exp: { lt: now } }, limit: 100 });
					if (stale.items.length) await replays.deleteMany(stale.items.map((item) => item.id));
					return (await replays.compareAndSet(`iss ${jti}`, null, { exp: now + 60 })).applied;
				};
				const raced = await Promise.all([remember("j1"), remember("j1"), remember("j1")]);
				eq(raced.filter(Boolean).length, 1, "only one use of a jti");
				eq(await replays.exists("old jti"), false, "stale pruned");
			},
			async "lease lock (lock.ts storageLock) serializes"() {
				const { locks } = ctx.storage;
				const acquire = async (name) => {
					for (;;) {
						const held = await locks.getVersioned(name);
						if (!held || held.value.expiresAt < Date.now()) {
							const claimed = await locks.compareAndSet(name, held?.revision ?? null, { expiresAt: Date.now() + 30_000 });
							if (claimed.applied) return claimed.revision;
						}
						await sleep(5);
					}
				};
				let inside = 0;
				let max = 0;
				await Promise.all(Array.from({ length: 5 }, async () => {
					const revision = await acquire("scope name");
					max = Math.max(max, ++inside);
					await sleep(10);
					inside--;
					await locks.compareAndDelete("scope name", revision);
				}));
				eq(max, 1, "one holder at a time");
			},
			async "settings key race (seal.ts sealingKey)"() {
				const keys = await Promise.all(Array.from({ length: 4 }, async (_, i) => {
					const created = `key-${i}`;
					const result = await ctx.settings.compareAndSet("sessionKey", null, created);
					return result.applied ? created : ctx.settings.get("sessionKey");
				}));
				eq(new Set(keys).size, 1, "every caller sees the one key");
				await ctx.settings.set("siteIntegrations", "at://a\nat://b");
				eq(await ctx.settings.get("siteIntegrations"), "at://a\nat://b", "set/get");
				eq((await ctx.settings.list("site")).map((entry) => entry.key), ["siteIntegrations"], "list prefix");
			},
			async "secret settings encrypted at rest"() {
				const key = await ctx.settings.get("sessionKey");
				const stored = (await rawRow(["settings", "sessionKey"])).value;
				assert(stored.$secret && !JSON.stringify(stored).includes(key), "ciphertext in IndexedDB");
				eq((await rawRow(["settings", "siteIntegrations"])).value, "at://a\nat://b", "non-secret stays plain");
				eq((await ctx.settings.list("session"))[0].value, key, "list decrypts");
				const meta = await rawRow(["meta", "secretKey"]);
				assert(meta instanceof CryptoKey && !meta.extractable, "non-extractable key");
				const other = createPluginContext({ id: "other-plugin", dbName, settingsSchema: { sessionKey: { type: "secret" } } });
				let refused = false;
				await other.settings.get("sessionKey").catch(() => (refused = true));
				assert(refused, "ciphertext bound to plugin id");
			},
			async "updateIf guard and counters"() {
				const { clients } = ctx.storage;
				await clients.put("stock", { left: 2, name: "x" });
				const take = () => clients.updateIf("stock", { where: { left: { gt: 0 } }, delta: { left: { dec: 1 } } });
				const results = await Promise.all([take(), take(), take()]);
				eq(results.filter((result) => result.applied).length, 2, "no oversell");
				eq(await clients.get("stock"), { left: 0, name: "x" }, "counter at zero");
				eq((await clients.updateIf("stock", { where: {}, set: { name: "y" } })).data, { left: 0, name: "y" }, "set");
				eq((await clients.updateIf("missing", { where: {}, set: { name: "y" } })).applied, false, "never inserts");
				eq((await clients.updateIf("stock", { where: {}, delta: { name: { inc: 1 } } })).applied, false, "non-integer counter");
				await clients.delete("stock");
			},
			async "persists across contexts"() {
				const again = createPluginContext({ id: "atproto-integrations", dbName, storage: { sessions: {} } });
				eq(await again.storage.sessions.count(), 3, "same db, new context");
				eq(again.storage.locks, undefined, "undeclared collection absent");
			},
			async "wasm runs statusphere module"() {
				const module = new Uint8Array(await (await ctx.http.fetch(new URL("../../../target/wasm32-unknown-unknown/release/statusphere.wasm", import.meta.url).href)).arrayBuffer());
				const output = await ctx.wasm.run({ module, entry: "set_status", input: { status: "🦋", did: "did:plc:x", now: "2026-11-07T00:00:00.000Z", state: null, event: { type: "start" } } });
				eq(output.calls?.[0]?.nsid, "com.atproto.repo.listRecords", "first call");
				let mismatch;
				await ctx.wasm.run({ module, entry: "set_status", sha256: "0".repeat(64) }).catch((error) => (mismatch = error.code));
				eq(mismatch, "WASM_DIGEST_MISMATCH", "digest check");
			},
			async "wasm that never returns times out without blocking the page"() {
				// exports memory, alloc, and spin(ptr, len) -> i64 that loops forever
				const spin = new Uint8Array([0,97,115,109,1,0,0,0, 1,12,2,96,1,127,1,127,96,2,127,127,1,126, 3,3,2,0,1, 5,3,1,0,1, 7,25,3,6,109,101,109,111,114,121,2,0,5,97,108,108,111,99,0,0,4,115,112,105,110,0,1, 10,16,2,4,0,65,0,11,9,0,3,64,12,0,11,66,0,11]);
				const quick = createPluginContext({ id: "spin", dbName, wasmTimeoutMs: 300 });
				let ticks = 0;
				const ticker = setInterval(() => ticks++, 20);
				const started = performance.now();
				let code;
				await quick.wasm.run({ module: spin, entry: "spin" }).catch((error) => (code = error.code));
				clearInterval(ticker);
				eq(code, "WASM_TIMEOUT", "timeout code");
				assert(performance.now() - started < 2000, "gave up near the limit");
				assert(ticks >= 5, `page kept running (${ticks} ticks)`);
			},
			async "site, url, log"() {
				eq(ctx.site.url, location.origin, "site url");
				eq(ctx.url("/x"), `${location.origin}/x`, "url helper");
				ctx.log.warn("log check", { ok: true });
			},
		};

		let failed = 0;
		for (const [name, test] of Object.entries(tests)) {
			try {
				await test();
				say(`ok   ${name}`);
			} catch (error) {
				failed++;
				say(`FAIL ${name}: ${error.message}`);
			}
		}
		indexedDB.deleteDatabase(dbName);
		document.title = failed ? `FAIL ${failed}` : "PASS";
		say(document.title);
	</script>
</body>
</html>