diff --git a/apps/site/src/pages/model.tsx b/apps/site/src/pages/model.tsx
index 66fd6e9..9c2083e 100644
--- a/apps/site/src/pages/model.tsx
+++ b/apps/site/src/pages/model.tsx
@@ -24,13 +24,14 @@ function decodeNameParam(raw: string | undefined): string | null {
}
}
-/** Latest CID: the version whose name matches the manifest, else the last listed. */
-function latestVersion(detail: NameDetail): NameVersion {
+/**
+ * Latest CID comes from the indexer's on-chain `contenthash` record, never from
+ * `manifest.versions[last].cid`: a manifest cannot contain its own CID (issue #30).
+ */
+function latestVersion(detail: NameDetail): NameVersion | null {
const match = detail.versions.find((v) => v.name === detail.manifest.name);
if (match !== undefined) return match;
- const last = detail.versions[detail.versions.length - 1];
- if (last !== undefined) return last;
- return detail.manifest.versions[0];
+ return detail.versions[detail.versions.length - 1] ?? null;
}
function ModelHeader({ detail }: { detail: NameDetail }) {
@@ -96,6 +97,14 @@ export function ModelPage() {
if (data === null) return ;
const latest = latestVersion(data);
+ if (latest === null) {
+ return (
+
+ );
+ }
return (
diff --git a/apps/site/test/fixtures/name-qwen.json b/apps/site/test/fixtures/name-qwen.json
index ccfec22..f6c2b22 100644
--- a/apps/site/test/fixtures/name-qwen.json
+++ b/apps/site/test/fixtures/name-qwen.json
@@ -132,7 +132,7 @@
{
"version": "1.0.0",
"name": "v1-0-0.qwen--qwen2-5-7b-instruct.mirrors.enspack.eth",
- "cid": "bafkreis5hm6e3jayscwqnqudbrnems5iuqts36dojc5hgq5yuvxl4vg42c",
+ "cid": "bafkreiduwtplaceholderdraftbytescidnotresolvable2a3b4c5d6e7f8g",
"createdAt": "2026-09-14T00:00:00Z"
}
]
diff --git a/apps/site/test/model.test.tsx b/apps/site/test/model.test.tsx
index 3f95ff8..507bd26 100644
--- a/apps/site/test/model.test.tsx
+++ b/apps/site/test/model.test.tsx
@@ -1,8 +1,9 @@
import { cleanup, fireEvent, screen, waitFor, within } from "@testing-library/react";
import { afterEach, describe, expect, it } from "vitest";
-import { ensAppUrl } from "../src/lib/format.js";
+import { FixtureIndexClient } from "../src/lib/client.js";
+import { ensAppUrl, ipfsUrl } from "../src/lib/format.js";
import { ModelPage } from "../src/pages/model.js";
-import { renderAt } from "./helpers.js";
+import { demoFixtures, renderAt } from "./helpers.js";
afterEach(() => {
cleanup();
@@ -95,6 +96,41 @@ describe("ModelPage", () => {
});
});
+ it("takes the latest CID from the indexer, never from manifest.versions[last] (issue #30)", async () => {
+ const detail = demoFixtures.details[QWEN];
+ if (detail === undefined) throw new Error("missing qwen fixture");
+ const indexerCid = detail.versions[0]?.cid;
+ const selfCid = detail.manifest.versions[detail.manifest.versions.length - 1]?.cid;
+ if (indexerCid === undefined || selfCid === undefined) throw new Error("fixture shape");
+ expect(selfCid).not.toBe(indexerCid);
+
+ renderName(QWEN);
+ await screen.findByRole("heading", { name: "Qwen2.5-7B-Instruct" });
+
+ expect(document.querySelector(`a[href="${ipfsUrl(indexerCid)}"]`)).not.toBeNull();
+ expect(document.querySelector(`a[href="${ipfsUrl(selfCid)}"]`)).toBeNull();
+ expect(screen.getByText(/"lockfileVersion": 1/)).toHaveTextContent(indexerCid);
+ expect(screen.getByText(/"lockfileVersion": 1/)).not.toHaveTextContent(selfCid);
+ // The placeholder may only appear inside the raw manifest JSON viewer.
+ for (const el of screen.getAllByText(new RegExp(selfCid))) {
+ expect(el.closest("details")).not.toBeNull();
+ }
+ });
+
+ it("shows an error when the indexer returns a name without versions", async () => {
+ const detail = demoFixtures.details[QWEN];
+ if (detail === undefined) throw new Error("missing qwen fixture");
+ const client = new FixtureIndexClient({
+ ...demoFixtures,
+ details: { [QWEN]: { ...detail, versions: [] } },
+ });
+ renderAt(`/name/${encodeURIComponent(QWEN)}`, , {
+ routePattern: "/name/:name",
+ client,
+ });
+ expect(await screen.findByRole("alert")).toHaveTextContent("Could not load name");
+ });
+
it("marks Jeff's latest version and links the older version name", async () => {
renderName(JEFF);
expect(await screen.findByRole("heading", { name: "tiny-random" })).toBeInTheDocument();