File size: 6,012 Bytes
eb3f11e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 | import { execFileSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import { assert, expect, it, vi } from "vitest";
import { writePackageDistInventory } from "../../../scripts/lib/package-dist-inventory.js";
import type { PackageUpdateTransaction } from "../../infra/package-update-steps.js";
import {
createNpmTarget,
writePackageRoot,
} from "../../infra/package-update-steps.test-support.js";
import * as updateGlobal from "../../infra/update-global.js";
import { finishUpdateRun } from "../../infra/update-run-ledger.js";
import * as shared from "./shared.js";
import * as execution from "./update-command-execution.js";
import { installFreshUpdateFixture } from "./update-command-fresh.test-support.js";
import * as packageUpdate from "./update-command-package.js";
import * as commandRun from "./update-command-run.js";
import * as servicePlan from "./update-command-service-plan.js";
import { updateCommand } from "./update-command.js";
const { fixture, dirs } = installFreshUpdateFixture();
it.each([false, true])(
"preserves local files through real fresh-profile staging (replay=%s)",
async (reapplyLocalOverrides) => {
const base = dirs.make("fresh-artifact-overrides-");
const target = createNpmTarget(path.join(base, "prefix", "lib", "node_modules"));
const npmVersion = execFileSync("npm", ["--version"], { encoding: "utf8" }).trim();
const [major = 0, minor = 0] = npmVersion.split(".").map(Number);
target.npmOwner = {
version: npmVersion,
lifecyclePolicy:
major >= 12
? "allow-scripts"
: major === 11 && minor >= 16
? "allow-scripts-advisory"
: "unflagged",
};
fixture.root = target.packageRoot!;
await writePackageRoot(fixture.root, "2026.9.3");
const relativeFile = "dist/local-reapply-probe.txt";
const marker = "operator-owned override\n";
fs.writeFileSync(path.join(fixture.root, relativeFile), marker, { mode: 0o600 });
const candidate = path.join(base, "package");
await writePackageRoot(candidate, "2026.9.4");
fs.writeFileSync(
path.join(candidate, "package.json"),
JSON.stringify({
name: "openclaw",
version: "2026.9.4",
type: "module",
openclaw: { schemaVersions: { state: 17, agent: 20 } },
}),
);
await writePackageDistInventory(candidate);
const artifact = path.join(base, "candidate.tgz");
execFileSync("tar", ["-czf", artifact, "-C", base, "package"], {
env: { ...process.env, COPYFILE_DISABLE: "1" },
});
vi.mocked(shared.resolveTargetVersion).mockResolvedValue(null);
vi.mocked(updateGlobal.resolveGlobalInstallTarget).mockResolvedValue(target);
vi.mocked(updateGlobal.createGlobalInstallEnv).mockResolvedValue({
...process.env,
npm_config_userconfig: path.join(base, "empty.npmrc"),
npm_config_globalconfig: path.join(base, "empty-global.npmrc"),
npm_config_cache: path.join(base, "npm-cache"),
});
vi.mocked(packageUpdate.stagePackageInstallUpdate).mockRestore();
const prepare = vi.mocked(commandRun.prepareUpdateCommand).getMockImplementation()!;
vi.mocked(commandRun.prepareUpdateCommand).mockImplementation(async (opts) => ({
...(await prepare(opts)),
timeoutMs: 30_000,
}));
vi.spyOn(servicePlan, "resolvePackageRuntimePreflight").mockResolvedValue({
ok: true,
value: { nodeRunner: process.execPath },
});
vi.spyOn(execution, "executeMutableUpdate").mockImplementation(async (params) => {
// Keep real admission, staging, npm, replay and backup ownership; this synthetic
// package has no Gateway lifecycle or post-core plugin convergence to finalize.
assert(params.stagedPackage);
const run = params.opts.run;
assert(run);
const assertCurrent = () => run.executorFence!.assertCurrent();
let transaction: PackageUpdateTransaction | undefined;
const result = await params.stagedPackage.run({
root: fixture.root,
installKind: "package",
tag: artifact,
timeoutMs: 30_000,
startedAt: params.startedAt,
progress: params.progress,
nodeRunner: process.execPath,
managedServiceEnv: run.env,
reapplyLocalOverrides: params.opts.reapplyLocalOverrides,
validateCandidate: async () => [],
beforeActivate: async () => assertCurrent(),
onTransaction: (value) => {
transaction = value;
},
});
expect(result.status).toBe("ok");
assert(transaction);
await transaction.complete({ activationVerified: true }, assertCurrent);
expect(fs.existsSync(path.join(fixture.root, relativeFile))).toBe(reapplyLocalOverrides);
if (reapplyLocalOverrides) {
expect(fs.readFileSync(path.join(fixture.root, relativeFile), "utf8")).toBe(marker);
expect(result.localOverrides?.warnings).toEqual([]);
} else {
expect(result.localOverrides?.warnings).toEqual([
expect.stringContaining("were not reapplied"),
]);
}
expect(result.localOverrides).toMatchObject({
status: reapplyLocalOverrides ? "applied" : "preserved",
added: 1,
applied: reapplyLocalOverrides ? 1 : 0,
conflicts: [],
});
const recoveryDir = result.localOverrides!.recoveryDir!;
expect(fs.readFileSync(path.join(recoveryDir, "files", relativeFile), "utf8")).toBe(marker);
expect(
result.steps.some(
(step) =>
step.name === "local package overrides" && step.advisory?.message.includes(recoveryDir),
),
).toBe(true);
finishUpdateRun(run.runId, { status: "succeeded" }, { env: run.env });
return null;
});
expect(fs.existsSync(fixture.databasePath)).toBe(false);
await updateCommand({
tag: `file:${artifact}`,
yes: true,
json: true,
restart: false,
reapplyLocalOverrides,
});
},
60_000,
);
|