
























11import fs from "node:fs";
22import os from "node:os";
33import path from "node:path";
4-import { afterEach, describe, expect, it } from "vitest";
4+import { afterEach, describe, expect, it, vi } from "vitest";
55import type { PluginInstallRecord } from "../config/types.plugins.js";
66import type { PluginCandidate } from "./discovery.js";
77import {
@@ -58,7 +58,16 @@ function expectRecordFields(record: unknown, expected: Record<string, unknown>)
5858return actual;
5959}
606061+function createDeferred<T>() {
62+let resolve: (value: T) => void = () => {};
63+const promise = new Promise<T>((done) => {
64+resolve = done;
65+});
66+return { promise, resolve };
67+}
68+6169afterEach(() => {
70+vi.doUnmock("../infra/json-files.js");
6271clearLoadInstalledPluginIndexInstallRecordsCache();
6372for (const dir of tempDirs.splice(0)) {
6473fs.rmSync(dir, { recursive: true, force: true });
@@ -235,7 +244,7 @@ describe("plugin index install records store", () => {
235244});
236245});
237246238-it("reloads cached records after an external index write", () => {
247+it("keeps cached records until cache clear after an external index write", () => {
239248const stateDir = makeStateDir();
240249const candidate = createPluginCandidate(stateDir, "external");
241250writePersistedInstalledPluginIndexInstallRecordsSync(
@@ -270,6 +279,15 @@ describe("plugin index install records store", () => {
270279"utf8",
271280);
272281282+expect(loadInstalledPluginIndexInstallRecordsSync({ stateDir })).toEqual({
283+external: {
284+source: "npm",
285+spec: "external@1.0.0",
286+},
287+});
288+289+clearLoadInstalledPluginIndexInstallRecordsCache();
290+273291expect(loadInstalledPluginIndexInstallRecordsSync({ stateDir })).toEqual({
274292external: {
275293source: "npm",
@@ -427,7 +445,7 @@ describe("plugin index install records store", () => {
427445});
428446});
429447430-it("reloads recovered managed npm records after package manifest changes", () => {
448+it("keeps recovered managed npm records cached until cache clear after package changes", () => {
431449const stateDir = makeStateDir();
432450const codexDir = writeManagedNpmPlugin({
433451 stateDir,
@@ -460,6 +478,17 @@ describe("plugin index install records store", () => {
460478"utf8",
461479);
462480481+expectRecordFields(loadInstalledPluginIndexInstallRecordsSync({ stateDir }).codex, {
482+source: "npm",
483+spec: "@openclaw/codex@2026.5.18-beta.1",
484+installPath: codexDir,
485+version: "2026.5.18-beta.1",
486+resolvedVersion: "2026.5.18-beta.1",
487+resolvedSpec: "@openclaw/codex@2026.5.18-beta.1",
488+});
489+490+clearLoadInstalledPluginIndexInstallRecordsCache();
491+463492expectRecordFields(loadInstalledPluginIndexInstallRecordsSync({ stateDir }).codex, {
464493source: "npm",
465494spec: "@openclaw/codex@2026.5.18-beta.1",
@@ -470,6 +499,90 @@ describe("plugin index install records store", () => {
470499});
471500});
472501502+it("does not probe install record files again on hot cache hits", () => {
503+const stateDir = makeStateDir();
504+const candidate = createPluginCandidate(stateDir, "hot-cache");
505+writePersistedInstalledPluginIndexInstallRecordsSync(
506+{
507+"hot-cache": {
508+source: "npm",
509+spec: "hot-cache@1.0.0",
510+},
511+},
512+{ stateDir, candidates: [candidate] },
513+);
514+expect(loadInstalledPluginIndexInstallRecordsSync({ stateDir })).toEqual({
515+"hot-cache": {
516+source: "npm",
517+spec: "hot-cache@1.0.0",
518+},
519+});
520+const statSpy = vi.spyOn(fs, "statSync");
521+const readSpy = vi.spyOn(fs, "readFileSync");
522+523+expect(loadInstalledPluginIndexInstallRecordsSync({ stateDir })).toEqual({
524+"hot-cache": {
525+source: "npm",
526+spec: "hot-cache@1.0.0",
527+},
528+});
529+530+expect(statSpy).not.toHaveBeenCalled();
531+expect(readSpy).not.toHaveBeenCalled();
532+});
533+534+it("does not cache stale async records when cache clears during load", async () => {
535+const stateDir = makeStateDir();
536+const firstRead = createDeferred<unknown>();
537+const firstReadStarted = createDeferred<void>();
538+let reads = 0;
539+vi.doMock("../infra/json-files.js", async (importOriginal) => {
540+const actual = await importOriginal<typeof import("../infra/json-files.js")>();
541+return {
542+ ...actual,
543+tryReadJson: vi.fn(async () => {
544+reads += 1;
545+if (reads === 1) {
546+firstReadStarted.resolve();
547+return await firstRead.promise;
548+}
549+return {
550+installRecords: {
551+demo: {
552+source: "npm",
553+spec: "demo@fresh",
554+},
555+},
556+};
557+}),
558+tryReadJsonSync: vi.fn(() => null),
559+};
560+});
561+const reader = (await import(
562+"./installed-plugin-index-record-reader.js?cache-race" as string
563+)) as typeof import("./installed-plugin-index-record-reader.js");
564+const load = reader.loadInstalledPluginIndexInstallRecords({ stateDir });
565+566+await firstReadStarted.promise;
567+reader.clearLoadInstalledPluginIndexInstallRecordsCache();
568+firstRead.resolve({
569+installRecords: {
570+demo: {
571+source: "npm",
572+spec: "demo@stale",
573+},
574+},
575+});
576+577+await expect(load).resolves.toEqual({
578+demo: {
579+source: "npm",
580+spec: "demo@fresh",
581+},
582+});
583+expect(reads).toBe(2);
584+});
585+473586it("preserves git install resolution fields in persisted records", async () => {
474587const stateDir = makeStateDir();
475588const candidate = createPluginCandidate(stateDir, "git-demo");
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。