mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 10:30:44 +00:00
test(e2e): add upgrade survivor scenario probes
This commit is contained in:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user