mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix: stabilize package-to-dev release upgrades (#115292)
* origin/test/parallels-main-3224005-host: docs(changelog): cover Windows prerequisite readiness fix(e2e): preserve Windows installer reboot results docs(changelog): include Windows stop safety gate fix(e2e): force Windows smoke gateway stop docs(changelog): note Parallels and update fixes fix(update): finalize install switches in fresh process fix(e2e): harden Parallels guest preparation
This commit is contained in:
@@ -51,6 +51,8 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- **Dev-channel updates:** finish package-to-git switches in a fresh CLI process even when source SHA and version metadata are unchanged, preventing stale hashed chunks from loading after the global package root changes.
|
||||
- **Parallels release smoke:** preserve Windows installer reboot results across Parallels, wait for WSL MSI/default-version readiness, force explicit test-owned gateway stops, and reset Linux package, config, and cache state before install lanes, preventing false prerequisite, safety-gate, and stale-config failures.
|
||||
- **OpenAI Realtime Talk auth:** remove the non-public Codex OAuth realtime fallback and require an OpenAI Platform API key for Talk, Voice Call, and Discord realtime voice, preventing OAuth-only gateways from advertising a browser session that the live service rejects. Fixes #115021.
|
||||
- **Codex native controls:** stop misclassifying valid thinking/fast runtime controls as provider overrides so Codex routes keep their native controls, while provider-native objects and invalid values stay fail-closed. Thanks @VACInc. (#107588)
|
||||
- **State snapshot verification:** run SQLite snapshot verification in a separate process so worker-thread file closes no longer drop the Gateway's POSIX WAL locks, eliminating spurious WAL misses and I/O errors. Thanks @VACInc. (#114016)
|
||||
|
||||
@@ -166,10 +166,31 @@ PY
|
||||
}
|
||||
|
||||
run_windows_installer() {
|
||||
local argv_base64
|
||||
argv_base64="$(
|
||||
python3 -c 'import base64, json, subprocess, sys; print(base64.b64encode(json.dumps({"executable": sys.argv[1], "argumentLine": subprocess.list2cmdline(sys.argv[2:])}).encode()).decode())' "$@"
|
||||
)"
|
||||
local guest_script
|
||||
guest_script="
|
||||
\$invocation = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${argv_base64}')) | ConvertFrom-Json
|
||||
\$executable = [string]\$invocation.executable
|
||||
\$process = Start-Process -FilePath \$executable -ArgumentList ([string]\$invocation.argumentLine) -NoNewWindow -Wait -PassThru
|
||||
\$installerExitCode = \$process.ExitCode
|
||||
switch (\$installerExitCode) {
|
||||
0 { exit 0 }
|
||||
1641 { exit 105 }
|
||||
3010 { exit 194 }
|
||||
default {
|
||||
Write-Error ('Windows installer failed with exit code ' + \$installerExitCode + ': ' + \$executable)
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
"
|
||||
local exit_code=0
|
||||
run_bounded 1800 "$@" || exit_code=$?
|
||||
# Windows success-with-reboot codes cross the POSIX boundary modulo 256.
|
||||
# Accept 1641/3010 only for explicit DISM/installer calls; preserve all other failures.
|
||||
run_bounded 1800 prlctl exec "$VM_NAME" powershell.exe \
|
||||
-NoProfile -ExecutionPolicy Bypass -Command "$guest_script" || exit_code=$?
|
||||
# Parallels collapses native Windows exit codes above 255 to 255. Normalize the two
|
||||
# installer success-with-reboot codes in the guest so every other failure stays fatal.
|
||||
case "$exit_code" in
|
||||
0) return 0 ;;
|
||||
105) WINDOWS_REBOOT_STARTED=1; return 0 ;;
|
||||
@@ -408,12 +429,12 @@ ensure_wsl_features() {
|
||||
local changed=0
|
||||
if [[ "$(feature_state Microsoft-Windows-Subsystem-Linux)" != "Enabled" ]]; then
|
||||
say "Enabling Microsoft-Windows-Subsystem-Linux"
|
||||
run_windows_installer prlctl exec "$VM_NAME" dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart
|
||||
run_windows_installer dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart
|
||||
changed=1
|
||||
fi
|
||||
if [[ "$(feature_state VirtualMachinePlatform)" != "Enabled" ]]; then
|
||||
say "Enabling VirtualMachinePlatform"
|
||||
run_windows_installer prlctl exec "$VM_NAME" dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart
|
||||
run_windows_installer dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart
|
||||
changed=1
|
||||
fi
|
||||
if [[ "$changed" == "1" ]]; then
|
||||
@@ -438,9 +459,28 @@ raise SystemExit("No matching signed WSL MSI asset found")
|
||||
' "$arch"
|
||||
}
|
||||
|
||||
get_wsl_default_version() {
|
||||
guest_user_ps "Get-ItemPropertyValue 'HKCU:/Software/Microsoft/Windows/CurrentVersion/Lxss' -Name DefaultVersion -ErrorAction SilentlyContinue" |
|
||||
tr -d '\r' |
|
||||
tail -n 1
|
||||
}
|
||||
|
||||
ensure_wsl_default_version() {
|
||||
local attempt
|
||||
for attempt in $(seq 1 40); do
|
||||
guest_user_cmd 'wsl.exe --set-default-version 2' >/dev/null 2>&1 || true
|
||||
if [[ "$(get_wsl_default_version)" == "2" ]]; then
|
||||
say "WSL default version ready"
|
||||
return
|
||||
fi
|
||||
sleep 3
|
||||
done
|
||||
die "WSL default version did not become 2 within 120 seconds"
|
||||
}
|
||||
|
||||
ensure_wsl_package() {
|
||||
if guest_user_cmd 'wsl.exe --version' >/dev/null 2>&1; then
|
||||
guest_user_cmd 'wsl.exe --set-default-version 2' >/dev/null
|
||||
ensure_wsl_default_version
|
||||
return
|
||||
fi
|
||||
local guest_arch asset_arch url signature wsl_msi
|
||||
@@ -457,13 +497,12 @@ ensure_wsl_package() {
|
||||
run_bounded 720 prlctl exec "$VM_NAME" curl.exe -fL --connect-timeout 20 --max-time 600 "$url" -o "$wsl_msi" || die "WSL download transport exceeded 12 minutes"
|
||||
signature="$(guest_system_ps "\$signature = Get-AuthenticodeSignature '${wsl_msi}'; if (\$signature.Status -eq 'Valid' -and \$signature.SignerCertificate.Subject -match 'Microsoft Corporation') { 'Valid' } else { \$signature.Status.ToString() + ': ' + \$signature.SignerCertificate.Subject }" | tr -d '\r' | tail -n 1)"
|
||||
[[ "$signature" == "Valid" ]] || die "WSL MSI signature was not valid Microsoft code: $signature"
|
||||
run_windows_installer prlctl exec "$VM_NAME" msiexec.exe /i 'C:\ProgramData\OpenClawPrerequisiteInstallers\WSL.msi' /qn /norestart '/L*v' 'C:\Windows\Temp\openclaw-wsl-install.log'
|
||||
run_windows_installer msiexec.exe /i 'C:\ProgramData\OpenClawPrerequisiteInstallers\WSL.msi' /qn /norestart '/L*v' 'C:\Windows\Temp\openclaw-wsl-install.log'
|
||||
finish_installer_reboot
|
||||
guest_user_cmd 'wsl.exe --version' >/dev/null || {
|
||||
guest_system_ps "Get-Content 'C:/Windows/Temp/openclaw-wsl-install.log' -Tail 80" >&2 || true
|
||||
die "WSL package install did not produce a working wsl.exe"
|
||||
}
|
||||
guest_user_cmd 'wsl.exe --set-default-version 2' >/dev/null
|
||||
# Parallels can return from the MSI client before the Windows Installer service
|
||||
# finishes publishing wsl.exe. Match the bounded readiness check used by Git and Node.
|
||||
wait_for_check WSL 'wsl.exe --version'
|
||||
ensure_wsl_default_version
|
||||
guest_system_ps "Remove-Item -LiteralPath '${wsl_msi}','C:/Windows/Temp/openclaw-wsl-install.log' -Force -ErrorAction SilentlyContinue"
|
||||
}
|
||||
|
||||
@@ -573,7 +612,7 @@ ensure_git() {
|
||||
[[ -n "$installer" ]] || die "winget did not download the Git installer"
|
||||
installer="$(stage_installer "$installer" Git 'Johannes Schindelin|Open Source Developer|Git for Windows' "$WINGET_EXPECTED_HASH")"
|
||||
say "Installing Git"
|
||||
run_windows_installer prlctl exec "$VM_NAME" "$installer" /VERYSILENT /NORESTART /SP- /ALLUSERS
|
||||
run_windows_installer "$installer" /VERYSILENT /NORESTART /SP- /ALLUSERS
|
||||
finish_installer_reboot
|
||||
wait_for_check Git 'where git.exe'
|
||||
}
|
||||
@@ -588,7 +627,7 @@ ensure_node() {
|
||||
[[ -n "$installer" ]] || die "winget did not download the Node.js installer"
|
||||
installer="$(stage_installer "$installer" NodeJS 'OpenJS Foundation' "$WINGET_EXPECTED_HASH")"
|
||||
say "Installing Node.js LTS"
|
||||
run_windows_installer prlctl exec "$VM_NAME" msiexec.exe /i "$installer" /qn /norestart
|
||||
run_windows_installer msiexec.exe /i "$installer" /qn /norestart
|
||||
finish_installer_reboot
|
||||
wait_for_check Node.js 'where node.exe'
|
||||
}
|
||||
@@ -605,7 +644,7 @@ verify_baseline() {
|
||||
guest_user_cmd 'wsl.exe --status' || die "WSL status failed"
|
||||
guest_system_ps "if (-not (Get-CimInstance Win32_ComputerSystem).HypervisorPresent) { throw 'Windows hypervisor is not active; WSL 2 workloads cannot start' }"
|
||||
local wsl_default
|
||||
wsl_default="$(guest_user_ps "Get-ItemPropertyValue 'HKCU:/Software/Microsoft/Windows/CurrentVersion/Lxss' -Name DefaultVersion -ErrorAction SilentlyContinue" | tr -d '\r' | tail -n 1)"
|
||||
wsl_default="$(get_wsl_default_version)"
|
||||
[[ "$wsl_default" == "2" ]] || die "WSL default version is ${wsl_default:-unset}, expected 2"
|
||||
assert_clean_product_state
|
||||
assert_no_pending_reboot
|
||||
@@ -631,7 +670,7 @@ prepare() {
|
||||
ensure_node
|
||||
cleanup_installers
|
||||
restart_guest
|
||||
guest_user_cmd 'wsl.exe --set-default-version 2' >/dev/null
|
||||
ensure_wsl_default_version
|
||||
verify_baseline
|
||||
create_snapshot "$BASELINE_SNAPSHOT" "E2E-ready OpenClaw Windows baseline with WSL 2, Git, Node/npm, and no OpenClaw product state."
|
||||
say "Baseline ready: $BASELINE_SNAPSHOT"
|
||||
|
||||
@@ -333,6 +333,7 @@ class LinuxSmoke extends SmokeRunController<LinuxOptions> {
|
||||
await this.phase("fresh.bootstrap-guest", BOOTSTRAP_TIMEOUT_SECONDS, () =>
|
||||
this.bootstrapGuest(),
|
||||
);
|
||||
await this.phase("fresh.reset-state", 180, () => this.resetState());
|
||||
await this.phase("fresh.preflight", 90, () => this.logGuestPreflight());
|
||||
await this.phase("fresh.install-latest-bootstrap", 420, () => this.installLatestRelease());
|
||||
await this.phase("fresh.install-main", 420, () =>
|
||||
@@ -361,6 +362,7 @@ class LinuxSmoke extends SmokeRunController<LinuxOptions> {
|
||||
await this.phase("upgrade.bootstrap-guest", BOOTSTRAP_TIMEOUT_SECONDS, () =>
|
||||
this.bootstrapGuest(),
|
||||
);
|
||||
await this.phase("upgrade.reset-state", 180, () => this.resetState());
|
||||
await this.phase("upgrade.preflight", 90, () => this.logGuestPreflight());
|
||||
await this.phase("upgrade.install-latest", 420, () => this.installLatestRelease());
|
||||
this.status.latestInstalledVersion = await this.extractLastVersion("upgrade.install-latest");
|
||||
@@ -491,6 +493,16 @@ run_apt_with_lock_retry apt-get -o Acquire::Check-Date=false -o DPkg::Lock::Time
|
||||
run_apt_with_lock_retry apt-get -o DPkg::Lock::Timeout=30 install -y curl ca-certificates`);
|
||||
}
|
||||
|
||||
private resetState(): void {
|
||||
this.guestBash(String.raw`set -euo pipefail
|
||||
pkill -f '[o]penclaw.*gateway run' >/dev/null 2>&1 || true
|
||||
pkill -f '[o]penclaw-gateway' >/dev/null 2>&1 || true
|
||||
pkill -f '[o]penclaw.mjs gateway' >/dev/null 2>&1 || true
|
||||
npm uninstall -g openclaw >/dev/null 2>&1 || true
|
||||
rm -rf /root/.openclaw /root/.npm/_cacache
|
||||
rm -f /tmp/openclaw-parallels-linux-gateway.log`);
|
||||
}
|
||||
|
||||
private installLatestRelease(): void {
|
||||
this.downloadGuestFile(this.options.installUrl, "/tmp/openclaw-install.sh");
|
||||
if (this.options.installVersion) {
|
||||
|
||||
@@ -696,11 +696,12 @@ Invoke-OpenClaw update status --json`,
|
||||
}
|
||||
|
||||
private gatewayAction(action: "restart" | "stop"): Promise<void> {
|
||||
const forceFlag = action === "stop" ? " --force" : "";
|
||||
return this.guestPowerShellBackground(
|
||||
`gateway-${action}`,
|
||||
`$ErrorActionPreference = 'Continue'
|
||||
$PSNativeCommandUseErrorActionPreference = $false
|
||||
Invoke-OpenClaw gateway ${action}
|
||||
Invoke-OpenClaw gateway ${action}${forceFlag}
|
||||
if ($LASTEXITCODE -ne 0) { throw "gateway ${action} failed with exit code $LASTEXITCODE" }`,
|
||||
420_000,
|
||||
);
|
||||
|
||||
@@ -3,7 +3,10 @@ import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { readPostCorePluginInstallRecordsFile } from "./update-command-post-core.js";
|
||||
import {
|
||||
readPostCorePluginInstallRecordsFile,
|
||||
shouldResumePostCoreUpdateInFreshProcess,
|
||||
} from "./update-command-post-core.js";
|
||||
|
||||
const tempDirs: string[] = [];
|
||||
|
||||
@@ -89,3 +92,45 @@ describe("readPostCorePluginInstallRecordsFile", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("shouldResumePostCoreUpdateInFreshProcess", () => {
|
||||
const unchangedGitResult = {
|
||||
status: "ok" as const,
|
||||
mode: "git" as const,
|
||||
root: "/tmp/openclaw",
|
||||
before: { sha: "abc123", version: "1.2.3" },
|
||||
after: { sha: "abc123", version: "1.2.3" },
|
||||
steps: [],
|
||||
durationMs: 1,
|
||||
};
|
||||
|
||||
it("uses the fresh CLI after an install-kind switch with unchanged git metadata", () => {
|
||||
expect(
|
||||
shouldResumePostCoreUpdateInFreshProcess({
|
||||
result: unchangedGitResult,
|
||||
downgradeRisk: false,
|
||||
installKindChanged: true,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps a metadata-identical git update in process when the install kind is unchanged", () => {
|
||||
expect(
|
||||
shouldResumePostCoreUpdateInFreshProcess({
|
||||
result: unchangedGitResult,
|
||||
downgradeRisk: false,
|
||||
installKindChanged: false,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("does not resume after a failed install-kind switch", () => {
|
||||
expect(
|
||||
shouldResumePostCoreUpdateInFreshProcess({
|
||||
result: { ...unchangedGitResult, status: "error" },
|
||||
downgradeRisk: false,
|
||||
installKindChanged: true,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -636,8 +636,15 @@ export function didCoreUpdateChangeInstall(result: UpdateRunResult): boolean {
|
||||
export function shouldResumePostCoreUpdateInFreshProcess(params: {
|
||||
result: UpdateRunResult;
|
||||
downgradeRisk: boolean;
|
||||
installKindChanged?: boolean;
|
||||
}): boolean {
|
||||
return !params.downgradeRisk && didCoreUpdateChangeInstall(params.result);
|
||||
// A package-to-git switch can land on the same version already cloned at its
|
||||
// target SHA. The package root still changed, so old hashed chunks are unsafe.
|
||||
return (
|
||||
params.result.status === "ok" &&
|
||||
!params.downgradeRisk &&
|
||||
(params.installKindChanged === true || didCoreUpdateChangeInstall(params.result))
|
||||
);
|
||||
}
|
||||
|
||||
export async function writeControlPlaneUpdateRestartSentinelBestEffort(params: {
|
||||
|
||||
@@ -81,6 +81,7 @@ function pickUpdateQuip(): string {
|
||||
export async function finishUpdate(params: {
|
||||
result: UpdateRunResult;
|
||||
root: string;
|
||||
installKindChanged: boolean;
|
||||
configSnapshot: Awaited<ReturnType<typeof readConfigFileSnapshot>>;
|
||||
requestedChannel: UpdateChannel | null;
|
||||
storedChannel: UpdateChannel | null;
|
||||
@@ -161,6 +162,7 @@ export async function finishUpdate(params: {
|
||||
const shouldResumePostCoreInFreshProcess = shouldResumePostCoreUpdateInFreshProcess({
|
||||
result: params.result,
|
||||
downgradeRisk: params.downgradeRisk,
|
||||
installKindChanged: params.installKindChanged,
|
||||
});
|
||||
|
||||
let postUpdateConfigSnapshot: Awaited<ReturnType<typeof readConfigFileSnapshot>> | undefined;
|
||||
|
||||
@@ -573,6 +573,7 @@ async function updateCommandInternal(
|
||||
await finishUpdate({
|
||||
result,
|
||||
root,
|
||||
installKindChanged: switchToGit || switchToPackage,
|
||||
configSnapshot,
|
||||
requestedChannel,
|
||||
storedChannel,
|
||||
|
||||
@@ -281,6 +281,13 @@ describe("Parallels smoke model selection", () => {
|
||||
expect(controller).toContain('prlctl stop "$VM_NAME" --acpi');
|
||||
expect(controller).toContain("HypervisorPresent");
|
||||
expect(controller).toContain("git --version && node --version && npm --version");
|
||||
expect(controller).toContain("wait_for_check WSL 'wsl.exe --version'");
|
||||
expect(controller).toContain("ensure_wsl_default_version");
|
||||
expect(controller).toContain("WSL default version did not become 2 within 120 seconds");
|
||||
expect(controller).toContain("1641 { exit 105 }");
|
||||
expect(controller).toContain("3010 { exit 194 }");
|
||||
expect(controller).toContain('run_bounded 1800 prlctl exec "$VM_NAME" powershell.exe');
|
||||
expect(controller).not.toContain('run_windows_installer prlctl exec "$VM_NAME"');
|
||||
expect(controller).toContain(
|
||||
"if (Test-Path -LiteralPath '${GUEST_PROFILE_PS}/Downloads/OpenClawPrereqs')",
|
||||
);
|
||||
@@ -289,6 +296,28 @@ describe("Parallels smoke model selection", () => {
|
||||
expect(controller).not.toContain("openclaw-windows-node");
|
||||
});
|
||||
|
||||
it("resets Linux product state before both install lanes", () => {
|
||||
const linux = readFileSync(TS_PATHS.linux, "utf8");
|
||||
for (const lane of ["fresh", "upgrade"]) {
|
||||
const restoreIndex = linux.indexOf(`this.phase("${lane}.restore-snapshot"`);
|
||||
const resetIndex = linux.indexOf(`this.phase("${lane}.reset-state"`);
|
||||
const installIndex = linux.indexOf(
|
||||
`this.phase("${lane}.${lane === "fresh" ? "install-latest-bootstrap" : "install-latest"}"`,
|
||||
);
|
||||
expect(restoreIndex).toBeGreaterThanOrEqual(0);
|
||||
expect(resetIndex).toBeGreaterThan(restoreIndex);
|
||||
expect(installIndex).toBeGreaterThan(resetIndex);
|
||||
}
|
||||
expect(linux).toContain("npm uninstall -g openclaw");
|
||||
expect(linux).toContain("rm -rf /root/.openclaw /root/.npm/_cacache");
|
||||
});
|
||||
|
||||
it("forces the explicit test-owned Windows gateway stop", () => {
|
||||
const windows = readFileSync(TS_PATHS.windows, "utf8");
|
||||
expect(windows).toContain('const forceFlag = action === "stop" ? " --force" : "";');
|
||||
expect(windows).toContain("Invoke-OpenClaw gateway ${action}${forceFlag}");
|
||||
});
|
||||
|
||||
it("preserves caller arguments when loaded as the Windows controller library", () => {
|
||||
const result = spawnSync(
|
||||
"bash",
|
||||
|
||||
Reference in New Issue
Block a user