1import { describe, expect, it } from "vitest";2import {3 eventReferencesRepository,4 gitRepositoryPath,5 isCanonicalCloneURL,6 isWebSocketURL,7 latestState,8 nostrRefEventID,9 parseRepositoryAnnouncement,10 parseRepositoryState,11 recursiveMaintainers,12 repositoryCoordinate,13 serviceListed,14 stateAllowsRefs,15} from "../../src/grasp-policy.ts";1617const owner = "a".repeat(64);18const maintainer = "b".repeat(64);19const eventID = "c".repeat(64);20const base = { id: eventID, pubkey: owner, kind: 30617, created_at: 10, tags: [] as string[][] };2122describe("GRASP-01 policy", () => {23 it("parses and validates repository announcements", () => {24 const parsed = parseRepositoryAnnouncement({ ...base, tags: [["d", "bindws"], ["clone", "https://git.example/bindws.git", "ssh://git.example/bindws"], ["relays", "wss://relay.example", "ws://localhost:8787"], ["maintainers", maintainer]] });25 expect(parsed.error).toBeUndefined();26 expect(parsed.value?.maintainers).toEqual([maintainer]);27 expect(repositoryCoordinate(parsed.value!)).toBe(`30617:${owner}:bindws`);28 expect(serviceListed(parsed.value!, "https://git.example/bindws.git", "wss://relay.example/")).toBe(true);29 });3031 it("accepts third-party clone schemes and rejects malformed relay URLs", () => {32 expect(isCanonicalCloneURL("http://git.example/x.git")).toBe(true);33 expect(isCanonicalCloneURL("ssh://git.example/x")).toBe(true);34 expect(isCanonicalCloneURL("git@git.example:x")).toBe(true);35 expect(isCanonicalCloneURL("nostr://danconwaydev.com/relay.ngit.dev/ngit")).toBe(true);36 expect(isWebSocketURL("https://relay.example")).toBe(false);37 expect(parseRepositoryAnnouncement({ ...base, tags: [["d", "x"], ["clone", "https://git.example/x.git"], ["relays", "https://relay.example"]] }).error).toContain("relays");38 });3940 it("preserves identifiers through the percent-encoded Git path", () => {41 expect(gitRepositoryPath("npub1example", "my/repo repo")).toBe("/npub1example/my%2Frepo%20repo.git");42 expect(gitRepositoryPath("npub1example", "a!b'c(d)*~")).toBe("/npub1example/a%21b%27c%28d%29%2A~.git");43 expect(() => gitRepositoryPath("hex", "x")).toThrow();44 });4546 it("uses the 256-byte identifier quota and rejects lone surrogates", () => {47 expect(() => gitRepositoryPath("npub1example", "a".repeat(257))).toThrow();48 expect(() => gitRepositoryPath("npub1example", "\ud800")).toThrow();49 expect(parseRepositoryAnnouncement({ ...base, tags: [["d", "a".repeat(257)], ["clone", "https://git.example/x.git"], ["relays", "wss://relay.example"]] }).error).toContain("identifier");50 });5152 it("rejects duplicate d tags but accepts multi-value tags", () => {53 const duplicate = parseRepositoryAnnouncement({ ...base, tags: [["d", "one"], ["d", "two"], ["clone", "http://git.example/x.git"], ["relays", "ws://relay.example"]] });54 expect(duplicate.error).toContain("identifier");55 const multi = parseRepositoryAnnouncement({ ...base, tags: [["d", "my/repo"], ["clone", "http://git.example/x.git", "ssh://git.example/x"], ["relays", "ws://relay.example", "wss://relay.example"]] });56 expect(multi.value?.clone).toHaveLength(2);57 expect(multi.value?.relays).toHaveLength(2);58 });5960 it("walks recursive maintainers with cycles", () => {61 const root = { id: eventID, owner, identifier: "x", clone: [], relays: [], maintainers: [maintainer], private: false };62 const second = { ...root, id: "d".repeat(64), owner: maintainer, maintainers: ["e".repeat(64)] };63 const third = { ...root, id: "e".repeat(64), owner: "e".repeat(64), maintainers: [owner] };64 expect(recursiveMaintainers(root, [second, third])).toEqual(new Set([owner, maintainer, "e".repeat(64)]));65 });6667 it("parses state and compares refs exactly", () => {68 const state = parseRepositoryState({ id: eventID, pubkey: owner, kind: 30618, created_at: 12, tags: [["d", "x"], ["refs/heads/main", "1".repeat(40)], ["HEAD", "ref: refs/heads/main"], ["refs/tags/v1^{}", "2".repeat(40)]] });69 expect(state.error).toBeUndefined();70 expect(state.value?.refs).toEqual({ "refs/heads/main": "1".repeat(40) });71 expect(state.value && stateAllowsRefs(state.value, { "refs/heads/main": "1".repeat(40) }, "refs/heads/main")).toBe(true);72 expect(state.value && stateAllowsRefs(state.value, {}, null)).toBe(false);73 });7475 it("rejects unsafe refs and zero object IDs", () => {76 for (const ref of ["refs/heads/a..b", "refs/heads/a@{b}", "refs/heads/a.lock", "refs/heads/a//b", "refs/heads/a~b"]) {77 expect(parseRepositoryState({ id: eventID, pubkey: owner, kind: 30618, created_at: 12, tags: [["d", "x"], [ref, "1".repeat(40)]] }).error).toContain("ref");78 }79 expect(parseRepositoryState({ id: eventID, pubkey: owner, kind: 30618, created_at: 12, tags: [["d", "x"], ["refs/heads/main", "0".repeat(40)]] }).error).toContain("ref");80 });8182 it("orders state ties by the lower event id and filters authors", () => {83 const make = (id: string, created_at: number, author = owner) => ({ id, owner: author, created_at, identifier: "x", refs: {}, head: null });84 const states = [make("f".repeat(64), 20), make("e".repeat(64), 20), make("0".repeat(64), 21, "z".repeat(64))];85 expect(latestState(states, owner, "x", new Set([maintainer]))?.id).toBe("e".repeat(64));86 });8788 it("recognizes unknown PR refs without requiring a stored event", () => {89 expect(nostrRefEventID(`refs/nostr/${eventID}`)).toBe(eventID);90 expect(nostrRefEventID("refs/heads/main")).toBeNull();91 });9293 it("matches valid repository a-tags only", () => {94 const coordinate = `30617:${owner}:x`;95 expect(eventReferencesRepository({ ...base, kind: 1618, tags: [["a", coordinate]] }, coordinate)).toBe(true);96 expect(eventReferencesRepository({ ...base, kind: 1618, tags: [["a", "30618:" + owner + ":x"]] }, coordinate)).toBe(false);97 });9899 it("fails closed when recursive maintainers exceed the bound", () => {100 const root = { id: eventID, owner, identifier: "x", clone: [], relays: [], maintainers: [maintainer], private: false };101 expect(recursiveMaintainers(root, [], 8, 1)).toBeNull();102 });103});