clone

nostr relays on demand at <name>.bind.ws: claim one with your key, pay in zaps past the free allowance

main
bindws / test / object / grasp.test.ts
grasp.test.ts 25.7K
1// The GRASP door: repository events name the service before Git answers, and2// pending authority stays out of ordinary relay reads until its data arrives.3import { SELF, env, runInDurableObject } from "cloudflare:test";4import { afterEach, describe, expect, it } from "vitest";5import { generateSecretKey } from "nostr-tools/pure";6import { npubEncode } from "nostr-tools/nip19";7import { KIND_REPO, KIND_REPO_STATE, KIND_GIT_PR, KIND_GIT_PR_UPDATE } from "../../src/kinds.ts";8import { gitRepository } from "../../src/grasp.ts";9import { repository as storedRepository } from "../../src/grasp-state.ts";10import { gitRepositoryPath } from "../../src/grasp-policy.ts";11import { ev, info, pk, rpc, sleep } from "../helpers/relay.ts";12import { WS } from "../helpers/ws.ts";13import { decodePack, encodePack } from "ntig";14import { GitSqliteRepository } from "../../src/git-sqlite.ts";1516const encoder = new TextEncoder();17const packet = (value: string | Uint8Array) => {18 const body = typeof value === "string" ? encoder.encode(value) : value;19 const header = encoder.encode((body.length + 4).toString(16).padStart(4, "0"));20 return new Uint8Array([...header, ...body]);21};22const flush = () => new Uint8Array([48, 48, 48, 48]);23const concat = (...parts: Uint8Array[]) => {24 const result = new Uint8Array(parts.reduce((length, part) => length + part.length, 0));25 let offset = 0;26 for (const part of parts) { result.set(part, offset); offset += part.length; }27 return result;28};29const objectID = async (type: string, data: Uint8Array) => {30 const header = encoder.encode(`${type} ${data.length}\0`);31 const value = new Uint8Array(header.length + data.length);32 value.set(header); value.set(data, header.length);33 return Array.from(new Uint8Array(await crypto.subtle.digest("SHA-1", value)), (byte) => byte.toString(16).padStart(2, "0")).join("");34};35const receivePack = (path: string, host: string, old: string | null, next: string | null, ref: string, pack?: Uint8Array, requestId?: string) => SELF.fetch(`http://${host}${path}/git-receive-pack`, {36 method: "POST",37 headers: { "content-type": "application/x-git-receive-pack-request", ...(requestId ? { "X-Git-Request-Id": requestId } : {}) },38 body: concat(packet(`${old ?? "0".repeat(40)} ${next ?? "0".repeat(40)} ${ref}\0report-status\n`), flush(), ...(pack ? [pack] : [])),39});40// receiveSettled waits for transient alarm contention before testing the Git response.41const receiveSettled = async (path: string, host: string, old: string | null, next: string | null, ref: string, pack?: Uint8Array, requestId?: string) => {42 for (let attempt = 0; attempt < 20; attempt++) {43 const response = await receivePack(path, host, old, next, ref, pack, requestId);44 if (response.status !== 429) return response;45 await response.arrayBuffer();46 await new Promise((resolve) => setTimeout(resolve, 10));47 }48 throw new Error("Git transaction did not become available");49};5051async function tinyRepository(content = "hello from grasp\n") {52 const blob = encoder.encode(content);53 const blobID = await objectID("blob", blob);54 const tree = new Uint8Array([...encoder.encode("100644 README\0"), ...Uint8Array.from(blobID.match(/../g)!, (part) => parseInt(part, 16))]);55 const treeID = await objectID("tree", tree);56 const commit = encoder.encode(`tree ${treeID}\nauthor GRASP <grasp@example.com> 1 +0000\ncommitter GRASP <grasp@example.com> 1 +0000\n\ninitial\n`);57 const commitID = await objectID("commit", commit);58 const pack = await encodePack([{ type: "blob", data: blob }, { type: "tree", data: tree }, { type: "commit", data: commit }]);59 return { blobID, treeID, commitID, pack };60}6162const repository = (sk: Uint8Array, host: string, identifier = "bindws") => {63 const npub = npubEncode(pk(sk));64 return ev(sk, KIND_REPO, "", [65 ["d", identifier],66 ["clone", `https://${host}${gitRepositoryPath(npub, identifier)}`],67 ["relays", `wss://${host}`],68 ["clone", `https://mirror.example/${identifier}.git`],69 ["relays", "wss://relay.example"],70 ["maintainers", pk(sk)],71 ]);72};7374describe("GRASP", () => {75 afterEach(() => sleep(20));76 it("is opt in and advertises the Git contract only while it is on", async () => {77 const host = "grasp-feature.bind.ws";78 const owner = generateSecretKey();79 await rpc(host, owner, "claim");8081 expect((await info(host)).supported_grasps).toBeUndefined();82 expect((await SELF.fetch(`http://${host}/npub1invalid/repo.git`)).status).toBe(404);8384 expect((await rpc(host, owner, "setpolicy", { features: { grasp: true } })).status).toBe(200);85 const document = await info(host);86 expect(document.supported_grasps).toEqual(["GRASP-01"]);87 expect(document.repo_acceptance_criteria).toContain("clone and relays");88 expect(document.nsites).toBeDefined();89 expect((await rpc(host, owner, "setpolicy", { features: { grasp: false } })).status).toBe(200);90 expect((await info(host)).supported_grasps).toBeUndefined();91 });9293 it("accepts a service-listed announcement with every clone and relay value", async () => {94 const host = "grasp-announcement.bind.ws";95 const owner = generateSecretKey();96 await rpc(host, owner, "claim");97 await rpc(host, owner, "setpolicy", { features: { grasp: true } });98 const c = await WS.connect(host);99 const event = repository(owner, host, "multi-source");100 const accepted = await c.ok(event);101 expect(accepted.ok, accepted.msg).toBe(true);102103 const found = await c.open("repo", { kinds: [KIND_REPO], authors: [pk(owner)] });104 expect(found.events.map((e) => e.id)).not.toContain(event.id);105 c.ws.close();106 });107108 it("rejects an announcement that does not list this relay in both services", async () => {109 const host = "grasp-rejection.bind.ws";110 const owner = generateSecretKey();111 await rpc(host, owner, "claim");112 await rpc(host, owner, "setpolicy", { features: { grasp: true } });113 const c = await WS.connect(host);114 const bad = ev(owner, KIND_REPO, "", [["d", "missing-service"], ["clone", "https://elsewhere.example/repo.git"], ["relays", "wss://elsewhere.example"]]);115 const result = await c.ok(bad);116 expect(result.ok).toBe(false);117 expect(result.msg).toMatch(/^restricted:/);118 c.ws.close();119 });120121 it("keeps malformed Git paths CORS readable and answers preflight without opening a relay door", async () => {122 const host = "grasp-paths.bind.ws";123 const owner = generateSecretKey();124 await rpc(host, owner, "claim");125 await rpc(host, owner, "setpolicy", { features: { grasp: true } });126 const paths = ["/npub1invalid/repo.git", "/npub1invalid/repo.git/unknown", "/prs/anything.git"];127 for (const path of paths) {128 const response = await SELF.fetch(`http://${host}${path}`);129 expect(response.status).toBe(404);130 expect(response.headers.get("access-control-allow-origin")).toBe("*");131 }132 const options = await SELF.fetch(`http://${host}/npub1invalid/repo.git/info/refs`, { method: "OPTIONS" });133 expect(options.status).toBe(204);134 expect(options.headers.get("access-control-allow-methods")).toContain("GET");135 });136137 it("keeps a repository path separate from a site hostname", async () => {138 const host = "grasp-site-isolation.bind.ws";139 const owner = generateSecretKey();140 await rpc(host, owner, "claim");141 const siteHost = `${npubEncode(pk(owner))}.bind.ws`;142 const siteResponse = await SELF.fetch(`http://${siteHost}/`);143 expect(siteResponse.status).toBe(404);144 expect((await SELF.fetch(`http://${siteHost}/repo.git`)).status).toBe(404);145 });146147 it("serves and updates a state-authorized repository through Smart HTTP", async () => {148 const host = "grasp-git.bind.ws";149 const owner = generateSecretKey();150 const repo = repository(owner, host, "lifecycle");151 const pack = await tinyRepository();152 await rpc(host, owner, "claim");153 await rpc(host, owner, "setpolicy", { features: { grasp: true } });154 const c = await WS.connect(host);155 const repoResult = await c.ok(repo);156 expect(repoResult.ok, repoResult.msg).toBe(true);157158159 const state = ev(owner, KIND_REPO_STATE, "", [["d", "lifecycle"], ["HEAD", "ref: refs/heads/main"], ["refs/heads/main", pack.commitID]]);160 expect((await c.ok(state)).ok).toBe(true);161162 c.ws.close();163164 const path = gitRepositoryPath(npubEncode(pk(owner)), "lifecycle");165 const refs = await SELF.fetch(`http://${host}${path}/info/refs?service=git-upload-pack`);166 expect(refs.status).toBe(200);167 expect(await refs.text()).toContain("symref=HEAD:refs/heads/main");168169 const receive = concat(packet(`${"0".repeat(40)} ${pack.commitID} refs/heads/main\0report-status\n`), flush(), pack.pack);170 const pushed = await SELF.fetch(`http://${host}${path}/git-receive-pack`, { method: "POST", headers: { "content-type": "application/x-git-receive-pack-request" }, body: receive });171 expect(pushed.status).toBe(200);172 expect(await pushed.text()).toContain("ok refs/heads/main");173 const visible = await WS.connect(host);174 expect((await visible.req({ ids: [repo.id, state.id] })).map((e) => e.id).sort()).toEqual([repo.id, state.id].sort());175 visible.ws.close();176177 const advertised = await SELF.fetch(`http://${host}${path}/info/refs?service=git-upload-pack`);178 expect(await advertised.text()).toContain(`${pack.commitID} refs/heads/main`);179180 const upload = concat(packet(`want ${pack.commitID}\n`), flush(), packet("done\n"));181 const fetched = await SELF.fetch(`http://${host}${path}/git-upload-pack`, { method: "POST", headers: { "content-type": "application/x-git-upload-pack-request" }, body: upload });182 expect(fetched.status).toBe(200);183 const bytes = new Uint8Array(await fetched.arrayBuffer());184 const objects = await decodePack(bytes.slice(8));185 expect(objects.map((object) => object.oid).sort()).toEqual([pack.blobID, pack.treeID, pack.commitID].sort());186 });187188 it("refuses a receive update whose resulting refs differ from accepted state", async () => {189 const host = "grasp-state-gate.bind.ws";190 const owner = generateSecretKey();191 const repo = repository(owner, host, "state-gate");192 const pack = await tinyRepository();193 await rpc(host, owner, "claim");194 await rpc(host, owner, "setpolicy", { features: { grasp: true } });195 const c = await WS.connect(host);196 const repoResult = await c.ok(repo);197 expect(repoResult.ok, repoResult.msg).toBe(true);198 const state = ev(owner, KIND_REPO_STATE, "", [["d", "state-gate"], ["HEAD", "ref: refs/heads/main"], ["refs/heads/main", pack.commitID]]);199 expect((await c.ok(state)).ok).toBe(true); c.ws.close();200 const path = gitRepositoryPath(npubEncode(pk(owner)), "state-gate");201 const wrong = concat(packet(`${"0".repeat(40)} ${"f".repeat(40)} refs/heads/other\0report-status\n`), flush(), pack.pack);202 const response = await SELF.fetch(`http://${host}${path}/git-receive-pack`, { method: "POST", headers: { "content-type": "application/x-git-receive-pack-request" }, body: wrong });203 expect(response.status).toBe(200);204 expect(await response.text()).toMatch(/ng|unpack/);205 const refs = await SELF.fetch(`http://${host}${path}/info/refs?service=git-upload-pack`);206 expect(await refs.text()).not.toContain("refs/heads/other");207 });208209 it("holds unknown PR refs, checks published PR tips, and refuses anonymous deletion", async () => {210 const host = "grasp-pr-refs.bind.ws";211 const owner = generateSecretKey();212 const repo = repository(owner, host, "pr-refs");213 const pack = await tinyRepository();214 await rpc(host, owner, "claim");215 await rpc(host, owner, "setpolicy", { features: { grasp: true } });216 const c = await WS.connect(host);217 expect((await c.ok(repo)).ok).toBe(true);218 const state = ev(owner, KIND_REPO_STATE, "", [["d", "pr-refs"], ["HEAD", "ref: refs/heads/main"], ["refs/heads/main", pack.commitID]]);219 expect((await c.ok(state)).ok).toBe(true); c.ws.close();220 const path = gitRepositoryPath(npubEncode(pk(owner)), "pr-refs");221222 expect((await receiveSettled(path, host, null, pack.commitID, "refs/heads/main", pack.pack)).status).toBe(200);223 const realPR = ev(owner, KIND_GIT_PR, "", [["a", `30617:${pk(owner)}:pr-refs`], ["c", pack.commitID]]);224 const unknownRef = `refs/nostr/${realPR.id}`;225 expect((await receiveSettled(path, host, null, pack.commitID, unknownRef, pack.pack)).status).toBe(200);226 expect(await (await SELF.fetch(`http://${host}${path}/info/refs?service=git-upload-pack`)).text()).toContain(`${pack.commitID} ${unknownRef}`);227 const deletion = await receiveSettled(path, host, pack.commitID, null, unknownRef);228 expect(deletion.status).toBe(200);229 expect(await deletion.text()).toContain(`ng ${unknownRef}`);230231 const late = await WS.connect(host);232 expect((await late.ok(realPR)).ok).toBe(true);233 expect((await receiveSettled(path, host, pack.commitID, pack.commitID, "refs/heads/main")).status).toBe(200);234 expect((await late.req({ ids: [realPR.id] })).map((e) => e.id)).toEqual([realPR.id]);235 const listed = await SELF.fetch(`http://${host}${path}/info/refs?service=git-upload-pack`);236 expect(await listed.text()).toContain(`refs/nostr/${realPR.id}`);237 const wrongPR = ev(owner, KIND_GIT_PR, "", [["a", `30617:${pk(owner)}:pr-refs`], ["c", "b".repeat(40)]]);238 const wrongEvent = await WS.connect(host);239 expect((await wrongEvent.ok(wrongPR)).ok).toBe(true);240 const mismatch = await receiveSettled(path, host, null, pack.commitID, `refs/nostr/${wrongPR.id}`);241 expect(await mismatch.text()).toContain(`ng refs/nostr/${wrongPR.id}`);242 wrongEvent.ws.close();243244 const expiringID = "c".repeat(64);245 const expiringRef = `refs/nostr/${expiringID}`;246 expect(await (await receiveSettled(path, host, null, pack.commitID, expiringRef, pack.pack)).text()).toContain(`ok ${expiringRef}`);247 await runInDurableObject(env.RELAY.getByName(host.split(".")[0]), (relay) => {248 relay.sql.exec("UPDATE grasp_pr_refs SET until=0 WHERE repo=? AND ref=?", `30617:${pk(owner)}:pr-refs`, expiringRef);249 });250 await runInDurableObject(env.RELAY.getByName(host.split(".")[0]), (relay) => relay.alarm());251 const afterExpiry = await SELF.fetch(`http://${host}${path}/info/refs?service=git-upload-pack`);252 expect(await afterExpiry.text()).not.toContain(expiringRef);253 late.ws.close();254 });255256 it("replays a hidden PR correction after more than 128 SQL transactions", async () => {257 const host = "grasp-pr-sql-retry.bind.ws", owner = generateSecretKey();258 const first = await tinyRepository(), next = await tinyRepository("corrected PR\n");259 await rpc(host, owner, "claim");260 await rpc(host, owner, "applypreset", "grasp");261 const c = await WS.connect(host);262 expect((await c.ok(repository(owner, host, "correction"))).ok).toBe(true);263 expect((await c.ok(ev(owner, KIND_REPO_STATE, "", [["d", "correction"], ["HEAD", "ref: refs/heads/main"], ["refs/heads/main", first.commitID]]))).ok).toBe(true);264 c.ws.close();265 const path = gitRepositoryPath(npubEncode(pk(owner)), "correction");266 expect(await (await receiveSettled(path, host, null, first.commitID, "refs/heads/main", first.pack, "sql-initial")).text()).toContain("ok refs/heads/main");267 const pr = ev(owner, KIND_GIT_PR, "", [["a", `30617:${pk(owner)}:correction`], ["c", next.commitID]]), ref = `refs/nostr/${pr.id}`;268 expect(await (await receiveSettled(path, host, null, first.commitID, ref, undefined, "sql-unknown")).text()).toContain(`ok ${ref}`);269 const publisher = await WS.connect(host);270 expect((await publisher.ok(pr)).ok).toBe(true); publisher.ws.close();271 expect(await (await SELF.fetch(`http://${host}${path}/info/refs?service=git-upload-pack`)).text()).not.toContain(ref);272 const stub = env.RELAY.getByName("grasp-pr-sql-retry");273 const before = await runInDurableObject(stub, async (relay) => relay.repositoryAccess.run("git", async () => {274 const git = await gitRepository(relay, storedRepository(relay, pk(owner), "correction")!);275 for (let i = 0; i < 130; i++) await git.commit({ id: `sql-later-${i}`, updates: [{ name: `refs/tags/later-${i}`, old: null, new: first.commitID }] });276 return (await git.loadRefs()).sequence;277 }, () => { throw new Error("Git scope unexpectedly refused"); }));278 expect(await (await receiveSettled(path, host, null, next.commitID, ref, next.pack, "sql-correction")).text()).toContain(`ok ${ref}`);279 const after = await runInDurableObject(stub, async (relay) => relay.repositoryAccess.run("git", async () => (await gitRepository(relay, storedRepository(relay, pk(owner), "correction")!)).loadRefs().then((snapshot) => snapshot.sequence), () => { throw new Error("Git scope unexpectedly refused"); }));280 expect(after).toBeGreaterThan(before);281 expect(await (await SELF.fetch(`http://${host}${path}/info/refs?service=git-upload-pack`)).text()).toContain(`${next.commitID} ${ref}`);282 await runInDurableObject(stub, (relay) => relay.settings.setEvent(pr.id, "hide"));283 expect(await (await receiveSettled(path, host, null, next.commitID, ref, next.pack, "sql-correction")).text()).toContain(`ng ${ref}`);284 });285286 it.each(["expired", "moderated"])("an identical Git retry completes interrupted promotion and rechecks %s candidates", async (mode) => {287 const host = `grasp-promotion-${mode}.bind.ws`;288 const owner = generateSecretKey();289 const pack = await tinyRepository();290 await rpc(host, owner, "claim");291 await rpc(host, owner, "applypreset", "grasp");292 const announcement = repository(owner, host, "promotion");293 const state = ev(owner, KIND_REPO_STATE, "", [["d", "promotion"], ["HEAD", "ref: refs/heads/main"], ["refs/heads/main", pack.commitID]]);294 const c = await WS.connect(host);295 expect((await c.ok(announcement)).ok).toBe(true);296 expect((await c.ok(state)).ok).toBe(true);297 // Keep the PR candidate pending: its advertised tip is not in this298 // repository, so promotion must recheck expiry/moderation before release.299 const pr = ev(owner, KIND_GIT_PR, "", [["a", `30617:${pk(owner)}:promotion`], ["c", "f".repeat(40)]]);300 expect((await c.ok(pr)).ok).toBe(true);301 c.ws.close();302 const path = gitRepositoryPath(npubEncode(pk(owner)), "promotion");303 const requestId = "promotion-recovery";304 const stub = env.RELAY.getByName(`grasp-promotion-${mode}`);305 const replay = await runInDurableObject(stub, async (relay, context) => {306 await context.storage.deleteAlarm();307 const deadline = Date.now() + 5000;308 while (relay.repositoryAccess.busy && Date.now() < deadline) await new Promise((resolve) => setTimeout(resolve, 10));309 return relay.repositoryAccess.run("git", async () => {310 await context.storage.deleteAlarm();311 const published: string[] = [];312 const broadcast = relay.broadcast.bind(relay);313 Object.defineProperty(relay, "broadcast", { configurable: true, value: (event: Parameters<typeof relay.broadcast>[0]) => {314 published.push(event.id);315 broadcast(event);316 } });317 // The SQL commit is durable before the injected caller failure, so the318 // identical request exercises receipt replay without an R2 root.319 {320 const originalCommit = GitSqliteRepository.prototype.commit;321 let published = false;322 GitSqliteRepository.prototype.commit = async function (request, beforeCommit) {323 await originalCommit.call(this, request, beforeCommit);324 published = true;325 throw new Error("publication succeeded before response failed");326 };327 try {328 const response = await relay.fetch(new Request(`http://${host}${path}/git-receive-pack`, {329 method: "POST",330 headers: { "content-type": "application/x-git-receive-pack-request", "X-Git-Request-Id": requestId },331 body: concat(packet(`${"0".repeat(40)} ${pack.commitID} refs/heads/main\0report-status\n`), flush(), pack.pack),332 }));333 expect(await response.text()).not.toContain("ok refs/heads/main");334 expect(published).toBe(true);335 } finally { GitSqliteRepository.prototype.commit = originalCommit; }336 const wal = await gitRepository(relay, storedRepository(relay, pk(owner), "promotion")!);337 expect((await wal.load()).sequence).toBe(1);338 // SQLite promotion commits the pending pair atomically before the339 // injected caller failure, so the retry must recover from receipts.340 expect(relay.sql.exec("SELECT id FROM grasp_pending WHERE id IN (?,?)", announcement.id, state.id).toArray()).toEqual([]);341 }342 {343 let reads = 0;344 const originalLoadRefs = GitSqliteRepository.prototype.loadRefs;345 // A receipt replay returns the durable SQL result without reopening346 // the repository. Change the candidate authority before retry so347 // the pending PR remains hidden in either expiry or moderation mode.348 if (mode === "expired") relay.sql.exec("UPDATE events SET expires=1 WHERE id=?", pr.id);349 else relay.settings.setEvent(pr.id, "hide");350 GitSqliteRepository.prototype.loadRefs = async function () {351 const result = await originalLoadRefs.call(this);352 ++reads;353 return result;354 };355 try {356 const response = await relay.fetch(new Request(`http://${host}${path}/git-receive-pack`, {357 method: "POST",358 headers: { "content-type": "application/x-git-receive-pack-request", "X-Git-Request-Id": requestId },359 body: concat(packet(`${"0".repeat(40)} ${pack.commitID} refs/heads/main\0report-status\n`), flush(), pack.pack),360 }));361 expect(published).toContain(announcement.id);362 expect(published).toContain(state.id);363 expect(published).not.toContain(pr.id);364 return await response.text();365 } finally {366 GitSqliteRepository.prototype.loadRefs = originalLoadRefs;367 Reflect.deleteProperty(relay, "broadcast");368 }369 }370 }, () => { throw new Error("promotion fixture did not settle"); });371 });372 expect(replay).toContain("ok refs/heads/main");373 await runInDurableObject(stub, async (relay) => {374 const wal = await gitRepository(relay, storedRepository(relay, pk(owner), "promotion")!);375 expect((await wal.load()).sequence).toBe(1);376 expect(relay.sql.exec("SELECT id FROM grasp_pending WHERE id IN (?,?)", announcement.id, state.id).toArray()).toEqual([]);377 // Expiry cleanup can remove the event and its pending row between378 // requests. If the event survives, it must still be held from readers.379 expect(relay.sql.exec("SELECT id FROM events WHERE id=? AND NOT EXISTS (SELECT 1 FROM grasp_pending WHERE grasp_pending.id=events.id)", pr.id).toArray()).toEqual([]);380 });381 const reader = await WS.connect(host);382 expect((await reader.query({ ids: [announcement.id, state.id] })).events).toHaveLength(2);383 reader.ws.close();384 });385386 it("removes the public Git contract when reads become restricted", async () => {387 const host = "grasp-restricted.bind.ws";388 const owner = generateSecretKey();389 await rpc(host, owner, "claim");390 await rpc(host, owner, "setpolicy", { features: { grasp: true } });391 expect((await info(host)).supported_grasps).toEqual(["GRASP-01"]);392 await rpc(host, owner, "setpolicy", { reads: "members" });393 const document = await info(host);394 expect(document.supported_grasps).toBeUndefined();395 const response = await SELF.fetch(`http://${host}/npub1${"q".repeat(58)}/repo.git`);396 expect(response.status).toBe(403);397 expect(await response.text()).toMatch(/^restricted:/);398 });399400 it("fences event and management mutations while Git owns the authority view", async () => {401 const host = "grasp-fence.bind.ws";402 const owner = generateSecretKey();403 await rpc(host, owner, "claim");404 await rpc(host, owner, "setpolicy", { features: { grasp: true } });405 const event = repository(owner, host, "fenced");406 await runInDurableObject(env.RELAY.getByName("grasp-fence"), async (relay) => {407 let entered!: () => void;408 let release!: () => void;409 const ready = new Promise<void>((resolve) => { entered = resolve; });410 const hold = new Promise<void>((resolve) => { release = resolve; });411 const active = relay.repositoryAccess.run("git", async () => {412 entered();413 await hold;414 }, () => { throw new Error("Git scope unexpectedly refused"); });415 try {416 await ready;417 expect(relay.repositoryAccess.busy).toBe(true);418 expect(relay.repositoryAccess.kind).toBe("git");419 const result = await relay.acceptAny(event, relay.virtualConn(host, pk(owner)));420 expect(result.ok).toBe(false);421 expect(result.msg).toContain("relay operation in progress");422 const response = await relay.fetch(new Request(`http://${host}/`, { method: "POST", body: "{}" }));423 expect(response.status).toBe(429);424 await response.text();425 expect(relay.sql.exec("SELECT 1 FROM events WHERE id=?", event.id).toArray()).toEqual([]);426 } finally {427 release();428 await active;429 expect(relay.repositoryAccess.busy).toBe(false);430 }431 });432 });433434 it("does not derive a PR update's repository from a moderated parent", async () => {435 const host = "grasp-parent.bind.ws";436 const owner = generateSecretKey();437 await rpc(host, owner, "claim");438 await rpc(host, owner, "setpolicy", { features: { grasp: true } });439 const c = await WS.connect(host);440 expect((await c.ok(repository(owner, host, "parent"))).ok).toBe(true);441 const parent = ev(owner, KIND_GIT_PR, "", [["a", `30617:${pk(owner)}:parent`], ["c", "a".repeat(40)]]);442 expect((await c.ok(parent)).ok).toBe(true);443 await runInDurableObject(env.RELAY.getByName("grasp-parent"), (relay) => relay.settings.setEvent(parent.id, "hide"));444 const update = ev(owner, KIND_GIT_PR_UPDATE, "", [["E", parent.id], ["c", "b".repeat(40)]]);445 const result = await c.ok(update);446 expect(result.ok).toBe(false);447 expect(result.msg).toContain("accepted repository");448 c.ws.close();449 });450451});