test(e2e): add upgrade survivor scenario probes

This commit is contained in:
Vincent Koc
2026-04-30 23:45:46 -07:00
parent 2500b5d4ec
commit dffc295a74
16 changed files with 503 additions and 25 deletions

View File

@@ -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 }}

View File

@@ -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

View File

@@ -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

View File

@@ -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.

View File

@@ -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

View File

@@ -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]) {

View 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);

View File

@@ -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"]
}
}
}
}

View File

@@ -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
}
}
}

View 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,
});

View File

@@ -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."

View File

@@ -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."
'

View File

@@ -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 [];

View File

@@ -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) {

View File

@@ -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"] });

View File

@@ -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(