mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:30:42 +00:00
test(e2e): add upgrade survivor scenario probes
This commit is contained in:
@@ -43,6 +43,11 @@ on:
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
published_upgrade_survivor_scenarios:
|
||||
description: Optional scenario list for published-upgrade-survivor lane expansion
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
package_artifact_name:
|
||||
description: Existing workflow artifact containing openclaw-current.tgz; blank packs the selected ref
|
||||
required: false
|
||||
@@ -133,6 +138,11 @@ on:
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
published_upgrade_survivor_scenarios:
|
||||
description: Optional scenario list for published-upgrade-survivor lane expansion
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
package_artifact_name:
|
||||
description: Existing workflow artifact containing openclaw-current.tgz; blank packs the selected ref
|
||||
required: false
|
||||
@@ -706,6 +716,7 @@ jobs:
|
||||
OPENCLAW_CURRENT_PACKAGE_TGZ: .artifacts/docker-e2e-package/openclaw-current.tgz
|
||||
OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC: ${{ inputs.published_upgrade_survivor_baseline }}
|
||||
OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPECS: ${{ inputs.published_upgrade_survivor_baselines }}
|
||||
OPENCLAW_UPGRADE_SURVIVOR_SCENARIOS: ${{ inputs.published_upgrade_survivor_scenarios }}
|
||||
OPENCLAW_SKIP_DOCKER_BUILD: "1"
|
||||
INCLUDE_OPENWEBUI: ${{ inputs.include_openwebui }}
|
||||
DOCKER_E2E_CHUNK: ${{ matrix.chunk_id }}
|
||||
@@ -941,6 +952,7 @@ jobs:
|
||||
OPENCLAW_CURRENT_PACKAGE_TGZ: .artifacts/docker-e2e-package/openclaw-current.tgz
|
||||
OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC: ${{ inputs.published_upgrade_survivor_baseline }}
|
||||
OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPECS: ${{ inputs.published_upgrade_survivor_baselines }}
|
||||
OPENCLAW_UPGRADE_SURVIVOR_SCENARIOS: ${{ inputs.published_upgrade_survivor_scenarios }}
|
||||
OPENCLAW_SKIP_DOCKER_BUILD: "1"
|
||||
INCLUDE_OPENWEBUI: ${{ inputs.include_openwebui }}
|
||||
DOCKER_E2E_LANES: ${{ matrix.group.docker_lanes }}
|
||||
|
||||
14
.github/workflows/package-acceptance.yml
vendored
14
.github/workflows/package-acceptance.yml
vendored
@@ -74,6 +74,11 @@ on:
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
published_upgrade_survivor_scenarios:
|
||||
description: Optional scenario list for published-upgrade-survivor; use reported-issues for known upgrade failure shapes
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
telegram_mode:
|
||||
description: Optional Telegram QA lane for the resolved package candidate
|
||||
required: true
|
||||
@@ -149,6 +154,11 @@ on:
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
published_upgrade_survivor_scenarios:
|
||||
description: Optional scenario list for published-upgrade-survivor; use reported-issues for known upgrade failure shapes
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
telegram_mode:
|
||||
description: Optional Telegram QA lane for the resolved package candidate
|
||||
required: false
|
||||
@@ -286,6 +296,7 @@ jobs:
|
||||
package_sha256: ${{ steps.resolve.outputs.sha256 }}
|
||||
package_version: ${{ steps.resolve.outputs.package_version }}
|
||||
published_upgrade_survivor_baselines: ${{ steps.upgrade_survivor_baselines.outputs.baselines }}
|
||||
published_upgrade_survivor_scenarios: ${{ inputs.published_upgrade_survivor_scenarios }}
|
||||
telegram_enabled: ${{ steps.profile.outputs.telegram_enabled }}
|
||||
telegram_mode: ${{ steps.profile.outputs.telegram_mode }}
|
||||
steps:
|
||||
@@ -470,6 +481,7 @@ jobs:
|
||||
WORKFLOW_REF: ${{ inputs.workflow_ref }}
|
||||
PUBLISHED_UPGRADE_SURVIVOR_BASELINE: ${{ inputs.published_upgrade_survivor_baseline }}
|
||||
PUBLISHED_UPGRADE_SURVIVOR_BASELINES: ${{ steps.upgrade_survivor_baselines.outputs.baselines }}
|
||||
PUBLISHED_UPGRADE_SURVIVOR_SCENARIOS: ${{ inputs.published_upgrade_survivor_scenarios }}
|
||||
shell: bash
|
||||
run: |
|
||||
{
|
||||
@@ -485,6 +497,7 @@ jobs:
|
||||
echo "- Profile: \`${SUITE_PROFILE}\`"
|
||||
echo "- Published upgrade survivor baseline: \`${PUBLISHED_UPGRADE_SURVIVOR_BASELINE}\`"
|
||||
echo "- Published upgrade survivor baselines: \`${PUBLISHED_UPGRADE_SURVIVOR_BASELINES}\`"
|
||||
echo "- Published upgrade survivor scenarios: \`${PUBLISHED_UPGRADE_SURVIVOR_SCENARIOS}\`"
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
docker_acceptance:
|
||||
@@ -499,6 +512,7 @@ jobs:
|
||||
docker_lanes: ${{ needs.resolve_package.outputs.docker_lanes }}
|
||||
published_upgrade_survivor_baseline: ${{ inputs.published_upgrade_survivor_baseline }}
|
||||
published_upgrade_survivor_baselines: ${{ needs.resolve_package.outputs.published_upgrade_survivor_baselines }}
|
||||
published_upgrade_survivor_scenarios: ${{ needs.resolve_package.outputs.published_upgrade_survivor_scenarios }}
|
||||
package_artifact_name: ${{ needs.resolve_package.outputs.package_artifact_name }}
|
||||
include_live_suites: ${{ needs.resolve_package.outputs.include_live_suites == 'true' }}
|
||||
live_models_only: false
|
||||
|
||||
@@ -188,7 +188,7 @@ Keep `workflow_ref` and `package_ref` separate. `workflow_ref` is the trusted wo
|
||||
|
||||
The `package` profile uses offline plugin coverage so published-package validation is not gated on live ClawHub availability. The optional Telegram lane reuses the `package-under-test` artifact in `NPM Telegram Beta E2E`, with the published npm spec path kept for standalone dispatches.
|
||||
|
||||
Release checks call Package Acceptance with `source=ref`, `package_ref=<release-ref>`, `workflow_ref=<release workflow ref>`, `suite_profile=custom`, `docker_lanes='bundled-channel-deps-compat plugins-offline'`, and `telegram_mode=mock-openai`. Release-path Docker chunks cover the overlapping package/update/plugin lanes; Package Acceptance keeps the artifact-native bundled-channel compat, offline plugin, and Telegram proof against the same resolved package tarball. Cross-OS release checks still cover OS-specific onboarding, installer, and platform behavior; package/update product validation should start with Package Acceptance. The `published-upgrade-survivor` Docker lane validates one published package baseline per run. In Package Acceptance, the resolved `package-under-test` tarball is always the candidate and `published_upgrade_survivor_baseline` selects the fallback published baseline, defaulting to `openclaw@latest`; failed-lane rerun commands preserve that baseline. Set `published_upgrade_survivor_baselines=release-history` to expand the lane across a deduped history matrix: the latest six stable releases, `2026.4.23`, and the latest stable release before `2026-03-15`. Local aggregate runs can pass exact package specs with `OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPECS`, or keep a single lane with `OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC` such as `openclaw@2026.4.15`. The published lane configures the baseline with a baked `openclaw config set` command recipe, then records recipe steps in `summary.json`. The Windows packaged and installer fresh lanes also verify that an installed package can import a browser-control override from a raw absolute Windows path. The OpenAI cross-OS agent-turn smoke defaults to `OPENCLAW_CROSS_OS_OPENAI_MODEL` when set, otherwise `openai/gpt-5.4-mini`, so the install and gateway proof stays fast and deterministic.
|
||||
Release checks call Package Acceptance with `source=ref`, `package_ref=<release-ref>`, `workflow_ref=<release workflow ref>`, `suite_profile=custom`, `docker_lanes='bundled-channel-deps-compat plugins-offline'`, and `telegram_mode=mock-openai`. Release-path Docker chunks cover the overlapping package/update/plugin lanes; Package Acceptance keeps the artifact-native bundled-channel compat, offline plugin, and Telegram proof against the same resolved package tarball. Cross-OS release checks still cover OS-specific onboarding, installer, and platform behavior; package/update product validation should start with Package Acceptance. The `published-upgrade-survivor` Docker lane validates one published package baseline per run. In Package Acceptance, the resolved `package-under-test` tarball is always the candidate and `published_upgrade_survivor_baseline` selects the fallback published baseline, defaulting to `openclaw@latest`; failed-lane rerun commands preserve that baseline. Set `published_upgrade_survivor_baselines=release-history` to expand the lane across a deduped history matrix: the latest six stable releases, `2026.4.23`, and the latest stable release before `2026-03-15`. Set `published_upgrade_survivor_scenarios=reported-issues` to expand the same baselines across issue-shaped fixtures for Feishu config/runtime-deps, preserved bootstrap/persona files, tilde log paths, and stale versioned runtime-deps roots. Local aggregate runs can pass exact package specs with `OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPECS`, keep a single lane with `OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC` such as `openclaw@2026.4.15`, or set `OPENCLAW_UPGRADE_SURVIVOR_SCENARIOS` for the scenario matrix. The published lane configures the baseline with a baked `openclaw config set` command recipe, records recipe steps in `summary.json`, and probes `/healthz`, `/readyz`, plus RPC status after Gateway start. The Windows packaged and installer fresh lanes also verify that an installed package can import a browser-control override from a raw absolute Windows path. The OpenAI cross-OS agent-turn smoke defaults to `OPENCLAW_CROSS_OS_OPENAI_MODEL` when set, otherwise `openai/gpt-5.4-mini`, so the install and gateway proof stays fast and deterministic.
|
||||
|
||||
### Legacy compatibility windows
|
||||
|
||||
|
||||
@@ -618,7 +618,7 @@ The live-model Docker runners also bind-mount only the needed CLI auth homes (or
|
||||
- Npm tarball onboarding/channel/agent smoke: `pnpm test:docker:npm-onboard-channel-agent` installs the packed OpenClaw tarball globally in Docker, configures OpenAI via env-ref onboarding plus Telegram by default, verifies doctor repairs activated plugin runtime deps, and runs one mocked OpenAI agent turn. Reuse a prebuilt tarball with `OPENCLAW_CURRENT_PACKAGE_TGZ=/path/to/openclaw-*.tgz`, skip the host rebuild with `OPENCLAW_NPM_ONBOARD_HOST_BUILD=0`, or switch channel with `OPENCLAW_NPM_ONBOARD_CHANNEL=discord`.
|
||||
- Update channel switch smoke: `pnpm test:docker:update-channel-switch` installs the packed OpenClaw tarball globally in Docker, switches from package `stable` to git `dev`, verifies the persisted channel and plugin post-update work, then switches back to package `stable` and checks update status.
|
||||
- Upgrade survivor smoke: `pnpm test:docker:upgrade-survivor` installs the packed OpenClaw tarball over a dirty old-user fixture with agents, channel config, plugin allowlists, stale plugin runtime-deps state, and existing workspace/session files. It runs package update plus non-interactive doctor without live provider or channel keys, then starts a loopback Gateway and checks config/state preservation plus startup/status budgets.
|
||||
- Published upgrade survivor smoke: `pnpm test:docker:published-upgrade-survivor` installs `openclaw@latest` by default, seeds realistic existing-user files, configures that baseline with a baked command recipe, validates the resulting config, updates that published install to the candidate tarball, runs non-interactive doctor, writes `.artifacts/upgrade-survivor/summary.json`, then starts a loopback Gateway and checks configured intents, state preservation, startup, and status budgets. Override one baseline with `OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC`, or ask the aggregate scheduler to expand exact baselines with `OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPECS`; Package Acceptance exposes those as `published_upgrade_survivor_baseline` and `published_upgrade_survivor_baselines`.
|
||||
- Published upgrade survivor smoke: `pnpm test:docker:published-upgrade-survivor` installs `openclaw@latest` by default, seeds realistic existing-user files, configures that baseline with a baked command recipe, validates the resulting config, updates that published install to the candidate tarball, runs non-interactive doctor, writes `.artifacts/upgrade-survivor/summary.json`, then starts a loopback Gateway and checks configured intents, state preservation, startup, `/healthz`, `/readyz`, and RPC status budgets. Override one baseline with `OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC`, ask the aggregate scheduler to expand exact baselines with `OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPECS`, and expand issue-shaped fixtures with `OPENCLAW_UPGRADE_SURVIVOR_SCENARIOS` such as `reported-issues`; Package Acceptance exposes those as `published_upgrade_survivor_baseline`, `published_upgrade_survivor_baselines`, and `published_upgrade_survivor_scenarios`.
|
||||
- Session runtime context smoke: `pnpm test:docker:session-runtime-context` verifies hidden runtime context transcript persistence plus doctor repair of affected duplicated prompt-rewrite branches.
|
||||
- Bun global install smoke: `bash scripts/e2e/bun-global-install-smoke.sh` packs the current tree, installs it with `bun install -g` in an isolated home, and verifies `openclaw infer image providers --json` returns bundled image providers instead of hanging. Reuse a prebuilt tarball with `OPENCLAW_BUN_GLOBAL_SMOKE_PACKAGE_TGZ=/path/to/openclaw-*.tgz`, skip the host build with `OPENCLAW_BUN_GLOBAL_SMOKE_HOST_BUILD=0`, or copy `dist/` from a built Docker image with `OPENCLAW_BUN_GLOBAL_SMOKE_DIST_IMAGE=openclaw-dockerfile-smoke:local`.
|
||||
- Installer Docker smoke: `bash scripts/test-install-sh-docker.sh` shares one npm cache across its root, update, and direct-npm containers. Update smoke defaults to npm `latest` as the stable baseline before upgrading to the candidate tarball. Override with `OPENCLAW_INSTALL_SMOKE_UPDATE_BASELINE=2026.4.22` locally, or with the Install Smoke workflow's `update_baseline_version` input on GitHub. Non-root installer checks keep an isolated npm cache so root-owned cache entries do not mask user-local install behavior. Set `OPENCLAW_INSTALL_SMOKE_NPM_CACHE_DIR=/path/to/cache` to reuse the root/update/direct-npm cache across local reruns.
|
||||
|
||||
@@ -43,7 +43,7 @@ title: "Tests"
|
||||
- `pnpm test:docker:openwebui`: Starts Dockerized OpenClaw + Open WebUI, signs in through Open WebUI, checks `/api/models`, then runs a real proxied chat through `/api/chat/completions`. Requires a usable live model key (for example OpenAI in `~/.profile`), pulls an external Open WebUI image, and is not expected to be CI-stable like the normal unit/e2e suites.
|
||||
- `pnpm test:docker:mcp-channels`: Starts a seeded Gateway container and a second client container that spawns `openclaw mcp serve`, then verifies routed conversation discovery, transcript reads, attachment metadata, live event queue behavior, outbound send routing, and Claude-style channel + permission notifications over the real stdio bridge. The Claude notification assertion reads the raw stdio MCP frames directly so the smoke reflects what the bridge actually emits.
|
||||
- `pnpm test:docker:upgrade-survivor`: Installs the packed OpenClaw tarball over a dirty old-user fixture, runs package update plus non-interactive doctor without live provider or channel keys, then starts a loopback Gateway and checks that agents, channel config, plugin allowlists, workspace/session files, stale plugin runtime-deps state, startup, and RPC status survive.
|
||||
- `pnpm test:docker:published-upgrade-survivor`: Installs `openclaw@latest` by default, seeds realistic existing-user files without live provider or channel keys, configures that baseline with a baked `openclaw config set` command recipe, updates that published install to the packed OpenClaw tarball, runs non-interactive doctor, writes `.artifacts/upgrade-survivor/summary.json`, then starts a loopback Gateway and checks that configured intents, workspace/session files, stale plugin config/runtime-deps state, startup, and RPC status survive or repair cleanly. Override one baseline with `OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC`, or expand an exact matrix with `OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPECS`; Package Acceptance exposes those as `published_upgrade_survivor_baseline` and `published_upgrade_survivor_baselines`.
|
||||
- `pnpm test:docker:published-upgrade-survivor`: Installs `openclaw@latest` by default, seeds realistic existing-user files without live provider or channel keys, configures that baseline with a baked `openclaw config set` command recipe, updates that published install to the packed OpenClaw tarball, runs non-interactive doctor, writes `.artifacts/upgrade-survivor/summary.json`, then starts a loopback Gateway and checks that configured intents, workspace/session files, stale plugin config/runtime-deps state, startup, `/healthz`, `/readyz`, and RPC status survive or repair cleanly. Override one baseline with `OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC`, expand an exact matrix with `OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPECS`, or add scenario fixtures with `OPENCLAW_UPGRADE_SURVIVOR_SCENARIOS=reported-issues`; Package Acceptance exposes those as `published_upgrade_survivor_baseline`, `published_upgrade_survivor_baselines`, and `published_upgrade_survivor_scenarios`.
|
||||
|
||||
## Local PR gate
|
||||
|
||||
|
||||
@@ -2,6 +2,20 @@ import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
const command = process.argv[2];
|
||||
const SCENARIOS = new Set([
|
||||
"base",
|
||||
"feishu-channel",
|
||||
"bootstrap-persona",
|
||||
"tilde-log-path",
|
||||
"versioned-runtime-deps",
|
||||
]);
|
||||
|
||||
const PERSONA_FILES = new Map([
|
||||
["BOOTSTRAP.md", "# Existing Bootstrap\n\nDo not overwrite me during update.\n"],
|
||||
["SOUL.md", "# Existing Soul\n\nKeep this voice intact.\n"],
|
||||
["USER.md", "# Existing User\n\nPrefers survivor tests.\n"],
|
||||
["MEMORY.md", "# Existing Memory\n\nUpgrade reports came from real users.\n"],
|
||||
]);
|
||||
|
||||
function requireEnv(name) {
|
||||
const value = process.env[name];
|
||||
@@ -30,6 +44,12 @@ function assert(condition, message) {
|
||||
}
|
||||
}
|
||||
|
||||
function getScenario() {
|
||||
const scenario = process.env.OPENCLAW_UPGRADE_SURVIVOR_SCENARIO || "base";
|
||||
assert(SCENARIOS.has(scenario), `unknown upgrade survivor scenario: ${scenario}`);
|
||||
return scenario;
|
||||
}
|
||||
|
||||
function getConfig() {
|
||||
return readJson(requireEnv("OPENCLAW_CONFIG_PATH"));
|
||||
}
|
||||
@@ -56,11 +76,17 @@ function hasCoverage(coverage) {
|
||||
function seedState() {
|
||||
const stateDir = requireEnv("OPENCLAW_STATE_DIR");
|
||||
const workspace = requireEnv("OPENCLAW_TEST_WORKSPACE_DIR");
|
||||
const scenario = getScenario();
|
||||
|
||||
write(
|
||||
path.join(workspace, "IDENTITY.md"),
|
||||
"# Upgrade Survivor\n\nThis workspace must survive package update and doctor repair.\n",
|
||||
);
|
||||
if (scenario === "bootstrap-persona") {
|
||||
for (const [fileName, contents] of PERSONA_FILES) {
|
||||
write(path.join(workspace, fileName), contents);
|
||||
}
|
||||
}
|
||||
writeJson(path.join(workspace, ".openclaw", "workspace-state.json"), {
|
||||
version: 1,
|
||||
setupCompletedAt: "2026-04-01T00:00:00.000Z",
|
||||
@@ -90,6 +116,33 @@ function seedState() {
|
||||
`${JSON.stringify({ name: "stale-sentinel", version: "0.0.0" }, null, 2)}\n`,
|
||||
);
|
||||
}
|
||||
if (scenario === "versioned-runtime-deps") {
|
||||
const version = process.env.OPENCLAW_UPGRADE_SURVIVOR_BASELINE_VERSION || "2026.4.24";
|
||||
for (const plugin of ["discord", "feishu", "telegram", "whatsapp"]) {
|
||||
writeJson(
|
||||
path.join(
|
||||
runtimeRoot,
|
||||
`openclaw-${version}-${plugin}`,
|
||||
".openclaw-runtime-deps-stamp.json",
|
||||
),
|
||||
{
|
||||
packageVersion: version,
|
||||
plugin,
|
||||
stale: true,
|
||||
},
|
||||
);
|
||||
write(
|
||||
path.join(
|
||||
runtimeRoot,
|
||||
`openclaw-${version}-${plugin}`,
|
||||
"node_modules",
|
||||
"stale-sentinel",
|
||||
"package.json",
|
||||
),
|
||||
`${JSON.stringify({ name: "stale-sentinel", version: "0.0.0" }, null, 2)}\n`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
writeJson(path.join(stateDir, "survivor-baseline.json"), {
|
||||
agents: ["main", "ops"],
|
||||
@@ -98,6 +151,7 @@ function seedState() {
|
||||
telegramGroup: "-1001234567890",
|
||||
whatsappGroup: "120363000000000000@g.us",
|
||||
workspaceIdentity: path.join(workspace, "IDENTITY.md"),
|
||||
scenario,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -150,6 +204,9 @@ function assertConfigSurvived() {
|
||||
assert(pluginAllow.includes("discord"), "discord plugin allow entry missing");
|
||||
assert(pluginAllow.includes("telegram"), "telegram plugin allow entry missing");
|
||||
assert(pluginAllow.includes("whatsapp"), "whatsapp plugin allow entry missing");
|
||||
if (hasCoverage(coverage) && acceptsIntent(coverage, "feishu-channel")) {
|
||||
assert(pluginAllow.includes("feishu"), "feishu plugin allow entry missing");
|
||||
}
|
||||
}
|
||||
|
||||
if (acceptsIntent(coverage, "discord-channel")) {
|
||||
@@ -192,11 +249,31 @@ function assertConfigSurvived() {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (hasCoverage(coverage) && acceptsIntent(coverage, "feishu-channel")) {
|
||||
const feishu = config.channels?.feishu;
|
||||
assert(feishu?.enabled === true, "feishu enabled flag changed");
|
||||
assert(feishu?.connectionMode === "webhook", "feishu connection mode changed");
|
||||
assert(feishu?.defaultAccount === "default", "feishu default account changed");
|
||||
assert(feishu?.accounts?.default?.appId === "cli_upgrade_survivor", "feishu account changed");
|
||||
assert(
|
||||
feishu.groups?.oc_upgrade_survivor?.requireMention === true,
|
||||
"feishu group mention policy changed",
|
||||
);
|
||||
}
|
||||
|
||||
if (hasCoverage(coverage) && acceptsIntent(coverage, "logging")) {
|
||||
assert(
|
||||
config.logging?.file === "~/openclaw-upgrade-survivor/gateway.jsonl",
|
||||
"logging.file tilde path changed",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function assertStateSurvived() {
|
||||
const stateDir = requireEnv("OPENCLAW_STATE_DIR");
|
||||
const workspace = requireEnv("OPENCLAW_TEST_WORKSPACE_DIR");
|
||||
const scenario = getScenario();
|
||||
assert(fs.existsSync(path.join(workspace, "IDENTITY.md")), "workspace identity file missing");
|
||||
assert(
|
||||
fs.existsSync(path.join(stateDir, "agents", "main", "sessions", "legacy-session.json")),
|
||||
@@ -206,6 +283,27 @@ function assertStateSurvived() {
|
||||
fs.existsSync(path.join(stateDir, "plugin-runtime-deps", "discord")),
|
||||
"plugin runtime deps root missing",
|
||||
);
|
||||
if (scenario === "bootstrap-persona") {
|
||||
for (const [fileName, contents] of PERSONA_FILES) {
|
||||
const actual = fs.readFileSync(path.join(workspace, fileName), "utf8");
|
||||
assert(actual === contents, `${fileName} was changed during update/doctor`);
|
||||
}
|
||||
}
|
||||
if (scenario === "versioned-runtime-deps") {
|
||||
const stage = process.env.OPENCLAW_UPGRADE_SURVIVOR_ASSERT_STAGE || "survival";
|
||||
if (stage === "baseline") {
|
||||
return;
|
||||
}
|
||||
const version = process.env.OPENCLAW_UPGRADE_SURVIVOR_BASELINE_VERSION || "2026.4.24";
|
||||
const runtimeRoot = path.join(stateDir, "plugin-runtime-deps");
|
||||
const staleVersionedRoots = fs.existsSync(runtimeRoot)
|
||||
? fs.readdirSync(runtimeRoot).filter((entry) => entry.startsWith(`openclaw-${version}-`))
|
||||
: [];
|
||||
assert(
|
||||
staleVersionedRoots.length === 0,
|
||||
`stale versioned runtime deps survived update/doctor: ${staleVersionedRoots.join(", ")}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function assertStatusJson([file]) {
|
||||
|
||||
@@ -68,6 +68,31 @@ const representativeConfigSteps = [
|
||||
),
|
||||
];
|
||||
|
||||
const scenarioConfigSteps = new Map([
|
||||
[
|
||||
"feishu-channel",
|
||||
[
|
||||
configSetJsonFile("plugins-feishu", "plugins", "plugins", "plugins-feishu.json"),
|
||||
configSetJsonFile(
|
||||
"channels-feishu",
|
||||
"feishu-channel",
|
||||
"channels.feishu",
|
||||
"channels-feishu.json",
|
||||
),
|
||||
],
|
||||
],
|
||||
[
|
||||
"tilde-log-path",
|
||||
[
|
||||
{
|
||||
id: "logging-file",
|
||||
intent: "logging",
|
||||
argv: ["config", "set", "logging.file", "~/openclaw-upgrade-survivor/gateway.jsonl"],
|
||||
},
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
const recipe = [
|
||||
{
|
||||
id: "update-channel",
|
||||
@@ -83,6 +108,10 @@ const recipe = [
|
||||
},
|
||||
];
|
||||
|
||||
function selectedScenario() {
|
||||
return process.env.OPENCLAW_UPGRADE_SURVIVOR_SCENARIO || "base";
|
||||
}
|
||||
|
||||
function runOpenClaw(step) {
|
||||
const result = spawnSync("openclaw", step.argv, {
|
||||
encoding: "utf8",
|
||||
@@ -103,10 +132,13 @@ function runOpenClaw(step) {
|
||||
function applyRecipe() {
|
||||
const summaryPath = option("--summary");
|
||||
const baselineVersion = option("--baseline-version", null);
|
||||
const scenario = selectedScenario();
|
||||
const scenarioSteps = scenarioConfigSteps.get(scenario) ?? [];
|
||||
const summary = {
|
||||
source: "baseline-cli-command-recipe",
|
||||
recipe: "upgrade-survivor-v1",
|
||||
baselineVersion,
|
||||
scenario,
|
||||
acceptedIntents: [
|
||||
"update",
|
||||
"gateway",
|
||||
@@ -117,12 +149,13 @@ function applyRecipe() {
|
||||
"discord-channel",
|
||||
"telegram-channel",
|
||||
"whatsapp-channel",
|
||||
...scenarioSteps.map((step) => step.intent),
|
||||
],
|
||||
skippedIntents: [],
|
||||
steps: [],
|
||||
};
|
||||
|
||||
for (const step of recipe) {
|
||||
for (const step of [...recipe.slice(0, -1), ...scenarioSteps, recipe.at(-1)]) {
|
||||
const outcome = runOpenClaw(step);
|
||||
summary.steps.push(outcome);
|
||||
writeJson(summaryPath, summary);
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"enabled": true,
|
||||
"domain": "feishu",
|
||||
"connectionMode": "webhook",
|
||||
"defaultAccount": "default",
|
||||
"verificationToken": "upgrade-survivor-feishu-verification",
|
||||
"encryptKey": "upgrade-survivor-feishu-encrypt",
|
||||
"webhookPath": "/feishu/events",
|
||||
"webhookHost": "127.0.0.1",
|
||||
"webhookPort": 3000,
|
||||
"accounts": {
|
||||
"default": {
|
||||
"enabled": true,
|
||||
"name": "Upgrade Survivor Feishu",
|
||||
"appId": "cli_upgrade_survivor",
|
||||
"appSecret": {
|
||||
"source": "env",
|
||||
"provider": "default",
|
||||
"id": "FEISHU_APP_SECRET"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dmPolicy": "allowlist",
|
||||
"allowFrom": ["ou_upgrade_survivor"],
|
||||
"groupPolicy": "allowlist",
|
||||
"groupAllowFrom": ["oc_upgrade_survivor"],
|
||||
"groups": {
|
||||
"oc_upgrade_survivor": {
|
||||
"enabled": true,
|
||||
"requireMention": true,
|
||||
"tools": {
|
||||
"allow": ["message_send"],
|
||||
"deny": ["exec"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"enabled": true,
|
||||
"allow": ["discord", "feishu", "memory", "telegram", "whatsapp"],
|
||||
"entries": {
|
||||
"discord": {
|
||||
"enabled": true
|
||||
},
|
||||
"feishu": {
|
||||
"enabled": true
|
||||
},
|
||||
"telegram": {
|
||||
"enabled": true
|
||||
},
|
||||
"whatsapp": {
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
}
|
||||
63
scripts/e2e/lib/upgrade-survivor/probe-gateway.mjs
Normal file
63
scripts/e2e/lib/upgrade-survivor/probe-gateway.mjs
Normal file
@@ -0,0 +1,63 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
function option(name, fallback) {
|
||||
const index = args.indexOf(name);
|
||||
if (index === -1) {
|
||||
return fallback;
|
||||
}
|
||||
const value = args[index + 1];
|
||||
if (!value) {
|
||||
throw new Error(`missing value for ${name}`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function writeJson(file, value) {
|
||||
fs.mkdirSync(path.dirname(file), { recursive: true });
|
||||
fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`);
|
||||
}
|
||||
|
||||
const baseUrl = option("--base-url");
|
||||
const probePath = option("--path");
|
||||
const expectKind = option("--expect");
|
||||
const out = option("--out");
|
||||
const url = new URL(probePath, baseUrl).toString();
|
||||
|
||||
const startedAt = Date.now();
|
||||
const response = await fetch(url, { method: "GET" });
|
||||
const text = await response.text();
|
||||
let body;
|
||||
try {
|
||||
body = text ? JSON.parse(text) : null;
|
||||
} catch (error) {
|
||||
throw new Error(`${url} returned non-JSON probe body: ${String(error)}`);
|
||||
}
|
||||
const elapsedMs = Date.now() - startedAt;
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`${url} probe failed with HTTP ${response.status}: ${text}`);
|
||||
}
|
||||
if (expectKind === "live") {
|
||||
if (body?.ok !== true || body?.status !== "live") {
|
||||
throw new Error(`${url} did not report live status: ${text}`);
|
||||
}
|
||||
} else if (expectKind === "ready") {
|
||||
if (body?.ready !== true) {
|
||||
throw new Error(`${url} did not report ready status: ${text}`);
|
||||
}
|
||||
} else {
|
||||
throw new Error(`unknown probe expectation: ${expectKind}`);
|
||||
}
|
||||
|
||||
writeJson(out, {
|
||||
body,
|
||||
elapsedMs,
|
||||
path: probePath,
|
||||
status: response.status,
|
||||
url,
|
||||
});
|
||||
@@ -16,6 +16,7 @@ export GATEWAY_AUTH_TOKEN_REF="upgrade-survivor-token"
|
||||
export OPENAI_API_KEY="sk-openclaw-upgrade-survivor"
|
||||
export DISCORD_BOT_TOKEN="upgrade-survivor-discord-token"
|
||||
export TELEGRAM_BOT_TOKEN="123456:upgrade-survivor-telegram-token"
|
||||
export FEISHU_APP_SECRET="upgrade-survivor-feishu-secret"
|
||||
|
||||
ARTIFACT_ROOT="$(dirname "${OPENCLAW_UPGRADE_SURVIVOR_SUMMARY_JSON:-/tmp/openclaw-upgrade-survivor-artifacts/summary.json}")"
|
||||
mkdir -p "$ARTIFACT_ROOT"
|
||||
@@ -33,6 +34,7 @@ PHASE_LOG="$ARTIFACT_ROOT/phases.jsonl"
|
||||
BASELINE_RAW="${OPENCLAW_UPGRADE_SURVIVOR_BASELINE:?missing OPENCLAW_UPGRADE_SURVIVOR_BASELINE}"
|
||||
CANDIDATE_KIND="${OPENCLAW_UPGRADE_SURVIVOR_CANDIDATE_KIND:-tarball}"
|
||||
CANDIDATE_SPEC="${OPENCLAW_UPGRADE_SURVIVOR_CANDIDATE_SPEC:-${OPENCLAW_CURRENT_PACKAGE_TGZ:-}}"
|
||||
SCENARIO="${OPENCLAW_UPGRADE_SURVIVOR_SCENARIO:-base}"
|
||||
CURRENT_PHASE="setup"
|
||||
FAILURE_PHASE=""
|
||||
FAILURE_MESSAGE=""
|
||||
@@ -44,12 +46,16 @@ candidate_version=""
|
||||
installed_version=""
|
||||
start_seconds=""
|
||||
status_seconds=""
|
||||
healthz_seconds=""
|
||||
readyz_seconds=""
|
||||
|
||||
BASELINE_INSTALL_LOG="$ARTIFACT_ROOT/baseline-install.log"
|
||||
UPDATE_JSON="$ARTIFACT_ROOT/update.json"
|
||||
UPDATE_ERR="$ARTIFACT_ROOT/update.err"
|
||||
DOCTOR_LOG="$ARTIFACT_ROOT/doctor.log"
|
||||
GATEWAY_LOG="$ARTIFACT_ROOT/gateway.log"
|
||||
HEALTHZ_JSON="$ARTIFACT_ROOT/healthz.json"
|
||||
READYZ_JSON="$ARTIFACT_ROOT/readyz.json"
|
||||
STATUS_JSON="$ARTIFACT_ROOT/status.json"
|
||||
STATUS_ERR="$ARTIFACT_ROOT/status.err"
|
||||
BASELINE_CONFIG_VALIDATE_LOG="$ARTIFACT_ROOT/baseline-config-validate.log"
|
||||
@@ -128,7 +134,10 @@ write_summary() {
|
||||
SUMMARY_BASELINE_VERSION="$baseline_version" \
|
||||
SUMMARY_CANDIDATE_VERSION="$candidate_version" \
|
||||
SUMMARY_INSTALLED_VERSION="$installed_version" \
|
||||
SUMMARY_SCENARIO="$SCENARIO" \
|
||||
SUMMARY_START_SECONDS="$start_seconds" \
|
||||
SUMMARY_HEALTHZ_SECONDS="$healthz_seconds" \
|
||||
SUMMARY_READYZ_SECONDS="$readyz_seconds" \
|
||||
SUMMARY_STATUS_SECONDS="$status_seconds" \
|
||||
SUMMARY_FAILURE_PHASE="$FAILURE_PHASE" \
|
||||
SUMMARY_CONFIG_COVERAGE="$CONFIG_COVERAGE_JSON" \
|
||||
@@ -153,6 +162,7 @@ const summary = {
|
||||
spec: process.env.SUMMARY_BASELINE_SPEC || null,
|
||||
version: process.env.SUMMARY_BASELINE_VERSION || null,
|
||||
},
|
||||
scenario: process.env.SUMMARY_SCENARIO || "base",
|
||||
candidate: {
|
||||
kind: process.env.OPENCLAW_UPGRADE_SURVIVOR_CANDIDATE_KIND || null,
|
||||
spec: process.env.OPENCLAW_UPGRADE_SURVIVOR_CANDIDATE_SPEC || process.env.OPENCLAW_CURRENT_PACKAGE_TGZ || null,
|
||||
@@ -161,6 +171,8 @@ const summary = {
|
||||
installedVersion: process.env.SUMMARY_INSTALLED_VERSION || null,
|
||||
timings: {
|
||||
startupSeconds: numberOrNull(process.env.SUMMARY_START_SECONDS),
|
||||
healthzSeconds: numberOrNull(process.env.SUMMARY_HEALTHZ_SECONDS),
|
||||
readyzSeconds: numberOrNull(process.env.SUMMARY_READYZ_SECONDS),
|
||||
statusSeconds: numberOrNull(process.env.SUMMARY_STATUS_SECONDS),
|
||||
},
|
||||
config: readJsonOrNull(process.env.SUMMARY_CONFIG_COVERAGE),
|
||||
@@ -273,6 +285,7 @@ install_baseline() {
|
||||
seed_state() {
|
||||
openclaw_e2e_eval_test_state_from_b64 "${OPENCLAW_TEST_STATE_FUNCTION_B64:?missing OPENCLAW_TEST_STATE_FUNCTION_B64}"
|
||||
openclaw_test_state_create "$ARTIFACT_ROOT/state-home" minimal
|
||||
export OPENCLAW_UPGRADE_SURVIVOR_BASELINE_VERSION="$baseline_version"
|
||||
node scripts/e2e/lib/upgrade-survivor/assertions.mjs seed
|
||||
}
|
||||
|
||||
@@ -291,8 +304,10 @@ validate_baseline_config() {
|
||||
}
|
||||
|
||||
assert_baseline_state() {
|
||||
node scripts/e2e/lib/upgrade-survivor/assertions.mjs assert-config
|
||||
node scripts/e2e/lib/upgrade-survivor/assertions.mjs assert-state
|
||||
OPENCLAW_UPGRADE_SURVIVOR_ASSERT_STAGE=baseline \
|
||||
node scripts/e2e/lib/upgrade-survivor/assertions.mjs assert-config
|
||||
OPENCLAW_UPGRADE_SURVIVOR_ASSERT_STAGE=baseline \
|
||||
node scripts/e2e/lib/upgrade-survivor/assertions.mjs assert-state
|
||||
}
|
||||
|
||||
resolve_candidate_version() {
|
||||
@@ -349,6 +364,14 @@ run_doctor() {
|
||||
fi
|
||||
}
|
||||
|
||||
validate_post_doctor_config() {
|
||||
if ! openclaw config validate >>"$DOCTOR_LOG" 2>&1; then
|
||||
echo "post-doctor config validation failed" >&2
|
||||
cat "$DOCTOR_LOG" >&2 || true
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
assert_survival() {
|
||||
node scripts/e2e/lib/upgrade-survivor/assertions.mjs assert-config
|
||||
node scripts/e2e/lib/upgrade-survivor/assertions.mjs assert-state
|
||||
@@ -359,6 +382,22 @@ assert_survival() {
|
||||
fi
|
||||
}
|
||||
|
||||
probe_gateway_endpoint() {
|
||||
local path="$1"
|
||||
local expect_kind="$2"
|
||||
local out_file="$3"
|
||||
local start_epoch
|
||||
local end_epoch
|
||||
start_epoch="$(node -e "process.stdout.write(String(Date.now()))")"
|
||||
node scripts/e2e/lib/upgrade-survivor/probe-gateway.mjs \
|
||||
--base-url "http://127.0.0.1:18789" \
|
||||
--path "$path" \
|
||||
--expect "$expect_kind" \
|
||||
--out "$out_file"
|
||||
end_epoch="$(node -e "process.stdout.write(String(Date.now()))")"
|
||||
printf '%s\n' "$(((end_epoch - start_epoch + 999) / 1000))"
|
||||
}
|
||||
|
||||
start_gateway() {
|
||||
local port=18789
|
||||
local budget="${OPENCLAW_UPGRADE_SURVIVOR_START_BUDGET_SECONDS:-90}"
|
||||
@@ -377,6 +416,11 @@ start_gateway() {
|
||||
fi
|
||||
}
|
||||
|
||||
check_gateway_probes() {
|
||||
healthz_seconds="$(probe_gateway_endpoint /healthz live "$HEALTHZ_JSON")"
|
||||
readyz_seconds="$(probe_gateway_endpoint /readyz ready "$READYZ_JSON")"
|
||||
}
|
||||
|
||||
check_gateway_status() {
|
||||
local port=18789
|
||||
local budget="${OPENCLAW_UPGRADE_SURVIVOR_STATUS_BUDGET_SECONDS:-30}"
|
||||
@@ -409,8 +453,10 @@ phase assert-baseline assert_baseline_state
|
||||
phase resolve-candidate resolve_candidate_version
|
||||
phase update-candidate update_candidate
|
||||
phase doctor run_doctor
|
||||
phase validate-post-doctor-config validate_post_doctor_config
|
||||
phase assert-survival assert_survival
|
||||
phase gateway-start start_gateway
|
||||
phase gateway-probes check_gateway_probes
|
||||
phase gateway-status check_gateway_status
|
||||
|
||||
echo "Upgrade survivor Docker E2E passed baseline=${baseline_spec} candidate=${candidate_version} startup=${start_seconds}s status=${status_seconds}s."
|
||||
echo "Upgrade survivor Docker E2E passed baseline=${baseline_spec} scenario=${SCENARIO} candidate=${candidate_version} startup=${start_seconds}s healthz=${healthz_seconds}s readyz=${readyz_seconds}s status=${status_seconds}s."
|
||||
|
||||
@@ -12,6 +12,7 @@ IMAGE_NAME="$(docker_e2e_resolve_image "openclaw-upgrade-survivor-e2e" OPENCLAW_
|
||||
SKIP_BUILD="${OPENCLAW_UPGRADE_SURVIVOR_E2E_SKIP_BUILD:-0}"
|
||||
DOCKER_RUN_TIMEOUT="${OPENCLAW_UPGRADE_SURVIVOR_DOCKER_RUN_TIMEOUT:-900s}"
|
||||
BASELINE_SPEC="${OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC:-}"
|
||||
SCENARIO="${OPENCLAW_UPGRADE_SURVIVOR_SCENARIO:-base}"
|
||||
ARTIFACT_DIR="${OPENCLAW_UPGRADE_SURVIVOR_ARTIFACT_DIR:-$ROOT_DIR/.artifacts/upgrade-survivor}"
|
||||
|
||||
normalize_npm_candidate() {
|
||||
@@ -81,6 +82,7 @@ if [ "${OPENCLAW_UPGRADE_SURVIVOR_PUBLISHED_BASELINE:-0}" = "1" ]; then
|
||||
-e OPENCLAW_UPGRADE_SURVIVOR_BASELINE="$BASELINE_SPEC" \
|
||||
-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_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}" \
|
||||
@@ -103,6 +105,7 @@ docker_e2e_run_with_harness \
|
||||
-e COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \
|
||||
-e OPENCLAW_TEST_STATE_SCRIPT_B64="$OPENCLAW_TEST_STATE_SCRIPT_B64" \
|
||||
-e OPENCLAW_UPGRADE_SURVIVOR_ARTIFACT_ROOT=/tmp/openclaw-upgrade-survivor-artifacts \
|
||||
-e OPENCLAW_UPGRADE_SURVIVOR_SCENARIO="$SCENARIO" \
|
||||
-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}" \
|
||||
-v "$ARTIFACT_DIR:/tmp/openclaw-upgrade-survivor-artifacts" \
|
||||
@@ -134,6 +137,7 @@ export GATEWAY_AUTH_TOKEN_REF="upgrade-survivor-token"
|
||||
export OPENAI_API_KEY="sk-openclaw-upgrade-survivor"
|
||||
export DISCORD_BOT_TOKEN="upgrade-survivor-discord-token"
|
||||
export TELEGRAM_BOT_TOKEN="123456:upgrade-survivor-telegram-token"
|
||||
export FEISHU_APP_SECRET="upgrade-survivor-feishu-secret"
|
||||
|
||||
gateway_pid=""
|
||||
cleanup() {
|
||||
@@ -153,8 +157,8 @@ OPENCLAW_PACKAGE_ACCEPTANCE_LEGACY_COMPAT="$(
|
||||
export OPENCLAW_PACKAGE_ACCEPTANCE_LEGACY_COMPAT
|
||||
|
||||
echo "Checking dirty-state config before update..."
|
||||
node scripts/e2e/lib/upgrade-survivor/assertions.mjs assert-config
|
||||
node scripts/e2e/lib/upgrade-survivor/assertions.mjs assert-state
|
||||
OPENCLAW_UPGRADE_SURVIVOR_ASSERT_STAGE=baseline node scripts/e2e/lib/upgrade-survivor/assertions.mjs assert-config
|
||||
OPENCLAW_UPGRADE_SURVIVOR_ASSERT_STAGE=baseline node scripts/e2e/lib/upgrade-survivor/assertions.mjs assert-state
|
||||
|
||||
echo "Running package update against the mounted tarball..."
|
||||
set +e
|
||||
@@ -174,6 +178,11 @@ if ! openclaw doctor --fix --non-interactive >/tmp/openclaw-upgrade-survivor-doc
|
||||
cat /tmp/openclaw-upgrade-survivor-doctor.log >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
if ! openclaw config validate >>/tmp/openclaw-upgrade-survivor-doctor.log 2>&1; then
|
||||
echo "post-doctor config validation failed" >&2
|
||||
cat /tmp/openclaw-upgrade-survivor-doctor.log >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Verifying config and state survived update/doctor..."
|
||||
node scripts/e2e/lib/upgrade-survivor/assertions.mjs assert-config
|
||||
@@ -196,6 +205,18 @@ if [ "$start_seconds" -gt "$START_BUDGET" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Checking gateway HTTP probes..."
|
||||
node scripts/e2e/lib/upgrade-survivor/probe-gateway.mjs \
|
||||
--base-url "http://127.0.0.1:$PORT" \
|
||||
--path /healthz \
|
||||
--expect live \
|
||||
--out /tmp/openclaw-upgrade-survivor-healthz.json
|
||||
node scripts/e2e/lib/upgrade-survivor/probe-gateway.mjs \
|
||||
--base-url "http://127.0.0.1:$PORT" \
|
||||
--path /readyz \
|
||||
--expect ready \
|
||||
--out /tmp/openclaw-upgrade-survivor-readyz.json
|
||||
|
||||
echo "Checking gateway RPC status..."
|
||||
status_start="$(node -e "process.stdout.write(String(Date.now()))")"
|
||||
if ! openclaw gateway status --url "ws://127.0.0.1:$PORT" --token "$GATEWAY_AUTH_TOKEN_REF" --require-rpc --timeout 30000 --json >/tmp/openclaw-upgrade-survivor-status.json 2>/tmp/openclaw-upgrade-survivor-status.err; then
|
||||
@@ -213,5 +234,5 @@ if [ "$status_seconds" -gt "$STATUS_BUDGET" ]; then
|
||||
fi
|
||||
node scripts/e2e/lib/upgrade-survivor/assertions.mjs assert-status-json /tmp/openclaw-upgrade-survivor-status.json
|
||||
|
||||
echo "Upgrade survivor Docker E2E passed in startup=${start_seconds}s status=${status_seconds}s."
|
||||
echo "Upgrade survivor Docker E2E passed scenario=${OPENCLAW_UPGRADE_SURVIVOR_SCENARIO:-base} startup=${start_seconds}s status=${status_seconds}s."
|
||||
'
|
||||
|
||||
@@ -70,6 +70,19 @@ function sanitizeLaneNameSuffix(value) {
|
||||
);
|
||||
}
|
||||
|
||||
export const UPGRADE_SURVIVOR_SCENARIOS = [
|
||||
"base",
|
||||
"feishu-channel",
|
||||
"bootstrap-persona",
|
||||
"tilde-log-path",
|
||||
"versioned-runtime-deps",
|
||||
];
|
||||
|
||||
const UPGRADE_SURVIVOR_SCENARIO_ALIASES = new Map([
|
||||
["reported-issues", UPGRADE_SURVIVOR_SCENARIOS],
|
||||
["far-reaching", UPGRADE_SURVIVOR_SCENARIOS],
|
||||
]);
|
||||
|
||||
export function normalizeUpgradeSurvivorBaselineSpec(raw) {
|
||||
const value = String(raw ?? "").trim();
|
||||
if (!value) {
|
||||
@@ -102,26 +115,75 @@ export function parseUpgradeSurvivorBaselineSpecs(raw) {
|
||||
];
|
||||
}
|
||||
|
||||
export function expandUpgradeSurvivorBaselineLanes(poolLanes, rawBaselineSpecs) {
|
||||
export function normalizeUpgradeSurvivorScenario(raw) {
|
||||
const value = String(raw ?? "").trim();
|
||||
if (!value) {
|
||||
return undefined;
|
||||
}
|
||||
if (!UPGRADE_SURVIVOR_SCENARIOS.includes(value)) {
|
||||
throw new Error(
|
||||
`invalid published upgrade survivor scenario: ${JSON.stringify(
|
||||
value,
|
||||
)}. Expected one of: ${UPGRADE_SURVIVOR_SCENARIOS.join(", ")}, reported-issues.`,
|
||||
);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export function parseUpgradeSurvivorScenarios(raw) {
|
||||
if (!raw) {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
...new Set(
|
||||
String(raw)
|
||||
.split(/[,\s]+/u)
|
||||
.map((token) => token.trim())
|
||||
.filter(Boolean)
|
||||
.flatMap((token) => UPGRADE_SURVIVOR_SCENARIO_ALIASES.get(token) ?? [token])
|
||||
.map(normalizeUpgradeSurvivorScenario)
|
||||
.filter(Boolean),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
export function expandUpgradeSurvivorBaselineLanes(poolLanes, rawBaselineSpecs, rawScenarios = "") {
|
||||
const baselineSpecs = parseUpgradeSurvivorBaselineSpecs(rawBaselineSpecs);
|
||||
if (baselineSpecs.length === 0) {
|
||||
const scenarios = parseUpgradeSurvivorScenarios(rawScenarios);
|
||||
if (baselineSpecs.length === 0 && scenarios.length === 0) {
|
||||
return poolLanes;
|
||||
}
|
||||
return poolLanes.flatMap((poolLane) => {
|
||||
if (poolLane.name !== "published-upgrade-survivor") {
|
||||
return [poolLane];
|
||||
}
|
||||
return baselineSpecs.map((baselineSpec) => {
|
||||
const suffix = sanitizeLaneNameSuffix(baselineSpec);
|
||||
const name = `${poolLane.name}-${suffix}`;
|
||||
return Object.assign({}, poolLane, {
|
||||
cacheKey: poolLane.cacheKey ? `${poolLane.cacheKey}-${suffix}` : name,
|
||||
command: `OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC=${shellQuote(
|
||||
baselineSpec,
|
||||
)} ${poolLane.command}`,
|
||||
name,
|
||||
});
|
||||
});
|
||||
const matrixBaselines = baselineSpecs.length > 0 ? baselineSpecs : [undefined];
|
||||
const matrixScenarios = scenarios.length > 0 ? scenarios : [undefined];
|
||||
return matrixBaselines.flatMap((baselineSpec) =>
|
||||
matrixScenarios.map((scenario) => {
|
||||
const suffixParts = [
|
||||
baselineSpec ? sanitizeLaneNameSuffix(baselineSpec) : "",
|
||||
scenario && scenario !== "base" ? sanitizeLaneNameSuffix(scenario) : "",
|
||||
].filter(Boolean);
|
||||
const suffix = suffixParts.join("-");
|
||||
const name = suffix ? `${poolLane.name}-${suffix}` : poolLane.name;
|
||||
const commandPrefix = [
|
||||
baselineSpec ? `OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC=${shellQuote(baselineSpec)}` : "",
|
||||
scenario ? `OPENCLAW_UPGRADE_SURVIVOR_SCENARIO=${shellQuote(scenario)}` : "",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
return Object.assign({}, poolLane, {
|
||||
cacheKey: poolLane.cacheKey
|
||||
? suffix
|
||||
? `${poolLane.cacheKey}-${suffix}`
|
||||
: poolLane.cacheKey
|
||||
: name,
|
||||
command: commandPrefix ? `${commandPrefix} ${poolLane.command}` : poolLane.command,
|
||||
name,
|
||||
});
|
||||
}),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -213,6 +275,7 @@ export function findLaneByName(name) {
|
||||
expandUpgradeSurvivorBaselineLanes(
|
||||
[...allReleasePathLanes({ includeOpenWebUI: true }), ...mainLanes, ...tailLanes],
|
||||
process.env.OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPECS,
|
||||
process.env.OPENCLAW_UPGRADE_SURVIVOR_SCENARIOS,
|
||||
),
|
||||
).find((poolLane) => poolLane.name === name);
|
||||
}
|
||||
@@ -277,13 +340,18 @@ export function resolveDockerE2ePlan(options) {
|
||||
const retriedMainLanes = applyLiveRetries(mainLanes, options.liveRetries);
|
||||
const retriedTailLanes = applyLiveRetries(tailLanes, options.liveRetries);
|
||||
const upgradeSurvivorBaselines = options.upgradeSurvivorBaselines ?? "";
|
||||
const upgradeSurvivorScenarios = options.upgradeSurvivorScenarios ?? "";
|
||||
const unexpandedSelectableLanes = dedupeLanes([
|
||||
...allReleasePathLanes({ includeOpenWebUI: options.includeOpenWebUI }),
|
||||
...retriedMainLanes,
|
||||
...retriedTailLanes,
|
||||
]);
|
||||
const selectableLanes = dedupeLanes(
|
||||
expandUpgradeSurvivorBaselineLanes(unexpandedSelectableLanes, upgradeSurvivorBaselines),
|
||||
expandUpgradeSurvivorBaselineLanes(
|
||||
unexpandedSelectableLanes,
|
||||
upgradeSurvivorBaselines,
|
||||
upgradeSurvivorScenarios,
|
||||
),
|
||||
);
|
||||
const releaseLanes =
|
||||
options.selectedLaneNames.length === 0 && options.profile === RELEASE_PATH_PROFILE
|
||||
@@ -291,12 +359,14 @@ export function resolveDockerE2ePlan(options) {
|
||||
? expandUpgradeSurvivorBaselineLanes(
|
||||
allReleasePathLanes({ includeOpenWebUI: options.includeOpenWebUI }),
|
||||
upgradeSurvivorBaselines,
|
||||
upgradeSurvivorScenarios,
|
||||
)
|
||||
: expandUpgradeSurvivorBaselineLanes(
|
||||
releasePathChunkLanes(options.releaseChunk, {
|
||||
includeOpenWebUI: options.includeOpenWebUI,
|
||||
}),
|
||||
upgradeSurvivorBaselines,
|
||||
upgradeSurvivorScenarios,
|
||||
)
|
||||
: undefined;
|
||||
const selectedLanes =
|
||||
@@ -310,7 +380,11 @@ export function resolveDockerE2ePlan(options) {
|
||||
(poolLane) => poolLane.name === selectedName,
|
||||
);
|
||||
if (unexpandedLane) {
|
||||
return expandUpgradeSurvivorBaselineLanes([unexpandedLane], upgradeSurvivorBaselines);
|
||||
return expandUpgradeSurvivorBaselineLanes(
|
||||
[unexpandedLane],
|
||||
upgradeSurvivorBaselines,
|
||||
upgradeSurvivorScenarios,
|
||||
);
|
||||
}
|
||||
selectNamedLanes(selectableLanes, [selectedName], "OPENCLAW_DOCKER_ALL_LANES");
|
||||
return [];
|
||||
|
||||
@@ -234,6 +234,12 @@ function githubWorkflowRerunCommand(laneNames, ref) {
|
||||
`published_upgrade_survivor_baselines=${shellQuote(process.env.OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPECS)}`,
|
||||
);
|
||||
}
|
||||
if (process.env.OPENCLAW_UPGRADE_SURVIVOR_SCENARIOS) {
|
||||
fields.push(
|
||||
"-f",
|
||||
`published_upgrade_survivor_scenarios=${shellQuote(process.env.OPENCLAW_UPGRADE_SURVIVOR_SCENARIOS)}`,
|
||||
);
|
||||
}
|
||||
if (process.env.OPENCLAW_DOCKER_E2E_BARE_IMAGE) {
|
||||
fields.push(
|
||||
"-f",
|
||||
@@ -264,6 +270,7 @@ function buildLaneRerunCommand(name, baseEnv) {
|
||||
["OPENCLAW_CURRENT_PACKAGE_TGZ", baseEnv.OPENCLAW_CURRENT_PACKAGE_TGZ],
|
||||
["OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC", baseEnv.OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC],
|
||||
["OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPECS", baseEnv.OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPECS],
|
||||
["OPENCLAW_UPGRADE_SURVIVOR_SCENARIOS", baseEnv.OPENCLAW_UPGRADE_SURVIVOR_SCENARIOS],
|
||||
];
|
||||
if (baseEnv.OPENCLAW_DOCKER_ALL_PNPM_COMMAND) {
|
||||
env.push(["OPENCLAW_DOCKER_ALL_PNPM_COMMAND", baseEnv.OPENCLAW_DOCKER_ALL_PNPM_COMMAND]);
|
||||
@@ -1133,6 +1140,7 @@ async function main() {
|
||||
selectedLaneNames,
|
||||
timingStore,
|
||||
upgradeSurvivorBaselines: process.env.OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPECS,
|
||||
upgradeSurvivorScenarios: process.env.OPENCLAW_UPGRADE_SURVIVOR_SCENARIOS,
|
||||
});
|
||||
|
||||
if (planJson) {
|
||||
|
||||
@@ -384,6 +384,49 @@ describe("scripts/lib/docker-e2e-plan", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("expands the published upgrade survivor lane across scenarios", () => {
|
||||
const plan = planFor({
|
||||
selectedLaneNames: ["published-upgrade-survivor"],
|
||||
upgradeSurvivorBaselines: "2026.4.29 2026.4.23",
|
||||
upgradeSurvivorScenarios: "base feishu-channel tilde-log-path",
|
||||
});
|
||||
|
||||
expect(plan.lanes.map((lane) => lane.name)).toEqual([
|
||||
"published-upgrade-survivor-2026.4.29",
|
||||
"published-upgrade-survivor-2026.4.29-feishu-channel",
|
||||
"published-upgrade-survivor-2026.4.29-tilde-log-path",
|
||||
"published-upgrade-survivor-2026.4.23",
|
||||
"published-upgrade-survivor-2026.4.23-feishu-channel",
|
||||
"published-upgrade-survivor-2026.4.23-tilde-log-path",
|
||||
]);
|
||||
expect(plan.lanes).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
command: expect.stringContaining("OPENCLAW_UPGRADE_SURVIVOR_SCENARIO='feishu-channel'"),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
command: expect.stringContaining("OPENCLAW_UPGRADE_SURVIVOR_SCENARIO='tilde-log-path'"),
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("expands reported upgrade issue scenarios", () => {
|
||||
const plan = planFor({
|
||||
selectedLaneNames: ["published-upgrade-survivor"],
|
||||
upgradeSurvivorBaselines: "2026.4.29",
|
||||
upgradeSurvivorScenarios: "reported-issues",
|
||||
});
|
||||
|
||||
expect(plan.lanes.map((lane) => lane.name)).toEqual([
|
||||
"published-upgrade-survivor-2026.4.29",
|
||||
"published-upgrade-survivor-2026.4.29-feishu-channel",
|
||||
"published-upgrade-survivor-2026.4.29-bootstrap-persona",
|
||||
"published-upgrade-survivor-2026.4.29-tilde-log-path",
|
||||
"published-upgrade-survivor-2026.4.29-versioned-runtime-deps",
|
||||
]);
|
||||
});
|
||||
|
||||
it("plans a live-only selected lane without package e2e images", () => {
|
||||
const plan = planFor({ selectedLaneNames: ["live-models"] });
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ describe("package acceptance workflow", () => {
|
||||
expect(workflow).toContain("suite_profile:");
|
||||
expect(workflow).toContain("published_upgrade_survivor_baseline:");
|
||||
expect(workflow).toContain("published_upgrade_survivor_baselines:");
|
||||
expect(workflow).toContain("published_upgrade_survivor_scenarios:");
|
||||
expect(workflow).toContain("scripts/resolve-upgrade-survivor-baselines.mjs");
|
||||
expect(workflow).toContain("--history-count 6");
|
||||
expect(workflow).toContain("--include-version 2026.4.23");
|
||||
@@ -76,8 +77,12 @@ describe("package acceptance workflow", () => {
|
||||
expect(workflow).toContain(
|
||||
"published_upgrade_survivor_baselines: ${{ needs.resolve_package.outputs.published_upgrade_survivor_baselines }}",
|
||||
);
|
||||
expect(workflow).toContain(
|
||||
"published_upgrade_survivor_scenarios: ${{ needs.resolve_package.outputs.published_upgrade_survivor_scenarios }}",
|
||||
);
|
||||
expect(workflow).toContain("Published upgrade survivor baseline:");
|
||||
expect(workflow).toContain("Published upgrade survivor baselines:");
|
||||
expect(workflow).toContain("Published upgrade survivor scenarios:");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -92,6 +97,7 @@ describe("package artifact reuse", () => {
|
||||
expect(workflow).toContain("package_artifact_run_id:");
|
||||
expect(workflow).toContain("published_upgrade_survivor_baseline:");
|
||||
expect(workflow).toContain("published_upgrade_survivor_baselines:");
|
||||
expect(workflow).toContain("published_upgrade_survivor_scenarios:");
|
||||
expect(workflow).toContain("docker_e2e_bare_image:");
|
||||
expect(workflow).toContain("docker_e2e_functional_image:");
|
||||
expect(workflow).toContain("OPENCLAW_DOCKER_E2E_SELECTED_SHA:");
|
||||
@@ -101,6 +107,9 @@ describe("package artifact reuse", () => {
|
||||
expect(workflow).toContain(
|
||||
"OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPECS: ${{ inputs.published_upgrade_survivor_baselines }}",
|
||||
);
|
||||
expect(workflow).toContain(
|
||||
"OPENCLAW_UPGRADE_SURVIVOR_SCENARIOS: ${{ inputs.published_upgrade_survivor_scenarios }}",
|
||||
);
|
||||
expect(workflow).toContain("Download current-run OpenClaw Docker E2E package");
|
||||
expect(workflow).toContain("Download previous-run OpenClaw Docker E2E package");
|
||||
expect(workflow).toContain("inputs.package_artifact_name != ''");
|
||||
@@ -133,9 +142,11 @@ describe("package artifact reuse", () => {
|
||||
'["OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC", baseEnv.OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC]',
|
||||
);
|
||||
expect(scheduler).toContain('["OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPECS",');
|
||||
expect(scheduler).toContain('["OPENCLAW_UPGRADE_SURVIVOR_SCENARIOS",');
|
||||
expect(packageJson).toContain("OPENCLAW_UPGRADE_SURVIVOR_PUBLISHED_BASELINE=1");
|
||||
expect(publishedUpgradeSurvivor).toContain("validate_baseline_package_spec");
|
||||
expect(publishedUpgradeSurvivor).toContain("openclaw@(beta|latest|");
|
||||
expect(publishedUpgradeSurvivor).toContain("probe_gateway_endpoint");
|
||||
expect(
|
||||
publishedUpgradeSurvivor.indexOf('validate_baseline_package_spec "$baseline_spec"'),
|
||||
).toBeLessThan(
|
||||
|
||||
Reference in New Issue
Block a user