Vendor OpenClaw source as Adolf fork baseline
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11), free to diverge. Tree copied sans upstream .git; upstream remote added for future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19. Preserves docs/ARCHITECTURE.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
This commit is contained in:
887
scripts/e2e/parallels/linux-smoke.ts
Executable file
887
scripts/e2e/parallels/linux-smoke.ts
Executable file
@@ -0,0 +1,887 @@
|
||||
#!/usr/bin/env -S pnpm tsx
|
||||
// Linux Smoke script supports OpenClaw repository automation.
|
||||
import { mkdir, readFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { posixAgentWorkspaceScript } from "./agent-workspace.ts";
|
||||
import {
|
||||
die,
|
||||
ensureValue,
|
||||
currentRunningSnapshotInfo,
|
||||
makeTempDir,
|
||||
parseBoolEnv,
|
||||
parseMode,
|
||||
parseTcpPort,
|
||||
parseProvider,
|
||||
readPositiveIntEnv,
|
||||
modelProviderConfigBatchJson,
|
||||
posixCodexPlatformPackageRepairFunction,
|
||||
posixProviderOnlyPluginIsolationScript,
|
||||
repoRoot,
|
||||
resolveParallelsModelTimeoutSeconds,
|
||||
resolveLatestVersion,
|
||||
resolveProviderAuth,
|
||||
resolveSnapshot,
|
||||
run,
|
||||
say,
|
||||
shouldSkipSnapshotRestore,
|
||||
shellQuote,
|
||||
validateSnapshotRestoreMode,
|
||||
warn,
|
||||
withProgressOnStderr,
|
||||
writeJson,
|
||||
writeSummaryMarkdown,
|
||||
type Mode,
|
||||
type PackageArtifact,
|
||||
type Provider,
|
||||
type ProviderAuth,
|
||||
type SnapshotInfo,
|
||||
} from "./common.ts";
|
||||
import { LinuxGuest } from "./guest-transports.ts";
|
||||
import { ensureVmRunning, resolveUbuntuVmName } from "./parallels-vm.ts";
|
||||
import { PhaseRunner } from "./phase-runner.ts";
|
||||
import {
|
||||
buildCommonSmokeSummary,
|
||||
expectedPackageBuildCommit,
|
||||
expectedPackageTargetVersion,
|
||||
extractLastOpenClawVersion,
|
||||
packAndServeSmokeArtifact,
|
||||
printSmokeTargetSummary,
|
||||
SmokeRunController,
|
||||
type SmokeHostOptions,
|
||||
type SmokeRunOptions,
|
||||
} from "./smoke-common.ts";
|
||||
|
||||
// Older published baselines predate this warning, but still need update coverage.
|
||||
const BAD_PLUGIN_DIAGNOSTIC_MIN_VERSION = "2026.5.7";
|
||||
// Restored Ubuntu snapshots may immediately run package maintenance for hours.
|
||||
// Reuse an existing downloader before touching apt, then bound the fallback.
|
||||
const APT_LOCK_RETRY_SECONDS = 900;
|
||||
const BOOTSTRAP_TIMEOUT_SECONDS = 1200;
|
||||
|
||||
function parseOpenClawPackageVersion(value: string): string | null {
|
||||
return value.match(/\b(\d{4}\.\d{1,2}\.\d{1,2}(?:-[A-Za-z0-9.]+)?)\b/u)?.[1] ?? null;
|
||||
}
|
||||
|
||||
function compareOpenClawPackageVersions(left: string, right: string): number {
|
||||
const parse = (value: string): [number, number, number] => {
|
||||
const match = parseOpenClawPackageVersion(value)?.match(/^(\d{4})\.(\d+)\.(\d+)/u);
|
||||
if (!match) {
|
||||
return [0, 0, 0];
|
||||
}
|
||||
return [Number(match[1]), Number(match[2]), Number(match[3])];
|
||||
};
|
||||
const leftParts = parse(left);
|
||||
const rightParts = parse(right);
|
||||
for (let index = 0; index < leftParts.length; index++) {
|
||||
const delta = leftParts[index] - rightParts[index];
|
||||
if (delta !== 0) {
|
||||
return delta;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
interface LinuxOptions extends SmokeHostOptions, SmokeRunOptions {
|
||||
vmName: string;
|
||||
vmNameExplicit: boolean;
|
||||
apiKeyEnv?: string;
|
||||
modelId?: string;
|
||||
installUrl: string;
|
||||
latestVersion?: string;
|
||||
}
|
||||
|
||||
interface LinuxSummary {
|
||||
vm: string;
|
||||
snapshotHint: string;
|
||||
snapshotId: string;
|
||||
mode: Mode;
|
||||
provider: Provider;
|
||||
latestVersion: string;
|
||||
installVersion: string;
|
||||
targetPackageSpec: string;
|
||||
currentHead: string;
|
||||
runDir: string;
|
||||
daemon: string;
|
||||
freshMain: {
|
||||
status: string;
|
||||
version: string;
|
||||
gateway: string;
|
||||
agent: string;
|
||||
};
|
||||
upgrade: {
|
||||
status: string;
|
||||
latestVersionInstalled: string;
|
||||
mainVersion: string;
|
||||
gateway: string;
|
||||
agent: string;
|
||||
};
|
||||
}
|
||||
|
||||
const defaultOptions = (): LinuxOptions => ({
|
||||
apiKeyEnv: undefined,
|
||||
hostIp: undefined,
|
||||
hostPort: 18427,
|
||||
hostPortExplicit: false,
|
||||
installUrl: "https://openclaw.ai/install.sh",
|
||||
installVersion: "",
|
||||
json: false,
|
||||
keepServer: false,
|
||||
latestVersion: "",
|
||||
mode: "both",
|
||||
modelId: undefined,
|
||||
provider: "openai",
|
||||
snapshotHint: "fresh",
|
||||
targetPackageSpec: "",
|
||||
vmName: "Ubuntu 26.04",
|
||||
vmNameExplicit: false,
|
||||
});
|
||||
|
||||
function usage(): string {
|
||||
return `Usage: bash scripts/e2e/parallels-linux-smoke.sh [options]
|
||||
|
||||
Options:
|
||||
--vm <name> Parallels VM name. Default: "Ubuntu 26.04"
|
||||
Falls back to the closest Ubuntu VM when omitted and unavailable.
|
||||
--snapshot-hint <name> Snapshot name substring/fuzzy match. Default: "fresh"
|
||||
--mode <fresh|upgrade|both>
|
||||
--provider <openai|anthropic|minimax>
|
||||
Provider auth/model lane. Default: openai
|
||||
--model <provider/model> Override the model used for the agent-turn smoke.
|
||||
--api-key-env <var> Host env var name for provider API key.
|
||||
--openai-api-key-env <var> Alias for --api-key-env (backward compatible)
|
||||
--install-url <url> Installer URL for latest release. Default: https://openclaw.ai/install.sh
|
||||
--host-port <port> Host HTTP port for current-main tgz. Default: 18427
|
||||
--host-ip <ip> Override Parallels host IP.
|
||||
--latest-version <ver> Override npm latest version lookup.
|
||||
--install-version <ver> Pin site-installer version/dist-tag for the baseline lane.
|
||||
--target-package-spec <npm-spec>
|
||||
Install this npm package tarball instead of packing current main.
|
||||
--keep-server Leave temp host HTTP server running.
|
||||
--json Print machine-readable JSON summary.
|
||||
-h, --help Show help.
|
||||
`;
|
||||
}
|
||||
|
||||
export function parseArgs(argv: string[]): LinuxOptions {
|
||||
const args = stripLeadingPackageManagerSeparator(argv);
|
||||
const options = defaultOptions();
|
||||
parseArgv: for (let i = 0; i < args.length; i++) {
|
||||
const arg = args[i];
|
||||
switch (arg) {
|
||||
case "--":
|
||||
break parseArgv;
|
||||
case "--vm":
|
||||
options.vmName = ensureValue(args, i, arg);
|
||||
options.vmNameExplicit = true;
|
||||
i++;
|
||||
break;
|
||||
case "--snapshot-hint":
|
||||
options.snapshotHint = ensureValue(args, i, arg);
|
||||
i++;
|
||||
break;
|
||||
case "--mode":
|
||||
options.mode = parseMode(ensureValue(args, i, arg));
|
||||
i++;
|
||||
break;
|
||||
case "--provider":
|
||||
options.provider = parseProvider(ensureValue(args, i, arg));
|
||||
i++;
|
||||
break;
|
||||
case "--model":
|
||||
options.modelId = ensureValue(args, i, arg);
|
||||
i++;
|
||||
break;
|
||||
case "--api-key-env":
|
||||
case "--openai-api-key-env":
|
||||
options.apiKeyEnv = ensureValue(args, i, arg);
|
||||
i++;
|
||||
break;
|
||||
case "--install-url":
|
||||
options.installUrl = ensureValue(args, i, arg);
|
||||
i++;
|
||||
break;
|
||||
case "--host-port":
|
||||
options.hostPort = parseTcpPort(ensureValue(args, i, arg), arg);
|
||||
options.hostPortExplicit = true;
|
||||
i++;
|
||||
break;
|
||||
case "--host-ip":
|
||||
options.hostIp = ensureValue(args, i, arg);
|
||||
i++;
|
||||
break;
|
||||
case "--latest-version":
|
||||
options.latestVersion = ensureValue(args, i, arg);
|
||||
i++;
|
||||
break;
|
||||
case "--install-version":
|
||||
options.installVersion = ensureValue(args, i, arg);
|
||||
i++;
|
||||
break;
|
||||
case "--target-package-spec":
|
||||
options.targetPackageSpec = ensureValue(args, i, arg);
|
||||
i++;
|
||||
break;
|
||||
case "--keep-server":
|
||||
options.keepServer = true;
|
||||
break;
|
||||
case "--json":
|
||||
options.json = true;
|
||||
break;
|
||||
case "-h":
|
||||
case "--help":
|
||||
process.stdout.write(usage());
|
||||
process.exit(0);
|
||||
default:
|
||||
die(`unknown arg: ${arg}`);
|
||||
}
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
function stripLeadingPackageManagerSeparator(argv: string[]): string[] {
|
||||
return argv[0] === "--" ? argv.slice(1) : argv;
|
||||
}
|
||||
|
||||
class LinuxSmoke extends SmokeRunController<LinuxOptions> {
|
||||
private auth: ProviderAuth;
|
||||
private disableBonjour = parseBoolEnv(process.env.OPENCLAW_PARALLELS_LINUX_DISABLE_BONJOUR);
|
||||
private agentTimeoutSeconds = readPositiveIntEnv(
|
||||
"OPENCLAW_PARALLELS_LINUX_AGENT_TIMEOUT_S",
|
||||
1500,
|
||||
);
|
||||
private artifact: PackageArtifact | null = null;
|
||||
private latestVersion = "";
|
||||
private snapshot!: SnapshotInfo;
|
||||
private phases!: PhaseRunner;
|
||||
private guest!: LinuxGuest;
|
||||
|
||||
protected status = {
|
||||
daemon: "systemd-user-unavailable",
|
||||
freshAgent: "skip",
|
||||
freshGateway: "skip",
|
||||
freshMain: "skip",
|
||||
freshVersion: "skip",
|
||||
latestInstalledVersion: "skip",
|
||||
upgrade: "skip",
|
||||
upgradeAgent: "skip",
|
||||
upgradeGateway: "skip",
|
||||
upgradeVersion: "skip",
|
||||
};
|
||||
|
||||
constructor(options: LinuxOptions) {
|
||||
super(options);
|
||||
this.auth = resolveProviderAuth({
|
||||
apiKeyEnv: options.apiKeyEnv,
|
||||
modelId: options.modelId,
|
||||
provider: options.provider,
|
||||
});
|
||||
}
|
||||
|
||||
async run(): Promise<void> {
|
||||
this.runDir = await makeTempDir("openclaw-parallels-linux.");
|
||||
this.phases = new PhaseRunner(this.runDir);
|
||||
this.tgzDir = await makeTempDir("openclaw-parallels-linux-tgz.");
|
||||
try {
|
||||
this.options.vmName = this.resolveVmName();
|
||||
validateSnapshotRestoreMode(this.options.mode, "Linux smoke");
|
||||
this.snapshot = shouldSkipSnapshotRestore()
|
||||
? currentRunningSnapshotInfo(this.options.vmName)
|
||||
: resolveSnapshot(this.options.vmName, this.options.snapshotHint);
|
||||
this.guest = new LinuxGuest(this.options.vmName, this.phases);
|
||||
this.latestVersion = resolveLatestVersion(this.options.latestVersion);
|
||||
await this.prepareHost(
|
||||
defaultOptions().hostPort,
|
||||
this.latestVersion,
|
||||
this.snapshot,
|
||||
this.options.vmName,
|
||||
);
|
||||
|
||||
[this.artifact, this.server, this.hostPort] = await packAndServeSmokeArtifact(
|
||||
this.tgzDir,
|
||||
this.options.targetPackageSpec,
|
||||
this.hostIp,
|
||||
this.hostPort,
|
||||
this.artifactLabel(),
|
||||
);
|
||||
|
||||
await this.runLanesAndFinish();
|
||||
} finally {
|
||||
await this.cleanupArtifacts();
|
||||
}
|
||||
}
|
||||
|
||||
private artifactLabel(): string {
|
||||
return this.options.targetPackageSpec ? "target package tgz" : "current main tgz";
|
||||
}
|
||||
|
||||
private resolveVmName(): string {
|
||||
return resolveUbuntuVmName(this.options.vmName, this.options.vmNameExplicit);
|
||||
}
|
||||
|
||||
protected async runFreshLane(): Promise<void> {
|
||||
await this.phase("fresh.restore-snapshot", 180, () => this.restoreSnapshot());
|
||||
await this.phase("fresh.bootstrap-guest", BOOTSTRAP_TIMEOUT_SECONDS, () =>
|
||||
this.bootstrapGuest(),
|
||||
);
|
||||
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, () =>
|
||||
this.installMainTgz("openclaw-main-fresh.tgz"),
|
||||
);
|
||||
this.status.freshVersion = await this.extractLastVersion("fresh.install-main");
|
||||
await this.phase("fresh.verify-main-version", 90, () => this.verifyTargetVersion());
|
||||
await this.phase("fresh.onboard-ref", 180, () => this.runRefOnboard());
|
||||
await this.phase("fresh.inject-bad-plugin", 90, () =>
|
||||
this.maybeInjectBadPluginFixture("fresh"),
|
||||
);
|
||||
await this.phase("fresh.gateway-start", 240, () => this.startGatewayBackground());
|
||||
await this.phase("fresh.bad-plugin-diagnostic", 90, () =>
|
||||
this.maybeVerifyBadPluginDiagnostic("fresh"),
|
||||
);
|
||||
await this.phase("fresh.gateway-status", 240, () => this.verifyGatewayStatus());
|
||||
this.status.freshGateway = "pass";
|
||||
await this.phase("fresh.first-local-agent-turn", this.agentTimeoutSeconds, () =>
|
||||
this.verifyLocalTurn(),
|
||||
);
|
||||
this.status.freshAgent = "pass";
|
||||
}
|
||||
|
||||
protected async runUpgradeLane(): Promise<void> {
|
||||
await this.phase("upgrade.restore-snapshot", 180, () => this.restoreSnapshot());
|
||||
await this.phase("upgrade.bootstrap-guest", BOOTSTRAP_TIMEOUT_SECONDS, () =>
|
||||
this.bootstrapGuest(),
|
||||
);
|
||||
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");
|
||||
await this.phase("upgrade.verify-latest-version", 90, () =>
|
||||
this.verifyVersionContains(this.latestVersion),
|
||||
);
|
||||
await this.phase("upgrade.install-main", 420, () =>
|
||||
this.installMainTgz("openclaw-main-upgrade.tgz"),
|
||||
);
|
||||
this.status.upgradeVersion = await this.extractLastVersion("upgrade.install-main");
|
||||
await this.phase("upgrade.verify-main-version", 90, () => this.verifyTargetVersion());
|
||||
await this.phase("upgrade.inject-bad-plugin", 90, () =>
|
||||
this.maybeInjectBadPluginFixture("upgrade"),
|
||||
);
|
||||
await this.phase("upgrade.onboard-ref", 180, () => this.runRefOnboard());
|
||||
await this.phase("upgrade.gateway-start", 240, () => this.startGatewayBackground());
|
||||
await this.phase("upgrade.bad-plugin-diagnostic", 90, () =>
|
||||
this.maybeVerifyBadPluginDiagnostic("upgrade"),
|
||||
);
|
||||
await this.phase("upgrade.gateway-status", 240, () => this.verifyGatewayStatus());
|
||||
this.status.upgradeGateway = "pass";
|
||||
await this.phase("upgrade.first-local-agent-turn", this.agentTimeoutSeconds, () =>
|
||||
this.verifyLocalTurn(),
|
||||
);
|
||||
this.status.upgradeAgent = "pass";
|
||||
}
|
||||
|
||||
private phase = async (name: string, timeoutSeconds: number, fn: () => Promise<void> | void) =>
|
||||
await this.phases.phase(name, timeoutSeconds, fn);
|
||||
|
||||
private remainingPhaseTimeoutMs = (fallbackMs?: number): number | undefined =>
|
||||
this.phases.remainingTimeoutMs(fallbackMs);
|
||||
|
||||
private logGuestPreflight(): void {
|
||||
this.guestBash(String.raw`set -euo pipefail
|
||||
printf 'preflight.user=%s\n' "$(whoami)"
|
||||
printf 'preflight.home=%s\n' "$HOME"
|
||||
printf 'preflight.path=%s\n' "$PATH"
|
||||
printf 'preflight.umask=%s\n' "$(umask)"
|
||||
printf 'preflight.npmRoot=%s\n' "$(npm root -g 2>/dev/null || true)"`);
|
||||
}
|
||||
|
||||
private log = (text: string): void => this.phases.append(text);
|
||||
|
||||
private guestExec = (
|
||||
args: string[],
|
||||
options: { check?: boolean; timeoutMs?: number } = {},
|
||||
): string => this.guest.exec(args, options);
|
||||
|
||||
private guestBash(script: string): string {
|
||||
return this.guest.bash(script);
|
||||
}
|
||||
|
||||
private waitForGuestReady(timeoutSeconds = 180): void {
|
||||
const deadline = Date.now() + timeoutSeconds * 1000;
|
||||
while (Date.now() < deadline) {
|
||||
if (
|
||||
run("prlctl", ["exec", this.options.vmName, "/usr/bin/env", "HOME=/root", "/bin/true"], {
|
||||
check: false,
|
||||
quiet: true,
|
||||
timeoutMs: this.remainingPhaseTimeoutMs(),
|
||||
}).status === 0
|
||||
) {
|
||||
return;
|
||||
}
|
||||
run("sleep", ["2"], { quiet: true });
|
||||
}
|
||||
die(`guest did not become ready in ${this.options.vmName}`);
|
||||
}
|
||||
|
||||
private restoreSnapshot(): void {
|
||||
if (shouldSkipSnapshotRestore()) {
|
||||
say(`Skip snapshot restore; using current running VM ${this.options.vmName}`);
|
||||
this.waitForGuestReady();
|
||||
return;
|
||||
}
|
||||
say(`Restore snapshot ${this.options.snapshotHint} (${this.snapshot.id})`);
|
||||
run("prlctl", ["snapshot-switch", this.options.vmName, "--id", this.snapshot.id], {
|
||||
quiet: true,
|
||||
timeoutMs: this.remainingPhaseTimeoutMs(),
|
||||
});
|
||||
ensureVmRunning(this.options.vmName, 180, {
|
||||
probeTimeoutMs: () => this.remainingPhaseTimeoutMs(30_000),
|
||||
transitionTimeoutMs: () => this.remainingPhaseTimeoutMs(120_000),
|
||||
});
|
||||
this.waitForGuestReady();
|
||||
}
|
||||
|
||||
private bootstrapGuest(): void {
|
||||
const hostNow = `@${Math.floor(Date.now() / 1000)}`;
|
||||
this.guestExec(["date", "-u", "-s", hostNow]);
|
||||
this.guestExec(["hwclock", "--systohc"], { check: false });
|
||||
this.guestExec(["timedatectl", "set-ntp", "true"], { check: false });
|
||||
this.guestExec(["systemctl", "restart", "systemd-timesyncd"], { check: false });
|
||||
this.guest.bash(`
|
||||
set -e
|
||||
if command -v curl >/dev/null 2>&1 || command -v wget >/dev/null 2>&1; then
|
||||
exit 0
|
||||
fi
|
||||
deadline=$((SECONDS + ${APT_LOCK_RETRY_SECONDS}))
|
||||
run_apt_with_lock_retry() {
|
||||
local output status
|
||||
while true; do
|
||||
if output="$("$@" 2>&1)"; then
|
||||
status=0
|
||||
else
|
||||
status=$?
|
||||
fi
|
||||
printf '%s\n' "$output"
|
||||
if [ "$status" -eq 0 ]; then
|
||||
return 0
|
||||
fi
|
||||
case "$output" in
|
||||
*"Could not get lock"*|*"Unable to acquire the dpkg frontend lock"*|*"Unable to lock directory"*)
|
||||
if [ "$SECONDS" -ge "$deadline" ]; then
|
||||
printf 'Timed out waiting for Ubuntu package maintenance locks\n' >&2
|
||||
return "$status"
|
||||
fi
|
||||
sleep 5
|
||||
;;
|
||||
*)
|
||||
return "$status"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
run_apt_with_lock_retry apt-get -o Acquire::Check-Date=false -o DPkg::Lock::Timeout=30 update
|
||||
run_apt_with_lock_retry apt-get -o DPkg::Lock::Timeout=30 install -y curl ca-certificates`);
|
||||
}
|
||||
|
||||
private installLatestRelease(): void {
|
||||
this.downloadGuestFile(this.options.installUrl, "/tmp/openclaw-install.sh");
|
||||
if (this.options.installVersion) {
|
||||
this.guestExec([
|
||||
"/usr/bin/env",
|
||||
"OPENCLAW_NO_ONBOARD=1",
|
||||
"bash",
|
||||
"/tmp/openclaw-install.sh",
|
||||
"--version",
|
||||
this.options.installVersion,
|
||||
"--no-onboard",
|
||||
]);
|
||||
} else {
|
||||
this.guestExec([
|
||||
"/usr/bin/env",
|
||||
"OPENCLAW_NO_ONBOARD=1",
|
||||
"bash",
|
||||
"/tmp/openclaw-install.sh",
|
||||
"--no-onboard",
|
||||
]);
|
||||
}
|
||||
this.guestExec(["openclaw", "--version"]);
|
||||
}
|
||||
|
||||
private downloadGuestFile(url: string, outputPath: string): void {
|
||||
this.guest.bash(`
|
||||
set -e
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
curl -fsSL --connect-timeout 10 --max-time 120 --retry 2 --retry-delay 2 ${shellQuote(
|
||||
url,
|
||||
)} -o ${shellQuote(outputPath)}
|
||||
else
|
||||
wget -q --timeout=10 --read-timeout=120 --tries=3 -O ${shellQuote(outputPath)} ${shellQuote(
|
||||
url,
|
||||
)}
|
||||
fi`);
|
||||
}
|
||||
|
||||
private installMainTgz(tempName: string): void {
|
||||
if (!this.artifact || !this.server) {
|
||||
die("package artifact/server missing");
|
||||
}
|
||||
const tgzUrl = this.server.urlFor(this.artifact.path);
|
||||
this.downloadGuestFile(tgzUrl, `/tmp/${tempName}`);
|
||||
this.guestExec(["npm", "install", "-g", `/tmp/${tempName}`, "--no-fund", "--no-audit"]);
|
||||
this.guestExec(["openclaw", "--version"]);
|
||||
}
|
||||
|
||||
private async verifyTargetVersion(): Promise<void> {
|
||||
if (!this.artifact) {
|
||||
die("package artifact missing");
|
||||
}
|
||||
if (this.options.targetPackageSpec) {
|
||||
this.verifyVersionContains(await expectedPackageTargetVersion(this.artifact));
|
||||
return;
|
||||
}
|
||||
this.verifyVersionContains(await expectedPackageBuildCommit(this.artifact));
|
||||
}
|
||||
|
||||
private verifyVersionContains(needle: string): void {
|
||||
const version = this.guestExec(["openclaw", "--version"]);
|
||||
if (!version.includes(needle)) {
|
||||
throw new Error(`version mismatch: expected substring ${needle}`);
|
||||
}
|
||||
}
|
||||
|
||||
private runRefOnboard(): void {
|
||||
this.guestExec([
|
||||
"/usr/bin/env",
|
||||
`${this.auth.apiKeyEnv}=${this.auth.apiKeyValue}`,
|
||||
"openclaw",
|
||||
"onboard",
|
||||
"--non-interactive",
|
||||
"--mode",
|
||||
"local",
|
||||
"--auth-choice",
|
||||
this.auth.authChoice,
|
||||
"--secret-input-mode",
|
||||
"ref",
|
||||
"--gateway-port",
|
||||
"18789",
|
||||
"--gateway-bind",
|
||||
"loopback",
|
||||
"--skip-skills",
|
||||
"--skip-health",
|
||||
"--accept-risk",
|
||||
"--json",
|
||||
]);
|
||||
}
|
||||
|
||||
private injectBadPluginFixture(): void {
|
||||
this.guestBash(String.raw`set -euo pipefail
|
||||
plugin_dir=/root/.openclaw/test-bad-plugin
|
||||
mkdir -p "$plugin_dir"
|
||||
cat >"$plugin_dir/package.json" <<'JSON'
|
||||
{"name":"@openclaw/test-bad-plugin","version":"1.0.0","openclaw":{"extensions":["./index.cjs"],"setupEntry":"./setup-entry.cjs"}}
|
||||
JSON
|
||||
cat >"$plugin_dir/openclaw.plugin.json" <<'JSON'
|
||||
{"id":"test-bad-plugin","configSchema":{"type":"object","additionalProperties":false,"properties":{}},"channels":["test-bad-plugin"]}
|
||||
JSON
|
||||
cat >"$plugin_dir/index.cjs" <<'JS'
|
||||
module.exports = { id: "test-bad-plugin", register() {} };
|
||||
JS
|
||||
cat >"$plugin_dir/setup-entry.cjs" <<'JS'
|
||||
module.exports = {
|
||||
kind: "bundled-channel-setup-entry",
|
||||
loadSetupPlugin() {
|
||||
throw new Error("boom: bad plugin smoke fixture");
|
||||
},
|
||||
};
|
||||
JS
|
||||
python3 - <<'PY'
|
||||
import json
|
||||
from pathlib import Path
|
||||
config_path = Path("/root/.openclaw/openclaw.json")
|
||||
config = json.loads(config_path.read_text()) if config_path.exists() else {}
|
||||
plugins = config.setdefault("plugins", {})
|
||||
load = plugins.setdefault("load", {})
|
||||
paths = load.setdefault("paths", [])
|
||||
plugin_dir = "/root/.openclaw/test-bad-plugin"
|
||||
if plugin_dir not in paths:
|
||||
paths.append(plugin_dir)
|
||||
allow = plugins.get("allow")
|
||||
if not isinstance(allow, list):
|
||||
allow = plugins["allow"] = ["openai"]
|
||||
for plugin_id in ("test-bad-plugin", "openai"):
|
||||
if plugin_id not in allow:
|
||||
allow.append(plugin_id)
|
||||
config_path.write_text(json.dumps(config, indent=2) + "\n")
|
||||
PY`);
|
||||
}
|
||||
|
||||
private versionForLane(lane: "fresh" | "upgrade"): string {
|
||||
return lane === "fresh" ? this.status.freshVersion : this.status.upgradeVersion;
|
||||
}
|
||||
|
||||
private shouldExpectBadPluginDiagnostic(lane: "fresh" | "upgrade"): boolean {
|
||||
const version = parseOpenClawPackageVersion(this.versionForLane(lane));
|
||||
if (!version) {
|
||||
return true;
|
||||
}
|
||||
return compareOpenClawPackageVersions(version, BAD_PLUGIN_DIAGNOSTIC_MIN_VERSION) >= 0;
|
||||
}
|
||||
|
||||
private maybeInjectBadPluginFixture(lane: "fresh" | "upgrade"): void {
|
||||
if (!this.shouldExpectBadPluginDiagnostic(lane)) {
|
||||
this.log(
|
||||
`Skipping bad plugin diagnostic fixture for ${lane}: installed ${this.versionForLane(lane)} predates ${BAD_PLUGIN_DIAGNOSTIC_MIN_VERSION}\n`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
this.injectBadPluginFixture();
|
||||
}
|
||||
|
||||
private startGatewayBackground(): void {
|
||||
const bonjourEnv = this.disableBonjour ? " OPENCLAW_DISABLE_BONJOUR=1" : "";
|
||||
this.guestBash(
|
||||
String.raw`pkill -f "openclaw gateway run" >/dev/null 2>&1 || true
|
||||
rm -f /tmp/openclaw-parallels-linux-gateway.log
|
||||
setsid sh -lc ` +
|
||||
shellQuote(
|
||||
`exec env OPENCLAW_HOME=/root OPENCLAW_STATE_DIR=/root/.openclaw OPENCLAW_CONFIG_PATH=/root/.openclaw/openclaw.json OPENCLAW_ALLOW_ROOT=1${bonjourEnv} ${this.auth.apiKeyEnv}=${shellQuote(
|
||||
this.auth.apiKeyValue,
|
||||
)} openclaw gateway run --bind loopback --port 18789 --force >/tmp/openclaw-parallels-linux-gateway.log 2>&1`,
|
||||
) +
|
||||
String.raw` >/dev/null 2>&1 < /dev/null &`,
|
||||
);
|
||||
const deadline = Date.now() + 240_000;
|
||||
while (Date.now() < deadline) {
|
||||
if (this.showGatewayStatusCompat(false)) {
|
||||
return;
|
||||
}
|
||||
run("sleep", ["2"], { quiet: true });
|
||||
}
|
||||
throw new Error("gateway did not become ready");
|
||||
}
|
||||
|
||||
private showGatewayStatusCompat(check = true): boolean {
|
||||
const help = this.guestExec(["openclaw", "gateway", "status", "--help"], { check: false });
|
||||
const args = help.includes("--require-rpc")
|
||||
? ["openclaw", "gateway", "status", "--deep", "--require-rpc"]
|
||||
: ["openclaw", "gateway", "status", "--deep"];
|
||||
const result = run(
|
||||
"prlctl",
|
||||
["exec", this.options.vmName, "/usr/bin/env", "HOME=/root", "OPENCLAW_ALLOW_ROOT=1", ...args],
|
||||
{
|
||||
check: false,
|
||||
quiet: true,
|
||||
timeoutMs: this.remainingPhaseTimeoutMs(),
|
||||
},
|
||||
);
|
||||
this.log(result.stdout);
|
||||
this.log(result.stderr);
|
||||
if (check && result.status !== 0) {
|
||||
throw new Error("gateway status failed");
|
||||
}
|
||||
return result.status === 0;
|
||||
}
|
||||
|
||||
private verifyGatewayStatus(): void {
|
||||
for (let attempt = 1; attempt <= 8; attempt++) {
|
||||
const result = run(
|
||||
"prlctl",
|
||||
[
|
||||
"exec",
|
||||
this.options.vmName,
|
||||
"/usr/bin/env",
|
||||
"HOME=/root",
|
||||
"OPENCLAW_ALLOW_ROOT=1",
|
||||
"openclaw",
|
||||
"gateway",
|
||||
"status",
|
||||
"--deep",
|
||||
"--require-rpc",
|
||||
"--timeout",
|
||||
"15000",
|
||||
],
|
||||
{ check: false, quiet: true, timeoutMs: this.remainingPhaseTimeoutMs() },
|
||||
);
|
||||
this.log(result.stdout);
|
||||
this.log(result.stderr);
|
||||
if (result.status === 0) {
|
||||
return;
|
||||
}
|
||||
if (attempt < 8) {
|
||||
warn(`gateway-status retry ${attempt}`);
|
||||
run("sleep", ["5"], { quiet: true });
|
||||
}
|
||||
}
|
||||
throw new Error("gateway status did not become RPC-ready");
|
||||
}
|
||||
|
||||
private async maybeVerifyBadPluginDiagnostic(lane: "fresh" | "upgrade"): Promise<void> {
|
||||
if (!this.shouldExpectBadPluginDiagnostic(lane)) {
|
||||
this.log(
|
||||
`Skipping bad plugin diagnostic assertion for ${lane}: installed ${this.versionForLane(lane)} predates ${BAD_PLUGIN_DIAGNOSTIC_MIN_VERSION}\n`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
const warning =
|
||||
"channel plugin manifest declares test-bad-plugin without channelConfigs metadata";
|
||||
const gatewayStartLog = await readFile(
|
||||
path.join(this.runDir, `${lane}.gateway-start.log`),
|
||||
"utf8",
|
||||
);
|
||||
if (!gatewayStartLog.includes(warning)) {
|
||||
throw new Error(`bad plugin diagnostic missing: ${warning}`);
|
||||
}
|
||||
this.log(warning);
|
||||
this.guestBash(String.raw`set -euo pipefail
|
||||
python3 - <<'PY'
|
||||
import json
|
||||
from pathlib import Path
|
||||
config_path = Path("/root/.openclaw/openclaw.json")
|
||||
config = json.loads(config_path.read_text()) if config_path.exists() else {}
|
||||
plugins = config.setdefault("plugins", {})
|
||||
load = plugins.setdefault("load", {})
|
||||
paths = load.get("paths")
|
||||
if isinstance(paths, list):
|
||||
load["paths"] = [path for path in paths if path != "/root/.openclaw/test-bad-plugin"]
|
||||
allow = plugins.get("allow")
|
||||
if isinstance(allow, list):
|
||||
plugins["allow"] = [plugin_id for plugin_id in allow if plugin_id != "test-bad-plugin"]
|
||||
config_path.write_text(json.dumps(config, indent=2) + "\n")
|
||||
PY
|
||||
rm -rf /root/.openclaw/test-bad-plugin`);
|
||||
}
|
||||
|
||||
private restrictAgentTurnPlugins(): void {
|
||||
this.guestBash(
|
||||
posixProviderOnlyPluginIsolationScript({
|
||||
fallbackPluginId: this.options.provider,
|
||||
modelId: this.auth.modelId,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private verifyLocalTurn(): void {
|
||||
this.guestExec(["openclaw", "models", "set", this.auth.modelId]);
|
||||
const modelProviderConfigBatch = modelProviderConfigBatchJson(this.auth.modelId, "linux");
|
||||
if (modelProviderConfigBatch) {
|
||||
this.guestBash(`provider_config_batch="$(mktemp)"
|
||||
cat >"$provider_config_batch" <<'JSON'
|
||||
${modelProviderConfigBatch}
|
||||
JSON
|
||||
openclaw config set --batch-file "$provider_config_batch" --strict-json
|
||||
rm -f "$provider_config_batch"`);
|
||||
}
|
||||
this.guestExec([
|
||||
"openclaw",
|
||||
"config",
|
||||
"set",
|
||||
"agents.defaults.skipBootstrap",
|
||||
"true",
|
||||
"--strict-json",
|
||||
]);
|
||||
this.guestExec(["openclaw", "config", "set", "tools.profile", "minimal"]);
|
||||
this.restrictAgentTurnPlugins();
|
||||
this.prepareAgentWorkspace();
|
||||
this.guestBash(
|
||||
`${posixCodexPlatformPackageRepairFunction()}
|
||||
agent_ok=false
|
||||
for attempt in 1 2; do
|
||||
session_id="parallels-linux-smoke"
|
||||
if [ "$attempt" -gt 1 ]; then session_id="parallels-linux-smoke-retry-$attempt"; fi
|
||||
rm -f "$HOME/.openclaw/agents/main/sessions/$session_id.jsonl"
|
||||
output_file="$(mktemp)"
|
||||
set +e
|
||||
/usr/bin/env OPENCLAW_ALLOW_ROOT=1 ${shellQuote(`${this.auth.apiKeyEnv}=${this.auth.apiKeyValue}`)} openclaw agent --local --agent main --session-id "$session_id" --message ${shellQuote(
|
||||
"Reply with exact ASCII text OK only.",
|
||||
)} --thinking off --timeout ${resolveParallelsModelTimeoutSeconds("linux")} --json >"$output_file" 2>&1
|
||||
rc=$?
|
||||
set -e
|
||||
cat "$output_file"
|
||||
if [ "$rc" -ne 0 ]; then
|
||||
if [ "$attempt" -lt 2 ] && repair_missing_codex_platform_package "$output_file"; then
|
||||
rm -f "$output_file"
|
||||
echo "agent turn attempt $attempt hit a missing Codex platform package; retrying"
|
||||
continue
|
||||
fi
|
||||
rm -f "$output_file"
|
||||
exit "$rc"
|
||||
fi
|
||||
if grep -Eq '"finalAssistant(Raw|Visible)Text"[[:space:]]*:[[:space:]]*"OK"' "$output_file"; then
|
||||
agent_ok=true
|
||||
rm -f "$output_file"
|
||||
break
|
||||
fi
|
||||
rm -f "$output_file"
|
||||
if [ "$attempt" -lt 2 ]; then
|
||||
echo "agent turn attempt $attempt finished without OK response; retrying"
|
||||
sleep 3
|
||||
fi
|
||||
done
|
||||
if [ "$agent_ok" != true ]; then
|
||||
echo "openclaw agent finished without OK response" >&2
|
||||
exit 1
|
||||
fi`,
|
||||
);
|
||||
}
|
||||
|
||||
private prepareAgentWorkspace(): void {
|
||||
this.guestBash(posixAgentWorkspaceScript("Parallels Linux smoke test assistant."));
|
||||
}
|
||||
|
||||
private async extractLastVersion(phaseId: string): Promise<string> {
|
||||
return await extractLastOpenClawVersion(
|
||||
this.runDir,
|
||||
phaseId,
|
||||
/(OpenClaw [^\r\n]+ \([0-9a-f]{7,}\))/g,
|
||||
);
|
||||
}
|
||||
|
||||
protected async writeSummary(): Promise<string> {
|
||||
const summaryPath = path.join(this.runDir, "summary.json");
|
||||
const summary: LinuxSummary = {
|
||||
daemon: this.status.daemon,
|
||||
...buildCommonSmokeSummary({
|
||||
artifact: this.artifact,
|
||||
latestVersion: this.latestVersion,
|
||||
options: this.options,
|
||||
runDir: this.runDir,
|
||||
snapshot: this.snapshot,
|
||||
status: this.status,
|
||||
vmName: this.options.vmName,
|
||||
}),
|
||||
};
|
||||
await writeJson(summaryPath, summary);
|
||||
await writeSummaryMarkdown({
|
||||
lines: [
|
||||
`- vm: ${summary.vm}`,
|
||||
`- target: ${summary.targetPackageSpec || "current main"}`,
|
||||
`- daemon: ${summary.daemon}`,
|
||||
`- fresh: ${summary.freshMain.status} ${summary.freshMain.version}`,
|
||||
`- fresh gateway/agent: ${summary.freshMain.gateway}/${summary.freshMain.agent}`,
|
||||
`- upgrade: ${summary.upgrade.status} ${summary.upgrade.mainVersion}`,
|
||||
`- logs: ${summary.runDir}`,
|
||||
],
|
||||
summaryPath,
|
||||
title: "Linux Parallels Smoke",
|
||||
});
|
||||
return summaryPath;
|
||||
}
|
||||
|
||||
protected printSummary(summaryPath: string): void {
|
||||
process.stdout.write("\nSummary:\n");
|
||||
printSmokeTargetSummary(this.options);
|
||||
process.stdout.write(` daemon: ${this.status.daemon}\n`);
|
||||
process.stdout.write(` fresh-main: ${this.status.freshMain} (${this.status.freshVersion})\n`);
|
||||
process.stdout.write(
|
||||
` latest->main: ${this.status.upgrade} (${this.status.upgradeVersion})\n`,
|
||||
);
|
||||
process.stdout.write(` logs: ${this.runDir}\n`);
|
||||
process.stdout.write(` summary: ${summaryPath}\n`);
|
||||
}
|
||||
}
|
||||
|
||||
if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
|
||||
const options = parseArgs(process.argv.slice(2));
|
||||
await mkdir(repoRoot, { recursive: true });
|
||||
const runSmoke = () => new LinuxSmoke(options).run();
|
||||
await (options.json ? withProgressOnStderr(runSmoke) : runSmoke());
|
||||
}
|
||||
Reference in New Issue
Block a user