import assert from "node:assert/strict";
import { access, mkdtemp, mkdir, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import { test } from "node:test";
import { collectClients, keepClient } from "./client.mjs";
import { fileOf, keyOf, keyOfUrl, prefixOf } from "./common.mjs";
import { countsChanged, dirtyPages, readDiff } from "./diff.mjs";
import { canonicalHtml, INLINE_CAP, inlinedOverCap, linksOf } from "./html.mjs";
import { latestPath, maskShared, sharedNamed, sharedOf, sharedPath } from "./shared.mjs";
test("keyOf sorts params, drops empty ones and matches a request url", () => {
assert.equal(keyOf("org.tangled.git.getTree", { ref: "main", repo: "did:plc:a", path: "" }), "org.tangled.git.getTree?ref=main&repo=did:plc:a");
const url = new URL("https://k.test/xrpc/org.tangled.git.getBlob?repo=did%3Aplc%3Aa&ref=main&path=src%2Fa%20b.rs");
assert.equal(keyOfUrl(url), "org.tangled.git.getBlob?path=src/a b.rs&ref=main&repo=did:plc:a");
assert.equal(keyOf("org.tangled.feed.countStars", { subject: "at://did:plc:a/org.tangled.repo.manifest/self" }), "org.tangled.feed.countStars?subject=did:plc:a");
});
test("prefixOf links by handle, or by owner did without one", () => {
assert.equal(prefixOf({ owner_handle: "Alice.Test", owner_did: "did:plc:o", name: "Core" }), "/alice.test/Core");
assert.equal(prefixOf({ owner_handle: null, owner_did: "did:web:o.test", name: "x" }), "/did:web:o.test/x");
});
const fetched = (url, body) => `<script type="application/json" data-sveltekit-fetched data-url="${url}">${body}</script>`;
test("canonicalHtml orders kit's inlined reads, whatever order they finished in", () => {
const a = fetched("/xrpc/a", "1");
const b = fetched("/xrpc/b", "2");
assert.equal(canonicalHtml(`<p>x</p>${b}${a}<i></i>`), `<p>x</p>${a}${b}<i></i>`);
assert.equal(canonicalHtml(`<p>x</p>${a}${b}`), `<p>x</p>${a}${b}`);
});
test("linksOf keeps same-repo routes, decoded and without trailing slashes", () => {
const html = '<a href="/o/r/tree/main/src/">x</a><a href="/o/r/blob/main/a%20b.rs#L1">y</a><a href="https://e.test/o/r">z</a><a href="/o/r/history/main/x">h</a>';
const found = linksOf(html, "https://k.test", (p) => (p.includes("history") ? null : p));
assert.deepEqual([...found], ["/o/r/tree/main/src", "/o/r/blob/main/a b.rs"]);
});
const line = (entry) => JSON.stringify(entry) + "\n";
const chain = async (gens) => {
const root = await mkdtemp(path.join(tmpdir(), "svfe-pages-"));
let previous = null;
let hookPath;
for (const [generation, entries] of gens) {
const dir = path.join(root, generation);
await mkdir(dir);
const hook = { generation, previous, paths: { diff: previous ? "xrpc-diff.jsonl" : null } };
if (previous) await writeFile(path.join(dir, "xrpc-diff.jsonl"), entries.map(line).join(""));
hookPath = path.join(dir, "hook.json");
await writeFile(hookPath, JSON.stringify(hook));
previous = generation;
}
return { hookPath, hook: JSON.parse(await (await import("node:fs/promises")).readFile(hookPath, "utf8")) };
};
const page = (...deps) => ({ deps, bytes: 1, links: [] });
test("a diff dirties the pages that read a changed key, unioned back to the page's generation", async () => {
const tree = { op: "changed", nsid: "org.tangled.git.getTree", params: { repo: "did:plc:a", ref: "main", path: "src" }, old: "x", new: "y" };
const blob = { op: "removed", nsid: "org.tangled.git.getBlob", params: { repo: "did:plc:a", ref: "main", path: "b" }, old: "x", new: null };
const { hookPath, hook } = await chain([["g1", []], ["g2", [tree]], ["g3", [blob]]]);
const prior = {
did: "did:plc:a",
pages: {
"/o/r/tree/main/src": page(keyOf(tree.nsid, tree.params)),
"/o/r/blob/main/b": page(keyOf(blob.nsid, blob.params)),
"/o/r": page("org.tangled.git.getTree?ref=main&repo=did:plc:a")
},
left: { "/o/r/blob/main/c": [keyOf(tree.nsid, tree.params)] }
};
const fromG1 = await readDiff(hookPath, hook, "g1");
assert.deepEqual([...dirtyPages(prior, fromG1)].sort(), ["/o/r/blob/main/b", "/o/r/blob/main/c", "/o/r/tree/main/src"]);
const fromG2 = await readDiff(hookPath, hook, "g2");
assert.deepEqual([...dirtyPages(prior, fromG2)], ["/o/r/blob/main/b"]);
assert.equal(await readDiff(hookPath, hook, "gone"), null);
});
test("default, any and reset lines", async () => {
const { hookPath, hook } = await chain([
["g1", []],
[
"g2",
[
{ op: "changed", nsid: "org.tangled.repo.countForks", params: {}, default: true, old: "a", new: "b" },
{ op: "changed", nsid: "org.tangled.search.query", params: { kind: "issue", repoDid: "did:plc:a" }, any: ["limit", "cursor"], old: "a", new: "b" },
{ op: "reset", params: { repo: "did:plc:z" } }
]
]
]);
const diffs = await readDiff(hookPath, hook, "g1");
const prior = {
did: "did:plc:a",
pages: {
"/o/r": page("org.tangled.repo.countForks?subject=did:plc:a"),
"/o/r/issues": page("org.tangled.search.query?cursor=3&kind=issue&limit=50&repoDid=did:plc:a"),
"/o/r/pulls": page("org.tangled.search.query?kind=pull&repoDid=did:plc:a")
}
};
assert.deepEqual([...dirtyPages(prior, diffs)].sort(), ["/o/r", "/o/r/issues"]);
assert.equal(dirtyPages({ ...prior, did: "did:plc:z" }, diffs), "all");
});
test("a comment or a state change dirties the ticket's page and the board, nothing else, through the params the knot marks any", async () => {
const ticket = (rkey) => `at://did:plc:a/org.tangled.track.ticket/${rkey}`;
const { hookPath, hook } = await chain([
["g1", []],
[
"g2",
[
{ op: "changed", nsid: "org.tangled.feed.listComments", params: { order: "desc", subject: ticket(3) }, any: ["limit", "offset"], old: "x", new: "y" },
{ op: "changed", nsid: "org.tangled.track.getTicketView", params: { uri: ticket(5) }, old: "x", new: "y" },
{ op: "changed", nsid: "org.tangled.search.query", params: { kind: "issue", repoDid: "did:plc:a" }, any: ["q", "state", "limit", "offset", "cursor"] },
{ op: "changed", nsid: "org.tangled.git.getTree", params: { ref: "main", repo: "did:plc:a" }, old: "x", new: "y" }
]
]
]);
const diffs = await readDiff(hookPath, hook, "g1");
assert.equal(diffs.global, false);
assert.deepEqual([...diffs.repos], ["did:plc:a"]);
const issue = (rkey) =>
page(
`org.tangled.feed.listComments?limit=100&order=desc&subject=${ticket(rkey)}`,
`org.tangled.track.getTicketView?uri=${ticket(rkey)}`,
"org.tangled.label.listDefinitions?limit=100&subject=did:plc:a"
);
const prior = {
did: "did:plc:a",
pages: {
"/o/r/issues": page("org.tangled.search.query?kind=issue&repoDid=did:plc:a"),
"/o/r/issues/3": issue(3),
"/o/r/issues/4": issue(4),
"/o/r/issues/5": issue(5),
"/o/r/tree/main/src": page("org.tangled.git.getTree?path=src&ref=main&repo=did:plc:a")
}
};
assert.deepEqual([...dirtyPages(prior, diffs)].sort(), ["/o/r/issues", "/o/r/issues/3", "/o/r/issues/5"]);
});
test("a line matches a read through its any params only, and without any only exactly", async () => {
const ticket = "at://did:plc:a/org.tangled.track.ticket/3";
const { hookPath, hook } = await chain([
["g1", []],
[
"g2",
[
{ op: "changed", nsid: "org.tangled.label.listDefinitions", params: { subject: "did:plc:a" }, any: ["limit", "offset", "order"], old: "x", new: "y" },
{ op: "changed", nsid: "org.tangled.temp.review.listPatches", params: { ticket }, old: "x", new: "y" }
]
]
]);
const diffs = await readDiff(hookPath, hook, "g1");
const prior = {
did: "did:plc:a",
pages: {
"/o/r/issues/1": page("org.tangled.label.listDefinitions?limit=100&subject=did:plc:a"),
"/o/r/pulls/3/latest": page(`org.tangled.temp.review.listPatches?limit=500&order=asc&ticket=${ticket}`),
"/o/r/pulls/4/latest": page("org.tangled.label.listDefinitions?limit=100&subject=did:plc:b")
}
};
assert.deepEqual([...dirtyPages(prior, diffs)], ["/o/r/issues/1"]);
});
test("a new ticket changes the repo's counts and no page that doesn't read them", async () => {
const { hookPath, hook } = await chain([
["g1", []],
["g2", [{ op: "changed", nsid: "org.tangled.repo.countIssues", params: { state: "open", subject: "did:plc:a" }, old: "1", new: "2" }]]
]);
const diffs = await readDiff(hookPath, hook, "g1");
const prior = {
did: "did:plc:a",
pages: { "/o/r": page("org.tangled.git.getTree?ref=main&repo=did:plc:a") },
counts: ["org.tangled.repo.countIssues?state=open&subject=did:plc:a", "org.tangled.repo.countPulls?status=open&subject=did:plc:a"]
};
assert.deepEqual([...dirtyPages(prior, diffs)], []);
assert.equal(countsChanged(prior, diffs), true);
assert.equal(countsChanged({ ...prior, counts: undefined }, diffs), false);
});
test("a shared value is named by its content", () => {
const a = sharedOf({ ref: "main", files: [] });
assert.equal(a.hash, sharedOf({ ref: "main", files: [] }).hash);
assert.notEqual(a.hash, sharedOf({ ref: "dev", files: [] }).hash);
assert.match(sharedPath(a.hash), /^_shared\/[0-9a-f]{16}\.json$/);
assert.equal(JSON.parse(a.body).ref, "main");
});
test("maskShared hides which copy of a shared file a page names", () => {
const page = (hash) => `<div class="sb" data-sidebar-src="/o/r/${sharedPath(hash)}">`;
assert.equal(maskShared(page("0123456789abcdef")), maskShared(page("fedcba9876543210")));
assert.notEqual(maskShared(page("0123456789abcdef")), maskShared('<div class="sb">'));
});
test("sharedNamed collects what the kept pages name", () => {
const pages = { "/o/r": { shared: ["a", "b"] }, "/o/r/tags": { shared: ["b"] }, "/o/r/pulls": {} };
assert.deepEqual([...sharedNamed(pages)].sort(), ["a", "b"]);
assert.deepEqual([...sharedNamed(pages, { "sidebar/main": "c" })].sort(), ["a", "b", "c"]);
});
test("a stable name is a file under the repo, with a ref's slashes as folders", () => {
assert.equal(latestPath("sidebar/main"), "_latest/sidebar/main.json");
assert.equal(latestPath("sidebar/feat/x"), "_latest/sidebar/feat/x.json");
});
test("a later page of a list is stored next to page 1, under a name no ref can have", () => {
assert.equal(fileOf("/o/r", "/o/r"), "index.html");
assert.equal(fileOf("/o/r", "/o/r/commits/main"), "commits/main.html");
assert.equal(fileOf("/o/r", "/o/r/commits/main?page=3"), "commits/main~page-3.html");
assert.equal(fileOf("/o/r", "/o/r/tags?page=2"), "tags~page-2.html");
});
test("linksOf keeps a later page's ?page=N and drops other queries", () => {
const paged = (route) => (/[?]page=/.test(route) ? (route.includes("/tags") ? route : null) : route);
const html = '<a href="/o/r/tags?page=2">older</a><a href="/o/r/tags?page=1">newer</a><a href="/o/r/blob/main/a?view=raw">raw</a><a href="/o/r/issues?page=2">more</a>';
assert.deepEqual([...linksOf(html, "https://k.test", paged)].sort(), ["/o/r/blob/main/a", "/o/r/issues", "/o/r/tags", "/o/r/tags?page=2"]);
});
test("a build's chunks stay until no served page was rendered by it", async () => {
const dir = await mkdtemp(path.join(tmpdir(), "svfe-pages-client-"));
const build = async (name, files) => {
for (const file of files) {
await mkdir(path.dirname(path.join(dir, name, "_app/immutable", file)), { recursive: true });
await writeFile(path.join(dir, name, "_app/immutable", file), name);
}
return path.join(dir, name);
};
const out = path.join(dir, "out");
const kept = (file) => access(path.join(out, "www/_app/immutable", file)).then(() => true, () => false);
assert.equal(await keepClient(await build("a", ["entry/app.A.js", "chunks/x.js"]), out, "a"), 2);
assert.equal(await keepClient(await build("b", ["entry/app.B.js", "chunks/x.js"]), out, "b"), 1);
assert.equal(await collectClients(out, new Set(["a", "b"])), 0);
assert.equal(await collectClients(out, new Set(["b"])), 1);
assert.deepEqual([await kept("entry/app.A.js"), await kept("entry/app.B.js"), await kept("chunks/x.js")], [false, true, true]);
});
test("a page's inlined responses past the cap are named with their size", () => {
const inlined = (nsid, body) => `<script type="application/json" data-sveltekit-fetched data-url="/xrpc/${nsid}?repo=did%3Aplc%3Ax">${body}</script>`;
const big = "x".repeat(INLINE_CAP + 1);
const html = inlined("org.tangled.git.getTree", big) + inlined("org.tangled.git.getBlob", "small") + `<script type="application/json" data-diff>${big}</script>`;
assert.deepEqual(inlinedOverCap(html), [{ nsid: "org.tangled.git.getTree", bytes: INLINE_CAP + 1 }]);
});