fix(cli): catalog says the registry could not be read instead of listing nothing (#7015)

This commit is contained in:
Miguel Ángel
2026-11-02 14:47:04 +00:00
committed by GitHub
parent 346f0e49fa
commit 5a07ea2e5f
4 changed files with 55 additions and 12 deletions
+20 -4
View File
@@ -73,6 +73,7 @@ describe("countUnindexed", () => {
const state = vi.hoisted(() => ({
registry: [] as Array<{ name: string; type: string; tags?: string[] }>,
registryUnreadable: "",
artifactRevision: "revision-current",
cachedVectorRevision: "revision-current",
vectorFetches: 0,
@@ -108,10 +109,10 @@ vi.mock("../registry/resolver.js", () => ({
}));
vi.mock("../registry/remote.js", () => ({
fetchRegistryManifest: async () => ({
items: state.registry,
catalogArtifact: { revision: state.artifactRevision },
}),
readRegistryManifest: async () => {
if (state.registryUnreadable) throw new Error(state.registryUnreadable);
return { items: state.registry, catalogArtifact: { revision: state.artifactRevision } };
},
}));
// The terminal and CI switches below decide attendance; the agent running the suite must not.
@@ -276,6 +277,7 @@ async function runForExit(
}
beforeEach(() => {
state.registryUnreadable = "";
state.modelStatus = "ready";
state.artifactRevision = "revision-current";
state.cachedVectorRevision = "revision-current";
@@ -301,6 +303,20 @@ beforeEach(() => {
];
});
describe("catalog when the registry cannot be read", () => {
it("exits non-zero with the reason instead of printing an empty list", async () => {
state.registryUnreadable =
"The registry could not be read: fetch failed (getaddrinfo ENOTFOUND raw.githubusercontent.com)";
const { exitCode, err } = await runForExit({ json: true });
expect(exitCode).toBe(1);
expect(err).toContain("The registry could not be read: fetch failed");
expect(err).toContain("raw.githubusercontent.com");
expect(err).not.toContain("[]");
});
});
describe("catalog --json meaning search", () => {
it("reports the registry moves meaning search cannot see", async () => {
// Published after the user's artifact was fetched: in the registry, absent
+7 -4
View File
@@ -14,7 +14,7 @@ import * as clack from "../ui/prompts.js";
import { realpath, type ItemType, type RegistryItem } from "@hyperframes/core";
import { c } from "../ui/colors.js";
import { loadAllItems } from "../registry/resolver.js";
import { fetchRegistryManifest } from "../registry/remote.js";
import { readRegistryManifest } from "../registry/remote.js";
import { loadProjectConfig, DEFAULT_PROJECT_CONFIG } from "../utils/projectConfig.js";
import { relative, resolve } from "node:path";
import { finishCommand } from "../utils/commandResult.js";
@@ -232,9 +232,12 @@ export default defineCommand({
// Asked for the whole manifest on purpose: its item list defines coverage,
// and its artifact revision is the one owner of vector freshness.
const manifest = await fetchRegistryManifest(config.registry);
const entries = manifest?.items ?? [];
const artifactRevision = manifest?.catalogArtifact?.revision;
const manifest = await readRegistryManifest(config.registry).catch((error: unknown) => {
console.error(error instanceof Error ? error.message : String(error));
return finishCommand(1);
});
const entries = manifest.items;
const artifactRevision = manifest.catalogArtifact?.revision;
const catalog = entries.filter((e) => e.type !== "hyperframes:example");
const registryNames = new Set(catalog.map((e) => e.name));
const filtered = typeFilter ? catalog.filter((e) => e.type === typeFilter) : catalog;
+16
View File
@@ -18,6 +18,7 @@ const {
fetchItemFile,
fetchItemManifest,
fetchRegistryManifest,
readRegistryManifest,
DEFAULT_REGISTRY_URL,
} = await import("./remote.js");
@@ -76,6 +77,21 @@ afterAll(() => {
rmSync(scratchHome, { recursive: true, force: true });
});
describe("readRegistryManifest", () => {
it("throws why when the registry host cannot be reached and nothing is cached", async () => {
vi.spyOn(globalThis, "fetch").mockRejectedValue(
Object.assign(new TypeError("fetch failed"), {
cause: Object.assign(new Error("getaddrinfo ENOTFOUND raw.githubusercontent.com"), {
code: "ENOTFOUND",
}),
}),
);
await expect(readRegistryManifest(DEFAULT_REGISTRY_URL)).rejects.toThrow(
"The registry could not be read: fetch failed (getaddrinfo ENOTFOUND raw.githubusercontent.com)",
);
});
});
describe("fetchRegistryManifest", () => {
it("rejects poisoned stale cache data instead of returning it on network failure", async () => {
vi.spyOn(globalThis, "fetch").mockResolvedValueOnce(ok(MANIFEST));
+12 -4
View File
@@ -123,10 +123,17 @@ async function fetchJson<T>(url: string, validate: (value: unknown) => value is
* because "check for something newer" and "rather have nothing than this" are
* different requests and only the first one is ever made.
*/
export async function fetchRegistryManifest(
export const fetchRegistryManifest = (
baseUrl: string = DEFAULT_REGISTRY_URL,
options?: { skipCache?: boolean },
): Promise<RegistryManifest | undefined> {
): Promise<RegistryManifest | undefined> =>
readRegistryManifest(baseUrl, options).catch(() => undefined);
/** The manifest as fetchRegistryManifest gives it, but unreachable and never cached throws why, never undefined. */
export async function readRegistryManifest(
baseUrl: string = DEFAULT_REGISTRY_URL,
options?: { skipCache?: boolean },
): Promise<RegistryManifest> {
const url = registryPathUrl(baseUrl, "registry.json");
const cacheFile = cachePath(baseUrl, "registry");
const cached = readCacheEntry(cacheFile, validRegistryManifest);
@@ -136,8 +143,9 @@ export async function fetchRegistryManifest(
const manifest = await fetchJson(url, validRegistryManifest);
writeCache(cacheFile, manifest);
return manifest;
} catch {
return cached?.data;
} catch (err) {
if (cached) return cached.data;
throw new Error(`The registry could not be read: ${describeCauseChain(err)}`, { cause: err });
}
}