mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 07:20:43 +00:00
test: cover legacy runtime deps update recovery (#75288)
This commit is contained in:
@@ -234,6 +234,80 @@ package_root() {
|
||||
printf '%s/lib/node_modules/openclaw\n' "$npm_config_prefix"
|
||||
}
|
||||
|
||||
legacy_runtime_deps_symlink_plugin() {
|
||||
local plugin="${OPENCLAW_UPGRADE_SURVIVOR_LEGACY_RUNTIME_DEPS_SYMLINK:-}"
|
||||
if [ -z "$plugin" ]; then
|
||||
return 1
|
||||
fi
|
||||
case "$plugin" in
|
||||
*[!A-Za-z0-9._-]*)
|
||||
echo "OPENCLAW_UPGRADE_SURVIVOR_LEGACY_RUNTIME_DEPS_SYMLINK must be a plugin id, got: $plugin" >&2
|
||||
return 2
|
||||
;;
|
||||
esac
|
||||
printf '%s\n' "$plugin"
|
||||
}
|
||||
|
||||
legacy_runtime_deps_symlink_target() {
|
||||
local plugin="$1"
|
||||
printf '%s/dist/extensions/%s/node_modules\n' "$(package_root)" "$plugin"
|
||||
}
|
||||
|
||||
legacy_runtime_deps_symlink_source() {
|
||||
local plugin="$1"
|
||||
printf '%s/.local/bundled-plugin-runtime-deps/%s-upgrade-survivor/node_modules\n' \
|
||||
"$(package_root)" \
|
||||
"$plugin"
|
||||
}
|
||||
|
||||
seed_legacy_runtime_deps_symlink() {
|
||||
local plugin
|
||||
plugin="$(legacy_runtime_deps_symlink_plugin)" || {
|
||||
local status=$?
|
||||
[ "$status" -eq 1 ] && return 0
|
||||
return "$status"
|
||||
}
|
||||
|
||||
local plugin_dir
|
||||
plugin_dir="$(package_root)/dist/extensions/$plugin"
|
||||
if [ ! -d "$plugin_dir" ]; then
|
||||
echo "cannot seed legacy runtime deps symlink; packaged plugin is missing: $plugin_dir" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
local source_dir
|
||||
local target_dir
|
||||
source_dir="$(legacy_runtime_deps_symlink_source "$plugin")"
|
||||
target_dir="$(legacy_runtime_deps_symlink_target "$plugin")"
|
||||
mkdir -p "$source_dir"
|
||||
printf '{"name":"openclaw-upgrade-survivor-legacy-runtime-deps","version":"0.0.0"}\n' \
|
||||
>"$source_dir/package.json"
|
||||
rm -rf "$target_dir"
|
||||
ln -s "$source_dir" "$target_dir"
|
||||
if [ ! -L "$target_dir" ]; then
|
||||
echo "failed to create legacy runtime deps symlink: $target_dir" >&2
|
||||
return 1
|
||||
fi
|
||||
echo "Seeded legacy runtime deps symlink for $plugin: $target_dir -> $source_dir"
|
||||
}
|
||||
|
||||
assert_legacy_runtime_deps_symlink_repaired() {
|
||||
local plugin
|
||||
plugin="$(legacy_runtime_deps_symlink_plugin)" || {
|
||||
local status=$?
|
||||
[ "$status" -eq 1 ] && return 0
|
||||
return "$status"
|
||||
}
|
||||
|
||||
local target_dir
|
||||
target_dir="$(legacy_runtime_deps_symlink_target "$plugin")"
|
||||
if [ -L "$target_dir" ]; then
|
||||
echo "legacy runtime deps symlink survived package update: $target_dir -> $(readlink "$target_dir")" >&2
|
||||
return 1
|
||||
fi
|
||||
echo "Legacy runtime deps symlink repaired for $plugin."
|
||||
}
|
||||
|
||||
read_installed_version() {
|
||||
node -p 'JSON.parse(require("node:fs").readFileSync(process.argv[1] + "/package.json", "utf8")).version' "$(package_root)"
|
||||
}
|
||||
@@ -450,8 +524,10 @@ phase seed-state seed_state
|
||||
phase apply-baseline-config-recipe apply_baseline_config_recipe
|
||||
phase validate-baseline-config validate_baseline_config
|
||||
phase assert-baseline assert_baseline_state
|
||||
phase seed-legacy-runtime-deps-symlink seed_legacy_runtime_deps_symlink
|
||||
phase resolve-candidate resolve_candidate_version
|
||||
phase update-candidate update_candidate
|
||||
phase assert-legacy-runtime-deps-symlink-repaired assert_legacy_runtime_deps_symlink_repaired
|
||||
phase doctor run_doctor
|
||||
phase validate-post-doctor-config validate_post_doctor_config
|
||||
phase assert-survival assert_survival
|
||||
|
||||
@@ -83,6 +83,7 @@ if [ "${OPENCLAW_UPGRADE_SURVIVOR_PUBLISHED_BASELINE:-0}" = "1" ]; then
|
||||
-e OPENCLAW_UPGRADE_SURVIVOR_CANDIDATE_KIND="$CANDIDATE_KIND" \
|
||||
-e OPENCLAW_UPGRADE_SURVIVOR_CANDIDATE_SPEC="$CANDIDATE_SPEC" \
|
||||
-e OPENCLAW_UPGRADE_SURVIVOR_SCENARIO="$SCENARIO" \
|
||||
-e OPENCLAW_UPGRADE_SURVIVOR_LEGACY_RUNTIME_DEPS_SYMLINK="${OPENCLAW_UPGRADE_SURVIVOR_LEGACY_RUNTIME_DEPS_SYMLINK:-}" \
|
||||
-e OPENCLAW_UPGRADE_SURVIVOR_SUMMARY_JSON=/tmp/openclaw-upgrade-survivor-artifacts/summary.json \
|
||||
-e OPENCLAW_UPGRADE_SURVIVOR_START_BUDGET_SECONDS="${OPENCLAW_UPGRADE_SURVIVOR_START_BUDGET_SECONDS:-90}" \
|
||||
-e OPENCLAW_UPGRADE_SURVIVOR_STATUS_BUDGET_SECONDS="${OPENCLAW_UPGRADE_SURVIVOR_STATUS_BUDGET_SECONDS:-30}" \
|
||||
|
||||
@@ -95,10 +95,22 @@ export function assertPathIsNotSymlink(targetPath, label) {
|
||||
}
|
||||
}
|
||||
|
||||
function isPathInside(parentPath, childPath) {
|
||||
function isDirectChildPath(parentPath, childPath) {
|
||||
const relativePath = path.relative(parentPath, childPath);
|
||||
return (
|
||||
relativePath.length > 0 && !relativePath.startsWith("..") && !path.isAbsolute(relativePath)
|
||||
relativePath.length > 0 &&
|
||||
!relativePath.startsWith("..") &&
|
||||
!path.isAbsolute(relativePath) &&
|
||||
!relativePath.includes(path.sep)
|
||||
);
|
||||
}
|
||||
|
||||
function isLegacyBundledRuntimeDepsNodeModulesPath(targetPath, repoRoot, linkedPath) {
|
||||
const legacyRuntimeDepsRoot = path.resolve(repoRoot, ".local", "bundled-plugin-runtime-deps");
|
||||
const resolvedLinkedPath = path.resolve(path.dirname(targetPath), linkedPath);
|
||||
return (
|
||||
path.basename(resolvedLinkedPath) === "node_modules" &&
|
||||
isDirectChildPath(legacyRuntimeDepsRoot, path.dirname(resolvedLinkedPath))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -116,18 +128,13 @@ export function removeLegacyBundledRuntimeDepsSymlink(targetPath, repoRoot) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const legacyRuntimeDepsRoot = path.resolve(repoRoot, ".local", "bundled-plugin-runtime-deps");
|
||||
let linkedPath;
|
||||
try {
|
||||
linkedPath = fs.readlinkSync(targetPath);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
const resolvedLinkedPath = path.resolve(path.dirname(targetPath), linkedPath);
|
||||
if (
|
||||
path.basename(resolvedLinkedPath) !== "node_modules" ||
|
||||
!isPathInside(legacyRuntimeDepsRoot, resolvedLinkedPath)
|
||||
) {
|
||||
if (!isLegacyBundledRuntimeDepsNodeModulesPath(targetPath, repoRoot, linkedPath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -46,6 +46,26 @@ describe("stageBundledPluginRuntimeDeps", () => {
|
||||
return path.join(repoRoot, ".artifacts", "bundled-runtime-deps-stamps", `${pluginId}.json`);
|
||||
}
|
||||
|
||||
function legacyRuntimeDepsNodeModulesPath(
|
||||
repoRoot: string,
|
||||
stageKey = "fixture-plugin-1234567890abcdef",
|
||||
) {
|
||||
return path.join(repoRoot, ".local", "bundled-plugin-runtime-deps", stageKey, "node_modules");
|
||||
}
|
||||
|
||||
function writeLegacyRuntimeDepsNodeModulesSymlink(params: {
|
||||
pluginDir: string;
|
||||
repoRoot: string;
|
||||
stageKey?: string;
|
||||
}) {
|
||||
const legacyNodeModulesDir = legacyRuntimeDepsNodeModulesPath(params.repoRoot, params.stageKey);
|
||||
const nodeModulesDir = path.join(params.pluginDir, "node_modules");
|
||||
fs.mkdirSync(legacyNodeModulesDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(legacyNodeModulesDir, "legacy.js"), "module.exports = 0;\n", "utf8");
|
||||
fs.symlinkSync(legacyNodeModulesDir, nodeModulesDir);
|
||||
return { legacyNodeModulesDir, nodeModulesDir };
|
||||
}
|
||||
|
||||
it("pins fallback install specs to exact installed versions", () => {
|
||||
const { repoRoot } = createBundledPluginFixture({
|
||||
packageJson: {
|
||||
@@ -718,24 +738,17 @@ describe("stageBundledPluginRuntimeDeps", () => {
|
||||
},
|
||||
});
|
||||
const directDir = path.join(repoRoot, "node_modules", "direct");
|
||||
const legacyNodeModulesDir = path.join(
|
||||
repoRoot,
|
||||
".local",
|
||||
"bundled-plugin-runtime-deps",
|
||||
"fixture-plugin-1234567890abcdef",
|
||||
"node_modules",
|
||||
);
|
||||
const nodeModulesDir = path.join(pluginDir, "node_modules");
|
||||
fs.mkdirSync(directDir, { recursive: true });
|
||||
fs.mkdirSync(legacyNodeModulesDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(directDir, "package.json"),
|
||||
'{ "name": "direct", "version": "1.0.0" }\n',
|
||||
"utf8",
|
||||
);
|
||||
fs.writeFileSync(path.join(directDir, "index.js"), "module.exports = 'direct';\n", "utf8");
|
||||
fs.writeFileSync(path.join(legacyNodeModulesDir, "legacy.js"), "module.exports = 0;\n", "utf8");
|
||||
fs.symlinkSync(legacyNodeModulesDir, nodeModulesDir);
|
||||
const { legacyNodeModulesDir, nodeModulesDir } = writeLegacyRuntimeDepsNodeModulesSymlink({
|
||||
pluginDir,
|
||||
repoRoot,
|
||||
});
|
||||
|
||||
stageBundledPluginRuntimeDeps({ cwd: repoRoot });
|
||||
|
||||
@@ -746,6 +759,62 @@ describe("stageBundledPluginRuntimeDeps", () => {
|
||||
expect(fs.existsSync(path.join(legacyNodeModulesDir, "legacy.js"))).toBe(true);
|
||||
});
|
||||
|
||||
it("removes legacy OpenClaw-owned symlinked plugin node_modules when deps converge to empty", () => {
|
||||
const { pluginDir, repoRoot } = createBundledPluginFixture({
|
||||
packageJson: {
|
||||
name: "@openclaw/fixture-plugin",
|
||||
version: "1.0.0",
|
||||
optionalDependencies: { optional: "1.0.0" },
|
||||
openclaw: { bundle: { stageRuntimeDependencies: true } },
|
||||
},
|
||||
});
|
||||
const rootNodeModulesDir = path.join(repoRoot, "node_modules");
|
||||
fs.mkdirSync(rootNodeModulesDir, { recursive: true });
|
||||
const { legacyNodeModulesDir, nodeModulesDir } = writeLegacyRuntimeDepsNodeModulesSymlink({
|
||||
pluginDir,
|
||||
repoRoot,
|
||||
});
|
||||
|
||||
stageBundledPluginRuntimeDeps({ cwd: repoRoot });
|
||||
|
||||
expect(fs.existsSync(nodeModulesDir)).toBe(false);
|
||||
expect(fs.existsSync(path.join(legacyNodeModulesDir, "legacy.js"))).toBe(true);
|
||||
});
|
||||
|
||||
it("refuses nested symlink targets under the legacy runtime deps root", () => {
|
||||
const { pluginDir, repoRoot } = createBundledPluginFixture({
|
||||
packageJson: {
|
||||
name: "@openclaw/fixture-plugin",
|
||||
version: "1.0.0",
|
||||
dependencies: { direct: "1.0.0" },
|
||||
openclaw: { bundle: { stageRuntimeDependencies: true } },
|
||||
},
|
||||
});
|
||||
const directDir = path.join(repoRoot, "node_modules", "direct");
|
||||
const nestedLegacyNodeModulesDir = path.join(
|
||||
repoRoot,
|
||||
".local",
|
||||
"bundled-plugin-runtime-deps",
|
||||
"fixture-plugin-1234567890abcdef",
|
||||
"nested",
|
||||
"node_modules",
|
||||
);
|
||||
const nodeModulesDir = path.join(pluginDir, "node_modules");
|
||||
fs.mkdirSync(directDir, { recursive: true });
|
||||
fs.mkdirSync(nestedLegacyNodeModulesDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(directDir, "package.json"),
|
||||
'{ "name": "direct", "version": "1.0.0" }\n',
|
||||
"utf8",
|
||||
);
|
||||
fs.writeFileSync(path.join(directDir, "index.js"), "module.exports = 'direct';\n", "utf8");
|
||||
fs.symlinkSync(nestedLegacyNodeModulesDir, nodeModulesDir);
|
||||
|
||||
expect(() => stageBundledPluginRuntimeDeps({ cwd: repoRoot })).toThrow(
|
||||
/refusing to replace runtime deps via symlinked path/u,
|
||||
);
|
||||
});
|
||||
|
||||
it("refuses to write a runtime deps stamp through a symlink", () => {
|
||||
const { repoRoot } = createBundledPluginFixture({
|
||||
packageJson: {
|
||||
|
||||
Reference in New Issue
Block a user