|
1 | 1 | import fs from "node:fs"; |
2 | 2 | import path from "node:path"; |
3 | | -import { afterEach, describe, expect, it } from "vitest"; |
| 3 | +import { afterEach, describe, expect, it, vi } from "vitest"; |
4 | 4 | import { |
5 | 5 | applyPluginAutoEnable, |
6 | 6 | materializePluginAutoEnableCandidates, |
@@ -104,6 +104,83 @@ describe("applyPluginAutoEnable channels", () => { |
104 | 104 | expect(result.config.plugins?.entries?.["env-primary"]).toBeUndefined(); |
105 | 105 | }); |
106 | 106 |
|
| 107 | + it("memoizes external catalog preferOver lookups within one auto-enable pass", () => { |
| 108 | + const stateDir = makeTempDir(); |
| 109 | + const catalogPath = path.join(stateDir, "plugins", "catalog.json"); |
| 110 | + fs.mkdirSync(path.dirname(catalogPath), { recursive: true }); |
| 111 | + fs.writeFileSync( |
| 112 | + catalogPath, |
| 113 | + JSON.stringify({ |
| 114 | + entries: [ |
| 115 | + { |
| 116 | + name: "@openclaw/env-primary", |
| 117 | + openclaw: { |
| 118 | + channel: { |
| 119 | + id: "env-primary", |
| 120 | + label: "Env Primary", |
| 121 | + selectionLabel: "Env Primary", |
| 122 | + docsPath: "/channels/env-primary", |
| 123 | + blurb: "Env primary entry", |
| 124 | + }, |
| 125 | + install: { |
| 126 | + npmSpec: "@openclaw/env-primary", |
| 127 | + }, |
| 128 | + }, |
| 129 | + }, |
| 130 | + { |
| 131 | + name: "@openclaw/env-secondary", |
| 132 | + openclaw: { |
| 133 | + channel: { |
| 134 | + id: "env-secondary", |
| 135 | + label: "Env Secondary", |
| 136 | + selectionLabel: "Env Secondary", |
| 137 | + docsPath: "/channels/env-secondary", |
| 138 | + blurb: "Env secondary entry", |
| 139 | + preferOver: ["env-primary"], |
| 140 | + }, |
| 141 | + install: { |
| 142 | + npmSpec: "@openclaw/env-secondary", |
| 143 | + }, |
| 144 | + }, |
| 145 | + }, |
| 146 | + ], |
| 147 | + }), |
| 148 | + "utf-8", |
| 149 | + ); |
| 150 | + |
| 151 | + const readFileSpy = vi.spyOn(fs, "readFileSync"); |
| 152 | + |
| 153 | + try { |
| 154 | + materializePluginAutoEnableCandidates({ |
| 155 | + config: { |
| 156 | + channels: { |
| 157 | + "env-primary": { token: "primary" }, |
| 158 | + "env-secondary": { token: "secondary" }, |
| 159 | + }, |
| 160 | + }, |
| 161 | + candidates: Array.from({ length: 20 }, (_, index) => ({ |
| 162 | + pluginId: index % 2 === 0 ? "env-primary" : "env-secondary", |
| 163 | + kind: "channel-configured" as const, |
| 164 | + channelId: index % 2 === 0 ? "env-primary" : "env-secondary", |
| 165 | + })), |
| 166 | + env: { |
| 167 | + ...makeIsolatedEnv(), |
| 168 | + OPENCLAW_STATE_DIR: stateDir, |
| 169 | + OPENCLAW_BUNDLED_PLUGINS_DIR: "/nonexistent/bundled/plugins", |
| 170 | + }, |
| 171 | + manifestRegistry: makeRegistry([]), |
| 172 | + }); |
| 173 | + |
| 174 | + expect( |
| 175 | + readFileSpy.mock.calls.filter(([filePath]) => |
| 176 | + String(filePath).endsWith("plugins/catalog.json"), |
| 177 | + ), |
| 178 | + ).toHaveLength(2); |
| 179 | + } finally { |
| 180 | + readFileSpy.mockRestore(); |
| 181 | + } |
| 182 | + }); |
| 183 | + |
107 | 184 | describe("third-party channel plugins (pluginId ≠ channelId)", () => { |
108 | 185 | it("uses the plugin manifest id, not the channel id, for plugins.entries", () => { |
109 | 186 | const result = applyWithApnChannelConfig(); |
|
0 commit comments