mirror of
https://seed.radicle.dev/rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5.git
synced 2026-11-14 11:00:34 +08:00
Repos need not carry a `xyz.radicle.project` payload; the default branch can come from `xyz.radicle.crefs` alone. Such repos got no counts and no default branch, and failed schema validation in explorer, which broke the parse of a whole listing. httpd: - Add top-level `cobs` with issue, patch and bucketed release counts - Add top-level `defaultBranch`, qualified like the crefs payload - Omit a count the node cannot determine instead of reporting zero - Drop `head`: the tip is `refs[defaultBranch]` - Keep counts in the project payload for older clients until a major release - Keep the project payload when a COB read fails - Keep `meta.releases` a single count: delegate + other - Log and skip canonical ref rules and refs that fail to read, so one bad ref doesn't drop the default branch tip explorer: - Model `cobs` and `defaultBranch`, adapting older nodes to one shape - Resolve the default branch from the crefs HEAD on older nodes - Collect the shims for older httpd in http-client/lib/compat.ts, each naming the httpd release that the client can drop it with - Add `defaultBranchTip`, replacing `head`, with a fallback to the legacy `meta.head` - Make the project payload and its legacy meta optional - Fall back to the RID for the repo name, and show a placeholder for a missing name in repo cards and the header - Hide a counter instead of reading a missing count as zero - Fall back to a full-page check for "More" without counts - Add a `repo.sourceUnavailable` route for repos without a default branch or tip - Browse explicit revisions and offer the branch selector without a default branch - List these repos in repo grids and on the landing page - Say the node is older or built without releases - Scope release pages in the client, so views never call the shims - Read OG worker repos through the same compat shims - Share the repo name and default branch helpers with the OG worker CI: - Add check:open-graph, a wrangler deploy dry run - Run it in GitHub CI, Buildkite and check:ci - Install explorer's dependencies first: the worker resolves compat.ts imports, such as zod, from there Tests: - Test that a build without releases omits the count - Test that one failing rule keeps the others - Test the repo shape across httpd versions and the release shims - Test `hasMorePages` and `repoName` - Test repos without a project payload end to end
317 lines
8.6 KiB
TypeScript
317 lines
8.6 KiB
TypeScript
import type { Release } from "@http-client";
|
|
|
|
import { describe, expect, test } from "vitest";
|
|
import {
|
|
legacyHasOtherReleases,
|
|
legacyReleasesQuery,
|
|
legacyScopeReleases,
|
|
} from "@http-client/lib/compat";
|
|
import { repoSchema } from "@http-client/lib/repo";
|
|
|
|
const delegate = { id: "did:key:z6MkDelegate" };
|
|
const other = { id: "did:key:z6MkOther" };
|
|
|
|
const issues = { open: 1, closed: 2 };
|
|
const patches = { open: 3, draft: 0, archived: 0, merged: 4 };
|
|
const buckets = {
|
|
delegate: 2,
|
|
delegateRedacted: 1,
|
|
other: 3,
|
|
otherRedacted: 0,
|
|
};
|
|
|
|
const project = {
|
|
data: { name: "heartwood", description: "", defaultBranch: "master" },
|
|
};
|
|
|
|
function repo(fields: Record<string, unknown>) {
|
|
return repoSchema.parse({
|
|
rid: "rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5",
|
|
payloads: {},
|
|
delegates: [delegate],
|
|
threshold: 1,
|
|
visibility: { type: "public" },
|
|
seeding: 1,
|
|
...fields,
|
|
});
|
|
}
|
|
|
|
function release(creator: { id: string }): Release {
|
|
return { creator } as Release;
|
|
}
|
|
|
|
describe("repo shape", () => {
|
|
test("reads the top-level fields of current httpd", () => {
|
|
const parsed = repo({
|
|
payloads: {
|
|
"xyz.radicle.project": {
|
|
...project,
|
|
meta: { head: "abc", issues, patches, releases: 5 },
|
|
},
|
|
},
|
|
cobs: { issues, patches, releases: buckets },
|
|
defaultBranch: "refs/heads/main",
|
|
});
|
|
|
|
expect(parsed.cobs).toEqual({ issues, patches, releases: buckets });
|
|
expect(parsed.defaultBranch).toBe("refs/heads/main");
|
|
expect(parsed.releaseCount).toBe(buckets.delegate);
|
|
});
|
|
|
|
test("adapts the project payload of httpd 0.28.0 and 0.29.0", () => {
|
|
const parsed = repo({
|
|
payloads: {
|
|
"xyz.radicle.project": {
|
|
...project,
|
|
meta: { head: "abc", issues, patches, releases: 7 },
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(parsed.cobs).toEqual({ issues, patches, releases: undefined });
|
|
expect(parsed.defaultBranch).toBe("refs/heads/master");
|
|
expect(parsed.releaseCount).toBe(7);
|
|
});
|
|
|
|
test("has no release count on httpd 0.27.0", () => {
|
|
const parsed = repo({
|
|
payloads: {
|
|
"xyz.radicle.project": {
|
|
...project,
|
|
meta: { head: "abc", issues, patches },
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(parsed.cobs?.issues).toEqual(issues);
|
|
expect(parsed.releaseCount).toBeUndefined();
|
|
});
|
|
|
|
test("has no release count on a build without releases", () => {
|
|
const parsed = repo({ cobs: { issues, patches } });
|
|
|
|
expect(parsed.releaseCount).toBeUndefined();
|
|
});
|
|
|
|
test("takes no default branch from a project payload without meta", () => {
|
|
const parsed = repo({
|
|
payloads: { "xyz.radicle.project": project },
|
|
cobs: { patches },
|
|
});
|
|
|
|
expect(parsed.cobs).toEqual({ patches });
|
|
expect(parsed.defaultBranch).toBeUndefined();
|
|
});
|
|
|
|
test("keeps a newer node's missing default branch missing", () => {
|
|
const parsed = repo({
|
|
payloads: {
|
|
"xyz.radicle.project": {
|
|
...project,
|
|
meta: { head: "abc", issues, patches, releases: 5 },
|
|
},
|
|
},
|
|
cobs: { issues, patches, releases: buckets },
|
|
});
|
|
|
|
expect(parsed.defaultBranch).toBeUndefined();
|
|
expect(parsed.releaseCount).toBe(buckets.delegate);
|
|
});
|
|
|
|
test.each([
|
|
["a direct HEAD", { HEAD: "refs/heads/main" }, "refs/heads/main"],
|
|
[
|
|
"a chained HEAD",
|
|
{ HEAD: "MAIN", MAIN: "refs/heads/trunk" },
|
|
"refs/heads/trunk",
|
|
],
|
|
["a HEAD at a tag", { HEAD: "refs/tags/v1" }, undefined],
|
|
["a dangling HEAD", { HEAD: "MAIN" }, undefined],
|
|
["a cycle", { HEAD: "A", A: "B", B: "A" }, undefined],
|
|
])(
|
|
"resolves the crefs default branch of older httpd from %s",
|
|
(_, symbolic, expected) => {
|
|
const parsed = repo({
|
|
payloads: {
|
|
"xyz.radicle.crefs": { data: { rules: {}, symbolic } },
|
|
},
|
|
});
|
|
|
|
expect(parsed.defaultBranch).toBe(expected);
|
|
},
|
|
);
|
|
|
|
test("prefers the crefs HEAD of older httpd to the project payload", () => {
|
|
const parsed = repo({
|
|
payloads: {
|
|
"xyz.radicle.project": {
|
|
...project,
|
|
meta: { head: "abc", issues, patches },
|
|
},
|
|
"xyz.radicle.crefs": {
|
|
data: { symbolic: { HEAD: "refs/heads/main" } },
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(parsed.defaultBranch).toBe("refs/heads/main");
|
|
});
|
|
|
|
test("falls back to the project payload without a crefs HEAD", () => {
|
|
const parsed = repo({
|
|
payloads: {
|
|
"xyz.radicle.project": {
|
|
...project,
|
|
meta: { head: "abc", issues, patches },
|
|
},
|
|
"xyz.radicle.crefs": { data: { rules: {} } },
|
|
},
|
|
});
|
|
|
|
expect(parsed.defaultBranch).toBe("refs/heads/master");
|
|
});
|
|
|
|
test("takes the default branch tip from meta without canonical refs", () => {
|
|
const parsed = repo({
|
|
payloads: {
|
|
"xyz.radicle.project": {
|
|
...project,
|
|
meta: { head: "abc", issues, patches },
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(parsed.refs).toEqual({
|
|
tags: {},
|
|
refs: { "refs/heads/master": "abc" },
|
|
});
|
|
});
|
|
|
|
test("adds the default branch tip from meta to canonical refs", () => {
|
|
const parsed = repo({
|
|
payloads: {
|
|
"xyz.radicle.project": {
|
|
...project,
|
|
meta: { head: "abc", issues, patches },
|
|
},
|
|
},
|
|
refs: { tags: {}, refs: { "refs/heads/dev": "def" } },
|
|
});
|
|
|
|
expect(parsed.refs?.refs).toEqual({
|
|
"refs/heads/dev": "def",
|
|
"refs/heads/master": "abc",
|
|
});
|
|
});
|
|
|
|
test("prefers the canonical default branch tip to meta", () => {
|
|
const parsed = repo({
|
|
payloads: {
|
|
"xyz.radicle.project": {
|
|
...project,
|
|
meta: { head: "abc", issues, patches },
|
|
},
|
|
},
|
|
refs: { tags: {}, refs: { "refs/heads/master": "def" } },
|
|
});
|
|
|
|
expect(parsed.refs?.refs["refs/heads/master"]).toBe("def");
|
|
});
|
|
|
|
test("takes no tip from meta for a crefs HEAD on another branch", () => {
|
|
const parsed = repo({
|
|
payloads: {
|
|
"xyz.radicle.project": {
|
|
...project,
|
|
meta: { head: "abc", issues, patches },
|
|
},
|
|
"xyz.radicle.crefs": {
|
|
data: { symbolic: { HEAD: "refs/heads/main" } },
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(parsed.refs).toBeUndefined();
|
|
});
|
|
|
|
test("takes no tip from meta on current httpd", () => {
|
|
const parsed = repo({
|
|
payloads: {
|
|
"xyz.radicle.project": {
|
|
...project,
|
|
meta: { head: "abc", issues, patches },
|
|
},
|
|
},
|
|
cobs: { issues, patches },
|
|
defaultBranch: "refs/heads/master",
|
|
refs: { tags: {}, refs: {} },
|
|
});
|
|
|
|
expect(parsed.refs?.refs).toEqual({});
|
|
});
|
|
|
|
test("parses a repo with a malformed crefs payload", () => {
|
|
const parsed = repo({
|
|
payloads: { "xyz.radicle.crefs": { data: { symbolic: 1 } } },
|
|
});
|
|
|
|
expect(parsed.defaultBranch).toBeUndefined();
|
|
});
|
|
|
|
test("parses a repo without a project payload", () => {
|
|
const parsed = repo({});
|
|
|
|
expect(parsed.cobs).toBeUndefined();
|
|
expect(parsed.defaultBranch).toBeUndefined();
|
|
expect(parsed.releaseCount).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe("legacyReleasesQuery", () => {
|
|
test("asks older nodes for every author in the untrusted scope", () => {
|
|
expect(legacyReleasesQuery("untrusted")).toEqual({ allAuthors: true });
|
|
});
|
|
|
|
test.each(["trusted" as const, undefined])("adds nothing for %s", scope => {
|
|
expect(legacyReleasesQuery(scope)).toEqual({});
|
|
});
|
|
});
|
|
|
|
describe("legacyScopeReleases", () => {
|
|
const releases = [release(delegate), release(other)];
|
|
|
|
test("keeps every release in the trusted scope", () => {
|
|
expect(legacyScopeReleases(releases, [delegate], "trusted")).toEqual(
|
|
releases,
|
|
);
|
|
});
|
|
|
|
test("drops delegate releases from the untrusted scope", () => {
|
|
expect(legacyScopeReleases(releases, [delegate], "untrusted")).toEqual([
|
|
release(other),
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe("legacyHasOtherReleases", () => {
|
|
test("finds releases by others", async () => {
|
|
const page = async () => [release(delegate), release(other)];
|
|
expect(await legacyHasOtherReleases(page, [delegate], 10)).toBe(true);
|
|
});
|
|
|
|
test("finds none when only delegates released", async () => {
|
|
const page = async () => [release(delegate)];
|
|
expect(await legacyHasOtherReleases(page, [delegate], 10)).toBe(false);
|
|
});
|
|
|
|
test("assumes others after a full page", async () => {
|
|
const page = async () => [release(delegate)];
|
|
expect(await legacyHasOtherReleases(page, [delegate], 1)).toBe(true);
|
|
});
|
|
|
|
test("assumes others when the fetch fails", async () => {
|
|
const page = () => Promise.reject(new Error("offline"));
|
|
expect(await legacyHasOtherReleases(page, [delegate], 10)).toBe(true);
|
|
});
|
|
});
|