diff --git a/src/cli/plugins-cli.policy.test.ts b/src/cli/plugins-cli.policy.test.ts index 87fc744612a3..14cc09d25a50 100644 --- a/src/cli/plugins-cli.policy.test.ts +++ b/src/cli/plugins-cli.policy.test.ts @@ -118,7 +118,7 @@ describe("plugins cli policy mutations", () => { plugins: { allow: ["other-plugin"] }, reason: "blocked by allowlist", }, - ])("does not mutate plugin state when $policy blocks enablement", async ({ plugins, reason }) => { + ])("fails without mutations when $policy blocks enablement", async ({ plugins, reason }) => { const sourceConfig = { plugins } as OpenClawConfig; loadConfig.mockReturnValue(sourceConfig); enablePluginInConfig.mockReturnValue({ @@ -129,11 +129,13 @@ describe("plugins cli policy mutations", () => { }); mockPluginRegistry(["alpha"]); - await runPluginsCommand(["plugins", "enable", "alpha"]); + await expect(runPluginsCommand(["plugins", "enable", "alpha"])).rejects.toThrow("__exit__:1"); + expect(replaceConfigFile).not.toHaveBeenCalled(); expect(writeConfigFile).not.toHaveBeenCalled(); expect(refreshPluginRegistry).not.toHaveBeenCalled(); - expect(runtimeLogs).toContain(`Plugin "alpha" could not be enabled (${reason}).`); + expect(runtimeErrors).toContain(`Plugin "alpha" could not be enabled (${reason}).`); + expect(runtimeLogs).not.toContain(`Plugin "alpha" could not be enabled (${reason}).`); }); it("refuses plugin enablement in Nix mode before config mutation", async () => { diff --git a/src/cli/plugins-cli.runtime.ts b/src/cli/plugins-cli.runtime.ts index e52f5dfa4679..1d1c44761cb7 100644 --- a/src/cli/plugins-cli.runtime.ts +++ b/src/cli/plugins-cli.runtime.ts @@ -205,12 +205,10 @@ async function runPluginsEnableCommandUnlocked(idInput: string): Promise { }); // A blocked request must not displace the active slot or rewrite persisted state. if (!enableResult.enabled) { - defaultRuntime.log( - theme.warn( - `Plugin "${id}" could not be enabled (${enableResult.reason ?? "unknown reason"}).`, - ), + defaultRuntime.error( + `Plugin "${id}" could not be enabled (${enableResult.reason ?? "unknown reason"}).`, ); - return; + return defaultRuntime.exit(1); } const { applySlotSelectionForPlugin } = await loadPluginSlotSelection(); diff --git a/src/plugins/installed-plugin-index-hash.ts b/src/plugins/installed-plugin-index-hash.ts index dce1f01186e4..f66b1a992344 100644 --- a/src/plugins/installed-plugin-index-hash.ts +++ b/src/plugins/installed-plugin-index-hash.ts @@ -59,25 +59,3 @@ export function safeFileSignature(filePath: string): InstalledPluginFileSignatur return undefined; } } - -/** Compares current file metadata with a stored installed-plugin file signature. */ -export function fileSignatureMatches( - filePath: string, - signature: InstalledPluginFileSignature | undefined, -): boolean | undefined { - if (!signature) { - return undefined; - } - if (typeof signature.ctimeMs !== "number") { - return undefined; - } - const current = safeFileSignature(filePath); - if (!current) { - return false; - } - return ( - current.size === signature.size && - current.mtimeMs === signature.mtimeMs && - current.ctimeMs === signature.ctimeMs - ); -} diff --git a/src/plugins/plugin-registry-contributions.current-snapshot.test.ts b/src/plugins/plugin-registry-contributions.current-snapshot.test.ts index c32ada20d875..fb17ac9d974b 100644 --- a/src/plugins/plugin-registry-contributions.current-snapshot.test.ts +++ b/src/plugins/plugin-registry-contributions.current-snapshot.test.ts @@ -1,5 +1,6 @@ // Verifies current plugin registry contribution snapshots. -import { afterEach, describe, expect, it } from "vitest"; +import fs from "node:fs"; +import { afterEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { setCurrentPluginMetadataSnapshot } from "./current-plugin-metadata-snapshot.js"; import { clearCurrentPluginMetadataSnapshot } from "./current-plugin-metadata-state.js"; @@ -8,6 +9,7 @@ import type { InstalledPluginIndex } from "./installed-plugin-index.js"; import type { PluginManifestRecord } from "./manifest-registry.js"; import type { PluginMetadataSnapshot } from "./plugin-metadata-snapshot.types.js"; import { loadPluginManifestRegistryForPluginRegistry } from "./plugin-registry-contributions.js"; +import { loadPluginRegistrySnapshotWithMetadata } from "./plugin-registry-snapshot.js"; afterEach(() => { clearCurrentPluginMetadataSnapshot(); @@ -141,7 +143,7 @@ describe("loadPluginManifestRegistryForPluginRegistry current snapshot", () => { expect(loadPluginManifestRegistryForPluginRegistry({ config, env }).plugins).toEqual([]); }); - it("does not reuse current metadata for explicit registry inputs or diagnostics", () => { + it("keeps explicit registry inputs authoritative and reuses current diagnostics", () => { const config: OpenClawConfig = {}; const env = { HOME: "/tmp/openclaw-test-home", @@ -190,11 +192,26 @@ describe("loadPluginManifestRegistryForPluginRegistry current snapshot", () => { }), { config, env, workspaceDir }, ); + const readDirectory = vi.spyOn(fs, "readdirSync"); + const readFile = vi.spyOn(fs, "readFileSync"); + const statFile = vi.spyOn(fs, "statSync"); expect( loadPluginManifestRegistryForPluginRegistry({ config, env, workspaceDir }).plugins.map( (plugin) => plugin.id, ), - ).toEqual([]); + ).toEqual(["enabled"]); + expect( + loadPluginRegistrySnapshotWithMetadata({ config, env, workspaceDir }).diagnostics, + ).toEqual([ + { + level: "info", + code: "persisted-registry-missing", + message: "missing", + }, + ]); + expect(readDirectory).not.toHaveBeenCalled(); + expect(readFile).not.toHaveBeenCalled(); + expect(statFile).not.toHaveBeenCalled(); }); }); diff --git a/src/plugins/plugin-registry-snapshot.lifecycle.test.ts b/src/plugins/plugin-registry-snapshot.lifecycle.test.ts deleted file mode 100644 index 92c662cb16c4..000000000000 --- a/src/plugins/plugin-registry-snapshot.lifecycle.test.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { afterEach, describe, expect, it, vi } from "vitest"; -import { - getCurrentPluginMetadataSnapshotState, - setCurrentPluginMetadataSnapshotState, -} from "./current-plugin-metadata-state.js"; -import { clearPluginMetadataLifecycleCaches } from "./plugin-metadata-lifecycle.js"; -import "./plugin-registry-snapshot.js"; - -vi.mock("./current-plugin-metadata-snapshot.js", () => ({ - getCurrentPluginMetadataSnapshot: vi.fn(() => undefined), -})); - -afterEach(() => { - clearPluginMetadataLifecycleCaches(); -}); - -describe("plugin registry snapshot lifecycle", () => { - it("clears registry metadata when the snapshot facade is mocked", () => { - setCurrentPluginMetadataSnapshotState({ plugins: [] }, "mocked-snapshot-facade"); - - expect(() => clearPluginMetadataLifecycleCaches()).not.toThrow(); - expect(getCurrentPluginMetadataSnapshotState().snapshot).toBeUndefined(); - }); -}); diff --git a/src/plugins/plugin-registry-snapshot.test.ts b/src/plugins/plugin-registry-snapshot.test.ts index 31b741432694..360c80bff4e8 100644 --- a/src/plugins/plugin-registry-snapshot.test.ts +++ b/src/plugins/plugin-registry-snapshot.test.ts @@ -16,7 +16,6 @@ import { } from "./installed-plugin-index.js"; import { markRetainedManagedNpmInstall } from "./managed-npm-retention.js"; import { loadPluginManifestRegistryForInstalledIndex } from "./manifest-registry-installed.js"; -import { clearPluginMetadataLifecycleCaches } from "./plugin-metadata-lifecycle.js"; import type { PluginMetadataSnapshot } from "./plugin-metadata-snapshot.types.js"; import { loadPluginRegistrySnapshotWithMetadata } from "./plugin-registry-snapshot.js"; import { cleanupTrackedTempDirs, makeTrackedTempDir } from "./test-helpers/fs-fixtures.js"; @@ -275,7 +274,7 @@ describe("loadPluginRegistrySnapshotWithMetadata", () => { }); }); - it("does not treat diagnostic current metadata as provided registry input", () => { + it("reuses diagnostic current metadata without promoting its registry source", () => { const env = { ...createHermeticEnv(makeTempDir()), OPENCLAW_DISABLE_BUNDLED_PLUGINS: "1", @@ -300,6 +299,7 @@ describe("loadPluginRegistrySnapshotWithMetadata", () => { configFingerprint: "", workspaceDir, index, + registrySource: "derived", registryDiagnostics: [ { level: "info", @@ -333,10 +333,27 @@ describe("loadPluginRegistrySnapshotWithMetadata", () => { }, { config, env, workspaceDir }, ); + const readDirectory = vi.spyOn(fs, "readdirSync"); + const readFile = vi.spyOn(fs, "readFileSync"); + const statFile = vi.spyOn(fs, "statSync"); const result = loadPluginRegistrySnapshotWithMetadata({ config, env, workspaceDir }); - expect(result.source).not.toBe("provided"); + expect(result).toEqual({ + snapshot: index, + source: "derived", + diagnostics: [ + { + level: "info", + code: "persisted-registry-missing", + message: "missing", + }, + ], + manifestRegistry: { plugins: [], diagnostics: [] }, + }); + expect(readDirectory).not.toHaveBeenCalled(); + expect(readFile).not.toHaveBeenCalled(); + expect(statFile).not.toHaveBeenCalled(); }); it("does not reuse current metadata when explicit derivation inputs are supplied", () => { @@ -559,75 +576,7 @@ describe("loadPluginRegistrySnapshotWithMetadata", () => { expect(result.diagnostics).toStrictEqual([]); }); - it("reuses a memoized registry without polling plugin files", () => { - const tempRoot = makeTempDir(); - const workspaceDir = path.join(tempRoot, "workspace"); - const env = { ...createHermeticEnv(tempRoot), OPENCLAW_DISABLE_BUNDLED_PLUGINS: "1" }; - const config = {}; - const first = loadPluginRegistrySnapshotWithMetadata({ config, env, workspaceDir }); - const readDirectory = vi.spyOn(fs, "readdirSync"); - const readFile = vi.spyOn(fs, "readFileSync"); - const statFile = vi.spyOn(fs, "statSync"); - - expect(loadPluginRegistrySnapshotWithMetadata({ config, env, workspaceDir })).toBe(first); - expect(readDirectory).not.toHaveBeenCalled(); - expect(readFile).not.toHaveBeenCalled(); - expect(statFile).not.toHaveBeenCalled(); - }); - - it("retains only the current process-lifecycle registry graph", () => { - const tempRoot = makeTempDir(); - const env = { ...createHermeticEnv(tempRoot), OPENCLAW_DISABLE_BUNDLED_PLUGINS: "1" }; - const firstWorkspace = path.join(tempRoot, "first-workspace"); - const secondWorkspace = path.join(tempRoot, "second-workspace"); - - const first = loadPluginRegistrySnapshotWithMetadata({ - config: {}, - env, - workspaceDir: firstWorkspace, - }); - const second = loadPluginRegistrySnapshotWithMetadata({ - config: {}, - env, - workspaceDir: secondWorkspace, - }); - const refreshedFirst = loadPluginRegistrySnapshotWithMetadata({ - config: {}, - env, - workspaceDir: firstWorkspace, - }); - - expect(second).not.toBe(first); - expect(refreshedFirst).not.toBe(first); - expect( - loadPluginRegistrySnapshotWithMetadata({ - config: {}, - env, - workspaceDir: firstWorkspace, - }), - ).toBe(refreshedFirst); - }); - - it("refreshes workspace plugin discovery on explicit metadata invalidation", () => { - const tempRoot = makeTempDir(); - const workspaceDir = path.join(tempRoot, "workspace"); - const env = { ...createHermeticEnv(tempRoot), OPENCLAW_DISABLE_BUNDLED_PLUGINS: "1" }; - - const first = loadPluginRegistrySnapshotWithMetadata({ config: {}, env, workspaceDir }); - expect(first.snapshot.plugins.map((plugin) => plugin.pluginId)).not.toContain("demo"); - - writePackagePlugin(path.join(workspaceDir, ".openclaw", "extensions", "demo")); - - const second = loadPluginRegistrySnapshotWithMetadata({ config: {}, env, workspaceDir }); - expect(second).toBe(first); - - clearPluginMetadataLifecycleCaches(); - - const refreshed = loadPluginRegistrySnapshotWithMetadata({ config: {}, env, workspaceDir }); - expect(refreshed.snapshot.plugins.map((plugin) => plugin.pluginId)).toContain("demo"); - }); - - it("ignores malformed load paths while memoizing snapshots", () => { + it("ignores malformed load paths while deriving snapshots", () => { const tempRoot = makeTempDir(); const env = { ...createHermeticEnv(tempRoot), OPENCLAW_DISABLE_BUNDLED_PLUGINS: "1" }; const config = { @@ -673,6 +622,36 @@ describe("loadPluginRegistrySnapshotWithMetadata", () => { expect(result.diagnostics).toStrictEqual([]); }); + it("rebuilds when an explicit candidate moves identical package metadata", () => { + const tempRoot = makeTempDir(); + const rootDir = path.join(tempRoot, "workspace"); + const stateDir = path.join(tempRoot, "state"); + const env = { ...createHermeticEnv(tempRoot), OPENCLAW_DISABLE_BUNDLED_PLUGINS: "1" }; + const packageContents = JSON.stringify({ name: "demo", version: "1.0.0" }); + const baseCandidate = createCandidate(rootDir); + fs.writeFileSync(path.join(rootDir, "package.json"), packageContents, "utf8"); + const persisted = loadInstalledPluginIndex({ + candidates: [{ ...baseCandidate, packageDir: rootDir }], + config: {}, + env, + }); + writePersistedInstalledPluginIndexSync(persisted, { stateDir }); + const nestedPackageDir = path.join(rootDir, "nested"); + fs.mkdirSync(nestedPackageDir, { recursive: true }); + fs.writeFileSync(path.join(nestedPackageDir, "package.json"), packageContents, "utf8"); + + const result = loadPluginRegistrySnapshotWithMetadata({ + candidates: [{ ...baseCandidate, packageDir: nestedPackageDir }], + config: {}, + env, + stateDir, + }); + + expect(result.source).toBe("derived"); + expectDiagnosticsContainCode(result.diagnostics, "persisted-registry-stale-source"); + expect(result.snapshot.plugins[0]?.packageJson?.path).toBe("nested/package.json"); + }); + it("derives a complete index when a configured load-path plugin is missing", () => { const tempRoot = makeTempDir(); const firstRoot = path.join(tempRoot, "first"); @@ -810,7 +789,24 @@ describe("loadPluginRegistrySnapshotWithMetadata", () => { const metaDir = path.join(rootDir, "..meta"); fs.mkdirSync(metaDir, { recursive: true }); const packageJsonPath = path.join(metaDir, "package.json"); - fs.writeFileSync(packageJsonPath, JSON.stringify({ name: "demo", version: "1.0.0" }), "utf8"); + fs.writeFileSync( + packageJsonPath, + JSON.stringify({ + name: "demo", + version: "1.0.0", + openclaw: { + channel: { + id: "demo", + label: "Demo", + commands: { + nativeCommandsAutoEnabled: true, + nativeSkillsAutoEnabled: false, + }, + }, + }, + }), + "utf8", + ); const index = loadInstalledPluginIndex({ config, env }); const [plugin] = index.plugins; if (!plugin) { @@ -842,6 +838,17 @@ describe("loadPluginRegistrySnapshotWithMetadata", () => { expect(result.source).toBe("persisted"); expect(result.diagnostics).toStrictEqual([]); + expect(result.manifestRegistry).toBeUndefined(); + const registry = loadPluginManifestRegistryForInstalledIndex({ + index: result.snapshot, + config, + env, + includeDisabled: true, + }); + expect(registry.plugins[0]?.channelCatalogMeta?.commands).toEqual({ + nativeCommandsAutoEnabled: true, + nativeSkillsAutoEnabled: false, + }); }); it.runIf(process.platform !== "win32")( @@ -857,6 +864,7 @@ describe("loadPluginRegistrySnapshotWithMetadata", () => { const config = { plugins: { load: { paths: [rootDir] }, + entries: { demo: { enabled: false } }, }, }; writePackagePlugin(rootDir); @@ -902,6 +910,72 @@ describe("loadPluginRegistrySnapshotWithMetadata", () => { }, ); + it.runIf(process.platform !== "win32")( + "rejects dangling root, source, and manifest links for disabled records", + () => { + for (const artifact of ["root", "source", "manifest"] as const) { + const tempRoot = makeTempDir(); + const rootDir = path.join(tempRoot, "workspace"); + const stateDir = path.join(tempRoot, "state"); + const env = { ...createHermeticEnv(tempRoot), OPENCLAW_DISABLE_BUNDLED_PLUGINS: "1" }; + const config = { + plugins: { + load: { paths: [rootDir] }, + entries: { demo: { enabled: false } }, + }, + }; + writePackagePlugin(rootDir); + writePersistedInstalledPluginIndexSync(loadInstalledPluginIndex({ config, env }), { + stateDir, + }); + const artifactPath = + artifact === "root" + ? rootDir + : path.join(rootDir, artifact === "source" ? "index.ts" : "openclaw.plugin.json"); + fs.rmSync(artifactPath, { recursive: artifact === "root" }); + fs.symlinkSync(path.join(tempRoot, "missing"), artifactPath); + + const result = loadPluginRegistrySnapshotWithMetadata({ config, env, stateDir }); + + expect([artifact, result.source]).toEqual([artifact, "derived"]); + expectDiagnosticsContainCode(result.diagnostics, "persisted-registry-stale-source"); + } + }, + ); + + it("rejects escaped missing package metadata for disabled records", () => { + const tempRoot = makeTempDir(); + const rootDir = path.join(tempRoot, "workspace"); + const stateDir = path.join(tempRoot, "state"); + const env = { ...createHermeticEnv(tempRoot), OPENCLAW_DISABLE_BUNDLED_PLUGINS: "1" }; + const config = { + plugins: { + load: { paths: [rootDir] }, + entries: { demo: { enabled: false } }, + }, + }; + writePackagePlugin(rootDir); + const index = loadInstalledPluginIndex({ config, env }); + const plugin = requirePluginRecord(index.plugins, "demo"); + writePersistedInstalledPluginIndexSync( + { + ...index, + plugins: [ + { + ...plugin, + packageJson: { path: "../gone/package.json", hash: "missing" }, + }, + ], + }, + { stateDir }, + ); + + const result = loadPluginRegistrySnapshotWithMetadata({ config, env, stateDir }); + + expect(result.source).toBe("derived"); + expectDiagnosticsContainCode(result.diagnostics, "persisted-registry-stale-source"); + }); + it("detects same-size same-mtime manifest replacements", () => { const tempRoot = makeTempDir(); const rootDir = path.join(tempRoot, "workspace"); @@ -1048,10 +1122,10 @@ describe("loadPluginRegistrySnapshotWithMetadata", () => { expect(result.snapshot.plugins.map((plugin) => plugin.pluginId)).toEqual(["codex", "whatsapp"]); }); - it("resolves a persisted bundled root only once per registry load", () => { + it("keeps missing disabled bundled records under the trusted bundled root", () => { const tempRoot = makeTempDir(); - const packageRoot = path.join(tempRoot, "openclaw"); - const bundledRoot = path.join(packageRoot, "dist", "extensions"); + const bundledRoot = path.join(tempRoot, "dist", "extensions"); + const pluginRoot = path.join(bundledRoot, "whatsapp"); const stateDir = path.join(tempRoot, "state"); const env = { OPENCLAW_BUNDLED_PLUGINS_DIR: bundledRoot, @@ -1059,22 +1133,41 @@ describe("loadPluginRegistrySnapshotWithMetadata", () => { OPENCLAW_VERSION: "2026.4.26", VITEST: "true", }; - const pluginIds = ["bundled-one", "bundled-two", "bundled-three", "bundled-four"]; - - for (const pluginId of pluginIds) { - writeBundledPlugin(path.join(bundledRoot, pluginId), pluginId, "index.js"); - } - const index = loadInstalledPluginIndex({ config: {}, env, stateDir }); + const config = { plugins: { entries: { whatsapp: { enabled: false } } } }; + writeBundledPlugin(pluginRoot, "whatsapp", "index.js"); + const index = loadInstalledPluginIndex({ config, env, stateDir }); writePersistedInstalledPluginIndexSync(index, { stateDir }); - const realpathSpy = vi.spyOn(fs, "realpathSync"); + fs.rmSync(pluginRoot, { recursive: true }); - const result = loadPluginRegistrySnapshotWithMetadata({ config: {}, env, stateDir }); + const result = loadPluginRegistrySnapshotWithMetadata({ config, env, stateDir }); expect(result.source).toBe("persisted"); - expect(result.snapshot.plugins.map((plugin) => plugin.pluginId).toSorted()).toEqual( - pluginIds.toSorted(), - ); - expect(realpathSpy.mock.calls.filter(([filePath]) => filePath === bundledRoot)).toHaveLength(1); + expect(result.snapshot.plugins.map((plugin) => plugin.pluginId)).toEqual(["whatsapp"]); + expect(result.snapshot.plugins[0]?.enabled).toBe(false); + }); + + it("keeps missing disabled inventory beside unchanged configured plugins", () => { + const tempRoot = makeTempDir(); + const liveRoot = path.join(tempRoot, "live"); + const missingRoot = path.join(tempRoot, "missing"); + const stateDir = path.join(tempRoot, "state"); + const env = { ...createHermeticEnv(tempRoot), OPENCLAW_DISABLE_BUNDLED_PLUGINS: "1" }; + const config = { + plugins: { + load: { paths: [liveRoot, missingRoot] }, + entries: { missing: { enabled: false } }, + }, + }; + writePackagePlugin(liveRoot, { pluginId: "live" }); + writePackagePlugin(missingRoot, { pluginId: "missing" }); + const index = loadInstalledPluginIndex({ config, env }); + writePersistedInstalledPluginIndexSync(index, { stateDir }); + fs.rmSync(missingRoot, { recursive: true }); + + const result = loadPluginRegistrySnapshotWithMetadata({ config, env, stateDir }); + + expect(result.source).toBe("persisted"); + expect(result.snapshot.plugins.map((plugin) => plugin.pluginId)).toEqual(["live", "missing"]); }); it("treats a persisted source bundled root as stale once its built peer appears", () => { @@ -1112,6 +1205,49 @@ describe("loadPluginRegistrySnapshotWithMetadata", () => { ]); }); + it("replaces a persisted built root when its source plugin opts out of bundled output", () => { + const tempRoot = makeTempDir(); + const packageRoot = path.join(tempRoot, "openclaw"); + const bundledRoot = path.join(packageRoot, "dist", "extensions"); + const sourcePluginDir = path.join(packageRoot, "extensions", "whatsapp"); + const stateDir = path.join(tempRoot, "state"); + const env = { + OPENCLAW_BUNDLED_PLUGINS_DIR: bundledRoot, + OPENCLAW_STATE_DIR: stateDir, + OPENCLAW_VERSION: "2026.4.26", + VITEST: "true", + }; + + fs.mkdirSync(path.join(packageRoot, "src"), { recursive: true }); + fs.writeFileSync(path.join(packageRoot, ".git"), "gitdir: /tmp/mock\n", "utf8"); + fs.writeFileSync(path.join(packageRoot, "pnpm-workspace.yaml"), "packages: []\n", "utf8"); + writeBundledPlugin(sourcePluginDir, "whatsapp", "index.ts"); + writeBundledPlugin(path.join(bundledRoot, "whatsapp"), "whatsapp", "index.js"); + + const builtIndex = loadInstalledPluginIndex({ config: {}, env, stateDir }); + expect(builtIndex.plugins.map((plugin) => plugin.rootDir)).toEqual([ + fs.realpathSync(path.join(bundledRoot, "whatsapp")), + ]); + writePersistedInstalledPluginIndexSync(builtIndex, { stateDir }); + fs.writeFileSync( + path.join(sourcePluginDir, "package.json"), + JSON.stringify({ + name: "@openclaw/whatsapp", + version: "1.0.0", + openclaw: { extensions: ["./index.ts"], build: { bundledDist: false } }, + }), + "utf8", + ); + + const result = loadPluginRegistrySnapshotWithMetadata({ config: {}, env, stateDir }); + + expect(result.source).toBe("derived"); + expectDiagnosticsContainCode(result.diagnostics, "persisted-registry-stale-source"); + expect(result.snapshot.plugins.map((plugin) => plugin.rootDir)).toEqual([ + fs.realpathSync(sourcePluginDir), + ]); + }); + it("keeps a persisted bind-mounted source overlay when its built peer exists", () => { const tempRoot = makeTempDir(); const packageRoot = path.join(tempRoot, "openclaw"); diff --git a/src/plugins/plugin-registry-snapshot.ts b/src/plugins/plugin-registry-snapshot.ts index 147590d83bb6..151692c971c4 100644 --- a/src/plugins/plugin-registry-snapshot.ts +++ b/src/plugins/plugin-registry-snapshot.ts @@ -1,20 +1,16 @@ // Builds stable snapshots of plugin registry contributions. -import crypto from "node:crypto"; import fs from "node:fs"; import path from "node:path"; +import { isDeepStrictEqual } from "node:util"; import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope-config.js"; import { tryReadJsonSync } from "../infra/json-files.js"; -import { resolveUserPath } from "../utils.js"; -import { resolveCompatibilityHostVersion } from "../version.js"; import { resolveBundledPluginsDir } from "./bundled-dir.js"; import { buildLegacyBundledRootPath } from "./bundled-load-path-aliases.js"; import { listBundledSourceOverlayDirs } from "./bundled-source-overlays.js"; import { normalizePluginsConfig } from "./config-state.js"; import { getCurrentPluginMetadataSnapshot } from "./current-plugin-metadata-snapshot.js"; -import { clearCurrentPluginMetadataSnapshot } from "./current-plugin-metadata-state.js"; -import { discoverConfiguredPluginLoadPaths, type PluginDiscoveryResult } from "./discovery.js"; -import { resolveActivePluginInstallRoots } from "./install-root-context.js"; -import { fileSignatureMatches, hashJson } from "./installed-plugin-index-hash.js"; +import type { PluginDiscoveryResult } from "./discovery.js"; +import { safeFileSignature, safeHashFile } from "./installed-plugin-index-hash.js"; import { hasOptionalMissingPluginManifestFile } from "./installed-plugin-index-manifest.js"; import { loadInstalledPluginIndexInstallRecordsSync } from "./installed-plugin-index-record-reader.js"; import { @@ -26,7 +22,6 @@ import { } from "./installed-plugin-index-store.js"; import { getInstalledPluginRecord, - extractPluginInstallRecordsFromInstalledPluginIndex, hasMissingConfigPathActivationMetadata, isInstalledPluginEnabled, loadInstalledPluginIndexWithDiscovery, @@ -36,12 +31,67 @@ import { type LoadInstalledPluginIndexParams, type RefreshInstalledPluginIndexParams, } from "./installed-plugin-index.js"; -import { loadPluginManifestRegistry, type PluginManifestRegistry } from "./manifest-registry.js"; +import type { PluginManifestRegistry } from "./manifest-registry.js"; import { getPackageManifestMetadata, type PackageManifest } from "./manifest.js"; -import { safeRealpathSync } from "./path-safety.js"; -import { registerPluginMetadataProcessMemoLifecycleClear } from "./plugin-metadata-lifecycle.js"; +import { isPathInside, safeRealpathSync } from "./path-safety.js"; import type { PluginRegistrySnapshotSource } from "./plugin-registry-snapshot.types.js"; +function resolvePluginRegistryContent( + index: InstalledPluginIndex, + comparePackageJsonPath: boolean, + excludedPlugins?: ReadonlyMap, +): unknown { + const { + generatedAtMs: _generatedAtMs, + refreshReason: _refreshReason, + warning: _warning, + ...content + } = index; + const excludedRoots = [...(excludedPlugins?.values() ?? [])].map((root) => path.resolve(root)); + const exclusionPathCache = new Map(); + return { + ...content, + diagnostics: excludedPlugins + ? content.diagnostics.filter( + (diagnostic) => + !( + (diagnostic.pluginId && excludedPlugins.has(diagnostic.pluginId)) || + (diagnostic.source && + excludedRoots.some((root) => + isContainedPluginPath(root, diagnostic.source!, exclusionPathCache), + )) + ), + ) + : content.diagnostics, + installRecords: excludedPlugins + ? Object.fromEntries( + Object.entries(content.installRecords).filter( + ([pluginId]) => !excludedPlugins.has(pluginId), + ), + ) + : content.installRecords, + plugins: content.plugins + .filter((plugin) => !excludedPlugins?.has(plugin.pluginId)) + .map((plugin) => { + const { manifestFile: _manifestFile, packageJson, ...record } = plugin; + if (!packageJson) { + return record; + } + if (!comparePackageJsonPath) { + return record; + } + const { + fileSignature: _fileSignature, + path: packageJsonPath, + ...stablePackageJson + } = packageJson; + return Object.assign(record, { + packageJson: Object.assign(stablePackageJson, { path: packageJsonPath }), + }); + }), + }; +} + export type PluginRegistrySnapshot = InstalledPluginIndex; export type PluginRegistryRecord = InstalledPluginIndexRecord; type PluginRegistryInspection = InstalledPluginIndexStoreInspection; @@ -65,36 +115,6 @@ type PluginRegistrySnapshotResult = { manifestRegistry?: PluginManifestRegistry; }; -const REGISTRY_SNAPSHOT_MEMO_ENV_KEYS = [ - "APPDATA", - "HOME", - "OPENCLAW_BUNDLED_PLUGINS_DIR", - "OPENCLAW_COMPATIBILITY_HOST_VERSION", - "OPENCLAW_CONFIG_PATH", - "OPENCLAW_DISABLE_BUNDLED_PLUGINS", - "OPENCLAW_DISABLE_BUNDLED_SOURCE_OVERLAYS", - "OPENCLAW_HOME", - "OPENCLAW_NIX_MODE", - "OPENCLAW_STATE_DIR", - "USERPROFILE", - "XDG_CONFIG_HOME", -] as const; - -type PluginRegistrySnapshotMemo = { - key: string; - result: PluginRegistrySnapshotResult; -}; - -let pluginRegistrySnapshotMemo: PluginRegistrySnapshotMemo | undefined; - -function clearLoadPluginRegistrySnapshotMemo(): void { - pluginRegistrySnapshotMemo = undefined; - // A retired registry must not leave its published metadata graph behind. - clearCurrentPluginMetadataSnapshot(); -} - -registerPluginMetadataProcessMemoLifecycleClear(clearLoadPluginRegistrySnapshotMemo); - export type LoadPluginRegistryParams = LoadInstalledPluginIndexParams & InstalledPluginIndexStoreOptions & { index?: PluginRegistrySnapshot; @@ -105,68 +125,6 @@ type GetPluginRecordParams = LoadPluginRegistryParams & { pluginId: string; }; -function pickRegistrySnapshotMemoEnv(env: NodeJS.ProcessEnv): Record { - return Object.fromEntries( - REGISTRY_SNAPSHOT_MEMO_ENV_KEYS.flatMap((key) => { - const value = env[key]; - return value === undefined ? [] : [[key, value]]; - }), - ); -} - -function canMemoizePluginRegistrySnapshot(params: LoadPluginRegistryParams): boolean { - return ( - params.index === undefined && - params.candidates === undefined && - params.diagnostics === undefined && - params.discovery === undefined && - params.installRecords === undefined && - params.now === undefined && - params.filePath === undefined && - params.pluginIndexFilePath === undefined - ); -} - -function resolvePluginRegistrySnapshotMemoKey( - params: LoadPluginRegistryParams, - env: NodeJS.ProcessEnv, -): string | undefined { - if (!canMemoizePluginRegistrySnapshot(params)) { - return undefined; - } - return hashJson({ - config: params.config ?? null, - cwd: process.cwd(), - env: pickRegistrySnapshotMemoEnv(env), - installRoots: resolveActivePluginInstallRoots(env), - hostContractVersion: resolveCompatibilityHostVersion(env), - preferPersisted: params.preferPersisted ?? null, - // Install, reload, and persisted-index writes clear this memo explicitly. - // Polling roots or SQLite here would put discovery back on every hot lookup. - stateDir: params.stateDir ? resolveUserPath(params.stateDir, env) : null, - workspaceDir: params.workspaceDir ? resolveUserPath(params.workspaceDir, env) : null, - }); -} - -function findPluginRegistrySnapshotMemo( - key: string | undefined, -): PluginRegistrySnapshotResult | undefined { - return key && pluginRegistrySnapshotMemo?.key === key - ? pluginRegistrySnapshotMemo.result - : undefined; -} - -function rememberPluginRegistrySnapshotMemo( - key: string | undefined, - result: PluginRegistrySnapshotResult, -): PluginRegistrySnapshotResult { - if (!key) { - return result; - } - pluginRegistrySnapshotMemo = { key, result }; - return result; -} - function canReuseCurrentPluginMetadataSnapshot(params: LoadPluginRegistryParams): boolean { return ( params.preferPersisted !== false && @@ -176,6 +134,7 @@ function canReuseCurrentPluginMetadataSnapshot(params: LoadPluginRegistryParams) params.installRecords === undefined && params.candidates === undefined && params.diagnostics === undefined && + params.discovery === undefined && params.now === undefined ); } @@ -186,266 +145,194 @@ function loadCurrentPluginRegistrySnapshotResult( if (!canReuseCurrentPluginMetadataSnapshot(params)) { return undefined; } - const env = params.env ?? process.env; const current = getCurrentPluginMetadataSnapshot({ config: params.config, - env, + env: params.env ?? process.env, ...(params.workspaceDir ? { workspaceDir: params.workspaceDir } : {}), }); - if (!current || current.registryDiagnostics.length > 0) { + if (!current) { return undefined; } return { snapshot: current.index, - source: "provided", + source: + current.registrySource ?? (current.registryDiagnostics.length > 0 ? "derived" : "provided"), diagnostics: current.registryDiagnostics, + ...(current.discovery ? { discovery: current.discovery } : {}), manifestRegistry: current.manifestRegistry, }; } -function hasMissingPersistedPluginSource(index: InstalledPluginIndex): boolean { +function fileContentMatches( + filePath: string, + hash: string, + signature?: InstalledPluginIndexRecord["manifestFile"], + trustSignature = true, +): boolean { + const current = safeFileSignature(filePath); + if (!current) { + return false; + } + if ( + trustSignature && + signature?.ctimeMs !== undefined && + current.size === signature.size && + current.mtimeMs === signature.mtimeMs && + current.ctimeMs === signature.ctimeMs + ) { + return true; + } + return safeHashFile({ filePath, diagnostics: [], required: false }) === hash; +} + +function isContainedPluginPath( + rootPath: string, + targetPath: string, + cache: Map, +): boolean { + // Project unresolved suffixes from the nearest real ancestor so missing disabled + // artifacts stay inspectable without accepting symlink or path-alias escapes. + const resolveProjectedPath = (inputPath: string): string | null => { + const target = path.resolve(inputPath); + for (let cursor = target; ; cursor = path.dirname(cursor)) { + try { + fs.lstatSync(cursor); + const realCursor = safeRealpathSync(cursor, cache); + return realCursor ? path.resolve(realCursor, path.relative(cursor, target)) : null; + } catch { + if (cursor === path.dirname(cursor)) { + return null; + } + } + } + }; + const root = resolveProjectedPath(rootPath); + const target = resolveProjectedPath(targetPath); + return Boolean(root && target && isPathInside(root, target)); +} + +function hasStalePersistedPluginFiles(index: InstalledPluginIndex): boolean { + const realpathCache = new Map(); return index.plugins.some((plugin) => { - if (!plugin.enabled) { + if (!isContainedPluginPath(plugin.rootDir, plugin.rootDir, realpathCache)) { + return true; + } + if (!fs.existsSync(plugin.rootDir) && plugin.enabled) { + return true; + } + for (const artifactPath of [plugin.source, plugin.setupSource, plugin.manifestPath]) { + if (artifactPath && !isContainedPluginPath(plugin.rootDir, artifactPath, realpathCache)) { + return true; + } + } + if ( + plugin.enabled && + ((plugin.source ? !fs.existsSync(plugin.source) : false) || + (plugin.setupSource ? !fs.existsSync(plugin.setupSource) : false)) + ) { + return true; + } + if (!hasOptionalMissingPluginManifestFile(plugin)) { + if (!fs.existsSync(plugin.manifestPath)) { + if (plugin.enabled) { + return true; + } + } else if ( + !fileContentMatches(plugin.manifestPath, plugin.manifestHash, plugin.manifestFile) + ) { + return true; + } + } + if (!plugin.packageJson) { return false; } - return ( - !fs.existsSync(plugin.rootDir) || - (!hasOptionalMissingPluginManifestFile(plugin) && !fs.existsSync(plugin.manifestPath)) || - (plugin.source ? !fs.existsSync(plugin.source) : false) || - (plugin.setupSource ? !fs.existsSync(plugin.setupSource) : false) + const packageJsonPath = path.resolve(plugin.rootDir, plugin.packageJson.path); + if (!isContainedPluginPath(plugin.rootDir, packageJsonPath, realpathCache)) { + return true; + } + if (!fs.existsSync(packageJsonPath)) { + return plugin.enabled; + } + if (!isRealPathInside(plugin.rootDir, packageJsonPath, realpathCache)) { + return true; + } + return !fileContentMatches( + packageJsonPath, + plugin.packageJson.hash, + plugin.packageJson.fileSignature, + plugin.origin === "bundled", ); }); } -function hasMismatchedPersistedConfigPathPlugins( - index: InstalledPluginIndex, - params: LoadPluginRegistryParams, - env: NodeJS.ProcessEnv, - realpathCache: Map, -): boolean { - const loadPaths = normalizePluginsConfig(params.config?.plugins).loadPaths; - const discovery = discoverConfiguredPluginLoadPaths({ - loadPaths, - workspaceDir: params.workspaceDir, - env, - }); - const configuredRoots = loadPluginManifestRegistry({ - config: params.config, - workspaceDir: params.workspaceDir, - env, - candidates: discovery.candidates, - diagnostics: discovery.diagnostics, - installRecords: extractPluginInstallRecordsFromInstalledPluginIndex(index), - }).plugins.map((plugin) => resolveComparablePath(plugin.rootDir, realpathCache)); - const persistedRoots = index.plugins - .filter((plugin) => plugin.origin === "config") - .map((plugin) => resolveComparablePath(plugin.rootDir, realpathCache)); - if (configuredRoots.length !== persistedRoots.length) { - return true; - } - return configuredRoots.some((rootDir, position) => rootDir !== persistedRoots[position]); -} - -function resolveComparablePath(filePath: string, realpathCache: Map): string { - return safeRealpathSync(filePath, realpathCache) ?? path.resolve(filePath); -} - -function isRelativePathInsideOrEqual(relativePath: string): boolean { - return ( - relativePath === "" || - (relativePath !== ".." && - !relativePath.startsWith(`..${path.sep}`) && - !path.isAbsolute(relativePath)) - ); -} - -function isPathInsideOrEqual( - childPath: string, +function isRealPathInside( parentPath: string, - realpathCache: Map, + childPath: string, + cache: Map, ): boolean { - const relative = path.relative( - resolveComparablePath(parentPath, realpathCache), - resolveComparablePath(childPath, realpathCache), - ); - return isRelativePathInsideOrEqual(relative); + const parent = safeRealpathSync(parentPath, cache); + const child = safeRealpathSync(childPath, cache); + return Boolean(parent && child && isPathInside(parent, child)); } -function hasMismatchedPersistedBundledPluginRoot( +function hasMismatchedPersistedBundledRoot( index: InstalledPluginIndex, env: NodeJS.ProcessEnv, - realpathCache: Map, ): boolean { - const bundledPluginsDir = resolveBundledPluginsDir(env); - if (!bundledPluginsDir) { + const bundledRoot = resolveBundledPluginsDir(env); + if (!bundledRoot) { return false; } - let sourceOverlayDirs: string[] | undefined; + const realpathCache = new Map(); + const overlays = listBundledSourceOverlayDirs({ bundledRoot, env }); + const legacyRoot = buildLegacyBundledRootPath(bundledRoot); + const sourceCheckout = + legacyRoot && + fs.existsSync(path.join(path.dirname(legacyRoot), ".git")) && + fs.existsSync(path.join(path.dirname(legacyRoot), "pnpm-workspace.yaml")) && + fs.existsSync(path.join(path.dirname(legacyRoot), "src")); return index.plugins.some((plugin) => { if (plugin.origin !== "bundled") { return false; } - sourceOverlayDirs ??= listBundledSourceOverlayDirs({ - bundledRoot: bundledPluginsDir, - env, - }); - return !isAllowedPersistedBundledPluginRoot( - plugin, - bundledPluginsDir, - sourceOverlayDirs, - realpathCache, - ); - }); -} - -function isAllowedPersistedBundledPluginRoot( - plugin: InstalledPluginIndexRecord, - bundledPluginsDir: string, - sourceOverlayDirs: readonly string[], - realpathCache: Map, -): boolean { - const pluginRootDir = plugin.rootDir; - const legacyRoot = buildLegacyBundledRootPath(bundledPluginsDir); - if (isPathInsideOrEqual(pluginRootDir, bundledPluginsDir, realpathCache)) { - if (!legacyRoot || !isSourceCheckoutBundledPluginRoot(legacyRoot)) { - return true; - } - const relativePluginRoot = path.relative( - resolveComparablePath(bundledPluginsDir, realpathCache), - resolveComparablePath(pluginRootDir, realpathCache), - ); - return !sourcePluginOptsOutOfBundledDist(path.join(legacyRoot, relativePluginRoot)); - } - if ( - sourceOverlayDirs.some((overlayDir) => - isPathInsideOrEqual(pluginRootDir, overlayDir, realpathCache), - ) - ) { - return true; - } - if (!legacyRoot || !isSourceCheckoutBundledPluginRoot(legacyRoot)) { - return false; - } - const relativePluginRoot = path.relative( - resolveComparablePath(legacyRoot, realpathCache), - resolveComparablePath(pluginRootDir, realpathCache), - ); - if (!isRelativePathInsideOrEqual(relativePluginRoot)) { - return false; - } - if (plugin.packageBuild?.bundledDist === false) { - return true; - } - if (sourcePluginOptsOutOfBundledDist(path.join(legacyRoot, relativePluginRoot))) { - // Older index records lack packageBuild. Re-derive once so runtime loading - // and OpenClaw fingerprint the same source-only artifact. - return false; - } - // Discovery prefers a built plugin whenever the same child exists in the - // packaged root. Keep source-only bundled plugins, but invalidate stale - // source records once their built peer appears. - return !fs.existsSync(path.join(bundledPluginsDir, relativePluginRoot)); -} - -function sourcePluginOptsOutOfBundledDist(pluginRootDir: string): boolean { - const packageJson = tryReadJsonSync(path.join(pluginRootDir, "package.json")); - return getPackageManifestMetadata(packageJson ?? undefined)?.build?.bundledDist === false; -} - -function isSourceCheckoutBundledPluginRoot(extensionsDir: string): boolean { - const packageRoot = path.dirname(extensionsDir); - return ( - fs.existsSync(extensionsDir) && - fs.existsSync(path.join(packageRoot, ".git")) && - fs.existsSync(path.join(packageRoot, "pnpm-workspace.yaml")) && - fs.existsSync(path.join(packageRoot, "src")) - ); -} - -function hashExistingFile(filePath: string): string | null { - try { - return crypto.createHash("sha256").update(fs.readFileSync(filePath)).digest("hex"); - } catch { - return null; - } -} - -function resolveRecordPackageJsonPath( - plugin: InstalledPluginIndexRecord, - realpathCache: Map, -): string | null { - const packageJsonPath = plugin.packageJson?.path; - if (!packageJsonPath) { - return null; - } - const rootDir = plugin.rootDir || path.dirname(plugin.manifestPath); - const resolved = path.resolve(rootDir, packageJsonPath); - const relative = path.relative(rootDir, resolved); - if (!isRelativePathInsideOrEqual(relative)) { - return null; - } - const realRelative = path.relative( - resolveComparablePath(rootDir, realpathCache), - resolveComparablePath(resolved, realpathCache), - ); - return isRelativePathInsideOrEqual(realRelative) ? resolved : null; -} - -function hasStalePersistedPluginDiagnostics(index: InstalledPluginIndex): boolean { - return index.diagnostics.some((diag) => { - const source = diag.source; - return ( - typeof diag.pluginId === "string" && - diag.pluginId.trim().length > 0 && - typeof source === "string" && - path.isAbsolute(source) && - !fs.existsSync(source) - ); - }); -} - -function hasStalePersistedPluginMetadata( - index: InstalledPluginIndex, - realpathCache: Map, -): boolean { - return index.plugins.some((plugin) => { - if (!hasOptionalMissingPluginManifestFile(plugin)) { - const manifestSignatureMatches = fileSignatureMatches( - plugin.manifestPath, - plugin.manifestFile, + if (!plugin.enabled && !fs.existsSync(plugin.rootDir)) { + const allowedRoots = [bundledRoot, ...overlays, ...(legacyRoot ? [legacyRoot] : [])]; + return !allowedRoots.some((root) => + isContainedPluginPath(root, plugin.rootDir, realpathCache), ); - if (manifestSignatureMatches !== true) { - const manifestHash = hashExistingFile(plugin.manifestPath); - if (manifestHash && manifestHash !== plugin.manifestHash) { - return true; - } + } + if (isRealPathInside(bundledRoot, plugin.rootDir, realpathCache)) { + if (!sourceCheckout) { + return false; } + const resolvedBundledRoot = safeRealpathSync(bundledRoot, realpathCache) ?? bundledRoot; + const resolvedPluginRoot = safeRealpathSync(plugin.rootDir, realpathCache) ?? plugin.rootDir; + const sourcePackage = tryReadJsonSync( + path.join( + legacyRoot, + path.relative(resolvedBundledRoot, resolvedPluginRoot), + "package.json", + ), + ); + return getPackageManifestMetadata(sourcePackage ?? undefined)?.build?.bundledDist === false; } - const packageJsonPath = resolveRecordPackageJsonPath(plugin, realpathCache); - if (!plugin.packageJson?.hash) { - return false; - } - if (!packageJsonPath) { - return true; - } - const packageJsonSignatureMatches = fileSignatureMatches( - packageJsonPath, - plugin.packageJson.fileSignature, + return ( + !overlays.some((root) => isRealPathInside(root, plugin.rootDir, realpathCache)) && + !( + plugin.packageBuild?.bundledDist === false && + legacyRoot && + isRealPathInside(legacyRoot, plugin.rootDir, realpathCache) + ) ); - if (packageJsonSignatureMatches === true && plugin.origin === "bundled") { - return false; - } - if (packageJsonSignatureMatches === false) { - return hashExistingFile(packageJsonPath) !== plugin.packageJson.hash; - } - // Fast same-size rewrites can preserve observable stat fields on some filesystems. - const packageJsonHash = hashExistingFile(packageJsonPath); - return packageJsonHash !== plugin.packageJson.hash; }); } -function loadSnapshotInstallRecords(params: LoadPluginRegistryParams, env: NodeJS.ProcessEnv) { - return loadInstalledPluginIndexInstallRecordsSync({ +function hasRecoveredInstallRecordsMissingFromPersistedIndex( + index: InstalledPluginIndex, + params: LoadPluginRegistryParams, + env: NodeJS.ProcessEnv, +): boolean { + const installRecords = loadInstalledPluginIndexInstallRecordsSync({ env, ...(params.stateDir ? { stateDir: params.stateDir } : {}), ...(params.filePath @@ -454,28 +341,32 @@ function loadSnapshotInstallRecords(params: LoadPluginRegistryParams, env: NodeJ ? { filePath: params.pluginIndexFilePath } : {}), }); + const pluginIds = new Set(index.plugins.map((plugin) => plugin.pluginId)); + return Object.keys(installRecords).some( + (pluginId) => !index.installRecords?.[pluginId] || !pluginIds.has(pluginId), + ); } -function hasRecoveredInstallRecordsMissingFromPersistedIndex( +function requiresDerivedRegistryValidation( index: InstalledPluginIndex, - installRecords: ReturnType, + params: LoadPluginRegistryParams, env: NodeJS.ProcessEnv, + hasStalePluginFiles: () => boolean, ): boolean { - const persistedRecords = extractPluginInstallRecordsFromInstalledPluginIndex(index); - const persistedPluginIds = new Set(index.plugins.map((plugin) => plugin.pluginId)); - return Object.entries(installRecords).some(([pluginId, record]) => { - if (persistedRecords[pluginId] && persistedPluginIds.has(pluginId)) { - return false; - } - const installPaths = [record.installPath, record.sourcePath].filter( - (candidate): candidate is string => - typeof candidate === "string" && candidate.trim().length > 0, - ); - if (installPaths.length === 0) { - return true; - } - return installPaths.some((installPath) => fs.existsSync(resolveUserPath(installPath, env))); - }); + return ( + params.candidates !== undefined || + params.discovery !== undefined || + params.diagnostics !== undefined || + params.installRecords !== undefined || + normalizePluginsConfig(params.config?.plugins).loadPaths.length > 0 || + hasMissingConfigPathActivationMetadata(index) || + index.diagnostics.some(({ pluginId, source }) => + Boolean(pluginId && source && path.isAbsolute(source) && !fs.existsSync(source)), + ) || + hasMismatchedPersistedBundledRoot(index, env) || + hasStalePluginFiles() || + hasRecoveredInstallRecordsMissingFromPersistedIndex(index, params, env) + ); } export function loadPluginRegistrySnapshotWithMetadata( @@ -494,96 +385,117 @@ export function loadPluginRegistrySnapshotWithMetadata( } const env = params.env ?? process.env; - const memoKey = resolvePluginRegistrySnapshotMemoKey(params, env); - const memo = findPluginRegistrySnapshotMemo(memoKey); - if (memo) { - return memo; - } - // Bound canonical paths to this registry build; lifecycle changes must - // never reuse security-sensitive symlink or plugin-root resolutions. - const realpathCache = new Map(); - const diagnostics: PluginRegistrySnapshotDiagnostic[] = []; const persistedReadsEnabled = params.preferPersisted !== false; - const pushStaleSourceDiagnostic = (message: string): void => { - diagnostics.push({ level: "warn", code: "persisted-registry-stale-source", message }); - }; - if (persistedReadsEnabled) { - const persistedIndex = readPersistedInstalledPluginIndexSync(params); - if (persistedIndex) { - if ( - params.config && - persistedIndex.policyHash !== resolveInstalledPluginIndexPolicyHash(params.config) - ) { - diagnostics.push({ - level: "warn", - code: "persisted-registry-stale-policy", - message: - "Persisted plugin registry policy does not match current config; using derived plugin index. Run `openclaw plugins registry --refresh` to update the persisted registry.", - }); - } else if (hasMissingPersistedPluginSource(persistedIndex)) { - pushStaleSourceDiagnostic( - "Persisted plugin registry points at missing plugin files; using derived plugin index. Run `openclaw plugins registry --refresh` to update the persisted registry.", - ); - } else if (hasMismatchedPersistedBundledPluginRoot(persistedIndex, env, realpathCache)) { - pushStaleSourceDiagnostic( - "Persisted plugin registry points at a different bundled plugin tree; using derived plugin index. Run `openclaw plugins registry --refresh` to update the persisted registry.", - ); - } else if ( - hasMismatchedPersistedConfigPathPlugins(persistedIndex, params, env, realpathCache) - ) { - pushStaleSourceDiagnostic( - "Persisted plugin registry does not match configured load-path plugins; using derived plugin index. Run `openclaw plugins registry --refresh` to update the persisted registry.", - ); - } else if (hasStalePersistedPluginDiagnostics(persistedIndex)) { - pushStaleSourceDiagnostic( - "Persisted plugin registry contains diagnostics referencing missing paths; using derived plugin index. Run `openclaw plugins registry --refresh` to update the persisted registry.", - ); - } else if (hasMissingConfigPathActivationMetadata(persistedIndex)) { - pushStaleSourceDiagnostic( - "Persisted plugin registry is missing config-path startup metadata; using derived plugin index. Run `openclaw plugins registry --refresh` to update the persisted registry.", - ); - } else if (hasStalePersistedPluginMetadata(persistedIndex, realpathCache)) { - pushStaleSourceDiagnostic( - "Persisted plugin registry metadata no longer matches plugin manifest or package files; using derived plugin index. Run `openclaw plugins registry --refresh` to update the persisted registry.", - ); - } else if ( - hasRecoveredInstallRecordsMissingFromPersistedIndex( - persistedIndex, - loadSnapshotInstallRecords(params, env), - env, - ) - ) { - pushStaleSourceDiagnostic( - "Persisted plugin registry is missing recoverable managed npm plugins; using derived plugin index. Run `openclaw plugins registry --refresh` to update the persisted registry.", - ); - } else { - const persistedResult: PluginRegistrySnapshotResult = { - snapshot: persistedIndex, - source: "persisted", - diagnostics, - }; - return rememberPluginRegistrySnapshotMemo(memoKey, persistedResult); - } - } else { - diagnostics.push({ - level: "info", - code: "persisted-registry-missing", - message: "Persisted plugin registry is missing or invalid; using derived plugin index.", - }); - } + if (!persistedReadsEnabled) { + const derived = loadInstalledPluginIndexWithDiscovery({ + ...params, + installRecords: params.installRecords ?? {}, + }); + return { + snapshot: derived.index, + source: "derived", + diagnostics: [], + discovery: derived.discovery, + manifestRegistry: derived.manifestRegistry, + }; + } + + const diagnostics: PluginRegistrySnapshotDiagnostic[] = []; + const persistedIndex = readPersistedInstalledPluginIndexSync(params); + let stalePluginFiles: boolean | undefined; + const hasStalePluginFiles = () => + (stalePluginFiles ??= persistedIndex ? hasStalePersistedPluginFiles(persistedIndex) : false); + if (!persistedIndex) { + diagnostics.push({ + level: "info", + code: "persisted-registry-missing", + message: "Persisted plugin registry is missing or invalid; using derived plugin index.", + }); + } else if ( + params.config && + persistedIndex.policyHash !== resolveInstalledPluginIndexPolicyHash(params.config) + ) { + diagnostics.push({ + level: "warn", + code: "persisted-registry-stale-policy", + message: + "Persisted plugin registry policy does not match current config; using derived plugin index. Run `openclaw plugins registry --refresh` to update the persisted registry.", + }); + } else if (!requiresDerivedRegistryValidation(persistedIndex, params, env, hasStalePluginFiles)) { + return { + snapshot: persistedIndex, + source: "persisted", + diagnostics, + }; } const derived = loadInstalledPluginIndexWithDiscovery({ ...params, - installRecords: persistedReadsEnabled ? params.installRecords : (params.installRecords ?? {}), + ...(params.filePath && !params.pluginIndexFilePath + ? { pluginIndexFilePath: params.filePath } + : {}), }); - return rememberPluginRegistrySnapshotMemo(memoKey, { + const comparePackageJsonPath = + params.candidates !== undefined || params.discovery !== undefined || hasStalePluginFiles(); + const excludedMissingDisabledPlugins = new Map(); + if ( + persistedIndex && + params.candidates === undefined && + params.discovery === undefined && + params.installRecords === undefined && + !hasStalePluginFiles() && + !hasMismatchedPersistedBundledRoot(persistedIndex, env) + ) { + const derivedPluginIds = new Set(derived.index.plugins.map((plugin) => plugin.pluginId)); + for (const plugin of persistedIndex.plugins) { + if (!plugin.enabled && !derivedPluginIds.has(plugin.pluginId)) { + excludedMissingDisabledPlugins.set(plugin.pluginId, plugin.rootDir); + } + } + } + const contentMatches = + persistedIndex && + diagnostics.length === 0 && + isDeepStrictEqual( + resolvePluginRegistryContent( + persistedIndex, + comparePackageJsonPath, + excludedMissingDisabledPlugins, + ), + resolvePluginRegistryContent( + derived.index, + comparePackageJsonPath, + excludedMissingDisabledPlugins, + ), + ); + if (persistedIndex && contentMatches) { + const packageMetadataMatches = isDeepStrictEqual( + resolvePluginRegistryContent(persistedIndex, true), + resolvePluginRegistryContent(derived.index, true), + ); + return { + snapshot: persistedIndex, + source: "persisted", + diagnostics, + discovery: derived.discovery, + ...(packageMetadataMatches ? { manifestRegistry: derived.manifestRegistry } : {}), + }; + } else if (persistedIndex && diagnostics.length === 0) { + diagnostics.push({ + level: "warn", + code: "persisted-registry-stale-source", + message: + "Persisted plugin registry no longer matches current plugin discovery or metadata; using derived plugin index. Run `openclaw plugins registry --refresh` to update the persisted registry.", + }); + } + + return { snapshot: derived.index, source: "derived", diagnostics, discovery: derived.discovery, manifestRegistry: derived.manifestRegistry, - }); + }; } function resolveSnapshot(params: LoadPluginRegistryParams = {}): PluginRegistrySnapshot { @@ -595,6 +507,7 @@ export function loadPluginRegistrySnapshot( ): PluginRegistrySnapshot { return resolveSnapshot(params); } + export function getPluginRecord(params: GetPluginRecordParams): PluginRegistryRecord | undefined { return getInstalledPluginRecord(resolveSnapshot(params), params.pluginId); } diff --git a/src/plugins/plugin-registry.test.ts b/src/plugins/plugin-registry.test.ts index 5dc51b2f8bc1..7bfd7dcdcd66 100644 --- a/src/plugins/plugin-registry.test.ts +++ b/src/plugins/plugin-registry.test.ts @@ -4,10 +4,7 @@ import fs from "node:fs"; import path from "node:path"; import { expectDefined } from "@openclaw/normalization-core"; import { afterEach, describe, expect, it, vi } from "vitest"; -import { - closeOpenClawStateDatabaseForTest, - runOpenClawStateWriteTransaction, -} from "../state/openclaw-state-db.js"; +import { closeOpenClawStateDatabaseForTest } from "../state/openclaw-state-db.js"; import type { PluginCandidate } from "./discovery.js"; import { readPersistedInstalledPluginIndex, @@ -169,15 +166,6 @@ function createIndex( }; } -function createPersistableIndex(pluginId: string): InstalledPluginIndex { - const index = createIndex(pluginId); - const plugins = index.plugins.map((plugin) => Object.assign({}, plugin, { enabled: false })); - return { - ...index, - plugins, - }; -} - function requireRecord(value: unknown, label: string): Record { if (!value || typeof value !== "object") { throw new Error(`expected ${label}`); @@ -330,6 +318,29 @@ describe("plugin registry facade", () => { ).toEqual(["demo"]); }); + it("keeps missing disabled records inspectable from the persisted registry", async () => { + const stateDir = makeTempDir(); + const rootDir = makeTempDir(); + const config = { plugins: { entries: { demo: { enabled: false } } } }; + const env = hermeticEnv(); + const persisted = loadPluginRegistrySnapshot({ + candidates: [createCandidate(rootDir)], + config, + env, + preferPersisted: false, + }); + await writePersistedInstalledPluginIndex(persisted, { stateDir }); + fs.rmSync(rootDir, { recursive: true }); + + const result = loadPluginRegistrySnapshotWithMetadata({ stateDir, config, env }); + + expect(result.source).toBe("persisted"); + expectPluginRecordFields(getPluginRecord({ index: result.snapshot, pluginId: "demo" }), { + pluginId: "demo", + enabled: false, + }); + }); + it("resolves contribution owners from a plugin lookup table without rereading manifests", () => { const rootDir = makeTempDir(); const candidate = createCandidate(rootDir); @@ -471,7 +482,7 @@ describe("plugin registry facade", () => { expect(normalizedConfig.allow).toEqual(["demo"]); }); - it("reads the persisted registry before deriving from discovered candidates", async () => { + it("treats explicit discovered candidates as authoritative", async () => { const stateDir = makeTempDir(); const rootDir = makeTempDir(); const persistedRootDir = makeTempDir(); @@ -509,13 +520,70 @@ describe("plugin registry facade", () => { env: hermeticEnv(), }); - expect(result.source).toBe("persisted"); - expect(result.diagnostics).toStrictEqual([]); + expect(result.source).toBe("derived"); + expectDiagnosticCodes(result.diagnostics, ["persisted-registry-stale-source"]); expect(listPluginRecords({ index: result.snapshot }).map((plugin) => plugin.pluginId)).toEqual([ - "persisted", + "demo", ]); }); + it("keeps content-equivalent timestamp changes on the persisted path", async () => { + const stateDir = makeTempDir(); + const rootDir = makeTempDir(); + const env = hermeticEnv(); + const persisted = loadPluginRegistrySnapshot({ + candidates: [createCandidate(rootDir)], + env, + preferPersisted: false, + }); + await writePersistedInstalledPluginIndex( + { + ...persisted, + plugins: [ + { + ...expectDefined(persisted.plugins[0], "persisted plugin test invariant"), + syntheticAuthRefs: ["demo"], + }, + ...persisted.plugins.slice(1), + ], + }, + { stateDir }, + ); + const manifestPath = path.join(rootDir, "openclaw.plugin.json"); + const future = new Date(Date.now() + 1_000); + fs.utimesSync(manifestPath, future, future); + + const result = loadPluginRegistrySnapshotWithMetadata({ stateDir, env }); + + expect(result.source).toBe("persisted"); + expect(result.snapshot.plugins[0]?.syntheticAuthRefs).toEqual(["demo"]); + }); + + it("reads install records from a custom SQLite registry path", async () => { + const tempDir = makeTempDir(); + const rootDir = makeTempDir(); + const filePath = path.join(tempDir, "custom-registry.sqlite"); + const env = hermeticEnv(); + const persisted = loadPluginRegistrySnapshot({ + candidates: [createCandidate(rootDir)], + env, + preferPersisted: false, + }); + persisted.installRecords = { + demo: { source: "npm", spec: "demo@1.0.0", installPath: rootDir }, + }; + await writePersistedInstalledPluginIndex(persisted, { filePath }); + + const result = loadPluginRegistrySnapshotWithMetadata({ filePath, env }); + + expect(result.source).toBe("persisted"); + expectInstallRecord(result.snapshot.installRecords, "demo", { + source: "npm", + spec: "demo@1.0.0", + installPath: rootDir, + }); + }); + it("falls back to the derived registry when persisted source paths are missing", async () => { const stateDir = makeTempDir(); const rootDir = makeTempDir(); @@ -819,7 +887,7 @@ describe("plugin registry facade", () => { expectSnapshotPluginIds(result.snapshot, ["demo"]); }); - it("reuses config-scoped derived registries within the process", () => { + it("derives config-scoped registries for cold callers", () => { const stateDir = makeTempDir(); const workspaceDir = makeTempDir(); const bundledRoot = makeTempDir(); @@ -853,7 +921,7 @@ describe("plugin registry facade", () => { expect(first.source).toBe("derived"); expect(second.source).toBe("derived"); expect(manifestReadsAfterFirst).toBeGreaterThan(0); - expect(manifestReadsAfterSecond).toBe(manifestReadsAfterFirst); + expect(manifestReadsAfterSecond).toBeGreaterThan(manifestReadsAfterFirst); }); it("reloads profile extensions after the metadata lifecycle is cleared", () => { @@ -881,7 +949,7 @@ describe("plugin registry facade", () => { expectSnapshotPluginIds(second.snapshot, ["first", "second"]); }); - it("keys the process registry memo by resolved host contract version", () => { + it("derives the resolved host contract version", () => { const stateDir = makeTempDir(); const bundledRoot = makeTempDir(); const rootDir = path.join(bundledRoot, "demo"); @@ -907,56 +975,6 @@ describe("plugin registry facade", () => { expect(second.snapshot.hostContractVersion).toBe("2026.4.26"); }); - it("clears the process registry memo after persisted registry writes", async () => { - const stateDir = makeTempDir(); - const env = hermeticEnv(); - await writePersistedInstalledPluginIndex(createPersistableIndex("first"), { stateDir }); - - const first = loadPluginRegistrySnapshotWithMetadata({ stateDir, env }); - await writePersistedInstalledPluginIndex(createPersistableIndex("second"), { stateDir }); - const second = loadPluginRegistrySnapshotWithMetadata({ stateDir, env }); - - expect(first.source).toBe("persisted"); - expect(second.source).toBe("persisted"); - expectSnapshotPluginIds(first.snapshot, ["first"]); - expectSnapshotPluginIds(second.snapshot, ["second"]); - }); - - it("reloads externally changed persisted state after the metadata lifecycle is cleared", async () => { - const stateDir = makeTempDir(); - const env = hermeticEnv(); - await writePersistedInstalledPluginIndex(createPersistableIndex("first"), { stateDir }); - const first = loadPluginRegistrySnapshotWithMetadata({ stateDir, env }); - const external = createPersistableIndex("second-external"); - runOpenClawStateWriteTransaction( - ({ db }) => { - db.prepare( - ` - UPDATE installed_plugin_index - SET plugins_json = ?, - install_records_json = ?, - diagnostics_json = ?, - updated_at_ms = ? - WHERE index_key = 'installed-plugin-index' - `, - ).run( - JSON.stringify(external.plugins), - JSON.stringify(external.installRecords), - JSON.stringify(external.diagnostics), - Date.now(), - ); - }, - { env: { ...env, OPENCLAW_STATE_DIR: stateDir } }, - ); - clearPluginMetadataLifecycleCaches(); - const second = loadPluginRegistrySnapshotWithMetadata({ stateDir, env }); - - expect(first.source).toBe("persisted"); - expect(second.source).toBe("persisted"); - expectSnapshotPluginIds(first.snapshot, ["first"]); - expectSnapshotPluginIds(second.snapshot, ["second-external"]); - }); - it("derives a fresh registry without persisted install records when caller disables persisted reads", async () => { const stateDir = makeTempDir(); const rootDir = makeTempDir(); diff --git a/src/secrets/provider-integrations.test.ts b/src/secrets/provider-integrations.test.ts index 458020d3faf0..4d81aa736fe3 100644 --- a/src/secrets/provider-integrations.test.ts +++ b/src/secrets/provider-integrations.test.ts @@ -35,6 +35,22 @@ function writeSecureFile(file: string, contents: string): void { fs.chmodSync(file, 0o600); } +function writePluginManifest(rootDir: string, manifest: Record): void { + fs.writeFileSync(path.join(rootDir, "index.ts"), "export default {};\n", "utf8"); + fs.writeFileSync( + path.join(rootDir, "openclaw.plugin.json"), + JSON.stringify({ + ...manifest, + configSchema: { + type: "object", + additionalProperties: false, + properties: {}, + }, + }), + "utf8", + ); +} + function createCandidate( rootDir: string, idHint: string, @@ -48,6 +64,16 @@ function createCandidate( }; } +function loadTestRegistry( + rootDir: string, + idHint: string, + origin: PluginOrigin = "global", +): PluginManifestRegistry { + return loadPluginManifestRegistry({ + candidates: [createCandidate(rootDir, idHint, origin)], + }); +} + function pluginIntegrationProviderConfig(pluginId: string, integrationId: string) { return { source: "exec" as const, @@ -67,45 +93,33 @@ afterEach(() => { describe("secret provider integration presets", () => { it("materializes plugin manifest exec providers without provider-specific core code", () => { const rootDir = makeTempDir(); - fs.writeFileSync(path.join(rootDir, "index.ts"), "export default {};\n", "utf8"); makeSecureDir(path.join(rootDir, "bin")); writeSecureFile(path.join(rootDir, "bin", "resolve.mjs"), "process.stdin.resume();\n"); - fs.writeFileSync( - path.join(rootDir, "openclaw.plugin.json"), - JSON.stringify({ - id: "acme-secrets", - name: "Acme Secrets", - secretProviderIntegrations: { - acme: { - providerAlias: "acme", - displayName: "Acme Vault", - description: "Acme exec resolver", - source: "exec", - command: "${node}", - args: ["./bin/resolve.mjs", "--profile", "work"], - timeoutMs: 3000, - noOutputTimeoutMs: 3000, - maxOutputBytes: 4096, - passEnv: ["HOME"], - env: { - ACME_PROFILE: "work", - }, - jsonOnly: false, + writePluginManifest(rootDir, { + id: "acme-secrets", + name: "Acme Secrets", + secretProviderIntegrations: { + acme: { + providerAlias: "acme", + displayName: "Acme Vault", + description: "Acme exec resolver", + source: "exec", + command: "${node}", + args: ["./bin/resolve.mjs", "--profile", "work"], + timeoutMs: 3000, + noOutputTimeoutMs: 3000, + maxOutputBytes: 4096, + passEnv: ["HOME"], + env: { + ACME_PROFILE: "work", }, + jsonOnly: false, }, - configSchema: { - type: "object", - additionalProperties: false, - properties: {}, - }, - }), - "utf8", - ); - - const registry = loadPluginManifestRegistry({ - candidates: [createCandidate(rootDir, "acme-secrets")], + }, }); + const registry = loadTestRegistry(rootDir, "acme-secrets"); + expect(registry.diagnostics).toEqual([]); expect(listSecretProviderIntegrationPresets({ manifestRegistry: registry })).toEqual([ { @@ -144,36 +158,24 @@ describe("secret provider integration presets", () => { it("normalizes manifest exec provider options to SecretRef provider schema limits", () => { const rootDir = makeTempDir(); - fs.writeFileSync(path.join(rootDir, "index.ts"), "export default {};\n", "utf8"); writeSecureFile(path.join(rootDir, "resolve.mjs"), "process.stdin.resume();\n"); - fs.writeFileSync( - path.join(rootDir, "openclaw.plugin.json"), - JSON.stringify({ - id: "bounded-secrets", - secretProviderIntegrations: { - bounded: { - source: "exec", - command: "${node}", - args: ["./resolve.mjs", "ok", "x".repeat(1025)], - timeoutMs: 120001, - noOutputTimeoutMs: 1.5, - maxOutputBytes: 20 * 1024 * 1024 + 1, - passEnv: ["GOOD_ENV", "bad-env"], - }, + writePluginManifest(rootDir, { + id: "bounded-secrets", + secretProviderIntegrations: { + bounded: { + source: "exec", + command: "${node}", + args: ["./resolve.mjs", "ok", "x".repeat(1025)], + timeoutMs: 120001, + noOutputTimeoutMs: 1.5, + maxOutputBytes: 20 * 1024 * 1024 + 1, + passEnv: ["GOOD_ENV", "bad-env"], }, - configSchema: { - type: "object", - additionalProperties: false, - properties: {}, - }, - }), - "utf8", - ); - - const registry = loadPluginManifestRegistry({ - candidates: [createCandidate(rootDir, "bounded-secrets")], + }, }); + const registry = loadTestRegistry(rootDir, "bounded-secrets"); + expect(listSecretProviderIntegrationPresets({ manifestRegistry: registry })).toEqual([ { id: "bounded", @@ -203,31 +205,19 @@ describe("secret provider integration presets", () => { it("skips presets whose provider alias cannot be used as a SecretRef provider", () => { const rootDir = makeTempDir(); - fs.writeFileSync(path.join(rootDir, "index.ts"), "export default {};\n", "utf8"); - fs.writeFileSync( - path.join(rootDir, "openclaw.plugin.json"), - JSON.stringify({ - id: "bad-secrets", - secretProviderIntegrations: { - bad: { - providerAlias: "../bad", - source: "exec", - command: "${node}", - }, + writePluginManifest(rootDir, { + id: "bad-secrets", + secretProviderIntegrations: { + bad: { + providerAlias: "../bad", + source: "exec", + command: "${node}", }, - configSchema: { - type: "object", - additionalProperties: false, - properties: {}, - }, - }), - "utf8", - ); - - const registry = loadPluginManifestRegistry({ - candidates: [createCandidate(rootDir, "bad-secrets")], + }, }); + const registry = loadTestRegistry(rootDir, "bad-secrets"); + expect(listSecretProviderIntegrationPresets({ manifestRegistry: registry })).toEqual([]); }); @@ -236,54 +226,34 @@ describe("secret provider integration presets", () => { const longPluginRootDir = makeTempDir(); const longPluginId = `plugin-${"x".repeat(129)}`; const longIntegrationId = `integration-${"x".repeat(129)}`; - fs.writeFileSync(path.join(rootDir, "index.ts"), "export default {};\n", "utf8"); fs.writeFileSync(path.join(rootDir, "resolve.mjs"), "process.stdin.resume();\n", "utf8"); - fs.writeFileSync(path.join(longPluginRootDir, "index.ts"), "export default {};\n", "utf8"); fs.writeFileSync( path.join(longPluginRootDir, "resolve.mjs"), "process.stdin.resume();\n", "utf8", ); - fs.writeFileSync( - path.join(rootDir, "openclaw.plugin.json"), - JSON.stringify({ - id: "long-integration-secrets", - secretProviderIntegrations: { - [longIntegrationId]: { - providerAlias: "short-alias", - source: "exec", - command: "${node}", - args: ["./resolve.mjs"], - }, + writePluginManifest(rootDir, { + id: "long-integration-secrets", + secretProviderIntegrations: { + [longIntegrationId]: { + providerAlias: "short-alias", + source: "exec", + command: "${node}", + args: ["./resolve.mjs"], }, - configSchema: { - type: "object", - additionalProperties: false, - properties: {}, + }, + }); + writePluginManifest(longPluginRootDir, { + id: longPluginId, + secretProviderIntegrations: { + vault: { + providerAlias: "short-plugin-alias", + source: "exec", + command: "${node}", + args: ["./resolve.mjs"], }, - }), - "utf8", - ); - fs.writeFileSync( - path.join(longPluginRootDir, "openclaw.plugin.json"), - JSON.stringify({ - id: longPluginId, - secretProviderIntegrations: { - vault: { - providerAlias: "short-plugin-alias", - source: "exec", - command: "${node}", - args: ["./resolve.mjs"], - }, - }, - configSchema: { - type: "object", - additionalProperties: false, - properties: {}, - }, - }), - "utf8", - ); + }, + }); const registry = loadPluginManifestRegistry({ candidates: [ @@ -299,61 +269,39 @@ describe("secret provider integration presets", () => { "skips non-node manifest preset commands for %s plugin roots", (origin) => { const rootDir = makeTempDir(); - fs.writeFileSync(path.join(rootDir, "index.ts"), "export default {};\n", "utf8"); fs.mkdirSync(path.join(rootDir, "bin")); - fs.writeFileSync( - path.join(rootDir, "openclaw.plugin.json"), - JSON.stringify({ - id: `${origin}-secrets`, - ...(origin === "bundled" ? { enabledByDefault: true } : {}), - secretProviderIntegrations: { - vault: { - providerAlias: "vault", - source: "exec", - command: "./bin/vault-resolver", - }, + writePluginManifest(rootDir, { + id: `${origin}-secrets`, + ...(origin === "bundled" ? { enabledByDefault: true } : {}), + secretProviderIntegrations: { + vault: { + providerAlias: "vault", + source: "exec", + command: "./bin/vault-resolver", }, - configSchema: { - type: "object", - additionalProperties: false, - properties: {}, - }, - }), - "utf8", - ); - - const registry = loadPluginManifestRegistry({ - candidates: [createCandidate(rootDir, `${origin}-secrets`, origin)], + }, }); + const registry = loadTestRegistry(rootDir, `${origin}-secrets`, origin); + expect(listSecretProviderIntegrationPresets({ manifestRegistry: registry })).toEqual([]); }, ); it("skips presets from disabled installed plugins", () => { const rootDir = makeTempDir(); - fs.writeFileSync(path.join(rootDir, "index.ts"), "export default {};\n", "utf8"); writeSecureFile(path.join(rootDir, "resolve.mjs"), "process.stdin.resume();\n"); - fs.writeFileSync( - path.join(rootDir, "openclaw.plugin.json"), - JSON.stringify({ - id: "disabled-secrets", - secretProviderIntegrations: { - vault: { - providerAlias: "vault", - source: "exec", - command: "${node}", - args: ["./resolve.mjs"], - }, + writePluginManifest(rootDir, { + id: "disabled-secrets", + secretProviderIntegrations: { + vault: { + providerAlias: "vault", + source: "exec", + command: "${node}", + args: ["./resolve.mjs"], }, - configSchema: { - type: "object", - additionalProperties: false, - properties: {}, - }, - }), - "utf8", - ); + }, + }); const registry = loadPluginManifestRegistry({ candidates: [createCandidate(rootDir, "disabled-secrets", "global")], @@ -386,28 +334,18 @@ describe("secret provider integration presets", () => { it("applies plugin id aliases when filtering disabled presets", () => { const rootDir = makeTempDir(); - fs.writeFileSync(path.join(rootDir, "index.ts"), "export default {};\n", "utf8"); writeSecureFile(path.join(rootDir, "resolve.mjs"), "process.stdin.resume();\n"); - fs.writeFileSync( - path.join(rootDir, "openclaw.plugin.json"), - JSON.stringify({ - id: "openai", - secretProviderIntegrations: { - vault: { - providerAlias: "vault", - source: "exec", - command: "${node}", - args: ["./resolve.mjs"], - }, + writePluginManifest(rootDir, { + id: "openai", + secretProviderIntegrations: { + vault: { + providerAlias: "vault", + source: "exec", + command: "${node}", + args: ["./resolve.mjs"], }, - configSchema: { - type: "object", - additionalProperties: false, - properties: {}, - }, - }), - "utf8", - ); + }, + }); const config = { plugins: { entries: { @@ -429,32 +367,20 @@ describe("secret provider integration presets", () => { it("exposes bundled presets enabled by platform default", () => { const rootDir = makeTempDir(); - fs.writeFileSync(path.join(rootDir, "index.ts"), "export default {};\n", "utf8"); writeSecureFile(path.join(rootDir, "resolve.mjs"), "process.stdin.resume();\n"); - fs.writeFileSync( - path.join(rootDir, "openclaw.plugin.json"), - JSON.stringify({ - id: "platform-secrets", - enabledByDefaultOnPlatforms: [process.platform], - secretProviderIntegrations: { - vault: { - providerAlias: "vault", - source: "exec", - command: "${node}", - args: ["./resolve.mjs"], - }, + writePluginManifest(rootDir, { + id: "platform-secrets", + enabledByDefaultOnPlatforms: [process.platform], + secretProviderIntegrations: { + vault: { + providerAlias: "vault", + source: "exec", + command: "${node}", + args: ["./resolve.mjs"], }, - configSchema: { - type: "object", - additionalProperties: false, - properties: {}, - }, - }), - "utf8", - ); - const registry = loadPluginManifestRegistry({ - candidates: [createCandidate(rootDir, "platform-secrets", "bundled")], + }, }); + const registry = loadTestRegistry(rootDir, "platform-secrets", "bundled"); expect(listSecretProviderIntegrationPresets({ manifestRegistry: registry })).toEqual([ { @@ -473,33 +399,21 @@ describe("secret provider integration presets", () => { const rootDir = makeTempDir(); const linkParent = makeTempDir(); const linkRoot = path.join(linkParent, "plugin-link"); - fs.writeFileSync(path.join(rootDir, "index.ts"), "export default {};\n", "utf8"); writeSecureFile(path.join(rootDir, "resolve.mjs"), "process.stdin.resume();\n"); - fs.writeFileSync( - path.join(rootDir, "openclaw.plugin.json"), - JSON.stringify({ - id: "linked-secrets", - secretProviderIntegrations: { - vault: { - providerAlias: "vault", - source: "exec", - command: "${node}", - args: ["./resolve.mjs"], - }, + writePluginManifest(rootDir, { + id: "linked-secrets", + secretProviderIntegrations: { + vault: { + providerAlias: "vault", + source: "exec", + command: "${node}", + args: ["./resolve.mjs"], }, - configSchema: { - type: "object", - additionalProperties: false, - properties: {}, - }, - }), - "utf8", - ); + }, + }); fs.symlinkSync(rootDir, linkRoot); - const registry = loadPluginManifestRegistry({ - candidates: [createCandidate(linkRoot, "linked-secrets", "global")], - }); + const registry = loadTestRegistry(linkRoot, "linked-secrets", "global"); expect(listSecretProviderIntegrationPresets({ manifestRegistry: registry })).toEqual([ { @@ -517,32 +431,20 @@ describe("secret provider integration presets", () => { "skips secret provider presets from %s plugin roots", (origin) => { const rootDir = makeTempDir(); - fs.writeFileSync(path.join(rootDir, "index.ts"), "export default {};\n", "utf8"); - fs.writeFileSync( - path.join(rootDir, "openclaw.plugin.json"), - JSON.stringify({ - id: `${origin}-secrets`, - secretProviderIntegrations: { - vault: { - providerAlias: "vault", - source: "exec", - command: "${node}", - args: ["./resolve.mjs"], - }, + writePluginManifest(rootDir, { + id: `${origin}-secrets`, + secretProviderIntegrations: { + vault: { + providerAlias: "vault", + source: "exec", + command: "${node}", + args: ["./resolve.mjs"], }, - configSchema: { - type: "object", - additionalProperties: false, - properties: {}, - }, - }), - "utf8", - ); - - const registry = loadPluginManifestRegistry({ - candidates: [createCandidate(rootDir, `${origin}-secrets`, origin)], + }, }); + const registry = loadTestRegistry(rootDir, `${origin}-secrets`, origin); + expect(listSecretProviderIntegrationPresets({ manifestRegistry: registry })).toEqual([]); }, ); @@ -550,7 +452,6 @@ describe("secret provider integration presets", () => { it("resolves a node-based plugin preset with plugin trusted dirs", async () => { const rootDir = makeTempDir(); const resolverPath = path.join(rootDir, "bin", "resolve.mjs"); - fs.writeFileSync(path.join(rootDir, "index.ts"), "export default {};\n", "utf8"); makeSecureDir(path.dirname(resolverPath)); writeSecureFile( resolverPath, @@ -565,32 +466,21 @@ describe("secret provider integration presets", () => { "});", ].join("\n"), ); - fs.writeFileSync( - path.join(rootDir, "openclaw.plugin.json"), - JSON.stringify({ - id: "vault-secrets", - secretProviderIntegrations: { - vault: { - providerAlias: "vault", - source: "exec", - command: "${node}", - args: ["./bin/resolve.mjs"], - allowInsecurePath: true, - }, + writePluginManifest(rootDir, { + id: "vault-secrets", + secretProviderIntegrations: { + vault: { + providerAlias: "vault", + source: "exec", + command: "${node}", + args: ["./bin/resolve.mjs"], + allowInsecurePath: true, }, - configSchema: { - type: "object", - additionalProperties: false, - properties: {}, - }, - }), - "utf8", - ); + }, + }); await withSecureTestNodeExecPath(async () => { - const registry = loadPluginManifestRegistry({ - candidates: [createCandidate(rootDir, "vault-secrets", "global")], - }); + const registry = loadTestRegistry(rootDir, "vault-secrets", "global"); const [preset] = listSecretProviderIntegrationPresets({ manifestRegistry: registry }); if (!preset) { throw new Error("Expected vault preset"); @@ -624,28 +514,18 @@ describe("secret provider integration presets", () => { it("fails closed when a plugin-managed provider is disabled", async () => { const rootDir = makeTempDir(); const resolverPath = path.join(rootDir, "resolve.mjs"); - fs.writeFileSync(path.join(rootDir, "index.ts"), "export default {};\n", "utf8"); fs.writeFileSync(resolverPath, "process.stdin.resume();\n", "utf8"); - fs.writeFileSync( - path.join(rootDir, "openclaw.plugin.json"), - JSON.stringify({ - id: "revoked-secrets", - secretProviderIntegrations: { - vault: { - providerAlias: "vault", - source: "exec", - command: "${node}", - args: ["./resolve.mjs"], - }, + writePluginManifest(rootDir, { + id: "revoked-secrets", + secretProviderIntegrations: { + vault: { + providerAlias: "vault", + source: "exec", + command: "${node}", + args: ["./resolve.mjs"], }, - configSchema: { - type: "object", - additionalProperties: false, - properties: {}, - }, - }), - "utf8", - ); + }, + }); const config = { plugins: { entries: { @@ -715,31 +595,19 @@ describe("secret provider integration presets", () => { it("skips node presets without a plugin-root relative entrypoint arg", () => { const rootDir = makeTempDir(); - fs.writeFileSync(path.join(rootDir, "index.ts"), "export default {};\n", "utf8"); - fs.writeFileSync( - path.join(rootDir, "openclaw.plugin.json"), - JSON.stringify({ - id: "bad-trust-secrets", - secretProviderIntegrations: { - bad: { - source: "exec", - command: "${node}", - args: ["--import", "./bin/hook.mjs", "./bin/resolve.mjs"], - }, + writePluginManifest(rootDir, { + id: "bad-trust-secrets", + secretProviderIntegrations: { + bad: { + source: "exec", + command: "${node}", + args: ["--import", "./bin/hook.mjs", "./bin/resolve.mjs"], }, - configSchema: { - type: "object", - additionalProperties: false, - properties: {}, - }, - }), - "utf8", - ); - - const registry = loadPluginManifestRegistry({ - candidates: [createCandidate(rootDir, "bad-trust-secrets")], + }, }); + const registry = loadTestRegistry(rootDir, "bad-trust-secrets"); + expect(listSecretProviderIntegrationPresets({ manifestRegistry: registry })).toEqual([]); }); @@ -748,38 +616,26 @@ describe("secret provider integration presets", () => { () => { const rootDir = makeTempDir(); const outsideDir = makeTempDir(); - fs.writeFileSync(path.join(rootDir, "index.ts"), "export default {};\n", "utf8"); fs.mkdirSync(path.join(rootDir, "bin")); fs.writeFileSync(path.join(outsideDir, "resolve.mjs"), "process.stdin.resume();\n"); fs.symlinkSync( path.join(outsideDir, "resolve.mjs"), path.join(rootDir, "bin", "resolve.mjs"), ); - fs.writeFileSync( - path.join(rootDir, "openclaw.plugin.json"), - JSON.stringify({ - id: "symlink-secrets", - secretProviderIntegrations: { - vault: { - providerAlias: "vault", - source: "exec", - command: "${node}", - args: ["./bin/resolve.mjs"], - }, + writePluginManifest(rootDir, { + id: "symlink-secrets", + secretProviderIntegrations: { + vault: { + providerAlias: "vault", + source: "exec", + command: "${node}", + args: ["./bin/resolve.mjs"], }, - configSchema: { - type: "object", - additionalProperties: false, - properties: {}, - }, - }), - "utf8", - ); - - const registry = loadPluginManifestRegistry({ - candidates: [createCandidate(rootDir, "symlink-secrets")], + }, }); + const registry = loadTestRegistry(rootDir, "symlink-secrets"); + expect(listSecretProviderIntegrationPresets({ manifestRegistry: registry })).toEqual([]); }, ); @@ -790,34 +646,22 @@ describe("secret provider integration presets", () => { const linkedRoot = path.join(parentDir, "linked-plugin"); makeSecureDir(realRoot); fs.symlinkSync(realRoot, linkedRoot, "dir"); - fs.writeFileSync(path.join(realRoot, "index.ts"), "export default {};\n", "utf8"); makeSecureDir(path.join(realRoot, "bin")); writeSecureFile(path.join(realRoot, "bin", "resolve.mjs"), "process.stdin.resume();\n"); - fs.writeFileSync( - path.join(realRoot, "openclaw.plugin.json"), - JSON.stringify({ - id: "linked-root-secrets", - secretProviderIntegrations: { - vault: { - providerAlias: "vault", - source: "exec", - command: "${node}", - args: ["./bin/resolve.mjs"], - }, + writePluginManifest(realRoot, { + id: "linked-root-secrets", + secretProviderIntegrations: { + vault: { + providerAlias: "vault", + source: "exec", + command: "${node}", + args: ["./bin/resolve.mjs"], }, - configSchema: { - type: "object", - additionalProperties: false, - properties: {}, - }, - }), - "utf8", - ); - - const registry = loadPluginManifestRegistry({ - candidates: [createCandidate(linkedRoot, "linked-root-secrets")], + }, }); + const registry = loadTestRegistry(linkedRoot, "linked-root-secrets"); + expect(listSecretProviderIntegrationPresets({ manifestRegistry: registry })).toEqual([ { id: "vault", @@ -845,36 +689,24 @@ describe("secret provider integration presets", () => { () => { const rootDir = makeTempDir(); const binDir = path.join(rootDir, "bin"); - fs.writeFileSync(path.join(rootDir, "index.ts"), "export default {};\n", "utf8"); fs.mkdirSync(binDir); fs.writeFileSync(path.join(binDir, "resolve.mjs"), "process.stdin.resume();\n"); fs.chmodSync(binDir, 0o777); try { - fs.writeFileSync( - path.join(rootDir, "openclaw.plugin.json"), - JSON.stringify({ - id: "writable-parent-secrets", - secretProviderIntegrations: { - vault: { - providerAlias: "vault", - source: "exec", - command: "${node}", - args: ["./bin/resolve.mjs"], - }, + writePluginManifest(rootDir, { + id: "writable-parent-secrets", + secretProviderIntegrations: { + vault: { + providerAlias: "vault", + source: "exec", + command: "${node}", + args: ["./bin/resolve.mjs"], }, - configSchema: { - type: "object", - additionalProperties: false, - properties: {}, - }, - }), - "utf8", - ); - - const registry = loadPluginManifestRegistry({ - candidates: [createCandidate(rootDir, "writable-parent-secrets")], + }, }); + const registry = loadTestRegistry(rootDir, "writable-parent-secrets"); + expect(listSecretProviderIntegrationPresets({ manifestRegistry: registry })).toEqual([]); } finally { fs.chmodSync(binDir, 0o700); diff --git a/src/secrets/runtime-state.test.ts b/src/secrets/runtime-state.test.ts index d5e56e103c49..54df4aec2e6a 100644 --- a/src/secrets/runtime-state.test.ts +++ b/src/secrets/runtime-state.test.ts @@ -73,6 +73,61 @@ function preparedGatewayAuthSnapshot( }); } +type ActivateOptions = Omit< + Parameters[0], + "snapshot" | "refreshContext" | "refreshHandler" +>; + +function activateSnapshot( + snapshot: PreparedSecretsRuntimeSnapshot, + options: ActivateOptions = {}, +): void { + activateSecretsRuntimeSnapshotState({ + snapshot, + refreshContext: null, + refreshHandler: null, + ...options, + }); +} + +type ActivateIfCurrentOptions = Omit< + Parameters[0], + "snapshot" | "expectedRevision" | "refreshContext" | "refreshHandler" +> & { expectedRevision?: number }; + +function activateSnapshotIfCurrent( + snapshot: PreparedSecretsRuntimeSnapshot, + options: ActivateIfCurrentOptions = {}, +): boolean { + return activateSecretsRuntimeSnapshotStateIfCurrent({ + snapshot, + expectedRevision: options.expectedRevision ?? getActiveSecretsRuntimeSnapshotRevision(), + refreshContext: null, + refreshHandler: null, + ...options, + }); +} + +type RestoreIfCurrentOptions = Omit< + Parameters[0], + "snapshot" | "ownedSnapshot" | "expectedRevision" | "refreshContext" | "refreshHandler" +> & { expectedRevision?: number }; + +function restoreSnapshotIfCurrent( + snapshot: PreparedSecretsRuntimeSnapshot, + ownedSnapshot: PreparedSecretsRuntimeSnapshot, + options: RestoreIfCurrentOptions = {}, +): boolean { + return restoreSecretsRuntimeSnapshotStateIfCurrent({ + snapshot, + ownedSnapshot, + expectedRevision: options.expectedRevision ?? getActiveSecretsRuntimeSnapshotRevision(), + refreshContext: null, + refreshHandler: null, + ...options, + }); +} + describe("secrets runtime state", () => { let envSnapshot: ReturnType; const autoCleanupTempDirs = useAutoCleanupTempDirTracker(afterEach); @@ -121,11 +176,7 @@ describe("secrets runtime state", () => { authStores: [], }); - activateSecretsRuntimeSnapshotState({ - snapshot, - refreshContext: null, - refreshHandler: null, - }); + activateSnapshot(snapshot); const configSnapshot = getActiveSecretsRuntimeConfigSnapshot(); const fullSnapshot = getActiveSecretsRuntimeSnapshot(); @@ -147,11 +198,7 @@ describe("secrets runtime state", () => { config: { gateway: { auth: { mode: "token", token: "resolved-debug-token" } } }, authStores: [], }); - activateSecretsRuntimeSnapshotState({ - snapshot, - refreshContext: null, - refreshHandler: null, - }); + activateSnapshot(snapshot); const rawSourceConfig = { gateway: { port: 19_030 } } satisfies OpenClawConfig; const secretsSourceConfig = { ...rawSourceConfig, @@ -159,13 +206,12 @@ describe("secrets runtime state", () => { } satisfies OpenClawConfig; expect( - activateSecretsRuntimeSnapshotStateIfCurrent({ - snapshot: { ...snapshot, sourceConfig: secretsSourceConfig }, - expectedRevision: getActiveSecretsRuntimeSnapshotRevision(), - refreshContext: null, - refreshHandler: null, - runtimeSourceConfig: rawSourceConfig, - }), + activateSnapshotIfCurrent( + { ...snapshot, sourceConfig: secretsSourceConfig }, + { + runtimeSourceConfig: rawSourceConfig, + }, + ), ).toBe(true); expect(getRuntimeConfigSourceSnapshot()).toEqual(rawSourceConfig); @@ -176,15 +222,13 @@ describe("secrets runtime state", () => { it("rejects a source-only secrets write after runtime config ownership changes", () => { const initialConfig = { gateway: { port: 19_030 } } satisfies OpenClawConfig; const concurrentConfig = { gateway: { port: 19_031 } } satisfies OpenClawConfig; - activateSecretsRuntimeSnapshotState({ - snapshot: preparedSnapshot({ + activateSnapshot( + preparedSnapshot({ sourceConfig: initialConfig, config: initialConfig, authStores: [], }), - refreshContext: null, - refreshHandler: null, - }); + ); const staleMetadata = getRuntimeConfigSnapshotMetadata(); if (!staleMetadata) { throw new Error("expected runtime config metadata"); @@ -213,16 +257,14 @@ describe("secrets runtime state", () => { }, }, } satisfies OpenClawConfig; - activateSecretsRuntimeSnapshotState({ - snapshot: preparedSnapshot({ + activateSnapshot( + preparedSnapshot({ sourceConfig: initialSource, config: runtimeConfig, authStores: [], }), - refreshContext: null, - refreshHandler: null, - runtimeSourceConfig: initialSource, - }); + { runtimeSourceConfig: initialSource }, + ); const runtimeMetadata = getRuntimeConfigSnapshotMetadata(); if (!runtimeMetadata) { throw new Error("expected runtime config metadata"); @@ -240,11 +282,8 @@ describe("secrets runtime state", () => { const descendant = structuredClone(active); descendant.config.models!.providers!.openai!.baseUrl = "https://refreshed.example.invalid/v1"; expect( - activateSecretsRuntimeSnapshotStateIfCurrent({ - snapshot: descendant, + activateSnapshotIfCurrent(descendant, { expectedRevision: committedRevision, - refreshContext: null, - refreshHandler: null, runtimeSourceConfig: nextSource, preserveActivationLineage: true, }), @@ -303,11 +342,7 @@ describe("secrets runtime state", () => { agentDir, ); - activateSecretsRuntimeSnapshotState({ - snapshot, - refreshContext: null, - refreshHandler: null, - }); + activateSnapshot(snapshot); expect( getRuntimeAuthProfileStoreSnapshot(agentDir)?.usageStats?.["openai:default"], @@ -323,11 +358,7 @@ describe("secrets runtime state", () => { "openai:default": { type: "api_key", provider: "openai", key }, }, }); - activateSecretsRuntimeSnapshotState({ - snapshot: snapshot("sk-old", 19_001), - refreshContext: null, - refreshHandler: null, - }); + activateSnapshot(snapshot("sk-old", 19_001)); const previous = getActiveSecretsRuntimeSnapshot(); const previousRevision = getActiveSecretsRuntimeSnapshotRevision(); const candidate = snapshot("sk-old", 19_002); @@ -337,23 +368,10 @@ describe("secrets runtime state", () => { key: "sk-rejected-candidate", }; expect(previous).not.toBeNull(); - expect( - activateSecretsRuntimeSnapshotStateIfCurrent({ - snapshot: candidate, - expectedRevision: previousRevision, - refreshContext: null, - refreshHandler: null, - }), - ).toBe(true); + expect(activateSnapshotIfCurrent(candidate, { expectedRevision: previousRevision })).toBe(true); const candidateRevision = getActiveSecretsRuntimeSnapshotRevision(); expect( - restoreSecretsRuntimeSnapshotStateIfCurrent({ - snapshot: previous!, - expectedRevision: candidateRevision, - ownedSnapshot: candidate, - refreshContext: null, - refreshHandler: null, - }), + restoreSnapshotIfCurrent(previous!, candidate, { expectedRevision: candidateRevision }), ).toBe(true); expect(getActiveSecretsRuntimeSnapshot()?.config.gateway?.port).toBe(19_001); expect(getRuntimeAuthProfileStoreSnapshot(agentDir)?.profiles["openai:default"]).toMatchObject({ @@ -389,11 +407,7 @@ describe("secrets runtime state", () => { lastGood: { provider: "provider-a:default" }, usageStats: { "provider-b:default": { lastUsed: 1 } }, }; - activateSecretsRuntimeSnapshotState({ - snapshot: snapshot(predecessorProfiles, 19_001, predecessorState), - refreshContext: null, - refreshHandler: null, - }); + activateSnapshot(snapshot(predecessorProfiles, 19_001, predecessorState)); const previous = getActiveSecretsRuntimeSnapshot()!; const previousRevision = getActiveSecretsRuntimeSnapshotRevision(); const activationProfiles = { @@ -426,14 +440,7 @@ describe("secrets runtime state", () => { 19_002, preparedState, ); - expect( - activateSecretsRuntimeSnapshotStateIfCurrent({ - snapshot: candidate, - expectedRevision: previousRevision, - refreshContext: null, - refreshHandler: null, - }), - ).toBe(true); + expect(activateSnapshotIfCurrent(candidate, { expectedRevision: previousRevision })).toBe(true); const liveAfterActivation = getRuntimeAuthProfileStoreSnapshot(agentDir)!; liveAfterActivation.order = { provider: ["provider-q:login", "provider-b:default"] }; liveAfterActivation.lastGood = { provider: "provider-q:login" }; @@ -442,15 +449,7 @@ describe("secrets runtime state", () => { }; setRuntimeAuthProfileStoreSnapshot(liveAfterActivation, agentDir); - expect( - restoreSecretsRuntimeSnapshotStateIfCurrent({ - snapshot: previous, - expectedRevision: getActiveSecretsRuntimeSnapshotRevision(), - ownedSnapshot: candidate, - refreshContext: null, - refreshHandler: null, - }), - ).toBe(true); + expect(restoreSnapshotIfCurrent(previous, candidate)).toBe(true); const restored = getRuntimeAuthProfileStoreSnapshot(agentDir)?.profiles; expect(restored?.["provider-a:default"]).toMatchObject({ key: "a-old" }); expect(restored?.["provider-b:default"]).toMatchObject({ key: "b-external" }); @@ -475,11 +474,7 @@ describe("secrets runtime state", () => { "openai:default": { type: "api_key", provider: "openai", key }, }, }); - activateSecretsRuntimeSnapshotState({ - snapshot: snapshot("sk-old", 19_001), - refreshContext: null, - refreshHandler: null, - }); + activateSnapshot(snapshot("sk-old", 19_001)); const previous = getActiveSecretsRuntimeSnapshot()!; const previousRevision = getActiveSecretsRuntimeSnapshotRevision(); setRuntimeAuthProfileStoreSnapshot( @@ -492,23 +487,8 @@ describe("secrets runtime state", () => { provider: "anthropic", key: "sk-rejected-candidate", }; - expect( - activateSecretsRuntimeSnapshotStateIfCurrent({ - snapshot: candidate, - expectedRevision: previousRevision, - refreshContext: null, - refreshHandler: null, - }), - ).toBe(true); - expect( - restoreSecretsRuntimeSnapshotStateIfCurrent({ - snapshot: previous, - expectedRevision: getActiveSecretsRuntimeSnapshotRevision(), - ownedSnapshot: candidate, - refreshContext: null, - refreshHandler: null, - }), - ).toBe(true); + expect(activateSnapshotIfCurrent(candidate, { expectedRevision: previousRevision })).toBe(true); + expect(restoreSnapshotIfCurrent(previous, candidate)).toBe(true); expect(getActiveSecretsRuntimeSnapshot()?.config.gateway?.port).toBe(19_001); expect(getRuntimeAuthProfileStoreSnapshot(agentDir)?.profiles["openai:default"]).toMatchObject({ key: finalKey, @@ -577,21 +557,10 @@ describe("secrets runtime state", () => { }, runtimeExternalProfileIds: aExternal ? ["provider-a:default"] : undefined, }); - activateSecretsRuntimeSnapshotState({ - snapshot: snapshot(baselineAKey, "b-old", 19_001), - refreshContext: null, - refreshHandler: null, - }); + activateSnapshot(snapshot(baselineAKey, "b-old", 19_001)); const previous = getActiveSecretsRuntimeSnapshot()!; const candidate = snapshot(candidateAKey, "b-old", 19_002); - expect( - activateSecretsRuntimeSnapshotStateIfCurrent({ - snapshot: candidate, - expectedRevision: getActiveSecretsRuntimeSnapshotRevision(), - refreshContext: null, - refreshHandler: null, - }), - ).toBe(true); + expect(activateSnapshotIfCurrent(candidate)).toBe(true); setRuntimeAuthProfileStoreSnapshot( snapshot(currentAKey, "b-external", 19_002, currentAExternal).authStores[0]!.store, agentDir, @@ -602,15 +571,7 @@ describe("secrets runtime state", () => { profileIds: ["provider-b:default"], }); - expect( - restoreSecretsRuntimeSnapshotStateIfCurrent({ - snapshot: previous, - expectedRevision: getActiveSecretsRuntimeSnapshotRevision(), - ownedSnapshot: candidate, - refreshContext: null, - refreshHandler: null, - }), - ).toBe(true); + expect(restoreSnapshotIfCurrent(previous, candidate)).toBe(true); const restored = getRuntimeAuthProfileStoreSnapshot(agentDir)?.profiles; if (expectedAKey === null) { expect(restored?.["provider-a:default"]).toBeUndefined(); @@ -639,21 +600,10 @@ describe("secrets runtime state", () => { }, runtimeLocalProfileIds, }); - activateSecretsRuntimeSnapshotState({ - snapshot: snapshot("sk-old", 19_001), - refreshContext: null, - refreshHandler: null, - }); + activateSnapshot(snapshot("sk-old", 19_001)); const previous = getActiveSecretsRuntimeSnapshot()!; const candidate = snapshot("sk-candidate", 19_002); - expect( - activateSecretsRuntimeSnapshotStateIfCurrent({ - snapshot: candidate, - expectedRevision: getActiveSecretsRuntimeSnapshotRevision(), - refreshContext: null, - refreshHandler: null, - }), - ).toBe(true); + expect(activateSnapshotIfCurrent(candidate)).toBe(true); noteRuntimeAuthProfileStorePersistedMutation(undefined, { credentialsChanged: true, stateChanged: false, @@ -661,15 +611,7 @@ describe("secrets runtime state", () => { }); setRuntimeAuthProfileStoreSnapshot(candidate.authStores[0]!.store, agentDir); - expect( - restoreSecretsRuntimeSnapshotStateIfCurrent({ - snapshot: previous, - expectedRevision: getActiveSecretsRuntimeSnapshotRevision(), - ownedSnapshot: candidate, - refreshContext: null, - refreshHandler: null, - }), - ).toBe(true); + expect(restoreSnapshotIfCurrent(previous, candidate)).toBe(true); expect(getRuntimeAuthProfileStoreSnapshot(agentDir)?.profiles["openai:default"]).toMatchObject({ key: expected, }); @@ -697,25 +639,12 @@ describe("secrets runtime state", () => { provider: "openai", key: "sk-external-y", }; - activateSecretsRuntimeSnapshotState({ - snapshot: snapshot( - { "openai:x": profileX, "openai:y": profileY }, - ["openai:x", "openai:y"], - 19_001, - ), - refreshContext: null, - refreshHandler: null, - }); + activateSnapshot( + snapshot({ "openai:x": profileX, "openai:y": profileY }, ["openai:x", "openai:y"], 19_001), + ); const previous = getActiveSecretsRuntimeSnapshot()!; const candidate = snapshot({ "openai:y": profileY }, ["openai:y"], 19_002); - expect( - activateSecretsRuntimeSnapshotStateIfCurrent({ - snapshot: candidate, - expectedRevision: getActiveSecretsRuntimeSnapshotRevision(), - refreshContext: null, - refreshHandler: null, - }), - ).toBe(true); + expect(activateSnapshotIfCurrent(candidate)).toBe(true); noteRuntimeAuthProfileStorePersistedMutation(undefined, { credentialsChanged: true, stateChanged: false, @@ -723,15 +652,7 @@ describe("secrets runtime state", () => { }); setRuntimeAuthProfileStoreSnapshot(candidate.authStores[0]!.store, agentDir); - expect( - restoreSecretsRuntimeSnapshotStateIfCurrent({ - snapshot: previous, - expectedRevision: getActiveSecretsRuntimeSnapshotRevision(), - ownedSnapshot: candidate, - refreshContext: null, - refreshHandler: null, - }), - ).toBe(true); + expect(restoreSnapshotIfCurrent(previous, candidate)).toBe(true); expect(getRuntimeAuthProfileStoreSnapshot(agentDir)).toBeUndefined(); }); @@ -753,21 +674,10 @@ describe("secrets runtime state", () => { runtimeExternalProfileIds: owner === "external" ? ["openai:x"] : [], runtimeLocalProfileIds: owner === "local" ? ["openai:x"] : [], }); - activateSecretsRuntimeSnapshotState({ - snapshot: snapshot("sk-external-old", "external", 19_001), - refreshContext: null, - refreshHandler: null, - }); + activateSnapshot(snapshot("sk-external-old", "external", 19_001)); const previous = getActiveSecretsRuntimeSnapshot()!; const candidate = snapshot("sk-candidate", candidateOwner, 19_002); - expect( - activateSecretsRuntimeSnapshotStateIfCurrent({ - snapshot: candidate, - expectedRevision: getActiveSecretsRuntimeSnapshotRevision(), - refreshContext: null, - refreshHandler: null, - }), - ).toBe(true); + expect(activateSnapshotIfCurrent(candidate)).toBe(true); if (mutateCandidateOwner) { noteRuntimeAuthProfileStorePersistedMutation( candidateOwner === "local" ? agentDir : undefined, @@ -784,15 +694,7 @@ describe("secrets runtime state", () => { agentDir, ); - expect( - restoreSecretsRuntimeSnapshotStateIfCurrent({ - snapshot: previous, - expectedRevision: getActiveSecretsRuntimeSnapshotRevision(), - ownedSnapshot: candidate, - refreshContext: null, - refreshHandler: null, - }), - ).toBe(true); + expect(restoreSnapshotIfCurrent(previous, candidate)).toBe(true); if (mutateCandidateOwner) { expect(getRuntimeAuthProfileStoreSnapshot(agentDir)).toBeUndefined(); } else { @@ -827,25 +729,16 @@ describe("secrets runtime state", () => { runtimeExternalProfileIds: owner === "external" ? ["openai:x"] : [], runtimeLocalProfileIds: ["anthropic:stable", ...(owner === "local" ? ["openai:x"] : [])], }); - activateSecretsRuntimeSnapshotState({ - snapshot: snapshot( + activateSnapshot( + snapshot( baselineOwner === "absent" ? null : "sk-baseline", baselineOwner === "local" ? "local" : "inherited", 19_001, ), - refreshContext: null, - refreshHandler: null, - }); + ); const previous = getActiveSecretsRuntimeSnapshot()!; const candidate = snapshot("sk-external", "external", 19_002); - expect( - activateSecretsRuntimeSnapshotStateIfCurrent({ - snapshot: candidate, - expectedRevision: getActiveSecretsRuntimeSnapshotRevision(), - refreshContext: null, - refreshHandler: null, - }), - ).toBe(true); + expect(activateSnapshotIfCurrent(candidate)).toBe(true); noteRuntimeAuthProfileStorePersistedMutation( baselineOwner === "inherited" ? undefined : agentDir, { @@ -856,15 +749,7 @@ describe("secrets runtime state", () => { ); setRuntimeAuthProfileStoreSnapshot(candidate.authStores[0]!.store, agentDir); - expect( - restoreSecretsRuntimeSnapshotStateIfCurrent({ - snapshot: previous, - expectedRevision: getActiveSecretsRuntimeSnapshotRevision(), - ownedSnapshot: candidate, - refreshContext: null, - refreshHandler: null, - }), - ).toBe(true); + expect(restoreSnapshotIfCurrent(previous, candidate)).toBe(true); expect(getRuntimeAuthProfileStoreSnapshot(agentDir)).toBeUndefined(); }, ); @@ -898,35 +783,16 @@ describe("secrets runtime state", () => { baselineOwner === "local" ? "local" : "inherited", 19_001, ); - activateSecretsRuntimeSnapshotState({ - snapshot: baseline, - refreshContext: null, - refreshHandler: null, - }); + activateSnapshot(baseline); const previous = getActiveSecretsRuntimeSnapshot()!; const candidate = snapshot("sk-external", "external", 19_002); - expect( - activateSecretsRuntimeSnapshotStateIfCurrent({ - snapshot: candidate, - expectedRevision: getActiveSecretsRuntimeSnapshotRevision(), - refreshContext: null, - refreshHandler: null, - }), - ).toBe(true); + expect(activateSnapshotIfCurrent(candidate)).toBe(true); setRuntimeAuthProfileStoreSnapshot( snapshot("sk-external-refresh", "external", 19_002).authStores[0]!.store, agentDir, ); - expect( - restoreSecretsRuntimeSnapshotStateIfCurrent({ - snapshot: previous, - expectedRevision: getActiveSecretsRuntimeSnapshotRevision(), - ownedSnapshot: candidate, - refreshContext: null, - refreshHandler: null, - }), - ).toBe(true); + expect(restoreSnapshotIfCurrent(previous, candidate)).toBe(true); const restored = getRuntimeAuthProfileStoreSnapshot(agentDir); if (baselineOwner === "absent") { expect(restored?.profiles["openai:x"]).toBeUndefined(); @@ -953,35 +819,16 @@ describe("secrets runtime state", () => { runtimeExternalProfileIds: owner === "external" ? ["openai:x"] : [], runtimeLocalProfileIds: owner === "local" ? ["openai:x"] : [], }); - activateSecretsRuntimeSnapshotState({ - snapshot: snapshot("sk-old", candidateOwner, 19_001), - refreshContext: null, - refreshHandler: null, - }); + activateSnapshot(snapshot("sk-old", candidateOwner, 19_001)); const previous = getActiveSecretsRuntimeSnapshot()!; const candidate = snapshot("sk-candidate", candidateOwner, 19_002); - expect( - activateSecretsRuntimeSnapshotStateIfCurrent({ - snapshot: candidate, - expectedRevision: getActiveSecretsRuntimeSnapshotRevision(), - refreshContext: null, - refreshHandler: null, - }), - ).toBe(true); + expect(activateSnapshotIfCurrent(candidate)).toBe(true); setRuntimeAuthProfileStoreSnapshot( snapshot("sk-candidate", currentOwner, 19_002).authStores[0]!.store, agentDir, ); - expect( - restoreSecretsRuntimeSnapshotStateIfCurrent({ - snapshot: previous, - expectedRevision: getActiveSecretsRuntimeSnapshotRevision(), - ownedSnapshot: candidate, - refreshContext: null, - refreshHandler: null, - }), - ).toBe(true); + expect(restoreSnapshotIfCurrent(previous, candidate)).toBe(true); const restored = getRuntimeAuthProfileStoreSnapshot(agentDir); expect(restored?.profiles["openai:x"]).toMatchObject({ key: "sk-candidate" }); if (currentOwner === "local") { @@ -1003,31 +850,12 @@ describe("secrets runtime state", () => { runtimeExternalProfileIds: [], runtimeExternalProfileIdsAuthoritative: authoritative ? true : undefined, }); - activateSecretsRuntimeSnapshotState({ - snapshot: snapshot(true, 19_001), - refreshContext: null, - refreshHandler: null, - }); + activateSnapshot(snapshot(true, 19_001)); const previous = getActiveSecretsRuntimeSnapshot()!; const candidate = snapshot(false, 19_002); - expect( - activateSecretsRuntimeSnapshotStateIfCurrent({ - snapshot: candidate, - expectedRevision: getActiveSecretsRuntimeSnapshotRevision(), - refreshContext: null, - refreshHandler: null, - }), - ).toBe(true); + expect(activateSnapshotIfCurrent(candidate)).toBe(true); - expect( - restoreSecretsRuntimeSnapshotStateIfCurrent({ - snapshot: previous, - expectedRevision: getActiveSecretsRuntimeSnapshotRevision(), - ownedSnapshot: candidate, - refreshContext: null, - refreshHandler: null, - }), - ).toBe(true); + expect(restoreSnapshotIfCurrent(previous, candidate)).toBe(true); expect(getRuntimeAuthProfileStoreSnapshot(agentDir)).toMatchObject({ runtimeExternalProfileIds: [], runtimeExternalProfileIdsAuthoritative: true, @@ -1046,35 +874,16 @@ describe("secrets runtime state", () => { runtimeExternalProfileIds: [], runtimeExternalProfileIdsAuthoritative: authoritative ? true : undefined, }); - activateSecretsRuntimeSnapshotState({ - snapshot: snapshot("sk-old", false, 19_001), - refreshContext: null, - refreshHandler: null, - }); + activateSnapshot(snapshot("sk-old", false, 19_001)); const previous = getActiveSecretsRuntimeSnapshot()!; const candidate = snapshot("sk-old", true, 19_002); - expect( - activateSecretsRuntimeSnapshotStateIfCurrent({ - snapshot: candidate, - expectedRevision: getActiveSecretsRuntimeSnapshotRevision(), - refreshContext: null, - refreshHandler: null, - }), - ).toBe(true); + expect(activateSnapshotIfCurrent(candidate)).toBe(true); setRuntimeAuthProfileStoreSnapshot( snapshot("sk-current", true, 19_002).authStores[0]!.store, agentDir, ); - expect( - restoreSecretsRuntimeSnapshotStateIfCurrent({ - snapshot: previous, - expectedRevision: getActiveSecretsRuntimeSnapshotRevision(), - ownedSnapshot: candidate, - refreshContext: null, - refreshHandler: null, - }), - ).toBe(true); + expect(restoreSnapshotIfCurrent(previous, candidate)).toBe(true); const restored = getRuntimeAuthProfileStoreSnapshot(agentDir); expect(restored?.profiles["openai:x"]).toMatchObject({ key: "sk-current" }); expect(restored?.runtimeExternalProfileIdsAuthoritative).toBeUndefined(); @@ -1093,21 +902,10 @@ describe("secrets runtime state", () => { }, runtimeExternalProfileIds: ["openai:external"], }); - activateSecretsRuntimeSnapshotState({ - snapshot: snapshot("sk-old", 19_001), - refreshContext: null, - refreshHandler: null, - }); + activateSnapshot(snapshot("sk-old", 19_001)); const previous = getActiveSecretsRuntimeSnapshot()!; const candidate = snapshot("sk-candidate", 19_002); - expect( - activateSecretsRuntimeSnapshotStateIfCurrent({ - snapshot: candidate, - expectedRevision: getActiveSecretsRuntimeSnapshotRevision(), - refreshContext: null, - refreshHandler: null, - }), - ).toBe(true); + expect(activateSnapshotIfCurrent(candidate)).toBe(true); noteRuntimeAuthProfileStorePersistedMutation(undefined, { credentialsChanged: true, stateChanged: false, @@ -1115,15 +913,7 @@ describe("secrets runtime state", () => { }); setRuntimeAuthProfileStoreSnapshot(snapshot(current, 19_002).authStores[0]!.store, agentDir); - expect( - restoreSecretsRuntimeSnapshotStateIfCurrent({ - snapshot: previous, - expectedRevision: getActiveSecretsRuntimeSnapshotRevision(), - ownedSnapshot: candidate, - refreshContext: null, - refreshHandler: null, - }), - ).toBe(true); + expect(restoreSnapshotIfCurrent(previous, candidate)).toBe(true); expect(getRuntimeAuthProfileStoreSnapshot(agentDir)?.profiles["openai:external"]).toMatchObject( { key: expected, @@ -1146,21 +936,10 @@ describe("secrets runtime state", () => { }, runtimeLocalProfileIds: ["anthropic:stable", "openai:default"], }); - activateSecretsRuntimeSnapshotState({ - snapshot: snapshot("sk-old", 19_001), - refreshContext: null, - refreshHandler: null, - }); + activateSnapshot(snapshot("sk-old", 19_001)); const previous = getActiveSecretsRuntimeSnapshot()!; const candidate = snapshot("sk-candidate", 19_002); - expect( - activateSecretsRuntimeSnapshotStateIfCurrent({ - snapshot: candidate, - expectedRevision: getActiveSecretsRuntimeSnapshotRevision(), - refreshContext: null, - refreshHandler: null, - }), - ).toBe(true); + expect(activateSnapshotIfCurrent(candidate)).toBe(true); for (let index = 0; index < 300; index += 1) { noteRuntimeAuthProfileStorePersistedMutation(agentDir, { credentialsChanged: true, @@ -1169,15 +948,7 @@ describe("secrets runtime state", () => { }); } - expect( - restoreSecretsRuntimeSnapshotStateIfCurrent({ - snapshot: previous, - expectedRevision: getActiveSecretsRuntimeSnapshotRevision(), - ownedSnapshot: candidate, - refreshContext: null, - refreshHandler: null, - }), - ).toBe(true); + expect(restoreSnapshotIfCurrent(previous, candidate)).toBe(true); expect(getRuntimeAuthProfileStoreSnapshot(agentDir)).toBeUndefined(); }); @@ -1206,21 +977,10 @@ describe("secrets runtime state", () => { snapshot("sk-old", previousRef, 19_001).authStores[0]!.store, agentDir, ); - activateSecretsRuntimeSnapshotState({ - snapshot: snapshot("sk-old", previousRef, 19_001), - refreshContext: null, - refreshHandler: null, - }); + activateSnapshot(snapshot("sk-old", previousRef, 19_001)); const previous = getActiveSecretsRuntimeSnapshot()!; const candidate = snapshot("sk-candidate", candidateRef, 19_002); - expect( - activateSecretsRuntimeSnapshotStateIfCurrent({ - snapshot: candidate, - expectedRevision: getActiveSecretsRuntimeSnapshotRevision(), - refreshContext: null, - refreshHandler: null, - }), - ).toBe(true); + expect(activateSnapshotIfCurrent(candidate)).toBe(true); setRuntimeAuthProfileStoreSnapshot( snapshot("sk-descendant", candidateRef, 19_002).authStores[0]!.store, agentDir, @@ -1236,15 +996,7 @@ describe("secrets runtime state", () => { ); } - expect( - restoreSecretsRuntimeSnapshotStateIfCurrent({ - snapshot: previous, - expectedRevision: getActiveSecretsRuntimeSnapshotRevision(), - ownedSnapshot: candidate, - refreshContext: null, - refreshHandler: null, - }), - ).toBe(true); + expect(restoreSnapshotIfCurrent(previous, candidate)).toBe(true); expect(getRuntimeAuthProfileStoreSnapshot(agentDir)).toBeUndefined(); expect( ensureAuthProfileStoreWithoutExternalProfiles(agentDir).profiles["openai:default"], @@ -1356,21 +1108,10 @@ describe("secrets runtime state", () => { ] : [], }); - activateSecretsRuntimeSnapshotState({ - snapshot: snapshot(true, 19_001), - refreshContext: null, - refreshHandler: null, - }); + activateSnapshot(snapshot(true, 19_001)); const previous = getActiveSecretsRuntimeSnapshot()!; const candidate = snapshot(false, 19_002); - expect( - activateSecretsRuntimeSnapshotStateIfCurrent({ - snapshot: candidate, - expectedRevision: getActiveSecretsRuntimeSnapshotRevision(), - refreshContext: null, - refreshHandler: null, - }), - ).toBe(true); + expect(activateSnapshotIfCurrent(candidate)).toBe(true); if (mutationOwner !== "none") { noteRuntimeAuthProfileStorePersistedMutation( mutationOwner === "custom" ? agentDir : undefined, @@ -1382,15 +1123,7 @@ describe("secrets runtime state", () => { ); } - expect( - restoreSecretsRuntimeSnapshotStateIfCurrent({ - snapshot: previous, - expectedRevision: getActiveSecretsRuntimeSnapshotRevision(), - ownedSnapshot: candidate, - refreshContext: null, - refreshHandler: null, - }), - ).toBe(true); + expect(restoreSnapshotIfCurrent(previous, candidate)).toBe(true); if (expectMissing) { expect(getRuntimeAuthProfileStoreSnapshot(agentDir)).toBeUndefined(); } else { @@ -1425,21 +1158,10 @@ describe("secrets runtime state", () => { ] : [], }); - activateSecretsRuntimeSnapshotState({ - snapshot: snapshot(true, 19_001), - refreshContext: null, - refreshHandler: null, - }); + activateSnapshot(snapshot(true, 19_001)); const previous = getActiveSecretsRuntimeSnapshot()!; const candidate = snapshot(false, 19_002); - expect( - activateSecretsRuntimeSnapshotStateIfCurrent({ - snapshot: candidate, - expectedRevision: getActiveSecretsRuntimeSnapshotRevision(), - refreshContext: null, - refreshHandler: null, - }), - ).toBe(true); + expect(activateSnapshotIfCurrent(candidate)).toBe(true); noteRuntimeAuthProfileStorePersistedMutation(undefined, { credentialsChanged: true, profileSetChanged: true, @@ -1447,15 +1169,7 @@ describe("secrets runtime state", () => { profileIds: ["openai:new-main"], }); - expect( - restoreSecretsRuntimeSnapshotStateIfCurrent({ - snapshot: previous, - expectedRevision: getActiveSecretsRuntimeSnapshotRevision(), - ownedSnapshot: candidate, - refreshContext: null, - refreshHandler: null, - }), - ).toBe(true); + expect(restoreSnapshotIfCurrent(previous, candidate)).toBe(true); expect(getRuntimeAuthProfileStoreSnapshot(agentDir)).toBeUndefined(); }); @@ -1468,32 +1182,13 @@ describe("secrets runtime state", () => { "openai:default": { type: "api_key", provider: "openai", key }, }, }); - activateSecretsRuntimeSnapshotState({ - snapshot: snapshot("sk-old", 19_001), - refreshContext: null, - refreshHandler: null, - }); + activateSnapshot(snapshot("sk-old", 19_001)); const previous = getActiveSecretsRuntimeSnapshot()!; const candidate = snapshot("sk-candidate", 19_002); - expect( - activateSecretsRuntimeSnapshotStateIfCurrent({ - snapshot: candidate, - expectedRevision: getActiveSecretsRuntimeSnapshotRevision(), - refreshContext: null, - refreshHandler: null, - }), - ).toBe(true); + expect(activateSnapshotIfCurrent(candidate)).toBe(true); clearRuntimeAuthProfileStoreSnapshots(); - expect( - restoreSecretsRuntimeSnapshotStateIfCurrent({ - snapshot: previous, - expectedRevision: getActiveSecretsRuntimeSnapshotRevision(), - ownedSnapshot: candidate, - refreshContext: null, - refreshHandler: null, - }), - ).toBe(true); + expect(restoreSnapshotIfCurrent(previous, candidate)).toBe(true); expect(getRuntimeAuthProfileStoreSnapshot(agentDir)).toBeUndefined(); }); @@ -1515,40 +1210,20 @@ describe("secrets runtime state", () => { "openai:default": { type: "api_key", provider: "openai", key, keyRef }, }, }); - activateSecretsRuntimeSnapshotState({ - snapshot: snapshot("sk-old", previousRef, 19_001), - refreshContext: null, - refreshHandler: null, - }); + activateSnapshot(snapshot("sk-old", previousRef, 19_001)); const previous = getActiveSecretsRuntimeSnapshot()!; const candidate = snapshot("sk-candidate", candidateRef, 19_002); - expect( - activateSecretsRuntimeSnapshotStateIfCurrent({ - snapshot: candidate, - expectedRevision: getActiveSecretsRuntimeSnapshotRevision(), - refreshContext: null, - refreshHandler: null, - }), - ).toBe(true); + expect(activateSnapshotIfCurrent(candidate)).toBe(true); const candidateRevision = getActiveSecretsRuntimeSnapshotRevision(); expect( - activateSecretsRuntimeSnapshotStateIfCurrent({ - snapshot: snapshot("sk-refreshed", candidateRef, 19_002), + activateSnapshotIfCurrent(snapshot("sk-refreshed", candidateRef, 19_002), { expectedRevision: candidateRevision, - refreshContext: null, - refreshHandler: null, preserveActivationLineage: true, }), ).toBe(true); expect( - restoreSecretsRuntimeSnapshotStateIfCurrent({ - snapshot: previous, - expectedRevision: candidateRevision, - ownedSnapshot: candidate, - refreshContext: null, - refreshHandler: null, - }), + restoreSnapshotIfCurrent(previous, candidate, { expectedRevision: candidateRevision }), ).toBe(true); expect(getRuntimeAuthProfileStoreSnapshot(agentDir)?.profiles["openai:default"]).toMatchObject({ key: changedRef ? "sk-old" : "sk-refreshed", @@ -1565,11 +1240,7 @@ describe("secrets runtime state", () => { "openai:default": { type: "api_key", provider: "openai", key }, }, }); - activateSecretsRuntimeSnapshotState({ - snapshot: snapshot("sk-old", 19_011), - refreshContext: null, - refreshHandler: null, - }); + activateSnapshot(snapshot("sk-old", 19_011)); setRuntimeAuthProfileStoreSnapshot( { version: 1, @@ -1583,24 +1254,9 @@ describe("secrets runtime state", () => { const previousRevision = getActiveSecretsRuntimeSnapshotRevision(); const candidate = snapshot("sk-live", 19_012); expect(previous).not.toBeNull(); - expect( - activateSecretsRuntimeSnapshotStateIfCurrent({ - snapshot: candidate, - expectedRevision: previousRevision, - refreshContext: null, - refreshHandler: null, - }), - ).toBe(true); + expect(activateSnapshotIfCurrent(candidate, { expectedRevision: previousRevision })).toBe(true); - expect( - restoreSecretsRuntimeSnapshotStateIfCurrent({ - snapshot: previous!, - expectedRevision: getActiveSecretsRuntimeSnapshotRevision(), - ownedSnapshot: candidate, - refreshContext: null, - refreshHandler: null, - }), - ).toBe(true); + expect(restoreSnapshotIfCurrent(previous!, candidate)).toBe(true); expect(getActiveSecretsRuntimeSnapshot()?.config.gateway?.port).toBe(19_011); expect(getRuntimeAuthProfileStoreSnapshot(agentDir)?.profiles["openai:default"]).toMatchObject({ key: "sk-live", @@ -1667,16 +1323,14 @@ describe("secrets runtime state", () => { }, authStores: [], }); - activateSecretsRuntimeSnapshotState({ - snapshot: snapshot({ + activateSnapshot( + snapshot({ sourcePort: 19_021, runtimePort: 19_021, apiKey: "sk-old", keyRef: previousKeyInput, }), - refreshContext: null, - refreshHandler: null, - }); + ); const previous = getActiveSecretsRuntimeSnapshot()!; const candidate = snapshot({ sourcePort: 19_022, @@ -1684,14 +1338,7 @@ describe("secrets runtime state", () => { apiKey: "sk-candidate", keyRef: candidateKeyInput, }); - expect( - activateSecretsRuntimeSnapshotStateIfCurrent({ - snapshot: candidate, - expectedRevision: getActiveSecretsRuntimeSnapshotRevision(), - refreshContext: null, - refreshHandler: null, - }), - ).toBe(true); + expect(activateSnapshotIfCurrent(candidate)).toBe(true); const candidateRevision = getActiveSecretsRuntimeSnapshotRevision(); const providerRefresh = snapshot({ sourcePort: 19_022, @@ -1700,23 +1347,14 @@ describe("secrets runtime state", () => { keyRef: candidateKeyInput, }); expect( - activateSecretsRuntimeSnapshotStateIfCurrent({ - snapshot: providerRefresh, + activateSnapshotIfCurrent(providerRefresh, { expectedRevision: candidateRevision, - refreshContext: null, - refreshHandler: null, preserveActivationLineage: true, }), ).toBe(true); expect( - restoreSecretsRuntimeSnapshotStateIfCurrent({ - snapshot: previous, - ownedSnapshot: candidate, - expectedRevision: candidateRevision, - refreshContext: null, - refreshHandler: null, - }), + restoreSnapshotIfCurrent(previous, candidate, { expectedRevision: candidateRevision }), ).toBe(true); expect(getActiveSecretsRuntimeSnapshot()?.config.gateway?.port).toBe(19_021); expect(getActiveSecretsRuntimeSnapshot()?.config.models?.providers?.openai?.apiKey).toBe( @@ -1822,25 +1460,16 @@ describe("secrets runtime state", () => { }, ], }); - activateSecretsRuntimeSnapshotState({ - snapshot: snapshot({ sourceConfig: previousSourceConfig, apiKey: "sk-old", port: 19_031 }), - refreshContext: null, - refreshHandler: null, - }); + activateSnapshot( + snapshot({ sourceConfig: previousSourceConfig, apiKey: "sk-old", port: 19_031 }), + ); const previous = getActiveSecretsRuntimeSnapshot()!; const candidate = snapshot({ sourceConfig: candidateSourceConfig, apiKey: "sk-candidate", port: 19_032, }); - expect( - activateSecretsRuntimeSnapshotStateIfCurrent({ - snapshot: candidate, - expectedRevision: getActiveSecretsRuntimeSnapshotRevision(), - refreshContext: null, - refreshHandler: null, - }), - ).toBe(true); + expect(activateSnapshotIfCurrent(candidate)).toBe(true); if (evictLineage) { for (let index = 0; index < 300; index += 1) { noteRuntimeAuthProfileStorePersistedMutation(agentDir, { @@ -1852,27 +1481,21 @@ describe("secrets runtime state", () => { } const candidateRevision = getActiveSecretsRuntimeSnapshotRevision(); expect( - activateSecretsRuntimeSnapshotStateIfCurrent({ - snapshot: snapshot({ + activateSnapshotIfCurrent( + snapshot({ sourceConfig: candidateSourceConfig, apiKey: "sk-refreshed", port: 19_032, }), - expectedRevision: candidateRevision, - refreshContext: null, - refreshHandler: null, - preserveActivationLineage: true, - }), + { + expectedRevision: candidateRevision, + preserveActivationLineage: true, + }, + ), ).toBe(true); expect( - restoreSecretsRuntimeSnapshotStateIfCurrent({ - snapshot: previous, - ownedSnapshot: candidate, - expectedRevision: candidateRevision, - refreshContext: null, - refreshHandler: null, - }), + restoreSnapshotIfCurrent(previous, candidate, { expectedRevision: candidateRevision }), ).toBe(true); const restored = getActiveSecretsRuntimeSnapshot(); expect(restored?.sourceConfig).toMatchObject(previousSourceConfig); @@ -1935,16 +1558,14 @@ describe("secrets runtime state", () => { }, ], }); - activateSecretsRuntimeSnapshotState({ - snapshot: snapshot({ + activateSnapshot( + snapshot({ key: "sk-old", owner: capturedOwner, providerPath: "/tmp/old-secrets.json", port: 19_041, }), - refreshContext: null, - refreshHandler: null, - }); + ); const previous = getActiveSecretsRuntimeSnapshot()!; const candidate = snapshot({ key: "sk-candidate", @@ -1952,14 +1573,7 @@ describe("secrets runtime state", () => { providerPath: "/tmp/rejected-secrets.json", port: 19_042, }); - expect( - activateSecretsRuntimeSnapshotStateIfCurrent({ - snapshot: candidate, - expectedRevision: getActiveSecretsRuntimeSnapshotRevision(), - refreshContext: null, - refreshHandler: null, - }), - ).toBe(true); + expect(activateSnapshotIfCurrent(candidate)).toBe(true); noteRuntimeAuthProfileStorePersistedMutation(agentDir, { credentialsChanged: true, stateChanged: false, @@ -1975,15 +1589,7 @@ describe("secrets runtime state", () => { agentDir, ); - expect( - restoreSecretsRuntimeSnapshotStateIfCurrent({ - snapshot: previous, - expectedRevision: getActiveSecretsRuntimeSnapshotRevision(), - ownedSnapshot: candidate, - refreshContext: null, - refreshHandler: null, - }), - ).toBe(true); + expect(restoreSnapshotIfCurrent(previous, candidate)).toBe(true); expect(getRuntimeAuthProfileStoreSnapshot(agentDir)).toBeUndefined(); }, ); @@ -2048,16 +1654,14 @@ describe("secrets runtime state", () => { }, ], }); - activateSecretsRuntimeSnapshotState({ - snapshot: snapshot({ + activateSnapshot( + snapshot({ key: "sk-old", keyRef: previousRef, port: 19_051, sourceConfig: previousSourceConfig, }), - refreshContext: null, - refreshHandler: null, - }); + ); const previous = getActiveSecretsRuntimeSnapshot()!; const candidate = snapshot({ key: "sk-candidate", @@ -2065,14 +1669,7 @@ describe("secrets runtime state", () => { port: 19_052, sourceConfig: candidateSourceConfig, }); - expect( - activateSecretsRuntimeSnapshotStateIfCurrent({ - snapshot: candidate, - expectedRevision: getActiveSecretsRuntimeSnapshotRevision(), - refreshContext: null, - refreshHandler: null, - }), - ).toBe(true); + expect(activateSnapshotIfCurrent(candidate)).toBe(true); noteRuntimeAuthProfileStorePersistedMutation(agentDir, { credentialsChanged: true, stateChanged: false, @@ -2088,15 +1685,7 @@ describe("secrets runtime state", () => { agentDir, ); - expect( - restoreSecretsRuntimeSnapshotStateIfCurrent({ - snapshot: previous, - expectedRevision: getActiveSecretsRuntimeSnapshotRevision(), - ownedSnapshot: candidate, - refreshContext: null, - refreshHandler: null, - }), - ).toBe(true); + expect(restoreSnapshotIfCurrent(previous, candidate)).toBe(true); if (affectedProvider) { expect(getRuntimeAuthProfileStoreSnapshot(agentDir)).toBeUndefined(); } else { @@ -2156,29 +1745,20 @@ describe("secrets runtime state", () => { }, ], }); - activateSecretsRuntimeSnapshotState({ - snapshot: snapshot({ + activateSnapshot( + snapshot({ includeProfile: false, providerPath: "/tmp/old-secrets.json", port: 19_061, }), - refreshContext: null, - refreshHandler: null, - }); + ); const previous = getActiveSecretsRuntimeSnapshot()!; const candidate = snapshot({ includeProfile: false, providerPath: "/tmp/rejected-secrets.json", port: 19_062, }); - expect( - activateSecretsRuntimeSnapshotStateIfCurrent({ - snapshot: candidate, - expectedRevision: getActiveSecretsRuntimeSnapshotRevision(), - refreshContext: null, - refreshHandler: null, - }), - ).toBe(true); + expect(activateSnapshotIfCurrent(candidate)).toBe(true); if (currentOwner === "local") { noteRuntimeAuthProfileStorePersistedMutation(agentDir, { credentialsChanged: true, @@ -2196,15 +1776,7 @@ describe("secrets runtime state", () => { agentDir, ); - expect( - restoreSecretsRuntimeSnapshotStateIfCurrent({ - snapshot: previous, - expectedRevision: getActiveSecretsRuntimeSnapshotRevision(), - ownedSnapshot: candidate, - refreshContext: null, - refreshHandler: null, - }), - ).toBe(true); + expect(restoreSnapshotIfCurrent(previous, candidate)).toBe(true); expect(getRuntimeAuthProfileStoreSnapshot(agentDir)).toBeUndefined(); }, );