Compare commits

...

39 Commits

Author SHA1 Message Date
Vincent Koc
ed6f489b72 test(parallels): assert package update status 2026-04-12 17:40:28 +01:00
Vincent Koc
634c2fcb78 fix(parallels): recover discord update progress 2026-04-12 17:38:18 +01:00
Vincent Koc
574b748372 fix(parallels): persist update progress before discord 2026-04-12 17:30:30 +01:00
Vincent Koc
d286eadda3 fix(parallels): recover fresh subchecks from child logs 2026-04-12 17:29:28 +01:00
Vincent Koc
de8a55b1c4 test(parallels): show wrapper target context 2026-04-12 17:26:30 +01:00
Vincent Koc
6b9dfafdc5 test(parallels): align standalone summaries 2026-04-12 17:25:49 +01:00
Vincent Koc
26737271ac fix(parallels): backfill update subchecks from logs 2026-04-12 17:24:59 +01:00
Vincent Koc
bab6ce5fae fix(parallels): emit wrapper failure summaries 2026-04-12 17:23:32 +01:00
Vincent Koc
71d8d6aa49 fix(parallels): mark standalone subcheck failures 2026-04-12 17:19:34 +01:00
Vincent Koc
56ec00a69f fix(parallels): backfill versions from logs 2026-04-12 17:18:10 +01:00
Vincent Koc
6ac79f5009 fix(parallels): preserve seeded summary statuses 2026-04-12 17:16:56 +01:00
Vincent Koc
e9412d8e02 refactor(parallels): share wrapper summary helpers 2026-04-12 17:15:48 +01:00
Vincent Koc
07f6b17ed4 fix(parallels): parse fresh summaries from mixed logs 2026-04-12 17:14:36 +01:00
Vincent Koc
7c43ee3f94 refactor(parallels): share posix permission checks 2026-04-12 17:14:03 +01:00
Vincent Koc
d2a1078007 refactor(parallels): share windows permission checks 2026-04-12 17:12:44 +01:00
Vincent Koc
27e8c8866f fix(parallels): fail closed on missing fresh summaries 2026-04-12 17:05:39 +01:00
Vincent Koc
bb1f876eb9 fix(parallels): persist update substatus summaries 2026-04-12 17:04:29 +01:00
Vincent Koc
3a63182dd7 test(parallels): mark permission failures explicitly 2026-04-12 17:02:04 +01:00
Vincent Koc
a76b6987fb test(parallels): verify macos dev update permissions 2026-04-12 17:00:41 +01:00
Vincent Koc
bc724a86cb test(parallels): verify windows install permissions 2026-04-12 16:59:42 +01:00
Vincent Koc
0382544c64 test(parallels): report update gateway recovery 2026-04-12 16:57:26 +01:00
Vincent Koc
bb90c72f1b test(parallels): report permission coverage 2026-04-12 16:56:41 +01:00
Vincent Koc
7d8592eacf test(parallels): verify update artifact permissions 2026-04-12 16:53:41 +01:00
Vincent Koc
5ccd1facf7 test(parallels): report channel probe status 2026-04-12 16:50:44 +01:00
Vincent Koc
65b7399f4a refactor(parallels): share discord smoke helpers 2026-04-12 16:45:58 +01:00
Vincent Koc
6ce4b02e22 test(parallels): report fresh child coverage 2026-04-12 16:42:01 +01:00
Vincent Koc
d1618353ac test(parallels): report update check coverage 2026-04-12 16:40:27 +01:00
Vincent Koc
ea6bfc76b7 test(parallels): add discord update roundtrips 2026-04-12 16:38:01 +01:00
Vincent Koc
6ea8ea7477 test(parallels): probe channels in standalone lanes 2026-04-12 16:33:52 +01:00
Vincent Koc
1ff168b2f0 test(parallels): probe channels after updates 2026-04-12 16:32:37 +01:00
Vincent Koc
a24f9b6c7d test(parallels): pass discord smoke through wrapper 2026-04-12 16:32:00 +01:00
Vincent Koc
19d369c750 test(parallels): add windows discord smoke 2026-04-12 16:30:21 +01:00
Vincent Koc
074ce1d8a7 test(parallels): add linux discord smoke 2026-04-12 16:28:26 +01:00
Vincent Koc
38081f8f90 test(parallels): check linux install artifact permissions 2026-04-12 16:25:01 +01:00
Vincent Koc
a59c5f41ad test(parallels): verify dashboard after same-guest updates 2026-04-12 16:20:41 +01:00
Vincent Koc
559556f437 test(parallels): add linux latest-ref precheck 2026-04-12 16:19:35 +01:00
Vincent Koc
e9fec40af7 test(parallels): add dashboard smoke on windows and linux 2026-04-12 16:18:45 +01:00
Vincent Koc
d35a10e6e0 test(parallels): log linux update status 2026-04-12 16:15:24 +01:00
Vincent Koc
acbb6bff44 fix(parallels): cover linux update paths 2026-04-12 16:11:55 +01:00
8 changed files with 1973 additions and 209 deletions

View File

@@ -0,0 +1,177 @@
#!/usr/bin/env bash
discord_python_bin() {
if [[ -n "${PYTHON_BIN:-}" ]]; then
printf '%s\n' "$PYTHON_BIN"
return
fi
command -v python3
}
discord_smoke_enabled() {
[[ -n "${DISCORD_TOKEN_VALUE:-}" && -n "${DISCORD_GUILD_ID:-}" && -n "${DISCORD_CHANNEL_ID:-}" ]]
}
discord_api_request() {
local method="$1"
local path="$2"
local payload="${3:-}"
local url="https://discord.com/api/v10$path"
if [[ -n "$payload" ]]; then
curl -fsS -X "$method" \
-H "Authorization: Bot $DISCORD_TOKEN_VALUE" \
-H "Content-Type: application/json" \
--data "$payload" \
"$url"
return
fi
curl -fsS -X "$method" \
-H "Authorization: Bot $DISCORD_TOKEN_VALUE" \
"$url"
}
json_contains_string() {
local needle="$1"
"$(discord_python_bin)" - "$needle" <<'PY'
import json
import sys
needle = sys.argv[1]
try:
payload = json.load(sys.stdin)
except Exception:
raise SystemExit(1)
def contains(value):
if isinstance(value, str):
return needle in value
if isinstance(value, list):
return any(contains(item) for item in value)
if isinstance(value, dict):
return any(contains(item) for item in value.values())
return False
raise SystemExit(0 if contains(payload) else 1)
PY
}
build_discord_guilds_json() {
DISCORD_GUILD_ID="$DISCORD_GUILD_ID" DISCORD_CHANNEL_ID="$DISCORD_CHANNEL_ID" "$(discord_python_bin)" - <<'PY'
import json
import os
print(
json.dumps(
{
os.environ["DISCORD_GUILD_ID"]: {
"channels": {
os.environ["DISCORD_CHANNEL_ID"]: {
"allow": True,
"requireMention": False,
}
}
}
}
)
)
PY
}
discord_message_id_from_send_log() {
local path="$1"
"$(discord_python_bin)" - "$path" <<'PY'
import json
import pathlib
import sys
payload = json.loads(pathlib.Path(sys.argv[1]).read_text())
message_id = payload.get("payload", {}).get("messageId")
if not message_id:
message_id = payload.get("payload", {}).get("result", {}).get("messageId")
if not message_id:
raise SystemExit("messageId missing from send output")
print(message_id)
PY
}
wait_for_discord_host_visibility() {
local nonce="$1"
local timeout_s="${2:-${TIMEOUT_DISCORD_S:-180}}"
local response
local deadline=$((SECONDS + timeout_s))
while (( SECONDS < deadline )); do
set +e
response="$(discord_api_request GET "/channels/$DISCORD_CHANNEL_ID/messages?limit=20")"
local rc=$?
set -e
if [[ $rc -eq 0 ]] && [[ -n "$response" ]] && printf '%s' "$response" | json_contains_string "$nonce"; then
return 0
fi
sleep 2
done
return 1
}
post_host_discord_message() {
local nonce="$1"
local prefix_or_id_file="$2"
local maybe_id_file="${3:-}"
local prefix id_file payload response
if [[ -n "$maybe_id_file" ]]; then
prefix="$prefix_or_id_file"
id_file="$maybe_id_file"
else
prefix="parallels-smoke"
id_file="$prefix_or_id_file"
fi
payload="$(
NONCE="$nonce" PREFIX="$prefix" "$(discord_python_bin)" - <<'PY'
import json
import os
print(
json.dumps(
{
"content": f"{os.environ['PREFIX']}-inbound-{os.environ['NONCE']}",
"flags": 4096,
}
)
)
PY
)"
response="$(discord_api_request POST "/channels/$DISCORD_CHANNEL_ID/messages" "$payload")"
printf '%s' "$response" | "$(discord_python_bin)" - "$id_file" <<'PY'
import json
import pathlib
import sys
payload = json.load(sys.stdin)
message_id = payload.get("id")
if not isinstance(message_id, str) or not message_id:
raise SystemExit("host Discord post missing message id")
pathlib.Path(sys.argv[1]).write_text(f"{message_id}\n", encoding="utf-8")
PY
}
discord_delete_message_id_file() {
local path="$1"
[[ -f "$path" ]] || return 0
[[ -s "$path" ]] || return 0
discord_smoke_enabled || return 0
local message_id
message_id="$(tr -d '\r\n' <"$path")"
[[ -n "$message_id" ]] || return 0
set +e
discord_api_request DELETE "/channels/$DISCORD_CHANNEL_ID/messages/$message_id" >/dev/null
set -e
}
cleanup_discord_message_files() {
local path
discord_smoke_enabled || return 0
for path in "$@"; do
discord_delete_message_id_file "$path"
done
}

View File

@@ -0,0 +1,49 @@
#!/usr/bin/env bash
parallels_macos_permission_check_snippet() {
cat <<'EOF'
root="$("/opt/homebrew/bin/npm" root -g)"
check_path() {
local path="$1"
[ -e "$path" ] || return 0
local perm perm_oct
perm="$(/usr/bin/stat -f '%OLp' "$path")"
perm_oct=$((8#$perm))
if (( perm_oct & 0002 )); then
echo "world-writable install artifact: $path ($perm)" >&2
exit 1
fi
}
check_path "$root/openclaw"
check_path "$root/openclaw/extensions"
if [ -d "$root/openclaw/extensions" ]; then
while IFS= read -r -d '' extension_dir; do
check_path "$extension_dir"
done < <(/usr/bin/find "$root/openclaw/extensions" -mindepth 1 -maxdepth 1 -type d -print0)
fi
EOF
}
parallels_linux_permission_check_snippet() {
cat <<'EOF'
root="$(npm root -g)"
check_path() {
local path="$1"
[ -e "$path" ] || return 0
local perm perm_oct
perm="$(stat -c '%a' "$path")"
perm_oct=$((8#$perm))
if (( perm_oct & 0002 )); then
echo "world-writable install artifact: $path ($perm)" >&2
exit 1
fi
}
check_path "$root/openclaw"
check_path "$root/openclaw/extensions"
if [ -d "$root/openclaw/extensions" ]; then
while IFS= read -r -d '' extension_dir; do
check_path "$extension_dir"
done < <(find "$root/openclaw/extensions" -mindepth 1 -maxdepth 1 -type d -print0)
fi
EOF
}

View File

@@ -0,0 +1,215 @@
#!/usr/bin/env bash
parallels_seed_fresh_child_summary() {
local prefix="$1"
local discord_status="skip"
if discord_smoke_enabled; then
discord_status="fail"
fi
eval "${prefix}_GATEWAY_STATUS='fail'"
eval "${prefix}_PERMISSION_STATUS='fail'"
eval "${prefix}_CHANNELS_STATUS='fail'"
eval "${prefix}_DASHBOARD_STATUS='fail'"
eval "${prefix}_AGENT_STATUS='fail'"
eval "${prefix}_DISCORD_STATUS='${discord_status}'"
}
parallels_load_fresh_child_summary() {
local prefix="$1"
local log_path="$2"
[[ -f "$log_path" ]] || return 0
local assignments
set +e
assignments="$(
PREFIX="$prefix" "$PYTHON_BIN" - "$log_path" <<'PY'
import json
import os
import pathlib
import shlex
import sys
prefix = os.environ["PREFIX"]
text = pathlib.Path(sys.argv[1]).read_text(encoding="utf-8", errors="replace").strip()
if not text:
raise SystemExit(0)
try:
payload = json.loads(text)
except Exception:
decoder = json.JSONDecoder()
payload = None
for index, char in enumerate(text):
if char != "{":
continue
try:
candidate, _ = decoder.raw_decode(text[index:])
except Exception:
continue
if isinstance(candidate, dict) and isinstance(candidate.get("freshMain"), dict):
payload = candidate
if payload is None:
raise SystemExit(0)
fresh = payload.get("freshMain")
if not isinstance(fresh, dict):
raise SystemExit(0)
field_map = {
"STATUS": "status",
"VERSION": "version",
"GATEWAY_STATUS": "gateway",
"PERMISSION_STATUS": "permissions",
"CHANNELS_STATUS": "channels",
"DASHBOARD_STATUS": "dashboard",
"AGENT_STATUS": "agent",
"DISCORD_STATUS": "discord",
}
for key, source_key in field_map.items():
value = fresh.get(source_key)
if isinstance(value, str):
print(f"{prefix}_{key}={shlex.quote(value)}")
PY
)"
local rc=$?
set -e
if [[ $rc -eq 0 && -n "$assignments" ]]; then
eval "$assignments"
fi
}
parallels_backfill_fresh_child_summary_from_log() {
local prefix="$1"
local log_path="$2"
[[ -f "$log_path" ]] || return 0
local assignments
set +e
assignments="$(
PREFIX="$prefix" "$PYTHON_BIN" - "$log_path" <<'PY'
import os
import pathlib
import shlex
import sys
prefix = os.environ["PREFIX"]
text = pathlib.Path(sys.argv[1]).read_text(encoding="utf-8", errors="replace")
field_map = {
"GATEWAY_STATUS": "fresh.gateway.ok",
"PERMISSION_STATUS": "fresh.permissions.ok",
"CHANNELS_STATUS": "fresh.channels.ok",
"DASHBOARD_STATUS": "fresh.dashboard.ok",
"AGENT_STATUS": "fresh.agent.ok",
"DISCORD_STATUS": "fresh.discord.ok",
}
for key, marker in field_map.items():
if f"==> {marker}" in text:
print(f"{prefix}_{key}={shlex.quote('pass')}")
PY
)"
local rc=$?
set -e
if [[ $rc -eq 0 && -n "$assignments" ]]; then
eval "$assignments"
fi
}
parallels_update_status_path() {
local run_dir="$1"
local os_name="$2"
printf '%s/%s-update-status.json\n' "$run_dir" "$os_name"
}
parallels_write_update_status_summary() {
local run_dir="$1"
local os_name="$2"
local gateway_status="$3"
local permission_status="$4"
local channels_status="$5"
local dashboard_status="$6"
local agent_status="$7"
local discord_status="$8"
local status_path
status_path="$(parallels_update_status_path "$run_dir" "$os_name")"
UPDATE_STATUS_PATH="$status_path" \
UPDATE_GATEWAY_STATUS="$gateway_status" \
UPDATE_PERMISSION_STATUS="$permission_status" \
UPDATE_CHANNELS_STATUS="$channels_status" \
UPDATE_DASHBOARD_STATUS="$dashboard_status" \
UPDATE_AGENT_STATUS="$agent_status" \
UPDATE_DISCORD_STATUS="$discord_status" \
"$PYTHON_BIN" - <<'PY'
import json
import os
payload = {
"gateway": os.environ["UPDATE_GATEWAY_STATUS"],
"permissions": os.environ["UPDATE_PERMISSION_STATUS"],
"channels": os.environ["UPDATE_CHANNELS_STATUS"],
"dashboard": os.environ["UPDATE_DASHBOARD_STATUS"],
"agent": os.environ["UPDATE_AGENT_STATUS"],
"discord": os.environ["UPDATE_DISCORD_STATUS"],
}
with open(os.environ["UPDATE_STATUS_PATH"], "w", encoding="utf-8") as handle:
json.dump(payload, handle, indent=2, sort_keys=True)
PY
}
parallels_seed_update_status_summary() {
local run_dir="$1"
local os_name="$2"
local discord_status="skip"
if discord_smoke_enabled; then
discord_status="fail"
fi
parallels_write_update_status_summary "$run_dir" "$os_name" "fail" "fail" "fail" "fail" "fail" "$discord_status"
}
parallels_load_update_status_summary() {
local prefix="$1"
local status_path="$2"
[[ -f "$status_path" ]] || return 0
local assignments
set +e
assignments="$(
PREFIX="$prefix" "$PYTHON_BIN" - "$status_path" <<'PY'
import json
import os
import pathlib
import shlex
import sys
prefix = os.environ["PREFIX"]
text = pathlib.Path(sys.argv[1]).read_text(encoding="utf-8", errors="replace").strip()
if not text:
raise SystemExit(0)
try:
payload = json.loads(text)
except Exception:
raise SystemExit(0)
field_map = {
"GATEWAY_STATUS": "gateway",
"PERMISSION_STATUS": "permissions",
"CHANNELS_STATUS": "channels",
"DASHBOARD_STATUS": "dashboard",
"AGENT_STATUS": "agent",
"DISCORD_STATUS": "discord",
}
for key, source_key in field_map.items():
value = payload.get(source_key)
if isinstance(value, str):
print(f"{prefix}_{key}={shlex.quote(value)}")
PY
)"
local rc=$?
set -e
if [[ $rc -eq 0 && -n "$assignments" ]]; then
eval "$assignments"
fi
}

View File

@@ -0,0 +1,59 @@
#!/usr/bin/env bash
parallels_windows_permission_helpers_ps() {
cat <<'EOF'
function Assert-NonBroadWritableInstall {
$broadSidValues = @(
([Security.Principal.SecurityIdentifier]::new([Security.Principal.WellKnownSidType]::WorldSid, $null)).Value,
([Security.Principal.SecurityIdentifier]::new([Security.Principal.WellKnownSidType]::BuiltinUsersSid, $null)).Value,
([Security.Principal.SecurityIdentifier]::new([Security.Principal.WellKnownSidType]::AuthenticatedUserSid, $null)).Value
)
$writeMask =
[Security.AccessControl.FileSystemRights]::Write -bor
[Security.AccessControl.FileSystemRights]::Modify -bor
[Security.AccessControl.FileSystemRights]::FullControl -bor
[Security.AccessControl.FileSystemRights]::CreateFiles -bor
[Security.AccessControl.FileSystemRights]::CreateDirectories -bor
[Security.AccessControl.FileSystemRights]::AppendData -bor
[Security.AccessControl.FileSystemRights]::WriteAttributes -bor
[Security.AccessControl.FileSystemRights]::WriteExtendedAttributes -bor
[Security.AccessControl.FileSystemRights]::Delete -bor
[Security.AccessControl.FileSystemRights]::DeleteSubdirectoriesAndFiles -bor
[Security.AccessControl.FileSystemRights]::ChangePermissions -bor
[Security.AccessControl.FileSystemRights]::TakeOwnership
function Assert-NonBroadWritablePath {
param([Parameter(Mandatory = $true)][string]$Path)
if (-not (Test-Path -LiteralPath $Path)) {
return
}
$acl = Get-Acl -LiteralPath $Path
foreach ($rule in $acl.Access) {
if ($rule.AccessControlType -ne [Security.AccessControl.AccessControlType]::Allow) {
continue
}
try {
$sid = $rule.IdentityReference.Translate([Security.Principal.SecurityIdentifier]).Value
} catch {
continue
}
if ($broadSidValues -notcontains $sid) {
continue
}
if (($rule.FileSystemRights -band $writeMask) -ne 0) {
throw "broad writable install artifact: $Path ($($rule.IdentityReference.Value): $($rule.FileSystemRights))"
}
}
}
$root = (& npm.cmd root -g).Trim()
Assert-NonBroadWritablePath -Path (Join-Path $root 'openclaw')
Assert-NonBroadWritablePath -Path (Join-Path $root 'openclaw\extensions')
Get-ChildItem -LiteralPath (Join-Path $root 'openclaw\extensions') -Directory -ErrorAction SilentlyContinue |
ForEach-Object {
Assert-NonBroadWritablePath -Path $_.FullName
}
}
EOF
}

View File

@@ -1,6 +1,10 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
source "$ROOT_DIR/scripts/e2e/lib/parallels-discord-common.sh"
source "$ROOT_DIR/scripts/e2e/lib/parallels-permissions-common.sh"
VM_NAME="Ubuntu 24.04.3 ARM64"
VM_NAME_EXPLICIT=0
SNAPSHOT_HINT="fresh"
@@ -19,6 +23,11 @@ INSTALL_VERSION=""
TARGET_PACKAGE_SPEC=""
JSON_OUTPUT=0
KEEP_SERVER=0
CHECK_LATEST_REF=1
DISCORD_TOKEN_ENV=""
DISCORD_TOKEN_VALUE=""
DISCORD_GUILD_ID=""
DISCORD_CHANNEL_ID=""
SNAPSHOT_ID=""
SNAPSHOT_STATE=""
SNAPSHOT_NAME=""
@@ -34,19 +43,31 @@ TIMEOUT_SNAPSHOT_S=180
TIMEOUT_BOOTSTRAP_S=600
TIMEOUT_INSTALL_S=1200
TIMEOUT_VERIFY_S=90
TIMEOUT_PERMISSION_S=60
TIMEOUT_ONBOARD_S=180
TIMEOUT_AGENT_S=180
TIMEOUT_GATEWAY_S=90
TIMEOUT_DASHBOARD_S=60
TIMEOUT_DISCORD_S=180
FRESH_MAIN_STATUS="skip"
FRESH_MAIN_VERSION="skip"
FRESH_GATEWAY_STATUS="skip"
FRESH_PERMISSION_STATUS="skip"
FRESH_CHANNELS_STATUS="skip"
FRESH_AGENT_STATUS="skip"
FRESH_DASHBOARD_STATUS="skip"
FRESH_DISCORD_STATUS="skip"
UPGRADE_STATUS="skip"
UPGRADE_PRECHECK_STATUS="skip"
LATEST_INSTALLED_VERSION="skip"
UPGRADE_MAIN_VERSION="skip"
UPGRADE_GATEWAY_STATUS="skip"
UPGRADE_PERMISSION_STATUS="skip"
UPGRADE_CHANNELS_STATUS="skip"
UPGRADE_AGENT_STATUS="skip"
UPGRADE_DASHBOARD_STATUS="skip"
UPGRADE_DISCORD_STATUS="skip"
DAEMON_STATUS="systemd-user-unavailable"
say() {
@@ -75,6 +96,9 @@ die() {
}
cleanup() {
if command -v cleanup_discord_smoke_messages >/dev/null 2>&1; then
cleanup_discord_smoke_messages
fi
if [[ -n "${SERVER_PID:-}" ]]; then
kill "$SERVER_PID" >/dev/null 2>&1 || true
fi
@@ -110,6 +134,10 @@ Options:
--target-package-spec <npm-spec>
Install this npm package tarball instead of packing current main.
Example: openclaw@2026.3.13-beta.1
--skip-latest-ref-check Skip latest-release ref-mode precheck.
--discord-token-env <var> Host env var name for Discord bot token.
--discord-guild-id <id> Discord guild ID for smoke roundtrip.
--discord-channel-id <id> Discord channel ID for smoke roundtrip.
--keep-server Leave temp host HTTP server running.
--json Print machine-readable JSON summary.
-h, --help Show help.
@@ -167,6 +195,22 @@ while [[ $# -gt 0 ]]; do
TARGET_PACKAGE_SPEC="$2"
shift 2
;;
--skip-latest-ref-check)
CHECK_LATEST_REF=0
shift
;;
--discord-token-env)
DISCORD_TOKEN_ENV="$2"
shift 2
;;
--discord-guild-id)
DISCORD_GUILD_ID="$2"
shift 2
;;
--discord-channel-id)
DISCORD_CHANNEL_ID="$2"
shift 2
;;
--keep-server)
KEEP_SERVER=1
shift
@@ -219,6 +263,24 @@ esac
API_KEY_VALUE="${!API_KEY_ENV:-}"
[[ -n "$API_KEY_VALUE" ]] || die "$API_KEY_ENV is required"
if [[ -n "$DISCORD_TOKEN_ENV" || -n "$DISCORD_GUILD_ID" || -n "$DISCORD_CHANNEL_ID" ]]; then
[[ -n "$DISCORD_TOKEN_ENV" ]] || die "--discord-token-env is required when Discord smoke args are set"
[[ -n "$DISCORD_GUILD_ID" ]] || die "--discord-guild-id is required when Discord smoke args are set"
[[ -n "$DISCORD_CHANNEL_ID" ]] || die "--discord-channel-id is required when Discord smoke args are set"
DISCORD_TOKEN_VALUE="${!DISCORD_TOKEN_ENV:-}"
[[ -n "$DISCORD_TOKEN_VALUE" ]] || die "$DISCORD_TOKEN_ENV is required for Discord smoke"
fi
cleanup_discord_smoke_messages() {
discord_smoke_enabled || return 0
[[ -d "$RUN_DIR" ]] || return 0
cleanup_discord_message_files \
"$RUN_DIR/fresh.discord-sent-message-id" \
"$RUN_DIR/fresh.discord-host-message-id" \
"$RUN_DIR/upgrade.discord-sent-message-id" \
"$RUN_DIR/upgrade.discord-host-message-id"
}
resolve_vm_name() {
local json requested explicit
json="$(prlctl list --all --json)"
@@ -596,6 +658,18 @@ install_main_tgz() {
guest_exec openclaw --version
}
run_main_package_update() {
local host_ip="$1"
local tgz_url="http://$host_ip:$HOST_PORT/$(basename "$MAIN_TGZ_PATH")"
local status_json
guest_exec openclaw update --tag "$tgz_url" --yes --json
status_json="$(guest_exec openclaw update status --json)"
printf '%s\n' "$status_json"
printf '%s\n' "$status_json" | grep -F '"installKind": "package"'
printf '%s\n' "$status_json" | grep -F '"packageManager": "npm"'
guest_exec openclaw --version
}
verify_version_contains() {
local needle="$1"
local version
@@ -667,6 +741,127 @@ verify_local_turn() {
--json
}
verify_bundle_permissions() {
local cmd
cmd="$(cat <<EOF
set -eu
set -o pipefail
$(parallels_linux_permission_check_snippet)
EOF
)"
guest_exec bash -lc "$cmd"
}
verify_dashboard_load() {
local cmd
cmd="$(cat <<'EOF'
set -eu
deadline=$((SECONDS + 30))
dashboard_ready=0
while [ $SECONDS -lt $deadline ]; do
if curl -fsSL --connect-timeout 2 --max-time 5 http://127.0.0.1:18789/ >/tmp/openclaw-dashboard-smoke.html 2>/dev/null; then
if grep -F '<title>OpenClaw Control</title>' /tmp/openclaw-dashboard-smoke.html >/dev/null; then
if grep -F '<openclaw-app></openclaw-app>' /tmp/openclaw-dashboard-smoke.html >/dev/null; then
dashboard_ready=1
break
fi
fi
fi
sleep 1
done
[ "$dashboard_ready" = "1" ] || {
echo "dashboard HTML did not become ready at http://127.0.0.1:18789/" >&2
exit 1
}
EOF
)"
guest_exec bash -lc "$cmd"
}
verify_channels_probe() {
guest_exec openclaw channels status --probe --json
}
configure_discord_smoke() {
local guilds_json cmd
guilds_json="$(build_discord_guilds_json)"
cmd="$(cat <<EOF
cat >/tmp/openclaw-discord-token <<'__OPENCLAW_TOKEN__'
$DISCORD_TOKEN_VALUE
__OPENCLAW_TOKEN__
cat >/tmp/openclaw-discord-guilds.json <<'__OPENCLAW_GUILDS__'
$guilds_json
__OPENCLAW_GUILDS__
token="\$(tr -d '\n' </tmp/openclaw-discord-token)"
guilds_json="\$(cat /tmp/openclaw-discord-guilds.json)"
openclaw config set channels.discord.token "\$token"
openclaw config set channels.discord.enabled true
openclaw config set channels.discord.groupPolicy allowlist
openclaw config set channels.discord.guilds "\$guilds_json" --strict-json
rm -f /tmp/openclaw-discord-token /tmp/openclaw-discord-guilds.json
EOF
)"
guest_exec bash -lc "$cmd"
start_gateway_background
show_gateway_status_compat
guest_exec openclaw channels status --probe --json
}
wait_for_guest_discord_readback() {
local nonce="$1"
local response rc
local last_response_path="$RUN_DIR/discord-last-readback.json"
local deadline=$((SECONDS + TIMEOUT_DISCORD_S))
while (( SECONDS < deadline )); do
set +e
response="$(
guest_exec \
openclaw \
message read \
--channel discord \
--target "channel:$DISCORD_CHANNEL_ID" \
--limit 20 \
--json
)"
rc=$?
set -e
if [[ -n "$response" ]]; then
printf '%s' "$response" >"$last_response_path"
fi
if [[ $rc -eq 0 ]] && [[ -n "$response" ]] && printf '%s' "$response" | json_contains_string "$nonce"; then
return 0
fi
sleep 3
done
return 1
}
run_discord_roundtrip_smoke() {
local phase="$1"
local nonce outbound_nonce inbound_nonce outbound_message outbound_log sent_id_file host_id_file
nonce="$(date +%s)-$RANDOM"
outbound_nonce="$phase-out-$nonce"
inbound_nonce="$phase-in-$nonce"
outbound_message="parallels-linux-smoke-outbound-$outbound_nonce"
outbound_log="$RUN_DIR/$phase.discord-send.json"
sent_id_file="$RUN_DIR/$phase.discord-sent-message-id"
host_id_file="$RUN_DIR/$phase.discord-host-message-id"
guest_exec \
openclaw \
message send \
--channel discord \
--target "channel:$DISCORD_CHANNEL_ID" \
--message "$outbound_message" \
--silent \
--json >"$outbound_log"
discord_message_id_from_send_log "$outbound_log" >"$sent_id_file"
wait_for_discord_host_visibility "$outbound_nonce"
post_host_discord_message "$inbound_nonce" "$host_id_file"
wait_for_guest_discord_readback "$inbound_nonce"
}
phase_log_path() {
printf '%s/%s.log\n' "$RUN_DIR" "$1"
}
@@ -761,14 +956,23 @@ summary = {
"status": os.environ["SUMMARY_FRESH_MAIN_STATUS"],
"version": os.environ["SUMMARY_FRESH_MAIN_VERSION"],
"gateway": os.environ["SUMMARY_FRESH_GATEWAY_STATUS"],
"permissions": os.environ["SUMMARY_FRESH_PERMISSION_STATUS"],
"channels": os.environ["SUMMARY_FRESH_CHANNELS_STATUS"],
"agent": os.environ["SUMMARY_FRESH_AGENT_STATUS"],
"dashboard": os.environ["SUMMARY_FRESH_DASHBOARD_STATUS"],
"discord": os.environ["SUMMARY_FRESH_DISCORD_STATUS"],
},
"upgrade": {
"precheck": os.environ["SUMMARY_UPGRADE_PRECHECK_STATUS"],
"status": os.environ["SUMMARY_UPGRADE_STATUS"],
"latestVersionInstalled": os.environ["SUMMARY_LATEST_INSTALLED_VERSION"],
"mainVersion": os.environ["SUMMARY_UPGRADE_MAIN_VERSION"],
"gateway": os.environ["SUMMARY_UPGRADE_GATEWAY_STATUS"],
"permissions": os.environ["SUMMARY_UPGRADE_PERMISSION_STATUS"],
"channels": os.environ["SUMMARY_UPGRADE_CHANNELS_STATUS"],
"agent": os.environ["SUMMARY_UPGRADE_AGENT_STATUS"],
"dashboard": os.environ["SUMMARY_UPGRADE_DASHBOARD_STATUS"],
"discord": os.environ["SUMMARY_UPGRADE_DISCORD_STATUS"],
},
}
with open(sys.argv[1], "w", encoding="utf-8") as handle:
@@ -786,12 +990,51 @@ run_fresh_main_lane() {
phase_run "fresh.install-main" "$TIMEOUT_INSTALL_S" install_main_tgz "$host_ip" "openclaw-main-fresh.tgz"
FRESH_MAIN_VERSION="$(extract_last_version "$(phase_log_path fresh.install-main)")"
phase_run "fresh.verify-main-version" "$TIMEOUT_VERIFY_S" verify_target_version
FRESH_PERMISSION_STATUS="fail"
phase_run "fresh.verify-bundle-permissions" "$TIMEOUT_PERMISSION_S" verify_bundle_permissions
FRESH_PERMISSION_STATUS="pass"
say "fresh.permissions.ok"
phase_run "fresh.onboard-ref" "$TIMEOUT_ONBOARD_S" run_ref_onboard
phase_run "fresh.gateway-start" "$TIMEOUT_GATEWAY_S" start_gateway_background
FRESH_GATEWAY_STATUS="fail"
phase_run "fresh.gateway-status" "$TIMEOUT_VERIFY_S" show_gateway_status_compat
FRESH_GATEWAY_STATUS="pass"
say "fresh.gateway.ok"
FRESH_CHANNELS_STATUS="fail"
phase_run "fresh.channels-status" "$TIMEOUT_VERIFY_S" verify_channels_probe
FRESH_CHANNELS_STATUS="pass"
say "fresh.channels.ok"
FRESH_DASHBOARD_STATUS="fail"
phase_run "fresh.dashboard-load" "$TIMEOUT_DASHBOARD_S" verify_dashboard_load
FRESH_DASHBOARD_STATUS="pass"
say "fresh.dashboard.ok"
FRESH_AGENT_STATUS="fail"
phase_run "fresh.first-local-agent-turn" "$TIMEOUT_AGENT_S" verify_local_turn
FRESH_AGENT_STATUS="pass"
say "fresh.agent.ok"
if discord_smoke_enabled; then
FRESH_DISCORD_STATUS="fail"
phase_run "fresh.discord-config" "$TIMEOUT_GATEWAY_S" configure_discord_smoke
phase_run "fresh.discord-roundtrip" "$TIMEOUT_DISCORD_S" run_discord_roundtrip_smoke "fresh"
FRESH_DISCORD_STATUS="pass"
say "fresh.discord.ok"
fi
}
capture_latest_ref_failure() {
set +e
run_ref_onboard
local rc=$?
set -e
if [[ $rc -eq 0 ]]; then
say "Latest release ref-mode onboard passed"
return 0
fi
warn "Latest release ref-mode onboard failed pre-upgrade"
set +e
show_gateway_status_compat || true
set -e
return 1
}
run_upgrade_lane() {
@@ -802,15 +1045,47 @@ run_upgrade_lane() {
phase_run "upgrade.install-latest" "$TIMEOUT_INSTALL_S" install_latest_release
LATEST_INSTALLED_VERSION="$(extract_last_version "$(phase_log_path upgrade.install-latest)")"
phase_run "upgrade.verify-latest-version" "$TIMEOUT_VERIFY_S" verify_version_contains "$LATEST_VERSION"
phase_run "upgrade.install-main" "$TIMEOUT_INSTALL_S" install_main_tgz "$host_ip" "openclaw-main-upgrade.tgz"
UPGRADE_MAIN_VERSION="$(extract_last_version "$(phase_log_path upgrade.install-main)")"
if [[ "$CHECK_LATEST_REF" -eq 1 ]]; then
if phase_run "upgrade.latest-ref-precheck" "$TIMEOUT_ONBOARD_S" capture_latest_ref_failure; then
UPGRADE_PRECHECK_STATUS="latest-ref-pass"
else
UPGRADE_PRECHECK_STATUS="latest-ref-fail"
fi
else
UPGRADE_PRECHECK_STATUS="skipped"
fi
phase_run "upgrade.update-main" "$TIMEOUT_INSTALL_S" run_main_package_update "$host_ip"
UPGRADE_MAIN_VERSION="$(extract_last_version "$(phase_log_path upgrade.update-main)")"
phase_run "upgrade.verify-main-version" "$TIMEOUT_VERIFY_S" verify_target_version
UPGRADE_PERMISSION_STATUS="fail"
phase_run "upgrade.verify-bundle-permissions" "$TIMEOUT_PERMISSION_S" verify_bundle_permissions
UPGRADE_PERMISSION_STATUS="pass"
say "upgrade.permissions.ok"
phase_run "upgrade.onboard-ref" "$TIMEOUT_ONBOARD_S" run_ref_onboard
phase_run "upgrade.gateway-start" "$TIMEOUT_GATEWAY_S" start_gateway_background
UPGRADE_GATEWAY_STATUS="fail"
phase_run "upgrade.gateway-status" "$TIMEOUT_VERIFY_S" show_gateway_status_compat
UPGRADE_GATEWAY_STATUS="pass"
say "upgrade.gateway.ok"
UPGRADE_CHANNELS_STATUS="fail"
phase_run "upgrade.channels-status" "$TIMEOUT_VERIFY_S" verify_channels_probe
UPGRADE_CHANNELS_STATUS="pass"
say "upgrade.channels.ok"
UPGRADE_DASHBOARD_STATUS="fail"
phase_run "upgrade.dashboard-load" "$TIMEOUT_DASHBOARD_S" verify_dashboard_load
UPGRADE_DASHBOARD_STATUS="pass"
say "upgrade.dashboard.ok"
UPGRADE_AGENT_STATUS="fail"
phase_run "upgrade.first-local-agent-turn" "$TIMEOUT_AGENT_S" verify_local_turn
UPGRADE_AGENT_STATUS="pass"
say "upgrade.agent.ok"
if discord_smoke_enabled; then
UPGRADE_DISCORD_STATUS="fail"
phase_run "upgrade.discord-config" "$TIMEOUT_GATEWAY_S" configure_discord_smoke
phase_run "upgrade.discord-roundtrip" "$TIMEOUT_DISCORD_S" run_discord_roundtrip_smoke "upgrade"
UPGRADE_DISCORD_STATUS="pass"
say "upgrade.discord.ok"
fi
}
RESOLVED_VM_NAME="$(resolve_vm_name)"
@@ -880,12 +1155,21 @@ SUMMARY_JSON_PATH="$(
SUMMARY_FRESH_MAIN_STATUS="$FRESH_MAIN_STATUS" \
SUMMARY_FRESH_MAIN_VERSION="$FRESH_MAIN_VERSION" \
SUMMARY_FRESH_GATEWAY_STATUS="$FRESH_GATEWAY_STATUS" \
SUMMARY_FRESH_PERMISSION_STATUS="$FRESH_PERMISSION_STATUS" \
SUMMARY_FRESH_CHANNELS_STATUS="$FRESH_CHANNELS_STATUS" \
SUMMARY_FRESH_AGENT_STATUS="$FRESH_AGENT_STATUS" \
SUMMARY_FRESH_DASHBOARD_STATUS="$FRESH_DASHBOARD_STATUS" \
SUMMARY_FRESH_DISCORD_STATUS="$FRESH_DISCORD_STATUS" \
SUMMARY_UPGRADE_PRECHECK_STATUS="$UPGRADE_PRECHECK_STATUS" \
SUMMARY_UPGRADE_STATUS="$UPGRADE_STATUS" \
SUMMARY_LATEST_INSTALLED_VERSION="$LATEST_INSTALLED_VERSION" \
SUMMARY_UPGRADE_MAIN_VERSION="$UPGRADE_MAIN_VERSION" \
SUMMARY_UPGRADE_GATEWAY_STATUS="$UPGRADE_GATEWAY_STATUS" \
SUMMARY_UPGRADE_PERMISSION_STATUS="$UPGRADE_PERMISSION_STATUS" \
SUMMARY_UPGRADE_CHANNELS_STATUS="$UPGRADE_CHANNELS_STATUS" \
SUMMARY_UPGRADE_AGENT_STATUS="$UPGRADE_AGENT_STATUS" \
SUMMARY_UPGRADE_DASHBOARD_STATUS="$UPGRADE_DASHBOARD_STATUS" \
SUMMARY_UPGRADE_DISCORD_STATUS="$UPGRADE_DISCORD_STATUS" \
write_summary_json
)"
@@ -900,8 +1184,9 @@ else
printf ' baseline-install-version: %s\n' "$INSTALL_VERSION"
fi
printf ' daemon: %s\n' "$DAEMON_STATUS"
printf ' fresh-main: %s (%s)\n' "$FRESH_MAIN_STATUS" "$FRESH_MAIN_VERSION"
printf ' latest->main: %s (%s)\n' "$UPGRADE_STATUS" "$UPGRADE_MAIN_VERSION"
printf ' fresh-main: %s (%s) gateway=%s permissions=%s channels=%s dashboard=%s agent=%s discord=%s\n' "$FRESH_MAIN_STATUS" "$FRESH_MAIN_VERSION" "$FRESH_GATEWAY_STATUS" "$FRESH_PERMISSION_STATUS" "$FRESH_CHANNELS_STATUS" "$FRESH_DASHBOARD_STATUS" "$FRESH_AGENT_STATUS" "$FRESH_DISCORD_STATUS"
printf ' latest->main precheck: %s (%s)\n' "$UPGRADE_PRECHECK_STATUS" "$LATEST_INSTALLED_VERSION"
printf ' latest->main: %s (%s) gateway=%s permissions=%s channels=%s dashboard=%s agent=%s discord=%s\n' "$UPGRADE_STATUS" "$UPGRADE_MAIN_VERSION" "$UPGRADE_GATEWAY_STATUS" "$UPGRADE_PERMISSION_STATUS" "$UPGRADE_CHANNELS_STATUS" "$UPGRADE_DASHBOARD_STATUS" "$UPGRADE_AGENT_STATUS" "$UPGRADE_DISCORD_STATUS"
printf ' logs: %s\n' "$RUN_DIR"
printf ' summary: %s\n' "$SUMMARY_JSON_PATH"
fi

View File

@@ -3,6 +3,8 @@ set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
source "$ROOT_DIR/scripts/e2e/lib/parallels-macos-common.sh"
source "$ROOT_DIR/scripts/e2e/lib/parallels-discord-common.sh"
source "$ROOT_DIR/scripts/e2e/lib/parallels-permissions-common.sh"
VM_NAME="macOS Tahoe"
SNAPSHOT_HINT="macOS 26.3.1 latest"
@@ -63,6 +65,10 @@ LATEST_INSTALLED_VERSION="skip"
UPGRADE_MAIN_VERSION="skip"
FRESH_GATEWAY_STATUS="skip"
UPGRADE_GATEWAY_STATUS="skip"
FRESH_PERMISSION_STATUS="skip"
UPGRADE_PERMISSION_STATUS="skip"
FRESH_CHANNELS_STATUS="skip"
UPGRADE_CHANNELS_STATUS="skip"
FRESH_AGENT_STATUS="skip"
UPGRADE_AGENT_STATUS="skip"
FRESH_DASHBOARD_STATUS="skip"
@@ -320,72 +326,14 @@ upgrade_summary_label() {
printf 'latest->dev'
}
discord_api_request() {
local method="$1"
local path="$2"
local payload="${3:-}"
local url="https://discord.com/api/v10$path"
if [[ -n "$payload" ]]; then
curl -fsS -X "$method" \
-H "Authorization: Bot $DISCORD_TOKEN_VALUE" \
-H "Content-Type: application/json" \
--data "$payload" \
"$url"
return
fi
curl -fsS -X "$method" \
-H "Authorization: Bot $DISCORD_TOKEN_VALUE" \
"$url"
}
json_contains_string() {
local needle="$1"
python3 - "$needle" <<'PY'
import json
import sys
needle = sys.argv[1]
try:
payload = json.load(sys.stdin)
except Exception:
raise SystemExit(1)
def contains(value):
if isinstance(value, str):
return needle in value
if isinstance(value, list):
return any(contains(item) for item in value)
if isinstance(value, dict):
return any(contains(item) for item in value.values())
return False
raise SystemExit(0 if contains(payload) else 1)
PY
}
discord_delete_message_id_file() {
local path="$1"
[[ -f "$path" ]] || return 0
[[ -s "$path" ]] || return 0
discord_smoke_enabled || return 0
local message_id
message_id="$(tr -d '\r\n' <"$path")"
[[ -n "$message_id" ]] || return 0
set +e
discord_api_request DELETE "/channels/$DISCORD_CHANNEL_ID/messages/$message_id" >/dev/null
set -e
}
cleanup_discord_smoke_messages() {
discord_smoke_enabled || return 0
[[ -d "$RUN_DIR" ]] || return 0
discord_delete_message_id_file "$RUN_DIR/fresh.discord-sent-message-id"
discord_delete_message_id_file "$RUN_DIR/fresh.discord-host-message-id"
discord_delete_message_id_file "$RUN_DIR/upgrade.discord-sent-message-id"
discord_delete_message_id_file "$RUN_DIR/upgrade.discord-host-message-id"
cleanup_discord_message_files \
"$RUN_DIR/fresh.discord-sent-message-id" \
"$RUN_DIR/fresh.discord-host-message-id" \
"$RUN_DIR/upgrade.discord-sent-message-id" \
"$RUN_DIR/upgrade.discord-host-message-id"
}
resolve_snapshot_info() {
@@ -1228,25 +1176,7 @@ verify_bundle_permissions() {
cmd="$(cat <<EOF
set -eu
set -o pipefail
root=\$($npm_q root -g)
check_path() {
local path="\$1"
[ -e "\$path" ] || return 0
local perm perm_oct
perm=\$(/usr/bin/stat -f '%OLp' "\$path")
perm_oct=\$((8#\$perm))
if (( perm_oct & 0002 )); then
echo "world-writable install artifact: \$path (\$perm)" >&2
exit 1
fi
}
check_path "\$root/openclaw"
check_path "\$root/openclaw/extensions"
if [ -d "\$root/openclaw/extensions" ]; then
while IFS= read -r -d '' extension_dir; do
check_path "\$extension_dir"
done < <(/usr/bin/find "\$root/openclaw/extensions" -mindepth 1 -maxdepth 1 -type d -print0)
fi
$(parallels_macos_permission_check_snippet | sed "s|/opt/homebrew/bin/npm|$npm_q|g")
EOF
)"
guest_current_user_exec /bin/bash -lc "$cmd"
@@ -1321,6 +1251,10 @@ EOF
)"
}
verify_channels_probe() {
guest_current_user_exec "$GUEST_OPENCLAW_BIN" channels status --probe --json
}
resolve_dashboard_url() {
local dashboard_url
dashboard_url="$(
@@ -1406,27 +1340,7 @@ EOF
configure_discord_smoke() {
local guilds_json script
guilds_json="$(
DISCORD_GUILD_ID="$DISCORD_GUILD_ID" DISCORD_CHANNEL_ID="$DISCORD_CHANNEL_ID" python3 - <<'PY'
import json
import os
print(
json.dumps(
{
os.environ["DISCORD_GUILD_ID"]: {
"channels": {
os.environ["DISCORD_CHANNEL_ID"]: {
"allow": True,
"requireMention": False,
}
}
}
}
)
)
PY
)"
guilds_json="$(build_discord_guilds_json)"
script="$(cat <<EOF
cat >/tmp/openclaw-discord-token <<'__OPENCLAW_TOKEN__'
$DISCORD_TOKEN_VALUE
@@ -1454,73 +1368,6 @@ EOF
guest_current_user_sh "$script"
}
discord_message_id_from_send_log() {
local path="$1"
python3 - "$path" <<'PY'
import json
import pathlib
import sys
payload = json.loads(pathlib.Path(sys.argv[1]).read_text())
message_id = payload.get("payload", {}).get("messageId")
if not message_id:
message_id = payload.get("payload", {}).get("result", {}).get("messageId")
if not message_id:
raise SystemExit("messageId missing from send output")
print(message_id)
PY
}
wait_for_discord_host_visibility() {
local nonce="$1"
local response
local deadline=$((SECONDS + TIMEOUT_DISCORD_S))
while (( SECONDS < deadline )); do
set +e
response="$(discord_api_request GET "/channels/$DISCORD_CHANNEL_ID/messages?limit=20")"
local rc=$?
set -e
if [[ $rc -eq 0 ]] && [[ -n "$response" ]] && printf '%s' "$response" | json_contains_string "$nonce"; then
return 0
fi
sleep 2
done
return 1
}
post_host_discord_message() {
local nonce="$1"
local id_file="$2"
local payload response
payload="$(
NONCE="$nonce" python3 - <<'PY'
import json
import os
print(
json.dumps(
{
"content": f"parallels-macos-smoke-inbound-{os.environ['NONCE']}",
"flags": 4096,
}
)
)
PY
)"
response="$(discord_api_request POST "/channels/$DISCORD_CHANNEL_ID/messages" "$payload")"
printf '%s' "$response" | python3 - "$id_file" <<'PY'
import json
import pathlib
import sys
payload = json.load(sys.stdin)
message_id = payload.get("id")
if not isinstance(message_id, str) or not message_id:
raise SystemExit("host Discord post missing message id")
pathlib.Path(sys.argv[1]).write_text(f"{message_id}\n", encoding="utf-8")
PY
}
wait_for_guest_discord_readback() {
local nonce="$1"
local response rc
@@ -1679,6 +1526,8 @@ summary = {
"status": os.environ["SUMMARY_FRESH_MAIN_STATUS"],
"version": os.environ["SUMMARY_FRESH_MAIN_VERSION"],
"gateway": os.environ["SUMMARY_FRESH_GATEWAY_STATUS"],
"permissions": os.environ["SUMMARY_FRESH_PERMISSION_STATUS"],
"channels": os.environ["SUMMARY_FRESH_CHANNELS_STATUS"],
"agent": os.environ["SUMMARY_FRESH_AGENT_STATUS"],
"dashboard": os.environ["SUMMARY_FRESH_DASHBOARD_STATUS"],
"discord": os.environ["SUMMARY_FRESH_DISCORD_STATUS"],
@@ -1691,6 +1540,8 @@ summary = {
"devVersion": os.environ["SUMMARY_UPGRADE_MAIN_VERSION"],
"mainVersion": os.environ["SUMMARY_UPGRADE_MAIN_VERSION"],
"gateway": os.environ["SUMMARY_UPGRADE_GATEWAY_STATUS"],
"permissions": os.environ["SUMMARY_UPGRADE_PERMISSION_STATUS"],
"channels": os.environ["SUMMARY_UPGRADE_CHANNELS_STATUS"],
"agent": os.environ["SUMMARY_UPGRADE_AGENT_STATUS"],
"dashboard": os.environ["SUMMARY_UPGRADE_DASHBOARD_STATUS"],
"discord": os.environ["SUMMARY_UPGRADE_DISCORD_STATUS"],
@@ -1725,20 +1576,34 @@ run_fresh_main_lane() {
phase_run "fresh.install-main" "$(install_main_timeout)" install_main_tgz "$host_ip" "openclaw-main-fresh.tgz"
FRESH_MAIN_VERSION="$(extract_last_version "$(phase_log_path fresh.install-main)")"
phase_run "fresh.verify-main-version" "$TIMEOUT_VERIFY_S" verify_target_version
FRESH_PERMISSION_STATUS="fail"
phase_run "fresh.verify-bundle-permissions" "$TIMEOUT_PERMISSION_S" verify_bundle_permissions
FRESH_PERMISSION_STATUS="pass"
say "fresh.permissions.ok"
phase_run "fresh.onboard-ref" "$TIMEOUT_ONBOARD_S" run_ref_onboard
phase_run "fresh.gateway-start" "$TIMEOUT_GATEWAY_S" start_manual_gateway_if_needed
FRESH_GATEWAY_STATUS="fail"
phase_run "fresh.gateway-status" "$TIMEOUT_GATEWAY_S" verify_gateway
FRESH_GATEWAY_STATUS="pass"
say "fresh.gateway.ok"
FRESH_CHANNELS_STATUS="fail"
phase_run "fresh.channels-status" "$TIMEOUT_VERIFY_S" verify_channels_probe
FRESH_CHANNELS_STATUS="pass"
say "fresh.channels.ok"
FRESH_DASHBOARD_STATUS="fail"
phase_run "fresh.dashboard-load" "$TIMEOUT_DASHBOARD_S" verify_dashboard_load
FRESH_DASHBOARD_STATUS="pass"
say "fresh.dashboard.ok"
FRESH_AGENT_STATUS="fail"
phase_run "fresh.first-agent-turn" "$TIMEOUT_AGENT_S" verify_turn
FRESH_AGENT_STATUS="pass"
say "fresh.agent.ok"
if discord_smoke_enabled; then
FRESH_DISCORD_STATUS="fail"
phase_run "fresh.discord-config" "$TIMEOUT_GATEWAY_S" configure_discord_smoke
phase_run "fresh.discord-roundtrip" "$TIMEOUT_DISCORD_S" run_discord_roundtrip_smoke "fresh"
FRESH_DISCORD_STATUS="pass"
say "fresh.discord.ok"
fi
}
@@ -1759,28 +1624,46 @@ run_upgrade_lane() {
UPGRADE_PRECHECK_STATUS="skipped"
fi
if upgrade_uses_host_tgz; then
UPGRADE_PERMISSION_STATUS="fail"
phase_run "upgrade.install-main" "$(install_main_timeout)" install_main_tgz "$host_ip" "openclaw-main-upgrade.tgz"
UPGRADE_MAIN_VERSION="$(extract_last_version "$(phase_log_path upgrade.install-main)")"
phase_run "upgrade.verify-main-version" "$TIMEOUT_VERIFY_S" verify_target_version
phase_run "upgrade.verify-bundle-permissions" "$TIMEOUT_PERMISSION_S" verify_bundle_permissions
UPGRADE_PERMISSION_STATUS="pass"
say "upgrade.permissions.ok"
else
phase_run "upgrade.update-dev" "$TIMEOUT_UPDATE_DEV_S" run_dev_channel_update
UPGRADE_MAIN_VERSION="$(extract_last_version "$(phase_log_path upgrade.update-dev)")"
phase_run "upgrade.verify-dev-channel" "$TIMEOUT_VERIFY_S" verify_dev_channel_update
UPGRADE_PERMISSION_STATUS="fail"
phase_run "upgrade.verify-bundle-permissions" "$TIMEOUT_PERMISSION_S" verify_bundle_permissions
UPGRADE_PERMISSION_STATUS="pass"
say "upgrade.permissions.ok"
fi
phase_run "upgrade.onboard-ref" "$TIMEOUT_ONBOARD_S" run_ref_onboard
phase_run "upgrade.gateway-start" "$TIMEOUT_GATEWAY_S" start_manual_gateway_if_needed
UPGRADE_GATEWAY_STATUS="fail"
phase_run "upgrade.gateway-status" "$TIMEOUT_GATEWAY_S" verify_gateway
UPGRADE_GATEWAY_STATUS="pass"
say "upgrade.gateway.ok"
UPGRADE_CHANNELS_STATUS="fail"
phase_run "upgrade.channels-status" "$TIMEOUT_VERIFY_S" verify_channels_probe
UPGRADE_CHANNELS_STATUS="pass"
say "upgrade.channels.ok"
UPGRADE_DASHBOARD_STATUS="fail"
phase_run "upgrade.dashboard-load" "$TIMEOUT_DASHBOARD_S" verify_dashboard_load
UPGRADE_DASHBOARD_STATUS="pass"
say "upgrade.dashboard.ok"
UPGRADE_AGENT_STATUS="fail"
phase_run "upgrade.first-agent-turn" "$TIMEOUT_AGENT_S" verify_turn
UPGRADE_AGENT_STATUS="pass"
say "upgrade.agent.ok"
if discord_smoke_enabled; then
UPGRADE_DISCORD_STATUS="fail"
phase_run "upgrade.discord-config" "$TIMEOUT_GATEWAY_S" configure_discord_smoke
phase_run "upgrade.discord-roundtrip" "$TIMEOUT_DISCORD_S" run_discord_roundtrip_smoke "upgrade"
UPGRADE_DISCORD_STATUS="pass"
say "upgrade.discord.ok"
fi
}
@@ -1858,6 +1741,8 @@ SUMMARY_JSON_PATH="$(
SUMMARY_FRESH_MAIN_STATUS="$FRESH_MAIN_STATUS" \
SUMMARY_FRESH_MAIN_VERSION="$FRESH_MAIN_VERSION" \
SUMMARY_FRESH_GATEWAY_STATUS="$FRESH_GATEWAY_STATUS" \
SUMMARY_FRESH_PERMISSION_STATUS="$FRESH_PERMISSION_STATUS" \
SUMMARY_FRESH_CHANNELS_STATUS="$FRESH_CHANNELS_STATUS" \
SUMMARY_FRESH_AGENT_STATUS="$FRESH_AGENT_STATUS" \
SUMMARY_FRESH_DASHBOARD_STATUS="$FRESH_DASHBOARD_STATUS" \
SUMMARY_FRESH_DISCORD_STATUS="$FRESH_DISCORD_STATUS" \
@@ -1866,6 +1751,8 @@ SUMMARY_JSON_PATH="$(
SUMMARY_LATEST_INSTALLED_VERSION="$LATEST_INSTALLED_VERSION" \
SUMMARY_UPGRADE_MAIN_VERSION="$UPGRADE_MAIN_VERSION" \
SUMMARY_UPGRADE_GATEWAY_STATUS="$UPGRADE_GATEWAY_STATUS" \
SUMMARY_UPGRADE_PERMISSION_STATUS="$UPGRADE_PERMISSION_STATUS" \
SUMMARY_UPGRADE_CHANNELS_STATUS="$UPGRADE_CHANNELS_STATUS" \
SUMMARY_UPGRADE_AGENT_STATUS="$UPGRADE_AGENT_STATUS" \
SUMMARY_UPGRADE_DASHBOARD_STATUS="$UPGRADE_DASHBOARD_STATUS" \
SUMMARY_UPGRADE_DISCORD_STATUS="$UPGRADE_DISCORD_STATUS" \
@@ -1883,9 +1770,9 @@ else
if [[ -n "$INSTALL_VERSION" ]]; then
printf ' baseline-install-version: %s\n' "$INSTALL_VERSION"
fi
printf ' fresh-main: %s (%s) discord=%s\n' "$FRESH_MAIN_STATUS" "$FRESH_MAIN_VERSION" "$FRESH_DISCORD_STATUS"
printf ' fresh-main: %s (%s) gateway=%s permissions=%s channels=%s dashboard=%s agent=%s discord=%s\n' "$FRESH_MAIN_STATUS" "$FRESH_MAIN_VERSION" "$FRESH_GATEWAY_STATUS" "$FRESH_PERMISSION_STATUS" "$FRESH_CHANNELS_STATUS" "$FRESH_DASHBOARD_STATUS" "$FRESH_AGENT_STATUS" "$FRESH_DISCORD_STATUS"
printf ' latest precheck: %s (%s)\n' "$UPGRADE_PRECHECK_STATUS" "$LATEST_INSTALLED_VERSION"
printf ' %s: %s (%s) discord=%s\n' "$(upgrade_summary_label)" "$UPGRADE_STATUS" "$UPGRADE_MAIN_VERSION" "$UPGRADE_DISCORD_STATUS"
printf ' %s: %s (%s) gateway=%s permissions=%s channels=%s dashboard=%s agent=%s discord=%s\n' "$(upgrade_summary_label)" "$UPGRADE_STATUS" "$UPGRADE_MAIN_VERSION" "$UPGRADE_GATEWAY_STATUS" "$UPGRADE_PERMISSION_STATUS" "$UPGRADE_CHANNELS_STATUS" "$UPGRADE_DASHBOARD_STATUS" "$UPGRADE_AGENT_STATUS" "$UPGRADE_DISCORD_STATUS"
printf ' logs: %s\n' "$RUN_DIR"
printf ' summary: %s\n' "$SUMMARY_JSON_PATH"
fi

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,10 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
source "$ROOT_DIR/scripts/e2e/lib/parallels-discord-common.sh"
source "$ROOT_DIR/scripts/e2e/lib/parallels-windows-common.sh"
VM_NAME="Windows 11"
SNAPSHOT_HINT="pre-openclaw-native-e2e-2026-03-12"
MODE="both"
@@ -20,6 +24,10 @@ UPGRADE_FROM_PACKED_MAIN=0
JSON_OUTPUT=0
KEEP_SERVER=0
CHECK_LATEST_REF=1
DISCORD_TOKEN_ENV=""
DISCORD_TOKEN_VALUE=""
DISCORD_GUILD_ID=""
DISCORD_CHANNEL_ID=""
SNAPSHOT_ID=""
SNAPSHOT_STATE=""
SNAPSHOT_NAME=""
@@ -45,17 +53,27 @@ TIMEOUT_ONBOARD_S=240
TIMEOUT_ONBOARD_PHASE_S=$((TIMEOUT_ONBOARD_S + 60))
TIMEOUT_GATEWAY_S=120
TIMEOUT_AGENT_S=180
TIMEOUT_DASHBOARD_S=60
TIMEOUT_DISCORD_S=180
FRESH_MAIN_STATUS="skip"
FRESH_MAIN_VERSION="skip"
FRESH_GATEWAY_STATUS="skip"
FRESH_PERMISSION_STATUS="skip"
FRESH_CHANNELS_STATUS="skip"
FRESH_AGENT_STATUS="skip"
FRESH_DASHBOARD_STATUS="skip"
FRESH_DISCORD_STATUS="skip"
UPGRADE_STATUS="skip"
UPGRADE_PRECHECK_STATUS="skip"
LATEST_INSTALLED_VERSION="skip"
UPGRADE_MAIN_VERSION="skip"
UPGRADE_GATEWAY_STATUS="skip"
UPGRADE_PERMISSION_STATUS="skip"
UPGRADE_CHANNELS_STATUS="skip"
UPGRADE_AGENT_STATUS="skip"
UPGRADE_DASHBOARD_STATUS="skip"
UPGRADE_DISCORD_STATUS="skip"
say() {
printf '==> %s\n' "$*"
@@ -111,6 +129,9 @@ die() {
}
cleanup() {
if command -v cleanup_discord_smoke_messages >/dev/null 2>&1; then
cleanup_discord_smoke_messages
fi
if [[ -n "${SERVER_PID:-}" ]]; then
kill "$SERVER_PID" >/dev/null 2>&1 || true
fi
@@ -148,6 +169,9 @@ Options:
Example: openclaw@2026.3.13-beta.1
Default upgrade lane without this flag: latest/site installer -> dev channel update.
--skip-latest-ref-check Skip latest-release ref-mode precheck.
--discord-token-env <var> Host env var name for Discord bot token.
--discord-guild-id <id> Discord guild ID for smoke roundtrip.
--discord-channel-id <id> Discord channel ID for smoke roundtrip.
--keep-server Leave temp host HTTP server running.
--json Print machine-readable JSON summary.
-h, --help Show help.
@@ -208,6 +232,18 @@ while [[ $# -gt 0 ]]; do
TARGET_PACKAGE_SPEC="$2"
shift 2
;;
--discord-token-env)
DISCORD_TOKEN_ENV="$2"
shift 2
;;
--discord-guild-id)
DISCORD_GUILD_ID="$2"
shift 2
;;
--discord-channel-id)
DISCORD_CHANNEL_ID="$2"
shift 2
;;
--skip-latest-ref-check)
CHECK_LATEST_REF=0
shift
@@ -264,6 +300,14 @@ esac
API_KEY_VALUE="${!API_KEY_ENV:-}"
[[ -n "$API_KEY_VALUE" ]] || die "$API_KEY_ENV is required"
if [[ -n "$DISCORD_TOKEN_ENV" || -n "$DISCORD_GUILD_ID" || -n "$DISCORD_CHANNEL_ID" ]]; then
[[ -n "$DISCORD_TOKEN_ENV" ]] || die "--discord-token-env is required when Discord smoke args are set"
[[ -n "$DISCORD_GUILD_ID" ]] || die "--discord-guild-id is required when Discord smoke args are set"
[[ -n "$DISCORD_CHANNEL_ID" ]] || die "--discord-channel-id is required when Discord smoke args are set"
DISCORD_TOKEN_VALUE="${!DISCORD_TOKEN_ENV:-}"
[[ -n "$DISCORD_TOKEN_VALUE" ]] || die "$DISCORD_TOKEN_ENV is required for Discord smoke"
fi
ps_single_quote() {
printf "%s" "$1" | sed "s/'/''/g"
}
@@ -285,6 +329,16 @@ ps_array_literal() {
printf '@(%s)' "$joined"
}
cleanup_discord_smoke_messages() {
discord_smoke_enabled || return 0
[[ -d "$RUN_DIR" ]] || return 0
cleanup_discord_message_files \
"$RUN_DIR/fresh.discord-sent-message-id" \
"$RUN_DIR/fresh.discord-host-message-id" \
"$RUN_DIR/upgrade.discord-sent-message-id" \
"$RUN_DIR/upgrade.discord-host-message-id"
}
resolve_snapshot_info() {
local json hint
json="$(prlctl snapshot-list "$VM_NAME" --json)"
@@ -753,7 +807,11 @@ summary = {
"status": os.environ["SUMMARY_FRESH_MAIN_STATUS"],
"version": os.environ["SUMMARY_FRESH_MAIN_VERSION"],
"gateway": os.environ["SUMMARY_FRESH_GATEWAY_STATUS"],
"permissions": os.environ["SUMMARY_FRESH_PERMISSION_STATUS"],
"channels": os.environ["SUMMARY_FRESH_CHANNELS_STATUS"],
"agent": os.environ["SUMMARY_FRESH_AGENT_STATUS"],
"dashboard": os.environ["SUMMARY_FRESH_DASHBOARD_STATUS"],
"discord": os.environ["SUMMARY_FRESH_DISCORD_STATUS"],
},
"upgrade": {
"precheck": os.environ["SUMMARY_UPGRADE_PRECHECK_STATUS"],
@@ -761,7 +819,11 @@ summary = {
"latestVersionInstalled": os.environ["SUMMARY_LATEST_INSTALLED_VERSION"],
"mainVersion": os.environ["SUMMARY_UPGRADE_MAIN_VERSION"],
"gateway": os.environ["SUMMARY_UPGRADE_GATEWAY_STATUS"],
"permissions": os.environ["SUMMARY_UPGRADE_PERMISSION_STATUS"],
"channels": os.environ["SUMMARY_UPGRADE_CHANNELS_STATUS"],
"agent": os.environ["SUMMARY_UPGRADE_AGENT_STATUS"],
"dashboard": os.environ["SUMMARY_UPGRADE_DASHBOARD_STATUS"],
"discord": os.environ["SUMMARY_UPGRADE_DISCORD_STATUS"],
},
}
with open(sys.argv[1], "w", encoding="utf-8") as handle:
@@ -1933,6 +1995,14 @@ verify_version_contains() {
esac
}
verify_bundle_permissions() {
guest_powershell "$(cat <<EOF
$(parallels_windows_permission_helpers_ps)
Assert-NonBroadWritableInstall
EOF
)"
}
write_onboard_runner_script() {
WINDOWS_ONBOARD_SCRIPT_PATH="$MAIN_TGZ_DIR/openclaw-onboard-$PROVIDER.ps1"
cat >"$WINDOWS_ONBOARD_SCRIPT_PATH" <<EOF
@@ -2226,6 +2296,109 @@ verify_turn() {
agent --agent main --message "Reply with exact ASCII text OK only." --json
}
verify_channels_probe() {
guest_run_openclaw "" "" channels status --probe --json
}
verify_dashboard_load() {
guest_powershell "$(cat <<'EOF'
$dashboardUrl = 'http://127.0.0.1:18789/'
$deadline = [DateTime]::UtcNow.AddSeconds(30)
$dashboardReady = $false
while ([DateTime]::UtcNow -lt $deadline) {
try {
$response = Invoke-WebRequest -Uri $dashboardUrl -UseBasicParsing -TimeoutSec 5
$content = [string]$response.Content
if ($content.Contains('<title>OpenClaw Control</title>') -and $content.Contains('<openclaw-app></openclaw-app>')) {
$dashboardReady = $true
break
}
} catch {
}
Start-Sleep -Seconds 1
}
if (-not $dashboardReady) {
throw "dashboard HTML did not become ready at $dashboardUrl"
}
EOF
)"
}
configure_discord_smoke() {
local guilds_json restart_rc
guilds_json="$(build_discord_guilds_json)"
guest_run_openclaw "" "" config set channels.discord.token "$DISCORD_TOKEN_VALUE"
guest_run_openclaw "" "" config set channels.discord.enabled true
guest_run_openclaw "" "" config set channels.discord.groupPolicy allowlist
guest_run_openclaw "" "" config set channels.discord.guilds "$guilds_json" --strict-json
set +e
restart_gateway
restart_rc=$?
set -e
if [[ $restart_rc -ne 0 ]]; then
warn "discord config gateway restart returned rc=$restart_rc; relying on readiness probe"
fi
show_gateway_status_compat
guest_run_openclaw "" "" channels status --probe --json
}
wait_for_guest_discord_readback() {
local nonce="$1"
local response rc
local last_response_path="$RUN_DIR/discord-last-readback.json"
local deadline=$((SECONDS + TIMEOUT_DISCORD_S))
while (( SECONDS < deadline )); do
set +e
response="$(
guest_run_openclaw \
"" \
"" \
message read \
--channel discord \
--target "channel:$DISCORD_CHANNEL_ID" \
--limit 20 \
--json
)"
rc=$?
set -e
if [[ -n "$response" ]]; then
printf '%s' "$response" >"$last_response_path"
fi
if [[ $rc -eq 0 ]] && [[ -n "$response" ]] && printf '%s' "$response" | json_contains_string "$nonce"; then
return 0
fi
sleep 3
done
return 1
}
run_discord_roundtrip_smoke() {
local phase="$1"
local nonce outbound_nonce inbound_nonce outbound_message outbound_log sent_id_file host_id_file
nonce="$(date +%s)-$RANDOM"
outbound_nonce="$phase-out-$nonce"
inbound_nonce="$phase-in-$nonce"
outbound_message="parallels-windows-smoke-outbound-$outbound_nonce"
outbound_log="$RUN_DIR/$phase.discord-send.json"
sent_id_file="$RUN_DIR/$phase.discord-sent-message-id"
host_id_file="$RUN_DIR/$phase.discord-host-message-id"
guest_run_openclaw \
"" \
"" \
message send \
--channel discord \
--target "channel:$DISCORD_CHANNEL_ID" \
--message "$outbound_message" \
--silent \
--json >"$outbound_log"
discord_message_id_from_send_log "$outbound_log" >"$sent_id_file"
wait_for_discord_host_visibility "$outbound_nonce"
post_host_discord_message "$inbound_nonce" "$host_id_file"
wait_for_guest_discord_readback "$inbound_nonce"
}
capture_latest_ref_failure() {
set +e
run_ref_onboard
@@ -2261,11 +2434,34 @@ run_fresh_main_lane() {
fi
FRESH_MAIN_VERSION="$(extract_last_version "$(phase_log_path "$install_log_phase")")"
phase_run "fresh.verify-main-version" "$TIMEOUT_VERIFY_S" verify_target_version || return $?
FRESH_PERMISSION_STATUS="fail"
phase_run "fresh.verify-bundle-permissions" "$TIMEOUT_VERIFY_S" verify_bundle_permissions || return $?
FRESH_PERMISSION_STATUS="pass"
say "fresh.permissions.ok"
phase_run "fresh.onboard-ref" "$TIMEOUT_ONBOARD_PHASE_S" run_ref_onboard || return $?
FRESH_GATEWAY_STATUS="fail"
phase_run "fresh.gateway-status" "$TIMEOUT_GATEWAY_S" verify_gateway_reachable || return $?
FRESH_GATEWAY_STATUS="pass"
say "fresh.gateway.ok"
FRESH_CHANNELS_STATUS="fail"
phase_run "fresh.channels-status" "$TIMEOUT_VERIFY_S" verify_channels_probe || return $?
FRESH_CHANNELS_STATUS="pass"
say "fresh.channels.ok"
FRESH_DASHBOARD_STATUS="fail"
phase_run "fresh.dashboard-load" "$TIMEOUT_DASHBOARD_S" verify_dashboard_load || return $?
FRESH_DASHBOARD_STATUS="pass"
say "fresh.dashboard.ok"
FRESH_AGENT_STATUS="fail"
phase_run "fresh.first-agent-turn" "$TIMEOUT_AGENT_S" verify_turn || return $?
FRESH_AGENT_STATUS="pass"
say "fresh.agent.ok"
if discord_smoke_enabled; then
FRESH_DISCORD_STATUS="fail"
phase_run "fresh.discord-config" "$TIMEOUT_GATEWAY_S" configure_discord_smoke || return $?
phase_run "fresh.discord-roundtrip" "$TIMEOUT_DISCORD_S" run_discord_roundtrip_smoke "fresh" || return $?
FRESH_DISCORD_STATUS="pass"
say "fresh.discord.ok"
fi
}
run_upgrade_lane() {
@@ -2300,16 +2496,39 @@ run_upgrade_lane() {
phase_run "upgrade.update-dev" "$TIMEOUT_INSTALL_S" run_dev_channel_update "$host_ip" || return $?
UPGRADE_MAIN_VERSION="$(extract_last_version "$(phase_log_path upgrade.update-dev)")"
phase_run "upgrade.verify-dev-channel" "$TIMEOUT_VERIFY_S" verify_dev_channel_update || return $?
UPGRADE_PERMISSION_STATUS="fail"
phase_run "upgrade.verify-bundle-permissions" "$TIMEOUT_VERIFY_S" verify_bundle_permissions || return $?
UPGRADE_PERMISSION_STATUS="pass"
say "upgrade.permissions.ok"
# Stop the old managed gateway before ref-mode onboard rewrites config and
# gateway auth. Restarting first can leave the old token alive and make the
# onboard health probe fail against a stale daemon.
phase_run "upgrade.gateway-stop" "$TIMEOUT_GATEWAY_S" stop_gateway || return $?
phase_run "upgrade.onboard-ref" "$TIMEOUT_ONBOARD_PHASE_S" run_ref_onboard || return $?
phase_run "upgrade.gateway-restart" "$TIMEOUT_GATEWAY_S" restart_gateway || return $?
UPGRADE_GATEWAY_STATUS="fail"
phase_run "upgrade.gateway-status" "$TIMEOUT_GATEWAY_S" verify_gateway_reachable || return $?
UPGRADE_GATEWAY_STATUS="pass"
say "upgrade.gateway.ok"
UPGRADE_CHANNELS_STATUS="fail"
phase_run "upgrade.channels-status" "$TIMEOUT_VERIFY_S" verify_channels_probe || return $?
UPGRADE_CHANNELS_STATUS="pass"
say "upgrade.channels.ok"
UPGRADE_DASHBOARD_STATUS="fail"
phase_run "upgrade.dashboard-load" "$TIMEOUT_DASHBOARD_S" verify_dashboard_load || return $?
UPGRADE_DASHBOARD_STATUS="pass"
say "upgrade.dashboard.ok"
UPGRADE_AGENT_STATUS="fail"
phase_run "upgrade.first-agent-turn" "$TIMEOUT_AGENT_S" verify_turn || return $?
UPGRADE_AGENT_STATUS="pass"
say "upgrade.agent.ok"
if discord_smoke_enabled; then
UPGRADE_DISCORD_STATUS="fail"
phase_run "upgrade.discord-config" "$TIMEOUT_GATEWAY_S" configure_discord_smoke || return $?
phase_run "upgrade.discord-roundtrip" "$TIMEOUT_DISCORD_S" run_discord_roundtrip_smoke "upgrade" || return $?
UPGRADE_DISCORD_STATUS="pass"
say "upgrade.discord.ok"
fi
}
IFS=$'\t' read -r SNAPSHOT_ID SNAPSHOT_STATE SNAPSHOT_NAME <<<"$(resolve_snapshot_info)"
@@ -2325,6 +2544,11 @@ say "Resolved snapshot: $SNAPSHOT_NAME [$SNAPSHOT_STATE]"
say "Latest npm version: $LATEST_VERSION"
say "Current head: $(git rev-parse --short HEAD)"
say "Run logs: $RUN_DIR"
if discord_smoke_enabled; then
say "Discord smoke: guild=$DISCORD_GUILD_ID channel=$DISCORD_CHANNEL_ID"
else
say "Discord smoke: disabled"
fi
if needs_host_tgz; then
pack_main_tgz
@@ -2376,13 +2600,21 @@ SUMMARY_JSON_PATH="$(
SUMMARY_FRESH_MAIN_STATUS="$FRESH_MAIN_STATUS" \
SUMMARY_FRESH_MAIN_VERSION="$FRESH_MAIN_VERSION" \
SUMMARY_FRESH_GATEWAY_STATUS="$FRESH_GATEWAY_STATUS" \
SUMMARY_FRESH_PERMISSION_STATUS="$FRESH_PERMISSION_STATUS" \
SUMMARY_FRESH_CHANNELS_STATUS="$FRESH_CHANNELS_STATUS" \
SUMMARY_FRESH_AGENT_STATUS="$FRESH_AGENT_STATUS" \
SUMMARY_FRESH_DASHBOARD_STATUS="$FRESH_DASHBOARD_STATUS" \
SUMMARY_FRESH_DISCORD_STATUS="$FRESH_DISCORD_STATUS" \
SUMMARY_UPGRADE_PRECHECK_STATUS="$UPGRADE_PRECHECK_STATUS" \
SUMMARY_UPGRADE_STATUS="$UPGRADE_STATUS" \
SUMMARY_LATEST_INSTALLED_VERSION="$LATEST_INSTALLED_VERSION" \
SUMMARY_UPGRADE_MAIN_VERSION="$UPGRADE_MAIN_VERSION" \
SUMMARY_UPGRADE_GATEWAY_STATUS="$UPGRADE_GATEWAY_STATUS" \
SUMMARY_UPGRADE_PERMISSION_STATUS="$UPGRADE_PERMISSION_STATUS" \
SUMMARY_UPGRADE_CHANNELS_STATUS="$UPGRADE_CHANNELS_STATUS" \
SUMMARY_UPGRADE_AGENT_STATUS="$UPGRADE_AGENT_STATUS" \
SUMMARY_UPGRADE_DASHBOARD_STATUS="$UPGRADE_DASHBOARD_STATUS" \
SUMMARY_UPGRADE_DISCORD_STATUS="$UPGRADE_DISCORD_STATUS" \
write_summary_json
)"
@@ -2399,9 +2631,9 @@ else
if [[ -n "$INSTALL_VERSION" ]]; then
printf ' baseline-install-version: %s\n' "$INSTALL_VERSION"
fi
printf ' fresh-main: %s (%s)\n' "$FRESH_MAIN_STATUS" "$FRESH_MAIN_VERSION"
printf ' fresh-main: %s (%s) gateway=%s permissions=%s channels=%s dashboard=%s agent=%s discord=%s\n' "$FRESH_MAIN_STATUS" "$FRESH_MAIN_VERSION" "$FRESH_GATEWAY_STATUS" "$FRESH_PERMISSION_STATUS" "$FRESH_CHANNELS_STATUS" "$FRESH_DASHBOARD_STATUS" "$FRESH_AGENT_STATUS" "$FRESH_DISCORD_STATUS"
printf ' %s precheck: %s (%s)\n' "$(upgrade_summary_label)" "$UPGRADE_PRECHECK_STATUS" "$LATEST_INSTALLED_VERSION"
printf ' %s: %s (%s)\n' "$(upgrade_summary_label)" "$UPGRADE_STATUS" "$UPGRADE_MAIN_VERSION"
printf ' %s: %s (%s) gateway=%s permissions=%s channels=%s dashboard=%s agent=%s discord=%s\n' "$(upgrade_summary_label)" "$UPGRADE_STATUS" "$UPGRADE_MAIN_VERSION" "$UPGRADE_GATEWAY_STATUS" "$UPGRADE_PERMISSION_STATUS" "$UPGRADE_CHANNELS_STATUS" "$UPGRADE_DASHBOARD_STATUS" "$UPGRADE_AGENT_STATUS" "$UPGRADE_DISCORD_STATUS"
printf ' logs: %s\n' "$RUN_DIR"
printf ' summary: %s\n' "$SUMMARY_JSON_PATH"
fi