mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-07 10:34:44 +00:00
chore(gateway): cover update and setup RPCs
* test(gateway): cover update and setup RPCs * test(gateway): cover target wizard lifecycle * test(gateway): normalize QA fixture ownership * test(gateway): include historical dist closure * test(gateway): isolate historical update fixture * test(qa): align target wizard lifecycle proof * test(qa): read structured gateway call errors
This commit is contained in:
@@ -6,11 +6,15 @@ scenario:
|
||||
coverage:
|
||||
primary:
|
||||
- cli.update-status-and-rpc
|
||||
- gateway.update-and-setup-apis
|
||||
secondary:
|
||||
- cli.managed-gateway-restart
|
||||
objective: Verify an installed OpenClaw 2026.4.26 package upgrades to the resolved latest package through the authenticated Gateway update.run RPC and returns healthy after restart.
|
||||
objective: Verify authenticated setup RPC compatibility before and after an installed OpenClaw 2026.4.26 package upgrades to the resolved latest package through update.run and returns healthy after restart.
|
||||
successCriteria:
|
||||
- The package-backed Docker lane installs and verifies openclaw@2026.4.26 before invoking the authenticated Gateway update.run RPC directly.
|
||||
- The package-backed Docker lane builds an isolated production install of openclaw@2026.4.26, overlays its exact-tag private dist, preflight-imports the linked historical QA channel, and separately installs the published source Gateway before invoking update.run.
|
||||
- Authenticated wizard.start and wizard.status return valid sanitized projections, the running historical session remains usable by wizard.next, and wizard.cancel terminates the setup flow.
|
||||
- A second wizard.start is rejected while setup is active, and cancellation purges the session so a replacement setup can start and cancel cleanly.
|
||||
- After restart, one target-package wizard session proves repeated wizard.status retains the running session before explicit cancellation and settlement-polled purge, while its replacement proves wizard.next, duplicate-start rejection, cancellation, settlement-polled purge, and another replacement setup.
|
||||
- The lane resolves openclaw@latest before the RPC and rejects a no-op or any installed version that differs from the resolved target.
|
||||
- The update RPC succeeds with source and target versions and writes the exact requested restart note into the update sentinel.
|
||||
- The restarted Gateway reports the resolved target version, passes health, readiness, and RPC status probes, and reports the QA channel account running.
|
||||
@@ -22,6 +26,7 @@ scenario:
|
||||
- docs/help/testing-updates-plugins.md
|
||||
codeRefs:
|
||||
- src/gateway/server-methods/update.ts
|
||||
- src/gateway/server-methods/wizard.ts
|
||||
- src/infra/restart.ts
|
||||
- scripts/e2e/update-run-package-self-upgrade-docker.sh
|
||||
- scripts/e2e/lib/upgrade-survivor/update-run-package-self-upgrade.sh
|
||||
@@ -29,7 +34,7 @@ scenario:
|
||||
execution:
|
||||
kind: script
|
||||
path: test/e2e/qa-lab/runtime/update-run-package-self-upgrade.ts
|
||||
summary: Runs the opt-in package-backed Docker lane for authenticated Gateway update.run self-upgrade and emits source/target, restart sentinel, Gateway health, and QA channel evidence.
|
||||
summary: Runs the opt-in installed-package Docker lane for authenticated historical and current wizard lifecycles plus update RPCs, then emits sanitized setup, source/target, restart sentinel, Gateway health, and QA channel evidence.
|
||||
allowBlockedEvidence: true
|
||||
timeoutMs: 3600000
|
||||
args:
|
||||
|
||||
@@ -57,6 +57,50 @@ GATEWAY_STATUS_JSON="$ARTIFACT_DIR/gateway-status.json"
|
||||
GATEWAY_STATUS_ERR="$ARTIFACT_DIR/gateway-status.err"
|
||||
CHANNELS_STATUS_JSON="$ARTIFACT_DIR/channels-status.json"
|
||||
CHANNELS_STATUS_ERR="$ARTIFACT_DIR/channels-status.err"
|
||||
WIZARD_START_JSON="$ARTIFACT_DIR/wizard-start.json"
|
||||
WIZARD_START_ERR="$ARTIFACT_DIR/wizard-start.err"
|
||||
WIZARD_STATUS_JSON="$ARTIFACT_DIR/wizard-status.json"
|
||||
WIZARD_STATUS_ERR="$ARTIFACT_DIR/wizard-status.err"
|
||||
WIZARD_NEXT_JSON="$ARTIFACT_DIR/wizard-next.json"
|
||||
WIZARD_NEXT_ERR="$ARTIFACT_DIR/wizard-next.err"
|
||||
WIZARD_DUPLICATE_JSON="$ARTIFACT_DIR/wizard-duplicate-start.json"
|
||||
WIZARD_DUPLICATE_ERR="$ARTIFACT_DIR/wizard-duplicate-start.err"
|
||||
WIZARD_CANCEL_JSON="$ARTIFACT_DIR/wizard-cancel.json"
|
||||
WIZARD_CANCEL_ERR="$ARTIFACT_DIR/wizard-cancel.err"
|
||||
WIZARD_CANCELLED_STATUS_JSON="$ARTIFACT_DIR/wizard-cancelled-status.json"
|
||||
WIZARD_CANCELLED_STATUS_ERR="$ARTIFACT_DIR/wizard-cancelled-status.err"
|
||||
WIZARD_REPLACEMENT_START_JSON="$ARTIFACT_DIR/wizard-replacement-start.json"
|
||||
WIZARD_REPLACEMENT_START_ERR="$ARTIFACT_DIR/wizard-replacement-start.err"
|
||||
WIZARD_REPLACEMENT_CANCEL_JSON="$ARTIFACT_DIR/wizard-replacement-cancel.json"
|
||||
WIZARD_REPLACEMENT_CANCEL_ERR="$ARTIFACT_DIR/wizard-replacement-cancel.err"
|
||||
WIZARD_REPLACEMENT_STATUS_JSON="$ARTIFACT_DIR/wizard-replacement-status.json"
|
||||
WIZARD_REPLACEMENT_STATUS_ERR="$ARTIFACT_DIR/wizard-replacement-status.err"
|
||||
WIZARD_FLOW_JSON="$ARTIFACT_DIR/wizard-flow.json"
|
||||
TARGET_WIZARD_STATUS_START_JSON="$ARTIFACT_DIR/target-wizard-status-start.json"
|
||||
TARGET_WIZARD_STATUS_START_ERR="$ARTIFACT_DIR/target-wizard-status-start.err"
|
||||
TARGET_WIZARD_STATUS_JSON="$ARTIFACT_DIR/target-wizard-status.json"
|
||||
TARGET_WIZARD_STATUS_ERR="$ARTIFACT_DIR/target-wizard-status.err"
|
||||
TARGET_WIZARD_STATUS_RETAINED_JSON="$ARTIFACT_DIR/target-wizard-status-retained.json"
|
||||
TARGET_WIZARD_STATUS_RETAINED_ERR="$ARTIFACT_DIR/target-wizard-status-retained.err"
|
||||
TARGET_WIZARD_STATUS_CANCEL_JSON="$ARTIFACT_DIR/target-wizard-status-cancel.json"
|
||||
TARGET_WIZARD_STATUS_CANCEL_ERR="$ARTIFACT_DIR/target-wizard-status-cancel.err"
|
||||
TARGET_WIZARD_STATUS_PURGED_JSON="$ARTIFACT_DIR/target-wizard-status-purged.json"
|
||||
TARGET_WIZARD_STATUS_PURGED_ERR="$ARTIFACT_DIR/target-wizard-status-purged.err"
|
||||
TARGET_WIZARD_ACTIVE_START_JSON="$ARTIFACT_DIR/target-wizard-active-start.json"
|
||||
TARGET_WIZARD_ACTIVE_START_ERR="$ARTIFACT_DIR/target-wizard-active-start.err"
|
||||
TARGET_WIZARD_NEXT_JSON="$ARTIFACT_DIR/target-wizard-next.json"
|
||||
TARGET_WIZARD_NEXT_ERR="$ARTIFACT_DIR/target-wizard-next.err"
|
||||
TARGET_WIZARD_DUPLICATE_JSON="$ARTIFACT_DIR/target-wizard-duplicate-start.json"
|
||||
TARGET_WIZARD_DUPLICATE_ERR="$ARTIFACT_DIR/target-wizard-duplicate-start.err"
|
||||
TARGET_WIZARD_CANCEL_JSON="$ARTIFACT_DIR/target-wizard-cancel.json"
|
||||
TARGET_WIZARD_CANCEL_ERR="$ARTIFACT_DIR/target-wizard-cancel.err"
|
||||
TARGET_WIZARD_REPLACEMENT_START_JSON="$ARTIFACT_DIR/target-wizard-replacement-start.json"
|
||||
TARGET_WIZARD_REPLACEMENT_START_ERR="$ARTIFACT_DIR/target-wizard-replacement-start.err"
|
||||
TARGET_WIZARD_REPLACEMENT_CANCEL_JSON="$ARTIFACT_DIR/target-wizard-replacement-cancel.json"
|
||||
TARGET_WIZARD_REPLACEMENT_CANCEL_ERR="$ARTIFACT_DIR/target-wizard-replacement-cancel.err"
|
||||
TARGET_WIZARD_PURGED_STATUS_JSON="$ARTIFACT_DIR/target-wizard-purged-status.json"
|
||||
TARGET_WIZARD_PURGED_STATUS_ERR="$ARTIFACT_DIR/target-wizard-purged-status.err"
|
||||
TARGET_WIZARD_FLOW_JSON="$ARTIFACT_DIR/target-wizard-flow.json"
|
||||
SUMMARY_JSON="$ARTIFACT_DIR/summary.json"
|
||||
SYSTEMCTL_SHIM_LOG="$ARTIFACT_DIR/systemctl-shim.log"
|
||||
SYSTEMCTL_SHIM_SETUP_LOG="$ARTIFACT_DIR/systemctl-shim-setup.log"
|
||||
@@ -86,6 +130,8 @@ rm -f \
|
||||
"$UPDATE_STATUS_JSON" \
|
||||
"$UPDATE_STATUS_ERR" \
|
||||
"$ARTIFACT_DIR/update-status.candidate.json" \
|
||||
"$WIZARD_FLOW_JSON" \
|
||||
"$TARGET_WIZARD_FLOW_JSON" \
|
||||
"$SUMMARY_JSON"
|
||||
: >"$SYSTEMCTL_SHIM_DAEMON_LOG"
|
||||
|
||||
@@ -330,10 +376,242 @@ gateway_call() {
|
||||
>"$output" 2>"$error_output"
|
||||
}
|
||||
|
||||
assert_gateway_call_error_message() {
|
||||
local output="$1"
|
||||
local error_output="$2"
|
||||
local expected="$3"
|
||||
local label="$4"
|
||||
local allow_stderr_fallback="${5:-0}"
|
||||
if EXPECTED_GATEWAY_ERROR="$expected" node -e '
|
||||
const fs = require("node:fs");
|
||||
const file = process.argv[1];
|
||||
const raw = fs.existsSync(file) ? fs.readFileSync(file, "utf8").trim() : "";
|
||||
if (!raw) {
|
||||
process.exit(1);
|
||||
}
|
||||
try {
|
||||
const payload = JSON.parse(raw);
|
||||
process.exit(
|
||||
payload?.ok === false &&
|
||||
payload?.error?.type === "gateway_request_error" &&
|
||||
payload.error.message === process.env.EXPECTED_GATEWAY_ERROR
|
||||
? 0
|
||||
: 1,
|
||||
);
|
||||
} catch {
|
||||
process.exit(1);
|
||||
}
|
||||
' "$output"; then
|
||||
return 0
|
||||
fi
|
||||
if [ "$allow_stderr_fallback" = "1" ] && grep -Fq "$expected" "$error_output"; then
|
||||
return 0
|
||||
fi
|
||||
echo "$label failed without the expected '$expected' result" >&2
|
||||
openclaw_e2e_print_log "$output" >&2
|
||||
openclaw_e2e_print_log "$error_output" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
gateway_call channels.status '{"probe":false,"timeoutMs":2000}' \
|
||||
"$ARTIFACT_DIR/channels-status-before.json" \
|
||||
"$ARTIFACT_DIR/channels-status-before.err"
|
||||
|
||||
echo "Exercising authenticated Gateway wizard RPC lifecycle"
|
||||
gateway_call wizard.start '{"mode":"local"}' "$WIZARD_START_JSON" "$WIZARD_START_ERR"
|
||||
wizard_session_id="$(
|
||||
node -e '
|
||||
const fs = require("node:fs");
|
||||
const payload = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
|
||||
if (
|
||||
typeof payload?.sessionId !== "string" ||
|
||||
!payload.sessionId ||
|
||||
payload.done !== false ||
|
||||
payload.status !== "running" ||
|
||||
typeof payload.step?.id !== "string"
|
||||
) {
|
||||
throw new Error(`unexpected wizard.start result: ${JSON.stringify(payload)}`);
|
||||
}
|
||||
process.stdout.write(payload.sessionId);
|
||||
' "$WIZARD_START_JSON"
|
||||
)"
|
||||
wizard_step_id="$(
|
||||
node -e '
|
||||
const fs = require("node:fs");
|
||||
const payload = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
|
||||
process.stdout.write(payload.step.id);
|
||||
' "$WIZARD_START_JSON"
|
||||
)"
|
||||
wizard_session_params="$(
|
||||
WIZARD_SESSION_ID="$wizard_session_id" node -e '
|
||||
process.stdout.write(JSON.stringify({ sessionId: process.env.WIZARD_SESSION_ID }));
|
||||
'
|
||||
)"
|
||||
gateway_call wizard.status "$wizard_session_params" "$WIZARD_STATUS_JSON" "$WIZARD_STATUS_ERR"
|
||||
wizard_next_params="$(
|
||||
WIZARD_SESSION_ID="$wizard_session_id" WIZARD_STEP_ID="$wizard_step_id" node -e '
|
||||
process.stdout.write(
|
||||
JSON.stringify({
|
||||
sessionId: process.env.WIZARD_SESSION_ID,
|
||||
answer: { stepId: process.env.WIZARD_STEP_ID, value: null },
|
||||
}),
|
||||
);
|
||||
'
|
||||
)"
|
||||
gateway_call wizard.next "$wizard_next_params" "$WIZARD_NEXT_JSON" "$WIZARD_NEXT_ERR"
|
||||
|
||||
if gateway_call wizard.start '{"mode":"local"}' \
|
||||
"$WIZARD_DUPLICATE_JSON" "$WIZARD_DUPLICATE_ERR"; then
|
||||
echo "wizard.start unexpectedly allowed an overlapping setup session" >&2
|
||||
exit 1
|
||||
fi
|
||||
assert_gateway_call_error_message \
|
||||
"$WIZARD_DUPLICATE_JSON" \
|
||||
"$WIZARD_DUPLICATE_ERR" \
|
||||
"wizard already running" \
|
||||
"overlapping wizard.start" \
|
||||
1
|
||||
|
||||
gateway_call wizard.cancel "$wizard_session_params" "$WIZARD_CANCEL_JSON" "$WIZARD_CANCEL_ERR"
|
||||
if gateway_call wizard.status "$wizard_session_params" \
|
||||
"$WIZARD_CANCELLED_STATUS_JSON" "$WIZARD_CANCELLED_STATUS_ERR"; then
|
||||
echo "cancelled wizard session remained reachable" >&2
|
||||
exit 1
|
||||
fi
|
||||
assert_gateway_call_error_message \
|
||||
"$WIZARD_CANCELLED_STATUS_JSON" \
|
||||
"$WIZARD_CANCELLED_STATUS_ERR" \
|
||||
"wizard not found" \
|
||||
"cancelled wizard cleanup" \
|
||||
1
|
||||
|
||||
gateway_call wizard.start '{"mode":"local"}' \
|
||||
"$WIZARD_REPLACEMENT_START_JSON" "$WIZARD_REPLACEMENT_START_ERR"
|
||||
replacement_session_id="$(
|
||||
node -e '
|
||||
const fs = require("node:fs");
|
||||
const payload = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
|
||||
if (
|
||||
typeof payload?.sessionId !== "string" ||
|
||||
!payload.sessionId ||
|
||||
payload.done !== false ||
|
||||
payload.status !== "running"
|
||||
) {
|
||||
throw new Error(`unexpected replacement wizard.start result: ${JSON.stringify(payload)}`);
|
||||
}
|
||||
process.stdout.write(payload.sessionId);
|
||||
' "$WIZARD_REPLACEMENT_START_JSON"
|
||||
)"
|
||||
replacement_session_params="$(
|
||||
WIZARD_SESSION_ID="$replacement_session_id" node -e '
|
||||
process.stdout.write(JSON.stringify({ sessionId: process.env.WIZARD_SESSION_ID }));
|
||||
'
|
||||
)"
|
||||
gateway_call wizard.cancel "$replacement_session_params" \
|
||||
"$WIZARD_REPLACEMENT_CANCEL_JSON" "$WIZARD_REPLACEMENT_CANCEL_ERR"
|
||||
if gateway_call wizard.status "$replacement_session_params" \
|
||||
"$WIZARD_REPLACEMENT_STATUS_JSON" "$WIZARD_REPLACEMENT_STATUS_ERR"; then
|
||||
echo "replacement wizard session remained reachable after cancellation" >&2
|
||||
exit 1
|
||||
fi
|
||||
assert_gateway_call_error_message \
|
||||
"$WIZARD_REPLACEMENT_STATUS_JSON" \
|
||||
"$WIZARD_REPLACEMENT_STATUS_ERR" \
|
||||
"wizard not found" \
|
||||
"replacement wizard cleanup" \
|
||||
1
|
||||
|
||||
WIZARD_START_JSON="$WIZARD_START_JSON" \
|
||||
WIZARD_STATUS_JSON="$WIZARD_STATUS_JSON" \
|
||||
WIZARD_NEXT_JSON="$WIZARD_NEXT_JSON" \
|
||||
WIZARD_CANCEL_JSON="$WIZARD_CANCEL_JSON" \
|
||||
WIZARD_REPLACEMENT_START_JSON="$WIZARD_REPLACEMENT_START_JSON" \
|
||||
WIZARD_REPLACEMENT_CANCEL_JSON="$WIZARD_REPLACEMENT_CANCEL_JSON" \
|
||||
WIZARD_FLOW_JSON="$WIZARD_FLOW_JSON" \
|
||||
node -e '
|
||||
const fs = require("node:fs");
|
||||
const readJson = (file) => JSON.parse(fs.readFileSync(file, "utf8"));
|
||||
const sanitizeStep = (step, label) => {
|
||||
if (!step || typeof step.id !== "string" || !step.id) {
|
||||
throw new Error(`${label} omitted its wizard step`);
|
||||
}
|
||||
const allowedTypes = new Set([
|
||||
"note",
|
||||
"select",
|
||||
"text",
|
||||
"confirm",
|
||||
"multiselect",
|
||||
"progress",
|
||||
"action",
|
||||
]);
|
||||
if (!allowedTypes.has(step.type)) {
|
||||
throw new Error(`${label} returned unsupported step type ${String(step.type)}`);
|
||||
}
|
||||
if (step.sensitive === true && Object.hasOwn(step, "initialValue")) {
|
||||
throw new Error(`${label} exposed a sensitive initial value`);
|
||||
}
|
||||
for (const field of ["title", "message", "placeholder"]) {
|
||||
if (step[field] !== undefined && typeof step[field] !== "string") {
|
||||
throw new Error(`${label} returned non-string ${field}`);
|
||||
}
|
||||
if (step[field]?.length > 8192) {
|
||||
throw new Error(`${label} returned oversized ${field}`);
|
||||
}
|
||||
}
|
||||
if (step.options !== undefined && !Array.isArray(step.options)) {
|
||||
throw new Error(`${label} returned malformed options`);
|
||||
}
|
||||
return {
|
||||
type: step.type,
|
||||
executor: step.executor,
|
||||
sensitive: step.sensitive === true,
|
||||
initialValuePresent: Object.hasOwn(step, "initialValue"),
|
||||
titleLength: step.title?.length ?? 0,
|
||||
messageLength: step.message?.length ?? 0,
|
||||
optionCount: step.options?.length ?? 0,
|
||||
};
|
||||
};
|
||||
const start = readJson(process.env.WIZARD_START_JSON);
|
||||
const status = readJson(process.env.WIZARD_STATUS_JSON);
|
||||
const next = readJson(process.env.WIZARD_NEXT_JSON);
|
||||
const cancel = readJson(process.env.WIZARD_CANCEL_JSON);
|
||||
const replacementStart = readJson(process.env.WIZARD_REPLACEMENT_START_JSON);
|
||||
const replacementCancel = readJson(process.env.WIZARD_REPLACEMENT_CANCEL_JSON);
|
||||
if (status.status !== "running") {
|
||||
throw new Error(`wizard.status was not running: ${JSON.stringify(status)}`);
|
||||
}
|
||||
if (next.done !== false || next.status !== "running") {
|
||||
throw new Error(`wizard.next did not advance a running session: ${JSON.stringify(next)}`);
|
||||
}
|
||||
if (cancel.status !== "cancelled" || replacementCancel.status !== "cancelled") {
|
||||
throw new Error("wizard.cancel did not cancel both sessions");
|
||||
}
|
||||
fs.writeFileSync(
|
||||
process.env.WIZARD_FLOW_JSON,
|
||||
`${JSON.stringify(
|
||||
{
|
||||
status: "passed",
|
||||
authenticated: true,
|
||||
start: { status: start.status, step: sanitizeStep(start.step, "wizard.start") },
|
||||
statusPoll: status.status,
|
||||
runningStatusRetained: true,
|
||||
next: { status: next.status, step: sanitizeStep(next.step, "wizard.next") },
|
||||
duplicateStartRejected: true,
|
||||
cancelStatus: cancel.status,
|
||||
cancelledSessionPurged: true,
|
||||
replacement: {
|
||||
status: replacementStart.status,
|
||||
step: sanitizeStep(replacementStart.step, "replacement wizard.start"),
|
||||
cancelStatus: replacementCancel.status,
|
||||
purged: true,
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
);
|
||||
'
|
||||
|
||||
update_params="$(
|
||||
RESTART_NOTE="$RESTART_NOTE" node -e '
|
||||
process.stdout.write(
|
||||
@@ -426,6 +704,288 @@ if [ ! -f "$UPDATE_STATUS_JSON" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Exercising current target Gateway wizard RPC lifecycle"
|
||||
wait_for_target_wizard_start() {
|
||||
local output="$1"
|
||||
local error_output="$2"
|
||||
local label="$3"
|
||||
local deadline=$((SECONDS + 30))
|
||||
local polls=0
|
||||
while [ "$SECONDS" -lt "$deadline" ]; do
|
||||
polls=$((polls + 1))
|
||||
if gateway_call wizard.start '{"mode":"local"}' "$output" "$error_output"; then
|
||||
local session_id
|
||||
session_id="$(
|
||||
TARGET_START_LABEL="$label" node -e '
|
||||
const fs = require("node:fs");
|
||||
const payload = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
|
||||
if (
|
||||
typeof payload?.sessionId !== "string" ||
|
||||
!payload.sessionId ||
|
||||
payload.done !== false ||
|
||||
payload.status !== "running" ||
|
||||
typeof payload.step?.id !== "string"
|
||||
) {
|
||||
throw new Error(
|
||||
`unexpected ${process.env.TARGET_START_LABEL}: ${JSON.stringify(payload)}`,
|
||||
);
|
||||
}
|
||||
process.stdout.write(payload.sessionId);
|
||||
' "$output"
|
||||
)"
|
||||
printf '%s\t%s\n' "$session_id" "$polls"
|
||||
return 0
|
||||
fi
|
||||
assert_gateway_call_error_message \
|
||||
"$output" "$error_output" "wizard already running" "$label"
|
||||
sleep 0.2
|
||||
done
|
||||
echo "timed out waiting for $label" >&2
|
||||
openclaw_e2e_print_log "$error_output" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
gateway_call wizard.start '{"mode":"local"}' \
|
||||
"$TARGET_WIZARD_STATUS_START_JSON" "$TARGET_WIZARD_STATUS_START_ERR"
|
||||
target_status_session_id="$(
|
||||
node -e '
|
||||
const fs = require("node:fs");
|
||||
const payload = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
|
||||
if (
|
||||
typeof payload?.sessionId !== "string" ||
|
||||
!payload.sessionId ||
|
||||
payload.done !== false ||
|
||||
payload.status !== "running" ||
|
||||
typeof payload.step?.id !== "string"
|
||||
) {
|
||||
throw new Error(`unexpected target wizard.start result: ${JSON.stringify(payload)}`);
|
||||
}
|
||||
process.stdout.write(payload.sessionId);
|
||||
' "$TARGET_WIZARD_STATUS_START_JSON"
|
||||
)"
|
||||
target_status_session_params="$(
|
||||
WIZARD_SESSION_ID="$target_status_session_id" node -e '
|
||||
process.stdout.write(JSON.stringify({ sessionId: process.env.WIZARD_SESSION_ID }));
|
||||
'
|
||||
)"
|
||||
gateway_call wizard.status "$target_status_session_params" \
|
||||
"$TARGET_WIZARD_STATUS_JSON" "$TARGET_WIZARD_STATUS_ERR"
|
||||
gateway_call wizard.status "$target_status_session_params" \
|
||||
"$TARGET_WIZARD_STATUS_RETAINED_JSON" "$TARGET_WIZARD_STATUS_RETAINED_ERR"
|
||||
gateway_call wizard.cancel "$target_status_session_params" \
|
||||
"$TARGET_WIZARD_STATUS_CANCEL_JSON" "$TARGET_WIZARD_STATUS_CANCEL_ERR"
|
||||
|
||||
target_active_start_result="$(
|
||||
wait_for_target_wizard_start \
|
||||
"$TARGET_WIZARD_ACTIVE_START_JSON" \
|
||||
"$TARGET_WIZARD_ACTIVE_START_ERR" \
|
||||
"target status cancellation settlement"
|
||||
)"
|
||||
IFS=$'\t' read -r target_active_session_id target_status_settlement_polls \
|
||||
<<<"$target_active_start_result"
|
||||
|
||||
if gateway_call wizard.status "$target_status_session_params" \
|
||||
"$TARGET_WIZARD_STATUS_PURGED_JSON" "$TARGET_WIZARD_STATUS_PURGED_ERR"; then
|
||||
echo "target cancelled status-session wizard remained reachable after settlement" >&2
|
||||
exit 1
|
||||
fi
|
||||
assert_gateway_call_error_message \
|
||||
"$TARGET_WIZARD_STATUS_PURGED_JSON" \
|
||||
"$TARGET_WIZARD_STATUS_PURGED_ERR" \
|
||||
"wizard not found" \
|
||||
"target cancelled status-session cleanup"
|
||||
|
||||
target_active_step_id="$(
|
||||
node -e '
|
||||
const fs = require("node:fs");
|
||||
const payload = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
|
||||
process.stdout.write(payload.step.id);
|
||||
' "$TARGET_WIZARD_ACTIVE_START_JSON"
|
||||
)"
|
||||
target_active_session_params="$(
|
||||
WIZARD_SESSION_ID="$target_active_session_id" node -e '
|
||||
process.stdout.write(JSON.stringify({ sessionId: process.env.WIZARD_SESSION_ID }));
|
||||
'
|
||||
)"
|
||||
target_active_next_params="$(
|
||||
WIZARD_SESSION_ID="$target_active_session_id" WIZARD_STEP_ID="$target_active_step_id" node -e '
|
||||
process.stdout.write(
|
||||
JSON.stringify({
|
||||
sessionId: process.env.WIZARD_SESSION_ID,
|
||||
answer: { stepId: process.env.WIZARD_STEP_ID, value: null },
|
||||
}),
|
||||
);
|
||||
'
|
||||
)"
|
||||
gateway_call wizard.next "$target_active_next_params" \
|
||||
"$TARGET_WIZARD_NEXT_JSON" "$TARGET_WIZARD_NEXT_ERR"
|
||||
if gateway_call wizard.start '{"mode":"local"}' \
|
||||
"$TARGET_WIZARD_DUPLICATE_JSON" "$TARGET_WIZARD_DUPLICATE_ERR"; then
|
||||
echo "target wizard.start unexpectedly allowed an overlapping setup session" >&2
|
||||
exit 1
|
||||
fi
|
||||
assert_gateway_call_error_message \
|
||||
"$TARGET_WIZARD_DUPLICATE_JSON" \
|
||||
"$TARGET_WIZARD_DUPLICATE_ERR" \
|
||||
"wizard already running" \
|
||||
"target overlapping wizard.start"
|
||||
|
||||
gateway_call wizard.cancel "$target_active_session_params" \
|
||||
"$TARGET_WIZARD_CANCEL_JSON" "$TARGET_WIZARD_CANCEL_ERR"
|
||||
target_replacement_start_result="$(
|
||||
wait_for_target_wizard_start \
|
||||
"$TARGET_WIZARD_REPLACEMENT_START_JSON" \
|
||||
"$TARGET_WIZARD_REPLACEMENT_START_ERR" \
|
||||
"target active cancellation settlement"
|
||||
)"
|
||||
IFS=$'\t' read -r target_replacement_session_id target_cancel_settlement_polls \
|
||||
<<<"$target_replacement_start_result"
|
||||
|
||||
if gateway_call wizard.status "$target_active_session_params" \
|
||||
"$TARGET_WIZARD_PURGED_STATUS_JSON" "$TARGET_WIZARD_PURGED_STATUS_ERR"; then
|
||||
echo "target cancelled wizard session remained reachable" >&2
|
||||
exit 1
|
||||
fi
|
||||
assert_gateway_call_error_message \
|
||||
"$TARGET_WIZARD_PURGED_STATUS_JSON" \
|
||||
"$TARGET_WIZARD_PURGED_STATUS_ERR" \
|
||||
"wizard not found" \
|
||||
"target cancelled wizard cleanup"
|
||||
|
||||
target_replacement_session_params="$(
|
||||
WIZARD_SESSION_ID="$target_replacement_session_id" node -e '
|
||||
process.stdout.write(JSON.stringify({ sessionId: process.env.WIZARD_SESSION_ID }));
|
||||
'
|
||||
)"
|
||||
gateway_call wizard.cancel "$target_replacement_session_params" \
|
||||
"$TARGET_WIZARD_REPLACEMENT_CANCEL_JSON" "$TARGET_WIZARD_REPLACEMENT_CANCEL_ERR"
|
||||
|
||||
TARGET_WIZARD_STATUS_START_JSON="$TARGET_WIZARD_STATUS_START_JSON" \
|
||||
TARGET_WIZARD_STATUS_JSON="$TARGET_WIZARD_STATUS_JSON" \
|
||||
TARGET_WIZARD_STATUS_RETAINED_JSON="$TARGET_WIZARD_STATUS_RETAINED_JSON" \
|
||||
TARGET_WIZARD_STATUS_CANCEL_JSON="$TARGET_WIZARD_STATUS_CANCEL_JSON" \
|
||||
TARGET_WIZARD_ACTIVE_START_JSON="$TARGET_WIZARD_ACTIVE_START_JSON" \
|
||||
TARGET_WIZARD_NEXT_JSON="$TARGET_WIZARD_NEXT_JSON" \
|
||||
TARGET_WIZARD_CANCEL_JSON="$TARGET_WIZARD_CANCEL_JSON" \
|
||||
TARGET_WIZARD_REPLACEMENT_START_JSON="$TARGET_WIZARD_REPLACEMENT_START_JSON" \
|
||||
TARGET_WIZARD_REPLACEMENT_CANCEL_JSON="$TARGET_WIZARD_REPLACEMENT_CANCEL_JSON" \
|
||||
TARGET_STATUS_SETTLEMENT_POLLS="$target_status_settlement_polls" \
|
||||
TARGET_CANCEL_SETTLEMENT_POLLS="$target_cancel_settlement_polls" \
|
||||
TARGET_WIZARD_FLOW_JSON="$TARGET_WIZARD_FLOW_JSON" \
|
||||
node -e '
|
||||
const fs = require("node:fs");
|
||||
const readJson = (file) => JSON.parse(fs.readFileSync(file, "utf8"));
|
||||
const sanitizeStep = (step, label) => {
|
||||
if (!step || typeof step.id !== "string" || !step.id) {
|
||||
throw new Error(`${label} omitted its wizard step`);
|
||||
}
|
||||
const allowedTypes = new Set([
|
||||
"note",
|
||||
"select",
|
||||
"text",
|
||||
"confirm",
|
||||
"multiselect",
|
||||
"progress",
|
||||
"action",
|
||||
]);
|
||||
if (!allowedTypes.has(step.type)) {
|
||||
throw new Error(`${label} returned unsupported step type ${String(step.type)}`);
|
||||
}
|
||||
if (step.sensitive === true && Object.hasOwn(step, "initialValue")) {
|
||||
throw new Error(`${label} exposed a sensitive initial value`);
|
||||
}
|
||||
for (const field of ["title", "message", "placeholder"]) {
|
||||
if (step[field] !== undefined && typeof step[field] !== "string") {
|
||||
throw new Error(`${label} returned non-string ${field}`);
|
||||
}
|
||||
if (step[field]?.length > 8192) {
|
||||
throw new Error(`${label} returned oversized ${field}`);
|
||||
}
|
||||
}
|
||||
if (step.options !== undefined && !Array.isArray(step.options)) {
|
||||
throw new Error(`${label} returned malformed options`);
|
||||
}
|
||||
return {
|
||||
type: step.type,
|
||||
executor: step.executor,
|
||||
sensitive: step.sensitive === true,
|
||||
initialValuePresent: Object.hasOwn(step, "initialValue"),
|
||||
titleLength: step.title?.length ?? 0,
|
||||
messageLength: step.message?.length ?? 0,
|
||||
optionCount: step.options?.length ?? 0,
|
||||
};
|
||||
};
|
||||
const statusStart = readJson(process.env.TARGET_WIZARD_STATUS_START_JSON);
|
||||
const status = readJson(process.env.TARGET_WIZARD_STATUS_JSON);
|
||||
const retainedStatus = readJson(process.env.TARGET_WIZARD_STATUS_RETAINED_JSON);
|
||||
const statusCancel = readJson(process.env.TARGET_WIZARD_STATUS_CANCEL_JSON);
|
||||
const activeStart = readJson(process.env.TARGET_WIZARD_ACTIVE_START_JSON);
|
||||
const next = readJson(process.env.TARGET_WIZARD_NEXT_JSON);
|
||||
const cancel = readJson(process.env.TARGET_WIZARD_CANCEL_JSON);
|
||||
const replacementStart = readJson(process.env.TARGET_WIZARD_REPLACEMENT_START_JSON);
|
||||
const replacementCancel = readJson(process.env.TARGET_WIZARD_REPLACEMENT_CANCEL_JSON);
|
||||
if (status.status !== "running" || retainedStatus.status !== "running") {
|
||||
throw new Error(
|
||||
`target wizard.status did not retain its running session: ${JSON.stringify({
|
||||
status,
|
||||
retainedStatus,
|
||||
})}`,
|
||||
);
|
||||
}
|
||||
if (next.done !== false || next.status !== "running") {
|
||||
throw new Error(`target wizard.next did not advance a running session: ${JSON.stringify(next)}`);
|
||||
}
|
||||
if (
|
||||
statusCancel.status !== "cancelled" ||
|
||||
cancel.status !== "cancelled" ||
|
||||
replacementCancel.status !== "cancelled"
|
||||
) {
|
||||
throw new Error("target wizard.cancel did not cancel every target session");
|
||||
}
|
||||
fs.writeFileSync(
|
||||
process.env.TARGET_WIZARD_FLOW_JSON,
|
||||
`${JSON.stringify(
|
||||
{
|
||||
status: "passed",
|
||||
authenticated: true,
|
||||
packagePhase: "target",
|
||||
statusSession: {
|
||||
start: {
|
||||
status: statusStart.status,
|
||||
step: sanitizeStep(statusStart.step, "target status wizard.start"),
|
||||
},
|
||||
observedStatuses: [status.status, retainedStatus.status],
|
||||
runningStatusRetained: true,
|
||||
cancelStatus: statusCancel.status,
|
||||
settlementPolls: Number(process.env.TARGET_STATUS_SETTLEMENT_POLLS),
|
||||
purged: true,
|
||||
},
|
||||
activeSession: {
|
||||
start: {
|
||||
status: activeStart.status,
|
||||
step: sanitizeStep(activeStart.step, "target active wizard.start"),
|
||||
},
|
||||
next: {
|
||||
status: next.status,
|
||||
step: sanitizeStep(next.step, "target wizard.next"),
|
||||
},
|
||||
duplicateStartRejected: true,
|
||||
cancelStatus: cancel.status,
|
||||
settlementPolls: Number(process.env.TARGET_CANCEL_SETTLEMENT_POLLS),
|
||||
purged: true,
|
||||
replacement: {
|
||||
status: replacementStart.status,
|
||||
step: sanitizeStep(replacementStart.step, "target replacement wizard.start"),
|
||||
cancelStatus: replacementCancel.status,
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
);
|
||||
'
|
||||
|
||||
post_restart_observed_at_ms="$(node -e 'process.stdout.write(String(Date.now()))')"
|
||||
deadline=$((SECONDS + 60))
|
||||
while [ "$SECONDS" -lt "$deadline" ]; do
|
||||
@@ -491,6 +1051,8 @@ SOURCE_VERSION="$SOURCE_VERSION" \
|
||||
POST_RESTART_OBSERVED_AT_MS="$post_restart_observed_at_ms" \
|
||||
UPDATE_RPC_JSON="$UPDATE_RPC_JSON" \
|
||||
UPDATE_STATUS_JSON="$UPDATE_STATUS_JSON" \
|
||||
WIZARD_FLOW_JSON="$WIZARD_FLOW_JSON" \
|
||||
TARGET_WIZARD_FLOW_JSON="$TARGET_WIZARD_FLOW_JSON" \
|
||||
QA_CHANNEL_INSTALL_RECORD_JSON="$QA_CHANNEL_INSTALL_RECORD_JSON" \
|
||||
TARGET_PLUGIN_INDEX_JSON="$TARGET_PLUGIN_INDEX_JSON" \
|
||||
SOURCE_PLUGIN_INSPECT_JSON="$SOURCE_PLUGIN_INSPECT_JSON" \
|
||||
@@ -533,6 +1095,8 @@ SOURCE_VERSION="$SOURCE_VERSION" \
|
||||
expectedRestartNote: process.env.RESTART_NOTE,
|
||||
updateRpcCompletedAtMs: Number(process.env.UPDATE_RPC_COMPLETED_AT_MS),
|
||||
postRestartObservedAtMs: postRestartAtMs,
|
||||
wizardFlow: readJson(process.env.WIZARD_FLOW_JSON),
|
||||
targetWizardFlow: readJson(process.env.TARGET_WIZARD_FLOW_JSON),
|
||||
updateRpcResult: readJson(process.env.UPDATE_RPC_JSON),
|
||||
restartSentinel: updateStatus.sentinel,
|
||||
qaChannelInstallRecord: readJson(process.env.QA_CHANNEL_INSTALL_RECORD_JSON),
|
||||
|
||||
@@ -25,6 +25,7 @@ SKIP_BUILD="${OPENCLAW_UPDATE_RUN_SELF_UPGRADE_E2E_SKIP_BUILD:-0}"
|
||||
DOCKER_RUN_TIMEOUT="${OPENCLAW_UPDATE_RUN_SELF_UPGRADE_DOCKER_RUN_TIMEOUT:-1800s}"
|
||||
ARTIFACT_DIR="${OPENCLAW_UPDATE_RUN_SELF_UPGRADE_ARTIFACT_DIR:-$ROOT_DIR/.artifacts/update-run-package-self-upgrade}"
|
||||
QA_CHANNEL_FIXTURE_ROOT="$(mktemp -d "${TMPDIR:-/tmp}/openclaw-update-run-qa-channel.XXXXXX")"
|
||||
HISTORICAL_DIST_ARCHIVE="$QA_CHANNEL_FIXTURE_ROOT/historical-dist.tar"
|
||||
|
||||
cleanup() {
|
||||
rm -rf "$QA_CHANNEL_FIXTURE_ROOT"
|
||||
@@ -72,6 +73,8 @@ prepare_qa_channel_fixture() {
|
||||
tagObject: process.env.TAG_OBJECT,
|
||||
commit: process.env.SOURCE_COMMIT,
|
||||
buildCommand: "OPENCLAW_BUILD_PRIVATE_QA=1 corepack pnpm build:docker",
|
||||
runtimeInstall: `npm install --prefix <isolated-root> --omit=dev openclaw@${process.env.SOURCE_TAG.slice(1)}`,
|
||||
privateDistOverlay: true,
|
||||
}, null, 2)}\n`,
|
||||
);
|
||||
' "$ARTIFACT_DIR/qa-channel-fixture-provenance.json"
|
||||
@@ -102,6 +105,7 @@ prepare_qa_channel_fixture() {
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
tar -C "$checkout_root" -cf "$HISTORICAL_DIST_ARCHIVE" dist
|
||||
}
|
||||
|
||||
mkdir -p "$ARTIFACT_DIR"
|
||||
@@ -123,7 +127,47 @@ docker_e2e_run_with_harness \
|
||||
-e OPENCLAW_UPDATE_RUN_SELF_UPGRADE_ARTIFACT_DIR=/tmp/openclaw-update-run-artifacts \
|
||||
-e OPENCLAW_UPDATE_RUN_SELF_UPGRADE_SOURCE_VERSION="$SOURCE_VERSION" \
|
||||
-v "$ARTIFACT_DIR:/tmp/openclaw-update-run-artifacts" \
|
||||
-v "$QA_CHANNEL_FIXTURE_ROOT/checkout:/tmp/openclaw-update-run-build:ro" \
|
||||
-v "$HISTORICAL_DIST_ARCHIVE:/tmp/openclaw-update-run-historical-dist.tar:ro" \
|
||||
"$IMAGE_NAME" \
|
||||
timeout --kill-after=30s "$DOCKER_RUN_TIMEOUT" \
|
||||
bash scripts/e2e/lib/upgrade-survivor/update-run-package-self-upgrade.sh
|
||||
bash -lc 'set -euo pipefail
|
||||
historical_install_root=/tmp/openclaw-update-run-historical-install
|
||||
historical_package_root="$historical_install_root/node_modules/openclaw"
|
||||
qa_plugin_link=/tmp/openclaw-update-run-build/dist/extensions/qa-channel
|
||||
npm install \
|
||||
--prefix "$historical_install_root" \
|
||||
--omit=dev \
|
||||
--no-fund \
|
||||
--no-audit \
|
||||
"openclaw@$OPENCLAW_UPDATE_RUN_SELF_UPGRADE_SOURCE_VERSION" \
|
||||
2>&1 | tee /tmp/openclaw-update-run-artifacts/historical-package-install.log
|
||||
tar --no-same-owner \
|
||||
-xf /tmp/openclaw-update-run-historical-dist.tar \
|
||||
-C "$historical_package_root"
|
||||
mkdir -p "$(dirname "$qa_plugin_link")"
|
||||
ln -s "$historical_package_root/dist/extensions/qa-channel" "$qa_plugin_link"
|
||||
HISTORICAL_PACKAGE_ROOT="$historical_package_root" \
|
||||
HISTORICAL_PLUGIN_ENTRY="$historical_package_root/dist/extensions/qa-channel/index.js" \
|
||||
HISTORICAL_PREFLIGHT_OUT=/tmp/openclaw-update-run-artifacts/historical-package-preflight.json \
|
||||
node --input-type=module <<NODE
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
const packageRoot = process.env.HISTORICAL_PACKAGE_ROOT;
|
||||
const pluginEntry = process.env.HISTORICAL_PLUGIN_ENTRY;
|
||||
const packageVersion = JSON.parse(
|
||||
fs.readFileSync(path.join(packageRoot, "package.json"), "utf8"),
|
||||
).version;
|
||||
const pluginVersion = JSON.parse(
|
||||
fs.readFileSync(path.join(path.dirname(pluginEntry), "package.json"), "utf8"),
|
||||
).version;
|
||||
if (packageVersion !== process.env.OPENCLAW_UPDATE_RUN_SELF_UPGRADE_SOURCE_VERSION) {
|
||||
throw new Error("isolated historical package version mismatch: " + packageVersion);
|
||||
}
|
||||
await import(pathToFileURL(pluginEntry).href);
|
||||
fs.writeFileSync(
|
||||
process.env.HISTORICAL_PREFLIGHT_OUT,
|
||||
JSON.stringify({ packageVersion, pluginVersion, pluginEntryImported: true }, null, 2) + "\n",
|
||||
);
|
||||
NODE
|
||||
exec bash scripts/e2e/lib/upgrade-survivor/update-run-package-self-upgrade.sh'
|
||||
|
||||
@@ -72,6 +72,100 @@ describe("update.run package self-upgrade producer", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("proves the authenticated setup lifecycle before updating", async () => {
|
||||
const script = await fs.readFile(
|
||||
path.join(
|
||||
process.cwd(),
|
||||
"scripts/e2e/lib/upgrade-survivor/update-run-package-self-upgrade.sh",
|
||||
),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
expect(script).toContain(
|
||||
'gateway_call wizard.start \'{"mode":"local"}\' "$WIZARD_START_JSON" "$WIZARD_START_ERR"',
|
||||
);
|
||||
expect(script).toContain(
|
||||
'gateway_call wizard.status "$wizard_session_params" "$WIZARD_STATUS_JSON" "$WIZARD_STATUS_ERR"',
|
||||
);
|
||||
expect(script).toContain(
|
||||
'gateway_call wizard.next "$wizard_next_params" "$WIZARD_NEXT_JSON" "$WIZARD_NEXT_ERR"',
|
||||
);
|
||||
expect(script).toContain("runningStatusRetained: true");
|
||||
expect(script).toContain("assert_gateway_call_error_message()");
|
||||
expect(script).toContain(
|
||||
'"$WIZARD_DUPLICATE_JSON" \\\n "$WIZARD_DUPLICATE_ERR" \\\n "wizard already running"',
|
||||
);
|
||||
expect(script).toContain('local allow_stderr_fallback="${5:-0}"');
|
||||
expect(script).toContain(
|
||||
'[ "$allow_stderr_fallback" = "1" ] && grep -Fq "$expected" "$error_output"',
|
||||
);
|
||||
expect(script).toContain(
|
||||
'gateway_call wizard.cancel "$wizard_session_params" "$WIZARD_CANCEL_JSON" "$WIZARD_CANCEL_ERR"',
|
||||
);
|
||||
expect(script).toContain(
|
||||
'"$WIZARD_CANCELLED_STATUS_JSON" \\\n "$WIZARD_CANCELLED_STATUS_ERR" \\\n "wizard not found"',
|
||||
);
|
||||
expect(script).toContain('step.sensitive === true && Object.hasOwn(step, "initialValue")');
|
||||
expect(script.indexOf("Exercising authenticated Gateway wizard RPC lifecycle")).toBeLessThan(
|
||||
script.indexOf("Invoking authenticated Gateway RPC update.run"),
|
||||
);
|
||||
});
|
||||
|
||||
it("proves the current target setup lifecycle after updating", async () => {
|
||||
const script = await fs.readFile(
|
||||
path.join(
|
||||
process.cwd(),
|
||||
"scripts/e2e/lib/upgrade-survivor/update-run-package-self-upgrade.sh",
|
||||
),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
expect(script).toContain("Exercising current target Gateway wizard RPC lifecycle");
|
||||
expect(script).toContain(
|
||||
'gateway_call wizard.status "$target_status_session_params" \\\n "$TARGET_WIZARD_STATUS_JSON" "$TARGET_WIZARD_STATUS_ERR"',
|
||||
);
|
||||
expect(script).toContain(
|
||||
'gateway_call wizard.status "$target_status_session_params" \\\n "$TARGET_WIZARD_STATUS_RETAINED_JSON" "$TARGET_WIZARD_STATUS_RETAINED_ERR"',
|
||||
);
|
||||
expect(script).toContain(
|
||||
'gateway_call wizard.cancel "$target_status_session_params" \\\n "$TARGET_WIZARD_STATUS_CANCEL_JSON" "$TARGET_WIZARD_STATUS_CANCEL_ERR"',
|
||||
);
|
||||
expect(script).toContain(
|
||||
'gateway_call wizard.next "$target_active_next_params" \\\n "$TARGET_WIZARD_NEXT_JSON" "$TARGET_WIZARD_NEXT_ERR"',
|
||||
);
|
||||
expect(script).toContain(
|
||||
'"$TARGET_WIZARD_DUPLICATE_JSON" \\\n "$TARGET_WIZARD_DUPLICATE_ERR" \\\n "wizard already running"',
|
||||
);
|
||||
expect(script).toContain(
|
||||
'gateway_call wizard.cancel "$target_active_session_params" \\\n "$TARGET_WIZARD_CANCEL_JSON" "$TARGET_WIZARD_CANCEL_ERR"',
|
||||
);
|
||||
expect(script).toContain("wait_for_target_wizard_start()");
|
||||
expect(script).toContain("target_status_settlement_polls");
|
||||
expect(script).toContain("target_cancel_settlement_polls");
|
||||
expect(script).toContain(
|
||||
'"$TARGET_WIZARD_REPLACEMENT_START_JSON" \\\n "$TARGET_WIZARD_REPLACEMENT_START_ERR"',
|
||||
);
|
||||
expect(script.indexOf("target_active_start_result=")).toBeLessThan(
|
||||
script.indexOf(
|
||||
'gateway_call wizard.status "$target_status_session_params" \\\n "$TARGET_WIZARD_STATUS_PURGED_JSON"',
|
||||
),
|
||||
);
|
||||
expect(script).toContain(
|
||||
'"$TARGET_WIZARD_PURGED_STATUS_JSON" \\\n "$TARGET_WIZARD_PURGED_STATUS_ERR" \\\n "wizard not found"',
|
||||
);
|
||||
expect(script.indexOf("target_replacement_start_result=")).toBeLessThan(
|
||||
script.indexOf(
|
||||
'gateway_call wizard.status "$target_active_session_params" \\\n "$TARGET_WIZARD_PURGED_STATUS_JSON"',
|
||||
),
|
||||
);
|
||||
expect(script.indexOf("Invoking authenticated Gateway RPC update.run")).toBeLessThan(
|
||||
script.indexOf("Exercising current target Gateway wizard RPC lifecycle"),
|
||||
);
|
||||
expect(script.indexOf("Exercising current target Gateway wizard RPC lifecycle")).toBeLessThan(
|
||||
script.indexOf("post_restart_observed_at_ms="),
|
||||
);
|
||||
});
|
||||
|
||||
it("falls back to the exact tag clone when commit or tag objects are missing", async () => {
|
||||
const script = await fs.readFile(
|
||||
path.join(process.cwd(), "scripts/e2e/update-run-package-self-upgrade-docker.sh"),
|
||||
@@ -84,6 +178,21 @@ describe("update.run package self-upgrade producer", () => {
|
||||
expect(script).toContain(
|
||||
'! git -C "$source_repo" cat-file -e "$SOURCE_TAG^{commit}" 2>/dev/null',
|
||||
);
|
||||
expect(script).toContain('tar -C "$checkout_root" -cf "$HISTORICAL_DIST_ARCHIVE" dist');
|
||||
expect(script).toContain(
|
||||
'npm install \\\n --prefix "$historical_install_root" \\\n --omit=dev',
|
||||
);
|
||||
expect(script).toContain(
|
||||
'tar --no-same-owner \\\n -xf /tmp/openclaw-update-run-historical-dist.tar \\\n -C "$historical_package_root"',
|
||||
);
|
||||
expect(script).toContain(
|
||||
'ln -s "$historical_package_root/dist/extensions/qa-channel" "$qa_plugin_link"',
|
||||
);
|
||||
expect(script).toContain("await import(pathToFileURL(pluginEntry).href)");
|
||||
expect(script).not.toContain(
|
||||
'-v "$QA_CHANNEL_FIXTURE_ROOT/checkout:/tmp/openclaw-update-run-build:ro"',
|
||||
);
|
||||
expect(script).not.toContain("NODE_PATH");
|
||||
});
|
||||
|
||||
it("formats the proven version transition and sentinel", () => {
|
||||
@@ -92,13 +201,32 @@ describe("update.run package self-upgrade producer", () => {
|
||||
installedVersion: "2026.7.2",
|
||||
source: { version: "2026.4.26" },
|
||||
target: { resolvedVersion: "2026.7.2", tag: "latest" },
|
||||
wizardFlow: {
|
||||
authenticated: true,
|
||||
cancelledSessionPurged: true,
|
||||
duplicateStartRejected: true,
|
||||
runningStatusRetained: true,
|
||||
status: "passed",
|
||||
},
|
||||
targetWizardFlow: {
|
||||
activeSession: {
|
||||
duplicateStartRejected: true,
|
||||
purged: true,
|
||||
},
|
||||
authenticated: true,
|
||||
status: "passed",
|
||||
statusSession: {
|
||||
purged: true,
|
||||
runningStatusRetained: true,
|
||||
},
|
||||
},
|
||||
restartSentinel: {
|
||||
message: "QA-UPDATE-RUN-PACKAGE-SELF-UPGRADE",
|
||||
status: "ok",
|
||||
},
|
||||
}),
|
||||
).toBe(
|
||||
"source=2026.4.26; target=latest:2026.7.2; installed=2026.7.2; sentinel=ok:QA-UPDATE-RUN-PACKAGE-SELF-UPGRADE",
|
||||
"wizard=passed:authenticated:status-retained:exclusive:purged; target-wizard=passed:authenticated:status-retained:status-purged:exclusive:purged; source=2026.4.26; target=latest:2026.7.2; installed=2026.7.2; sentinel=ok:QA-UPDATE-RUN-PACKAGE-SELF-UPGRADE",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -23,6 +23,25 @@ type UpdateRunSelfUpgradeSummary = {
|
||||
source?: { version?: string };
|
||||
target?: { resolvedVersion?: string; tag?: string };
|
||||
restartSentinel?: { message?: string; status?: string };
|
||||
wizardFlow?: {
|
||||
authenticated?: boolean;
|
||||
duplicateStartRejected?: boolean;
|
||||
cancelledSessionPurged?: boolean;
|
||||
runningStatusRetained?: boolean;
|
||||
status?: string;
|
||||
};
|
||||
targetWizardFlow?: {
|
||||
activeSession?: {
|
||||
duplicateStartRejected?: boolean;
|
||||
purged?: boolean;
|
||||
};
|
||||
authenticated?: boolean;
|
||||
status?: string;
|
||||
statusSession?: {
|
||||
purged?: boolean;
|
||||
runningStatusRetained?: boolean;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
function formatErrorMessage(error: unknown) {
|
||||
@@ -63,6 +82,8 @@ export function resolveUpdateRunSelfUpgradePermission(
|
||||
|
||||
export function formatUpdateRunSelfUpgradeDetails(summary: UpdateRunSelfUpgradeSummary) {
|
||||
return [
|
||||
`wizard=${summary.wizardFlow?.status ?? "unknown"}:${summary.wizardFlow?.authenticated === true ? "authenticated" : "unauthenticated"}:${summary.wizardFlow?.runningStatusRetained === true ? "status-retained" : "status-unknown"}:${summary.wizardFlow?.duplicateStartRejected === true ? "exclusive" : "overlap-unknown"}:${summary.wizardFlow?.cancelledSessionPurged === true ? "purged" : "cleanup-unknown"}`,
|
||||
`target-wizard=${summary.targetWizardFlow?.status ?? "unknown"}:${summary.targetWizardFlow?.authenticated === true ? "authenticated" : "unauthenticated"}:${summary.targetWizardFlow?.statusSession?.runningStatusRetained === true ? "status-retained" : "status-unknown"}:${summary.targetWizardFlow?.statusSession?.purged === true ? "status-purged" : "status-cleanup-unknown"}:${summary.targetWizardFlow?.activeSession?.duplicateStartRejected === true ? "exclusive" : "overlap-unknown"}:${summary.targetWizardFlow?.activeSession?.purged === true ? "purged" : "cleanup-unknown"}`,
|
||||
`source=${summary.source?.version ?? "unknown"}`,
|
||||
`target=${summary.target?.tag ?? "unknown"}:${summary.target?.resolvedVersion ?? "unknown"}`,
|
||||
`installed=${summary.installedVersion ?? "unknown"}`,
|
||||
@@ -111,7 +132,7 @@ async function runProducer(options: ProducerOptions): Promise<QaEvidenceSummaryJ
|
||||
const writer = createQaScriptEvidenceWriter({
|
||||
artifactBase: options.artifactBase,
|
||||
logFileName: "update-run-package-self-upgrade.log",
|
||||
primaryModel: "gateway/update.run",
|
||||
primaryModel: "gateway/update-and-setup",
|
||||
providerMode: "mock-openai",
|
||||
repoRoot: options.repoRoot,
|
||||
target: {
|
||||
@@ -122,6 +143,7 @@ async function runProducer(options: ProducerOptions): Promise<QaEvidenceSummaryJ
|
||||
"scripts/e2e/lib/upgrade-survivor/assertions.mjs",
|
||||
"scripts/lib/docker-e2e-scenarios.mjs",
|
||||
"src/gateway/server-methods/update.ts",
|
||||
"src/gateway/server-methods/wizard.ts",
|
||||
],
|
||||
docsRefs: [
|
||||
"docs/cli/update.md",
|
||||
@@ -159,6 +181,8 @@ async function runProducer(options: ProducerOptions): Promise<QaEvidenceSummaryJ
|
||||
return await writer.write({
|
||||
artifacts: [
|
||||
{ kind: "summary", filePath: path.join("lane", "summary.json") },
|
||||
{ kind: "rpc", filePath: path.join("lane", "wizard-flow.json") },
|
||||
{ kind: "rpc", filePath: path.join("lane", "target-wizard-flow.json") },
|
||||
{ kind: "rpc", filePath: path.join("lane", "update-rpc.json") },
|
||||
{ kind: "sentinel", filePath: path.join("lane", "update-status.json") },
|
||||
{
|
||||
@@ -169,6 +193,11 @@ async function runProducer(options: ProducerOptions): Promise<QaEvidenceSummaryJ
|
||||
{ kind: "summary", filePath: path.join("lane", "source-plugin-inspect.json") },
|
||||
{ kind: "summary", filePath: path.join("lane", "target-plugin-index.json") },
|
||||
{ kind: "log", filePath: path.join("lane", "historical-qa-channel-build.log") },
|
||||
{ kind: "log", filePath: path.join("lane", "historical-package-install.log") },
|
||||
{
|
||||
kind: "summary",
|
||||
filePath: path.join("lane", "historical-package-preflight.json"),
|
||||
},
|
||||
{
|
||||
kind: "summary",
|
||||
filePath: path.join("lane", "qa-channel-fixture-provenance.json"),
|
||||
|
||||
Reference in New Issue
Block a user