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

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