mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-25 03:45:46 -06: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:
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user