Skip to content

Commit 4798264

Browse files
committed
fix(e2e): harden bundled lifecycle probe on Windows
1 parent 60c0f24 commit 4798264

3 files changed

Lines changed: 106 additions & 26 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ Docs: https://docs.openclaw.ai
99
### Fixes
1010

1111
- Scripts: remove stale Knip unused-file allowlist entries so the dead-code gate fails only on current findings.
12+
- Tests: normalize bundled plugin lifecycle probe paths and state-root lookup so native Windows release sweeps accept valid packaged plugin installs.
1213
- Config: keep benign legacy metadata write anomalies out of default doctor and config command output while preserving explicit anomaly logging for diagnostics.
1314
- Codex: log when implicit app-server `never` approvals are promoted for OpenClaw tool policy, including whether the trigger was a `before_tool_call` hook or trusted tool policy.
1415
- Google Vertex: support production ADC modes such as Workload Identity Federation, service-account credentials, and metadata-server ADC for the native Vertex transport. (#83971) Thanks @damianFelixPago.

scripts/e2e/lib/bundled-plugin-install-uninstall/probe.mjs

Lines changed: 29 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,26 @@
11
import { spawnSync } from "node:child_process";
22
import fs from "node:fs";
3+
import os from "node:os";
34
import path from "node:path";
45

56
const readJson = (file) => JSON.parse(fs.readFileSync(file, "utf8"));
7+
const normalizePathForProbe = (value) => String(value ?? "").replace(/\\/g, "/");
8+
const bundledRuntimeFragments = (pluginDir) => [
9+
`/dist/extensions/${pluginDir}`,
10+
`/dist-runtime/extensions/${pluginDir}`,
11+
];
12+
13+
function resolveStateDir() {
14+
if (process.env.OPENCLAW_STATE_DIR) {
15+
return process.env.OPENCLAW_STATE_DIR;
16+
}
17+
return path.join(process.env.HOME || os.homedir(), ".openclaw");
18+
}
19+
20+
function pathReferencesBundledRuntime(value, pluginDir) {
21+
const normalized = normalizePathForProbe(value);
22+
return bundledRuntimeFragments(pluginDir).some((fragment) => normalized.includes(fragment));
23+
}
624

725
function resolveOpenClawEntry() {
826
if (process.env.OPENCLAW_ENTRY) {
@@ -109,8 +127,9 @@ async function selectedManifestEntries() {
109127
}
110128

111129
function assertInstalled(pluginId, pluginDir, requiresConfig) {
112-
const configPath = path.join(process.env.HOME, ".openclaw", "openclaw.json");
113-
const indexPath = path.join(process.env.HOME, ".openclaw", "plugins", "installs.json");
130+
const stateDir = resolveStateDir();
131+
const configPath = path.join(stateDir, "openclaw.json");
132+
const indexPath = path.join(stateDir, "plugins", "installs.json");
114133
const config = readJson(configPath);
115134
const index = readJson(indexPath);
116135
const records = index.installRecords ?? index.records ?? {};
@@ -125,23 +144,15 @@ function assertInstalled(pluginId, pluginDir, requiresConfig) {
125144
}
126145
if (
127146
typeof record.sourcePath !== "string" ||
128-
![`/dist/extensions/${pluginDir}`, `/dist-runtime/extensions/${pluginDir}`].some((fragment) =>
129-
record.sourcePath.includes(fragment),
130-
)
147+
!pathReferencesBundledRuntime(record.sourcePath, pluginDir)
131148
) {
132149
throw new Error(`unexpected bundled source path for ${pluginId}: ${record.sourcePath}`);
133150
}
134-
if (record.installPath !== record.sourcePath) {
151+
if (normalizePathForProbe(record.installPath) !== normalizePathForProbe(record.sourcePath)) {
135152
throw new Error(`bundled install path should equal source path for ${pluginId}`);
136153
}
137154
const paths = config.plugins?.load?.paths || [];
138-
if (
139-
paths.some((entry) =>
140-
[`/dist/extensions/${pluginDir}`, `/dist-runtime/extensions/${pluginDir}`].some(
141-
(fragment) => String(entry).includes(fragment),
142-
),
143-
)
144-
) {
155+
if (paths.some((entry) => pathReferencesBundledRuntime(entry, pluginDir))) {
145156
throw new Error(`config load paths should not include bundled install path for ${pluginId}`);
146157
}
147158
if (requiresConfig && config.plugins?.entries?.[pluginId]?.enabled === true) {
@@ -162,22 +173,17 @@ function assertInstalled(pluginId, pluginDir, requiresConfig) {
162173
}
163174

164175
function assertUninstalled(pluginId, pluginDir) {
165-
const configPath = path.join(process.env.HOME, ".openclaw", "openclaw.json");
166-
const indexPath = path.join(process.env.HOME, ".openclaw", "plugins", "installs.json");
176+
const stateDir = resolveStateDir();
177+
const configPath = path.join(stateDir, "openclaw.json");
178+
const indexPath = path.join(stateDir, "plugins", "installs.json");
167179
const config = fs.existsSync(configPath) ? readJson(configPath) : {};
168180
const index = fs.existsSync(indexPath) ? readJson(indexPath) : {};
169181
const records = index.installRecords ?? index.records ?? {};
170182
if (records[pluginId]) {
171183
throw new Error(`install record still present after uninstall for ${pluginId}`);
172184
}
173185
const paths = config.plugins?.load?.paths || [];
174-
if (
175-
paths.some((entry) =>
176-
[`/dist/extensions/${pluginDir}`, `/dist-runtime/extensions/${pluginDir}`].some(
177-
(fragment) => String(entry).includes(fragment),
178-
),
179-
)
180-
) {
186+
if (paths.some((entry) => pathReferencesBundledRuntime(entry, pluginDir))) {
181187
throw new Error(`load path still present after uninstall for ${pluginId}`);
182188
}
183189
if (config.plugins?.entries?.[pluginId]) {
@@ -189,7 +195,7 @@ function assertUninstalled(pluginId, pluginDir) {
189195
if ((config.plugins?.deny || []).includes(pluginId)) {
190196
throw new Error(`denylist still contains ${pluginId} after uninstall`);
191197
}
192-
const managedPath = path.join(process.env.HOME, ".openclaw", "extensions", pluginId);
198+
const managedPath = path.join(stateDir, "extensions", pluginId);
193199
if (fs.existsSync(managedPath)) {
194200
throw new Error(
195201
`managed install directory unexpectedly exists for bundled plugin ${pluginId}: ${managedPath}`,

test/scripts/bundled-plugin-install-uninstall-probe.test.ts

Lines changed: 76 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,7 @@ import path from "node:path";
55
import { afterEach, describe, expect, it } from "vitest";
66

77
const tempDirs: string[] = [];
8-
const probePath = path.resolve(
9-
"scripts/e2e/lib/bundled-plugin-install-uninstall/probe.mjs",
10-
);
8+
const probePath = path.resolve("scripts/e2e/lib/bundled-plugin-install-uninstall/probe.mjs");
119
const runtimeSmokePath = path.resolve(
1210
"scripts/e2e/lib/bundled-plugin-install-uninstall/runtime-smoke.mjs",
1311
);
@@ -67,6 +65,21 @@ function runProbe(root: string, env: Record<string, string | undefined> = {}) {
6765
});
6866
}
6967

68+
function runProbeCommand(root: string, args: string[], env: Record<string, string | undefined>) {
69+
const childEnv = { ...process.env, ...env };
70+
for (const [key, value] of Object.entries(childEnv)) {
71+
if (value === undefined) {
72+
delete childEnv[key];
73+
}
74+
}
75+
childEnv.OPENCLAW_ENTRY = path.join(root, "dist", "index.js");
76+
return spawnSync(process.execPath, [probePath, ...args], {
77+
cwd: root,
78+
encoding: "utf8",
79+
env: childEnv as NodeJS.ProcessEnv,
80+
});
81+
}
82+
7083
function runRuntimeSmoke(root: string, args: string[]) {
7184
return spawnSync(process.execPath, [runtimeSmokePath, ...args], {
7285
cwd: root,
@@ -169,4 +182,64 @@ describe("bundled plugin install/uninstall probe", () => {
169182
"Global-disable TTS smoke skipped for runtime-only: no speech provider contract",
170183
);
171184
});
185+
186+
it("accepts native Windows bundled source paths when asserting install state", () => {
187+
const root = makePackageRoot();
188+
const stateDir = path.join(root, "state");
189+
const windowsSourcePath = "C:\\crabbox\\qa-windows\\dist\\extensions\\nostr";
190+
fs.mkdirSync(path.join(stateDir, "plugins"), { recursive: true });
191+
fs.writeFileSync(
192+
path.join(stateDir, "openclaw.json"),
193+
JSON.stringify({ plugins: { entries: { nostr: { enabled: true } } } }),
194+
"utf8",
195+
);
196+
fs.writeFileSync(
197+
path.join(stateDir, "plugins", "installs.json"),
198+
JSON.stringify({
199+
installRecords: {
200+
nostr: {
201+
source: "path",
202+
sourcePath: windowsSourcePath,
203+
installPath: windowsSourcePath,
204+
},
205+
},
206+
}),
207+
"utf8",
208+
);
209+
writePluginsList(root, []);
210+
211+
const result = runProbeCommand(root, ["assert-installed", "nostr", "nostr", "0"], {
212+
HOME: undefined,
213+
OPENCLAW_STATE_DIR: stateDir,
214+
});
215+
216+
expect(result.status).toBe(0);
217+
});
218+
219+
it("detects native Windows bundled load paths after uninstall", () => {
220+
const root = makePackageRoot();
221+
const stateDir = path.join(root, "state");
222+
fs.mkdirSync(path.join(stateDir, "plugins"), { recursive: true });
223+
fs.writeFileSync(
224+
path.join(stateDir, "openclaw.json"),
225+
JSON.stringify({
226+
plugins: { load: { paths: ["C:\\crabbox\\qa-windows\\dist\\extensions\\nostr"] } },
227+
}),
228+
"utf8",
229+
);
230+
fs.writeFileSync(
231+
path.join(stateDir, "plugins", "installs.json"),
232+
JSON.stringify({ installRecords: {} }),
233+
"utf8",
234+
);
235+
writePluginsList(root, []);
236+
237+
const result = runProbeCommand(root, ["assert-uninstalled", "nostr", "nostr"], {
238+
HOME: undefined,
239+
OPENCLAW_STATE_DIR: stateDir,
240+
});
241+
242+
expect(result.status).toBe(1);
243+
expect(result.stderr).toContain("load path still present after uninstall for nostr");
244+
});
172245
});

0 commit comments

Comments
 (0)