import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { OAuthUserAgent } from "@atcute/oauth-browser-client";
import type { AttestationSubject } from "#lib/api/attestation.ts";
import { atUriOf, cidOf, didOf, gitOidOf, knotOf } from "#lib/api/syntax.ts";
vi.mock("$app/env", () => ({ browser: true }));
const agent = { sub: "did:plc:writer" } as unknown as OAuthUserAgent;
const store = new Map<string, string>();
const fakeStorage = {
getItem: (key: string) => store.get(key) ?? null,
setItem: (key: string, value: string) => void store.set(key, value),
removeItem: (key: string) => void store.delete(key),
key: (index: number) => [...store.keys()][index] ?? null,
get length() {
return store.size;
}
};
const settledPrefix = "tangled:attestations:settled:did:plc:writer:";
const subject: AttestationSubject = {
uri: atUriOf("at://did:web:knot.test:repo/org.tangled.track.ticket/3labc"),
cid: cidOf("bafyreimister" + "a".repeat(46))
};
const other: AttestationSubject = {
uri: atUriOf(`${subject.uri}2`),
cid: cidOf("bafyreimagoo" + "a".repeat(47))
};
interface Written {
rkey: string;
record: Record<string, unknown>;
}
type PutRecord = (rkey?: string, record?: unknown) => Promise<unknown>;
const loadWith = async (putRecord: PutRecord) => {
vi.resetModules();
const writes: Written[] = [];
const attempts = { count: 0 };
vi.doMock("#lib/api/write.ts", () => ({
putRecord: vi.fn(
async (_agent: unknown, _collection: unknown, rkey: string, record: unknown) => {
try {
await putRecord(rkey, record);
writes.push({ rkey, record: record as Record<string, unknown> });
} finally {
attempts.count += 1;
}
}
)
}));
vi.doMock("#lib/api/cache.ts", () => ({ clear: () => {} }));
const attestation = await import("./attestation");
const queued = async (count: number) => {
await vi.waitFor(() => {
expect(attestation.pendingAttestations(didOf(agent.sub)).length).toBe(count);
});
};
return { attestation, writes, attempts, queued };
};
const succeed: PutRecord = async () => undefined;
const fail: PutRecord = async () => {
throw new Error("The PDS is unreachable.");
};
const failFirst = (() => {
let first = true;
return async (...args: Parameters<PutRecord>) => {
if (first) {
first = false;
return fail(...args);
}
return succeed(...args);
};
})();
beforeEach(() => {
store.clear();
vi.stubGlobal("localStorage", fakeStorage);
});
afterEach(() => {
vi.unstubAllGlobals();
});
describe("attest and flushAttestations", () => {
it("queues one subject once, retries with its minted rkey, and never requeues settled subjects", async () => {
const { attestation, writes, attempts, queued } = await loadWith(failFirst);
attestation.attest(agent, subject);
await vi.waitFor(() => expect(attempts.count).toBe(1));
attestation.attest(agent, subject);
expect(attestation.pendingAttestations(didOf(agent.sub)).length).toBe(1);
const failures = await attestation.flushAttestations(agent);
expect(failures).toEqual([]);
await queued(0);
expect(writes).toHaveLength(1);
attestation.attest(agent, subject);
await queued(0);
expect(writes).toHaveLength(1);
expect([...store.keys()].filter((key) => key.startsWith(settledPrefix))).toHaveLength(1);
});
it("attests a repeated cid under a new change, and still dedupes the same change and changeless replays", async () => {
const first = gitOidOf("1".repeat(40));
const reopened = gitOidOf("2".repeat(40));
const { attestation, writes, queued } = await loadWith(succeed);
attestation.attest(agent, subject, undefined, first);
await queued(0);
attestation.attest(agent, subject, undefined, reopened);
await queued(0);
attestation.attest(agent, subject, undefined, reopened);
await queued(0);
expect(writes.map((write) => write.record.change)).toEqual([first, reopened]);
attestation.attest(agent, other);
await queued(0);
attestation.attest(agent, other);
await queued(0);
expect(writes).toHaveLength(3);
});
it("reports each failure message, keeps failed entries queued, and survives corrupt rows and full quotas", async () => {
store.set("tangled:attestations:did:plc:writer:3lacorrupt", "{not json");
const { attestation, queued } = await loadWith(fail);
attestation.attest(agent, subject);
await queued(1);
const failures = await attestation.flushAttestations(agent);
expect(failures[0]).toBe("The PDS is unreachable.");
expect(attestation.pendingAttestations(didOf(agent.sub)).length).toBe(1);
vi.stubGlobal("localStorage", {
...fakeStorage,
setItem: () => {
throw new DOMException("Local storage is full.", "QuotaExceededError");
}
});
expect(() => attestation.attest(agent, subject)).not.toThrow();
});
it("only reports entries as failed once a write for them has failed", async () => {
let go: (() => void) | undefined;
const until = new Promise<void>((resolve) => {
go = resolve;
});
const { attestation, attempts, queued } = await loadWith(async () => {
await until;
return fail();
});
const did = didOf(agent.sub);
attestation.attest(agent, subject);
await queued(1);
expect(attestation.failedAttestations(did)).toHaveLength(0);
go?.();
await vi.waitFor(() => expect(attempts.count).toBe(1));
await vi.waitFor(() => expect(attestation.failedAttestations(did)).toHaveLength(1));
expect(attestation.pendingAttestations(did)[0]?.rkey).toBe(
attestation.failedAttestations(did)[0]?.rkey
);
});
it("takes the newest change listCobVersions has for a cid, and writes without one when the listing fails", async () => {
const pages = [
{
versions: [
{ cid: subject.cid, changeId: "1c0ffee1c0ffee1c0ffee1c0ffee1c0ffee1c0ff" }
],
cursor: "500"
},
{
versions: [
{ cid: subject.cid, changeId: "2".repeat(40) },
{ cid: subject.cid, changeId: "5d1f0a3c9b8e47d2a1c6f4e8b3d7a2c5e9f1b6d8" }
]
}
];
const asked: string[] = [];
const orders: (string | null)[] = [];
const limits: (string | null)[] = [];
vi.stubGlobal(
"fetch",
vi.fn(async (input: URL) => {
if (String(input).includes("knot.other"))
return { ok: false } as unknown as Response;
asked.push(`${new URL(String(input)).searchParams.get("cursor")}`);
orders.push(new URL(String(input)).searchParams.get("order"));
limits.push(new URL(String(input)).searchParams.get("limit"));
return {
ok: true,
json: async () => pages[asked.length - 1]
} as unknown as Response;
})
);
const { attestation, writes, queued } = await loadWith(succeed);
attestation.attest(agent, subject, knotOf("knot.test"));
await queued(0);
attestation.attest(agent, other, knotOf("knot.other"));
await queued(0);
expect(writes[0]?.record.change).toBe("5d1f0a3c9b8e47d2a1c6f4e8b3d7a2c5e9f1b6d8");
expect(writes[1]?.record.change).toBeUndefined();
expect(asked).toEqual(["0", "500"]);
expect(orders).toEqual([null, null]);
expect(limits).toEqual(["500", "500"]);
});
it("writes no change when the walk ends before the newest match could be seen", async () => {
const answers = [
{
versions: [
{ cid: subject.cid, changeId: "1c0ffee1c0ffee1c0ffee1c0ffee1c0ffee1c0ff" }
],
cursor: "500"
},
undefined
];
vi.stubGlobal(
"fetch",
vi.fn(async () => {
const answer = answers.shift();
return {
ok: answer !== undefined,
json: async () => answer
} as unknown as Response;
})
);
const { attestation, writes, queued } = await loadWith(succeed);
attestation.attest(agent, subject, knotOf("knot.test"));
await queued(0);
expect(writes[0]?.record.change).toBeUndefined();
});
it("doesn't write again for a replay that names no change", async () => {
const change = gitOidOf("1".repeat(40));
const { attestation, writes, queued } = await loadWith(succeed);
attestation.attest(agent, subject, knotOf("knot.test"), change);
await queued(0);
attestation.attest(agent, subject, knotOf("knot.test"));
await queued(0);
expect(writes).toHaveLength(1);
});
it("writes the change the knot handed over without asking listCobVersions", async () => {
const asked = vi.fn();
vi.stubGlobal("fetch", asked);
const change = gitOidOf("5d1f0a3c9b8e47d2a1c6f4e8b3d7a2c5e9f1b6d8");
const { attestation, writes, queued } = await loadWith(succeed);
attestation.attest(agent, subject, knotOf("knot.test"), change);
await queued(0);
expect(writes[0]?.record.change).toBe(change);
expect(asked).not.toHaveBeenCalled();
});
it("dismissal mid-flush drops queued entries and lets in-flight writes finish", async () => {
let go: (() => void) | undefined;
const until = new Promise<void>((resolve) => {
go = resolve;
});
let dispatched = 0;
const { attestation, writes, queued } = await loadWith(async () => {
dispatched += 1;
if (dispatched === 1) await until;
return succeed();
});
attestation.attest(agent, subject);
await vi.waitFor(() => expect(dispatched).toBe(1));
attestation.attest(agent, other);
await queued(2);
attestation.dismissAttestations(didOf(agent.sub));
go?.();
await queued(0);
await vi.waitFor(() => expect(writes).toHaveLength(1));
});
});