openclaw/test/scripts/update-gateway.test.ts
Peter Steinberger 7fbe426cda
test(core,plugins,scripts): remove low-value tests (batch d023) (#158911)
* test(media): deslop s155 tests

* test(googlechat): deslop s156 tests

* test(scripts): deslop s140 tests

* test(reef): deslop s158 tests

* test(infra): deslop s153 tests

* test(microsoft-foundry): deslop s163 tests

* test(ai): deslop s164 tests

* test(sessions): deslop s160 tests

* test(browser): deslop s165 tests

* test(config): deslop s162 tests

* test: preserve redaction and current release identity coverage

* test: retain helpers used by current main regressions

* test(googlechat): use current config type owner

* test(googlechat): remove unused config type import
2026-09-26 15:29:21 +00:00

285 lines
11 KiB
TypeScript

import { spawnSync } from "node:child_process";
import {
chmodSync,
existsSync,
mkdirSync,
mkdtempSync,
readFileSync,
readdirSync,
rmSync,
symlinkSync,
writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import { resolveTestNodeExecPath } from "../../src/test-utils/node-process.js";
import { linkPnpmBootstrapShellTools } from "./test-helpers.js";
const testNodeExecPath = resolveTestNodeExecPath();
describe("source-server updater bootstrap", () => {
it.each(
[
"success",
"explicit-declarations",
"missing",
"enable-failure",
"install-failure",
"build-failure",
"dirty",
"symlink",
"rebase-failure",
"interrupted",
"probe-failure",
"wrong-version",
"missing-pin",
"invalid-pin",
"invalid-json",
"missing-manifest",
"hash-pin",
"local-pin",
].flatMap((scenario) =>
(scenario === "success"
? ["main", "server"]
: [scenario === "rebase-failure" || scenario === "local-pin" ? "server" : "main"]
).map((branch) => ({ scenario, branch })),
),
)("keeps checkout and restart boundaries for $scenario on $branch", ({ scenario, branch }) => {
const root = mkdtempSync(join(tmpdir(), "openclaw-server-bootstrap-"));
const repo = join(root, "repo");
const bin = join(root, "bin");
const temp = join(root, "temp");
for (const dir of [join(repo, "scripts"), join(repo, ".git"), bin, temp]) {
mkdirSync(dir, { recursive: true });
}
linkPnpmBootstrapShellTools(bin);
symlinkSync(testNodeExecPath, join(bin, "node"));
const script = join(repo, "scripts/update-gateway.sh");
writeFileSync(script, readFileSync("scripts/update-gateway.sh"));
const originalManifest = '{"packageManager":"pnpm@11.15.1"}';
const originalHead = "b".repeat(40);
const targetSha = "a".repeat(40);
const targetPin = `pnpm@12.0.0${scenario === "hash-pin" ? `+sha512.${"c".repeat(128)}` : ""}`;
writeFileSync(join(repo, "package.json"), originalManifest);
writeFileSync(join(repo, ".git/HEAD"), originalHead);
writeFileSync(join(root, "remote-head"), targetSha);
writeFileSync(
join(root, "fetched-package.json"),
scenario === "invalid-json"
? "{"
: JSON.stringify({
packageManager:
scenario === "missing-pin"
? undefined
: scenario === "invalid-pin"
? "pnpm@latest"
: targetPin,
scripts: { preinstall: "must-not-run" },
dependencies: { "must-not-install": "1.0.0" },
}),
);
writeFileSync(join(repo, "pnpm-lock.yaml"), "untouched\n");
writeFileSync(join(repo, "pnpm-workspace.yaml"), "packages: []\n");
const executable = (name: string, body: string) => {
writeFileSync(join(bin, name), `#!/bin/bash\nset -eu\n${body}\n`);
chmodSync(join(bin, name), 0o755);
};
executable(
"git",
`
echo "$*" >> "$FIXTURE/git-calls"
case "$*" in
'rev-parse --git-dir') echo .git ;;
'diff --quiet') [[ "$SCENARIO" != dirty ]] ;;
'diff --cached --quiet'|'ls-files --others --exclude-standard') exit 0 ;;
'status --short') echo ' M package.json' ;;
'rev-parse --abbrev-ref HEAD') echo "$TEST_BRANCH" ;;
'rev-parse --verify FETCH_HEAD^{commit}') echo "$TARGET_SHA" ;;
show*)
[[ "$2" == "$TARGET_SHA:package.json" ]]
[[ "$SCENARIO" != missing-manifest ]] || exit 1
printf '%s\\n' "$(<"$FIXTURE/fetched-package.json")"
;;
'rev-parse --short HEAD') echo abc123 ;;
*)
echo "$*" >> "$FIXTURE/git-mutations"
[[ "$SCENARIO" != rebase-failure || "$1" != rebase || "$2" == --abort ]] || exit 1
if [[ "$1" == merge || ( "$1" == rebase && "$2" != --abort ) ]]; then
if [[ "$SCENARIO" == local-pin ]]; then
echo '{"packageManager":"pnpm@11.15.1"}' > package.json
else
cp "$FIXTURE/fetched-package.json" package.json
fi
mutation_target="\${!#}"
if [[ "$mutation_target" == origin/main ]]; then mutation_target="$(<"$FIXTURE/remote-head")"; fi
printf '%s' "$mutation_target" > .git/HEAD
fi
;;
esac
`,
);
executable("pnpm", 'echo ambient >> "$FIXTURE/steps"; exit 93');
executable(
"selected",
`
[[ "\${COREPACK_ENABLE_DOWNLOAD_PROMPT:-}" == 0 ]] || { echo "Corepack would await terminal input" >&2; exit 91; }
[[ -z "\${CI:-}" ]]
[[ "$NPM_CONFIG_WORKSPACE_DIR" == "$PWD" && "$npm_config_workspace_dir" == "$PWD" ]]
[[ "$PNPM_CONFIG_LOCKFILE_DIR" == "$PWD" && "$pnpm_config_lockfile_dir" == "$PWD" ]]
if [[ "$1" == --version ]]; then
[[ "$PWD" == "$TMPDIR/"* && "$PWD" != "$TARGET" ]]
node - <<'NODE'
const assert = require('node:assert/strict');
const fs = require('node:fs');
assert.deepEqual(JSON.parse(fs.readFileSync('package.json', 'utf8')), { private: true, packageManager: process.env.TARGET_PIN });
assert.equal(fs.readFileSync('pnpm-workspace.yaml', 'utf8').trim(), 'packages: []');
assert.deepEqual(fs.readdirSync('.').sort(), ['package.json', 'pnpm-workspace.yaml']);
assert.equal(fs.readFileSync(process.env.TARGET + '/.git/HEAD', 'utf8'), 'b'.repeat(40));
assert.equal(process.env.OPENCLAW_UPDATE_IN_PROGRESS, undefined);
NODE
echo probe >> "$FIXTURE/steps"
# A later remote-ref move must not change the probed commit's mutation target.
echo cccccccccccccccccccccccccccccccccccccccc > "$FIXTURE/remote-head"
[[ "$SCENARIO" != probe-failure ]] || exit 42
if [[ "$SCENARIO" == wrong-version ]]; then echo 0.0.0; else echo 12.0.0; fi
exit 0
fi
[[ "$PWD" == "$TARGET" ]]
[[ "$SCENARIO" != probe-failure ]] || exit 42
node - "$1" <<'NODE'
const assert = require('node:assert/strict');
const fs = require('node:fs');
const expected = process.env.SCENARIO === 'local-pin' ? 'pnpm@11.15.1' : process.env.TARGET_PIN;
assert.equal(JSON.parse(fs.readFileSync('package.json', 'utf8')).packageManager, expected);
const building = ['build', 'nested'].includes(process.argv[2]);
assert.equal(process.env.OPENCLAW_UPDATE_IN_PROGRESS, building ? '1' : undefined, 'update runtime context must be scoped to the build and its children');
assert.equal(process.env.OPENCLAW_RUN_NODE_SKIP_DTS_BUILD, process.env.SCENARIO === 'explicit-declarations' ? '0' : undefined, 'the caller owns the declaration override');
NODE
case "$1" in
install) [[ "$2" == --frozen-lockfile ]]; echo install >> "$FIXTURE/steps"; [[ "$SCENARIO" != install-failure ]] || exit 42 ;;
build) echo build >> "$FIXTURE/steps"; pnpm nested ;;
nested) [[ "$SCENARIO" != build-failure ]] || exit 42; echo nested >> "$FIXTURE/steps" ;;
*) exit 94 ;;
esac
`,
);
if (scenario !== "missing") {
executable(
"corepack",
`
[[ "$1 $2" == 'enable --install-directory' && "$4" == pnpm ]]
[[ "$3" == "$FIXTURE/"* ]]
[[ "$SCENARIO" != enable-failure ]] || exit 1
cp "$FIXTURE/bin/selected" "$3/pnpm"
`,
);
}
if (scenario === "symlink") {
symlinkSync(root, join(repo, "dist"));
}
if (scenario === "interrupted") {
mkdirSync(join(repo, ".git/rebase-merge"));
}
try {
const result = spawnSync("/bin/bash", [script], {
encoding: "utf8",
env: {
PATH: bin,
COREPACK_ENABLE_DOWNLOAD_PROMPT: "1",
HOME: root,
TMPDIR: temp,
FIXTURE: root,
TARGET: repo,
SCENARIO: scenario,
TEST_BRANCH: branch,
TARGET_SHA: targetSha,
TARGET_PIN: targetPin,
NPM_CONFIG_WORKSPACE_DIR: root,
npm_config_workspace_dir: root,
PNPM_CONFIG_LOCKFILE_DIR: root,
pnpm_config_lockfile_dir: root,
OPENCLAW_RUN_NODE_SKIP_DTS_BUILD: scenario === "explicit-declarations" ? "0" : undefined,
// Bootstrap coverage uses the documented operator-owned lifecycle.
// Automatic recovery and lifecycle environment are covered separately.
OPENCLAW_UPDATE_RESTART_CMD: "",
},
});
const preflightFailure = [
"probe-failure",
"wrong-version",
"missing-pin",
"invalid-pin",
"invalid-json",
"missing-manifest",
].includes(scenario);
const lines = (name: string) =>
existsSync(join(root, name))
? readFileSync(join(root, name), "utf8").trim().split("\n")
: [];
if (preflightFailure) {
expect(
readFileSync(join(repo, ".git/HEAD"), "utf8"),
"failed target preflight advanced HEAD",
).toBe(originalHead);
expect(readFileSync(join(repo, "package.json"), "utf8")).toBe(originalManifest);
expect(lines("git-mutations")).toEqual(["fetch origin main"]);
expect(lines("git-calls")).toContain(`show ${targetSha}:package.json`);
expect(result.stdout + result.stderr).toContain("no checkout update or restart");
}
const succeeded = ["success", "explicit-declarations", "hash-pin", "local-pin"].includes(
scenario,
);
expect(result.status, result.stdout + result.stderr).toBe(
succeeded
? 0
: scenario.endsWith("-failure") && ["install-failure", "build-failure"].includes(scenario)
? 42
: 1,
);
const steps = lines("steps");
const beforeFetch = ["missing", "enable-failure", "dirty", "interrupted"].includes(scenario);
const beforeProbe =
beforeFetch ||
["missing-pin", "invalid-pin", "invalid-json", "missing-manifest"].includes(scenario);
expect(steps).toEqual(
beforeProbe
? []
: succeeded
? ["probe", "install", "build", "nested"]
: scenario === "build-failure"
? ["probe", "install", "build"]
: ["install-failure", "symlink"].includes(scenario)
? ["probe", "install"]
: ["probe"],
);
if (beforeFetch) {
expect(existsSync(join(root, "git-mutations"))).toBe(false);
} else if (!preflightFailure) {
expect(lines("git-calls")).toContain(`show ${targetSha}:package.json`);
expect(lines("git-mutations")).toEqual([
"fetch origin main",
branch === "main"
? `merge --ff-only ${targetSha}`
: `rebase --rebase-merges ${targetSha}`,
...(scenario === "rebase-failure" ? ["rebase --abort"] : []),
]);
if (scenario !== "rebase-failure") {
expect(readFileSync(join(repo, ".git/HEAD"), "utf8")).toBe(targetSha);
}
}
if (scenario === "missing" || scenario === "enable-failure") {
expect(result.stdout + result.stderr).toContain("Corepack");
}
if (scenario === "rebase-failure") {
expect(readFileSync(join(root, "git-mutations"), "utf8")).toContain("rebase --abort");
}
expect(readFileSync(join(repo, "pnpm-lock.yaml"), "utf8")).toBe("untouched\n");
expect(readFileSync(join(repo, "pnpm-workspace.yaml"), "utf8")).toBe("packages: []\n");
expect(readdirSync(temp)).toEqual([]);
expect(result.stdout.includes("OK abc123")).toBe(succeeded);
} finally {
rmSync(root, { recursive: true, force: true });
}
});
});