Files
clawhub/convex/httpApiV1.catalogFeed.test.ts
Gio Della-LiberaandPatrick Erichsen 5a3b050751 Add gated Claw hosted feed and lifecycle proof (#3092)
* feat(claws): publish hosted feed with OpenClaw proof

* test(claws): prove package-local profile feed flow

* fix(claws): encode scoped package artifact routes

* fix(claws): enforce feed rollback and binding

* test(claws): pin hosted OpenClaw contract proof

* test(claws): add Convex feed runtime smoke

* chore(schema): refresh experimental feed declarations

---------

Co-authored-by: Patrick Erichsen <patrick.a.erichsen@gmail.com>
2026-07-24 19:29:12 -05:00

128 lines
4.2 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { internal } from "./_generated/api";
import { catalogClawsFeedV1Handler, catalogFeedV1Handler } from "./httpApiV1/catalogFeedV1";
type QueryCtx = {
runQuery: ReturnType<typeof vi.fn>;
};
const publication = {
feedId: "clawhub-official",
sequence: 4,
generatedAt: "2026-06-23T00:00:00.000Z",
expiresAt: "2026-06-30T00:00:00.000Z",
payload: '{"schemaVersion":1,"id":"clawhub-official","entries":[]}',
payloadSha256: "abc123",
publishedAt: Date.parse("2026-06-23T00:00:00.000Z"),
};
describe("catalogFeedV1Handler", () => {
let ctx: QueryCtx;
beforeEach(() => {
ctx = { runQuery: vi.fn().mockResolvedValue(publication) };
});
afterEach(() => {
vi.unstubAllEnvs();
});
it("serves the exact published payload with edge cache validators", async () => {
const response = await catalogFeedV1Handler(
ctx as never,
new Request("https://clawhub.ai/feed"),
);
expect(response.status).toBe(200);
expect(await response.text()).toBe(publication.payload);
expect(response.headers.get("etag")).toBe('"sha256:abc123"');
expect(response.headers.get("last-modified")).toBe("Tue, 23 Jun 2026 00:00:00 GMT");
expect(response.headers.get("cache-control")).toContain("s-maxage=300");
expect(response.headers.get("surrogate-control")).toContain("stale-while-revalidate=86400");
expect(ctx.runQuery).toHaveBeenCalledWith(internal.catalogFeed.getLatestPublication, {
feedId: "clawhub-official",
});
});
it("returns 304 for a matching validator", async () => {
const response = await catalogFeedV1Handler(
ctx as never,
new Request("https://clawhub.ai/feed", {
headers: { "If-None-Match": '"sha256:abc123"' },
}),
);
expect(response.status).toBe(304);
expect(await response.text()).toBe("");
});
it("accepts weak etags and last-modified validators", async () => {
const weakEtagResponse = await catalogFeedV1Handler(
ctx as never,
new Request("https://clawhub.ai/feed", {
headers: { "If-None-Match": 'W/"sha256:abc123"' },
}),
);
expect(weakEtagResponse.status).toBe(304);
const lastModifiedResponse = await catalogFeedV1Handler(
ctx as never,
new Request("https://clawhub.ai/feed", {
headers: { "If-Modified-Since": "Tue, 23 Jun 2026 00:00:00 GMT" },
}),
);
expect(lastModifiedResponse.status).toBe(304);
});
it("gives etag precedence over last-modified", async () => {
const response = await catalogFeedV1Handler(
ctx as never,
new Request("https://clawhub.ai/feed", {
headers: {
"If-None-Match": '"sha256:different"',
"If-Modified-Since": "Tue, 23 Jun 2026 00:00:00 GMT",
},
}),
);
expect(response.status).toBe(200);
});
it("does not cache an unpublished feed", async () => {
ctx.runQuery.mockResolvedValue(null);
const response = await catalogFeedV1Handler(
ctx as never,
new Request("https://clawhub.ai/feed"),
);
expect(response.status).toBe(503);
expect(response.headers.get("cache-control")).toBe("no-store");
});
it("hides the Claws feed while the experiment is disabled", async () => {
const response = await catalogClawsFeedV1Handler(
ctx as never,
new Request("https://clawhub.ai/api/v1/feeds/claws"),
);
expect(response.status).toBe(404);
expect(response.headers.get("cache-control")).toBe("no-store");
expect(ctx.runQuery).not.toHaveBeenCalled();
});
it("serves the Claws publication while the experiment is enabled", async () => {
vi.stubEnv("CLAWHUB_EXPERIMENTAL_CLAWS", "1");
const response = await catalogClawsFeedV1Handler(
ctx as never,
new Request("https://clawhub.ai/api/v1/feeds/claws"),
);
expect(response.status).toBe(200);
expect(response.headers.get("cache-control")).toBe("no-store");
expect(response.headers.get("surrogate-control")).toBeNull();
expect(response.headers.get("etag")).toBe('"sha256:abc123"');
expect(ctx.runQuery).toHaveBeenCalledWith(internal.catalogFeed.getLatestPublication, {
feedId: "clawhub-official-claws",
});
});
});