
























1-import { readFileSync } from "node:fs";
1+import { mkdtempSync, readFileSync, rmSync } from "node:fs";
2+import os from "node:os";
3+import path from "node:path";
24import { describe, expect, it, vi } from "vitest";
35import officialExternalPluginCatalog from "../../scripts/lib/official-external-plugin-catalog.json" with { type: "json" };
6+import { closeOpenClawStateDatabaseForTest } from "../state/openclaw-state-db.js";
7+import { createSqliteHostedOfficialExternalPluginCatalogSnapshotStore } from "./official-external-plugin-catalog-snapshot-store.js";
48import {
59type OfficialExternalPluginCatalogEntry,
610DEFAULT_OFFICIAL_EXTERNAL_PLUGIN_CATALOG_FEED_URL,
@@ -245,6 +249,7 @@ describe("official external plugin catalog", () => {
245249 fetchImpl,
246250ifNoneMatch: '"old"',
247251ifModifiedSince: "Mon, 22 Jun 2026 00:00:00 GMT",
252+snapshotStore: null,
248253});
249254250255expect(result.source).toBe("hosted");
@@ -315,6 +320,7 @@ describe("official external plugin catalog", () => {
315320316321it("falls back to the bundled catalog when hosted feed validation fails", async () => {
317322const result = await loadHostedOfficialExternalPluginCatalogEntries({
323+snapshotStore: null,
318324fetchImpl: vi.fn(
319325async () =>
320326new Response(JSON.stringify({ schemaVersion: 1, id: " ", entries: [] }), {
@@ -333,6 +339,7 @@ describe("official external plugin catalog", () => {
333339334340it("falls back to the bundled catalog on HTTP 304 until a snapshot cache exists", async () => {
335341const result = await loadHostedOfficialExternalPluginCatalogEntries({
342+snapshotStore: null,
336343fetchImpl: vi.fn(
337344async () =>
338345new Response(null, {
@@ -395,6 +402,108 @@ describe("official external plugin catalog", () => {
395402expect(snapshot?.metadata.checksum).toMatch(/^sha256:[0-9a-f]{64}$/);
396403});
397404405+it("persists hosted feed snapshots in OpenClaw state for HTTP 304 reuse", async () => {
406+const stateDir = mkdtempSync(path.join(os.tmpdir(), "openclaw-hosted-catalog-"));
407+try {
408+const body = JSON.stringify({
409+schemaVersion: 1,
410+id: "openclaw-official-external-plugins",
411+generatedAt: "2026-06-22T00:00:00.000Z",
412+sequence: 7,
413+entries: [
414+{
415+name: "@openclaw/sqlite-snapshot-proof",
416+kind: "plugin",
417+openclaw: { plugin: { id: "sqlite-snapshot-proof" } },
418+},
419+],
420+});
421+422+const seeded = await loadHostedOfficialExternalPluginCatalogEntries({
423+ stateDir,
424+now: () => new Date("2026-06-22T02:03:04.000Z"),
425+fetchImpl: vi.fn(
426+async () =>
427+new Response(body, {
428+status: 200,
429+headers: {
430+etag: '"sqlite"',
431+"last-modified": "Mon, 22 Jun 2026 02:00:00 GMT",
432+},
433+}),
434+),
435+});
436+if (seeded.source !== "hosted") {
437+throw new Error("expected seeded hosted feed");
438+}
439+closeOpenClawStateDatabaseForTest();
440+441+const result = await loadHostedOfficialExternalPluginCatalogEntries({
442+ stateDir,
443+fetchImpl: vi.fn(async () => new Response(null, { status: 304 })),
444+});
445+446+expect(result.source).toBe("hosted-snapshot");
447+expect(result.entries.map((entry) => entry.name)).toEqual([
448+"@openclaw/sqlite-snapshot-proof",
449+]);
450+if (result.source === "hosted-snapshot") {
451+expect(result.snapshot.savedAt).toBe("2026-06-22T02:03:04.000Z");
452+expect(result.metadata.checksum).toBe(seeded.metadata.checksum);
453+}
454+} finally {
455+closeOpenClawStateDatabaseForTest();
456+rmSync(stateDir, { recursive: true, force: true });
457+}
458+});
459+460+it("reads and updates hosted catalog snapshots in the SQLite store", async () => {
461+const stateDir = mkdtempSync(path.join(os.tmpdir(), "openclaw-hosted-store-"));
462+try {
463+const store = createSqliteHostedOfficialExternalPluginCatalogSnapshotStore({ stateDir });
464+const url = "https://register.openclaw.ai/official-external-plugin-catalog.json";
465+466+const firstBody = JSON.stringify({ entries: [] });
467+const secondBody = JSON.stringify({ entries: [{}] });
468+469+await expect(store.read(url)).resolves.toBeNull();
470+await store.write({
471+body: firstBody,
472+metadata: {
473+ url,
474+status: 200,
475+etag: '"first"',
476+checksum: "sha256:first",
477+},
478+savedAt: "2026-06-22T02:03:04.000Z",
479+});
480+await store.write({
481+body: secondBody,
482+metadata: {
483+ url,
484+status: 200,
485+lastModified: "Mon, 22 Jun 2026 03:00:00 GMT",
486+checksum: "sha256:second",
487+},
488+savedAt: "2026-06-22T03:04:05.000Z",
489+});
490+491+await expect(store.read(url)).resolves.toMatchObject({
492+body: secondBody,
493+metadata: {
494+ url,
495+status: 200,
496+lastModified: "Mon, 22 Jun 2026 03:00:00 GMT",
497+checksum: "sha256:second",
498+},
499+savedAt: "2026-06-22T03:04:05.000Z",
500+});
501+} finally {
502+closeOpenClawStateDatabaseForTest();
503+rmSync(stateDir, { recursive: true, force: true });
504+}
505+});
506+398507it("uses the last known good snapshot when the hosted feed returns HTTP 304", async () => {
399508const body = JSON.stringify({
400509schemaVersion: 1,
@@ -655,6 +764,7 @@ describe("official external plugin catalog", () => {
655764656765it("falls back to the bundled catalog on checksum mismatch and oversized bodies", async () => {
657766const mismatch = await loadHostedOfficialExternalPluginCatalogEntries({
767+snapshotStore: null,
658768expectedSha256: "sha256:not-current",
659769fetchImpl: vi.fn(
660770async () =>
@@ -677,6 +787,7 @@ describe("official external plugin catalog", () => {
677787}
678788679789const oversized = await loadHostedOfficialExternalPluginCatalogEntries({
790+snapshotStore: null,
680791maxBytes: 4,
681792fetchImpl: vi.fn(async () => new Response("12345", { status: 200 })),
682793});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。