- edge-node-runtime-wire 계약 스키마 동기화 - edge-node-execution, provider-pool-config-refresh 스펙 갱신 - provider-resource-admission-ownership-alignment 마일스톤 상태 동기화 - control-plane-portal-ops phase 신규 추가 (multi-edge-operations) - e2e-smoke: 재연결 테스트 플로우, bind timeout, reconnect 설정 반영
836 lines
29 KiB
Bash
Executable file
836 lines
29 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
|
|
echo "[e2e] NOTE: auxiliary smoke only; completion requires scripts/dev/edge.sh + scripts/dev/node.sh user-flow verification."
|
|
|
|
if command -v shellcheck >/dev/null 2>&1; then
|
|
echo "[e2e] running shellcheck..."
|
|
shellcheck "$0" "$REPO_ROOT/scripts/dev/edge.sh" "$REPO_ROOT/scripts/dev/node.sh"
|
|
else
|
|
echo "[e2e] shellcheck not found, skipping"
|
|
fi
|
|
|
|
TMP_DIR=$(mktemp -d)
|
|
|
|
kill_process_tree() {
|
|
local pid="$1"
|
|
local child
|
|
|
|
if [ -z "$pid" ]; then
|
|
return 0
|
|
fi
|
|
|
|
if command -v pgrep >/dev/null 2>&1; then
|
|
while IFS= read -r child; do
|
|
if [ -n "$child" ]; then
|
|
kill_process_tree "$child"
|
|
fi
|
|
done <<EOF_CHILDREN
|
|
$(pgrep -P "$pid" 2>/dev/null || true)
|
|
EOF_CHILDREN
|
|
fi
|
|
|
|
kill "$pid" 2>/dev/null || true
|
|
}
|
|
|
|
cleanup() {
|
|
set +e
|
|
exec 3>&- 2>/dev/null || true
|
|
kill_process_tree "${NODE_PID:-}"
|
|
kill_process_tree "${EDGE_PID:-}"
|
|
rm -rf "$TMP_DIR" 2>/dev/null || true
|
|
}
|
|
|
|
trap cleanup EXIT
|
|
|
|
EDGE_CONFIG="$TMP_DIR/edge.yaml"
|
|
NODE_CONFIG="$TMP_DIR/node.yaml"
|
|
|
|
PORT=$((30000 + RANDOM % 10000))
|
|
BOOTSTRAP_PORT=$((20000 + RANDOM % 10000))
|
|
EDGE_METRICS_PORT=$((40000 + RANDOM % 10000))
|
|
NODE_METRICS_PORT=$((50000 + RANDOM % 10000))
|
|
PROFILE="${IOP_E2E_PROFILE:-mock}"
|
|
IDLE_SECONDS="${IOP_E2E_IDLE_SECONDS:-0}"
|
|
REGISTER_TIMEOUT="${IOP_E2E_REGISTER_TIMEOUT:-60}"
|
|
EVENT_TIMEOUT="${IOP_E2E_EVENT_TIMEOUT:-30}"
|
|
RUN_TIMEOUT="${IOP_E2E_RUN_TIMEOUT:-120}"
|
|
STATUS_TIMEOUT="${IOP_E2E_STATUS_TIMEOUT:-$RUN_TIMEOUT}"
|
|
COMMAND_SETTLE_SECONDS="${IOP_E2E_COMMAND_SETTLE_SECONDS:-0.2}"
|
|
EDGE_BIND_TIMEOUT="${IOP_E2E_BIND_TIMEOUT:-10}"
|
|
IS_PERSISTENT=0
|
|
IS_APP_SERVER=0
|
|
HAS_STATUS=0
|
|
EXPECT_TAIL_MESSAGES=0
|
|
PROMPT_TEMPLATE_FILE="${IOP_E2E_PROMPT_TEMPLATES:-$REPO_ROOT/scripts/fixtures/user-e2e-prompts.tsv}"
|
|
|
|
if ! [[ "$EDGE_BIND_TIMEOUT" =~ ^[1-9][0-9]*$ ]]; then
|
|
echo "[e2e] IOP_E2E_BIND_TIMEOUT must be a positive integer, got: $EDGE_BIND_TIMEOUT" >&2
|
|
exit 1
|
|
fi
|
|
|
|
prompt_template_count() {
|
|
awk -F'|' 'NF >= 3 && $1 !~ /^#/ { n++ } END { print n + 0 }' "$PROMPT_TEMPLATE_FILE"
|
|
}
|
|
|
|
select_prompt_template() {
|
|
local offset="$1"
|
|
local total="$2"
|
|
local idx
|
|
idx=$(( (PROMPT_TEMPLATE_BASE + offset - 1) % total + 1 ))
|
|
awk -F'|' -v idx="$idx" '
|
|
NF >= 3 && $1 !~ /^#/ {
|
|
n++
|
|
if (n == idx) {
|
|
print $1 "\t" $2 "\t" $3
|
|
exit
|
|
}
|
|
}
|
|
' "$PROMPT_TEMPLATE_FILE"
|
|
}
|
|
|
|
if [ ! -f "$PROMPT_TEMPLATE_FILE" ]; then
|
|
echo "[e2e] prompt template file not found: $PROMPT_TEMPLATE_FILE" >&2
|
|
exit 1
|
|
fi
|
|
PROMPT_TEMPLATE_TOTAL=$(prompt_template_count)
|
|
if [ "$PROMPT_TEMPLATE_TOTAL" -lt 3 ]; then
|
|
echo "[e2e] prompt template file needs at least 3 entries: $PROMPT_TEMPLATE_FILE" >&2
|
|
exit 1
|
|
fi
|
|
PROMPT_TEMPLATE_BASE="${IOP_E2E_PROMPT_INDEX:-$((RANDOM % PROMPT_TEMPLATE_TOTAL + 1))}"
|
|
IFS=$'\t' read -r FIRST_TEMPLATE FIRST_EXPECTED FIRST_PROMPT <<EOF_PROMPT
|
|
$(select_prompt_template 0 "$PROMPT_TEMPLATE_TOTAL")
|
|
EOF_PROMPT
|
|
IFS=$'\t' read -r SECOND_TEMPLATE SECOND_EXPECTED SECOND_PROMPT <<EOF_PROMPT
|
|
$(select_prompt_template 1 "$PROMPT_TEMPLATE_TOTAL")
|
|
EOF_PROMPT
|
|
IFS=$'\t' read -r BACKGROUND_TEMPLATE BACKGROUND_EXPECTED BACKGROUND_PROMPT <<EOF_PROMPT
|
|
$(select_prompt_template 2 "$PROMPT_TEMPLATE_TOTAL")
|
|
EOF_PROMPT
|
|
IFS=$'\t' read -r FOURTH_TEMPLATE FOURTH_EXPECTED FOURTH_PROMPT <<EOF_PROMPT
|
|
$(select_prompt_template 3 "$PROMPT_TEMPLATE_TOTAL")
|
|
EOF_PROMPT
|
|
FIRST_TAIL_EXPECTED="${FIRST_EXPECTED}_TAIL"
|
|
SECOND_TAIL_EXPECTED="${SECOND_EXPECTED}_TAIL"
|
|
BACKGROUND_TAIL_EXPECTED="${BACKGROUND_EXPECTED}_TAIL"
|
|
FOURTH_TAIL_EXPECTED="${FOURTH_EXPECTED}_TAIL"
|
|
echo "[e2e] prompt templates: first=$FIRST_TEMPLATE second=$SECOND_TEMPLATE background=$BACKGROUND_TEMPLATE fourth=$FOURTH_TEMPLATE base=$PROMPT_TEMPLATE_BASE"
|
|
|
|
if [ "$PROFILE" = "mock" ]; then
|
|
echo "[e2e] preparing honest mock smoke test (using scripted cli adapter)..."
|
|
IS_PERSISTENT=1
|
|
EXPECT_TAIL_MESSAGES=1
|
|
TARGET="fake-cli"
|
|
MOCK_CLI="$TMP_DIR/fake-cli.sh"
|
|
cat <<'EOF' > "$MOCK_CLI"
|
|
#!/usr/bin/env sh
|
|
while IFS= read -r line; do
|
|
token=$(printf '%s\n' "$line" | sed -nE 's/.*(IOP_E2E_[A-Z0-9_]+).*/\1/p' | head -n 1)
|
|
if [ -n "$token" ]; then
|
|
printf '%s\n%s_TAIL\n' "$token" "$token"
|
|
else
|
|
printf 'IOP_E2E_UNKNOWN\n'
|
|
fi
|
|
done
|
|
EOF
|
|
chmod +x "$MOCK_CLI"
|
|
cat <<EOF > "$EDGE_CONFIG"
|
|
server:
|
|
listen: "127.0.0.1:$PORT"
|
|
metrics:
|
|
port: $EDGE_METRICS_PORT
|
|
bootstrap:
|
|
listen: "127.0.0.1:$BOOTSTRAP_PORT"
|
|
artifact_dir: "$TMP_DIR/artifacts"
|
|
nodes:
|
|
- id: test-node
|
|
alias: test-node
|
|
token: test-token
|
|
adapters:
|
|
mock:
|
|
enabled: true
|
|
cli:
|
|
enabled: true
|
|
profiles:
|
|
fake-cli:
|
|
command: "$MOCK_CLI"
|
|
persistent: true
|
|
response_idle_timeout_ms: 1000
|
|
console:
|
|
adapter: cli
|
|
target: fake-cli
|
|
session_id: default
|
|
EOF
|
|
else
|
|
echo "[e2e] preparing real profile smoke test (profile: $PROFILE)..."
|
|
TARGET="$PROFILE"
|
|
PROFILE_BLOCK=$(awk -v profile="$PROFILE" '
|
|
$0 ~ "^[[:space:]]+"profile":" {
|
|
found=1;
|
|
print $0;
|
|
indent=index($0, profile) - 1;
|
|
next;
|
|
}
|
|
found {
|
|
if ($0 ~ "^[[:space:]]*$") { print $0; next; }
|
|
curr_indent=0;
|
|
while(curr_indent < length($0) && substr($0, curr_indent+1, 1) == " ") curr_indent++;
|
|
if(curr_indent > indent) {
|
|
print $0;
|
|
} else {
|
|
exit;
|
|
}
|
|
}
|
|
' "$REPO_ROOT/configs/edge.yaml")
|
|
|
|
if [ -z "$PROFILE_BLOCK" ]; then
|
|
echo "[e2e] BLOCKED: Profile '$PROFILE' not found in configs/edge.yaml"
|
|
exit 1
|
|
fi
|
|
|
|
if echo "$PROFILE_BLOCK" | grep -q "persistent:[[:space:]]*true"; then
|
|
IS_PERSISTENT=1
|
|
fi
|
|
# codex-app-server mode maintains a long-lived process per session — treat as persistent
|
|
# so /sessions and /terminate-session are verified.
|
|
if echo "$PROFILE_BLOCK" | grep -q 'mode:[[:space:]]*"codex-app-server"'; then
|
|
IS_PERSISTENT=1
|
|
IS_APP_SERVER=1
|
|
fi
|
|
|
|
# Simple check for status support based on command name
|
|
if echo "$PROFILE_BLOCK" | grep -i -E "antigravity|agy|claude|codex" >/dev/null; then
|
|
HAS_STATUS=1
|
|
fi
|
|
|
|
cat <<EOF > "$EDGE_CONFIG"
|
|
server:
|
|
listen: "127.0.0.1:$PORT"
|
|
metrics:
|
|
port: $EDGE_METRICS_PORT
|
|
bootstrap:
|
|
listen: "127.0.0.1:$BOOTSTRAP_PORT"
|
|
artifact_dir: "$TMP_DIR/artifacts"
|
|
nodes:
|
|
- id: test-node
|
|
alias: test-node
|
|
token: test-token
|
|
adapters:
|
|
cli:
|
|
enabled: true
|
|
profiles:
|
|
$PROFILE_BLOCK
|
|
console:
|
|
adapter: cli
|
|
target: $PROFILE
|
|
session_id: default
|
|
EOF
|
|
fi
|
|
|
|
cat <<EOF > "$NODE_CONFIG"
|
|
transport:
|
|
edge_addr: "127.0.0.1:$PORT"
|
|
token: test-token
|
|
reconnect:
|
|
interval_sec: 1
|
|
max_attempts: 0
|
|
metrics:
|
|
port: $NODE_METRICS_PORT
|
|
node:
|
|
id: test-node
|
|
alias: test-node
|
|
EOF
|
|
|
|
echo "[e2e] starting smoke test (profile: $PROFILE, port: $PORT, persistent: $IS_PERSISTENT, has_status: $HAS_STATUS)"
|
|
|
|
EDGE_OUT="$TMP_DIR/edge_out"
|
|
NODE_OUT="$TMP_DIR/node_out"
|
|
rm -f "$TMP_DIR/edge_fifo"
|
|
mkfifo "$TMP_DIR/edge_fifo"
|
|
|
|
IOP_EDGE_CONFIG="$EDGE_CONFIG" "$REPO_ROOT/scripts/dev/edge.sh" < "$TMP_DIR/edge_fifo" > "$EDGE_OUT" 2>&1 &
|
|
EDGE_PID=$!
|
|
exec 3> "$TMP_DIR/edge_fifo"
|
|
|
|
deadline=$((SECONDS + EDGE_BIND_TIMEOUT))
|
|
while ! timeout 1 bash -c 'cat < /dev/null > /dev/tcp/127.0.0.1/'"$PORT" 2>/dev/null; do
|
|
if (( SECONDS >= deadline )); then
|
|
echo "[e2e] edge failed to bind port $PORT"
|
|
cat "$EDGE_OUT"
|
|
exit 1
|
|
fi
|
|
sleep 0.5
|
|
done
|
|
|
|
IOP_NODE_CONFIG="$NODE_CONFIG" "$REPO_ROOT/scripts/dev/node.sh" > "$NODE_OUT" 2>&1 &
|
|
NODE_PID=$!
|
|
LAST_CMD_START_LINE=1
|
|
|
|
ensure_smoke_processes_running() {
|
|
local context="$1"
|
|
|
|
if ! kill -0 "$EDGE_PID" 2>/dev/null; then
|
|
echo "[e2e] edge exited while waiting for $context"
|
|
echo "=== EDGE OUTPUT ==="
|
|
cat "$EDGE_OUT"
|
|
exit 1
|
|
fi
|
|
if ! kill -0 "$NODE_PID" 2>/dev/null; then
|
|
echo "[e2e] node exited while waiting for $context"
|
|
echo "=== NODE OUTPUT ==="
|
|
cat "$NODE_OUT"
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
wait_for_node_registration() {
|
|
local deadline
|
|
deadline=$((SECONDS + REGISTER_TIMEOUT))
|
|
|
|
echo "[e2e] waiting for node registration (timeout: ${REGISTER_TIMEOUT}s)"
|
|
while ! grep -q '\[node0-evt\] connected reason="registered"' "$EDGE_OUT"; do
|
|
ensure_smoke_processes_running "node registration"
|
|
if (( SECONDS >= deadline )); then
|
|
echo "[e2e] node registration timed out"
|
|
echo "=== EDGE OUTPUT ==="
|
|
cat "$EDGE_OUT"
|
|
echo "=== NODE OUTPUT ==="
|
|
cat "$NODE_OUT"
|
|
exit 1
|
|
fi
|
|
sleep 0.5
|
|
done
|
|
}
|
|
|
|
wait_for_edge_pattern_since() {
|
|
local pattern="$1"
|
|
local start_line="$2"
|
|
local msg="$3"
|
|
local timeout="${4:-$EVENT_TIMEOUT}"
|
|
local deadline
|
|
local slice
|
|
|
|
deadline=$((SECONDS + timeout))
|
|
slice="$TMP_DIR/edge_wait_slice"
|
|
|
|
while true; do
|
|
tail -n +"$start_line" "$EDGE_OUT" > "$slice" || true
|
|
if grep -qE "$pattern" "$slice"; then
|
|
return 0
|
|
fi
|
|
ensure_smoke_processes_running "$msg"
|
|
if (( SECONDS >= deadline )); then
|
|
echo "[e2e] FAIL: timed out waiting for $msg (pattern: '$pattern', timeout: ${timeout}s)"
|
|
echo "=== EDGE OUTPUT ==="
|
|
cat "$EDGE_OUT"
|
|
echo "=== NODE OUTPUT ==="
|
|
cat "$NODE_OUT"
|
|
exit 1
|
|
fi
|
|
sleep 0.5
|
|
done
|
|
}
|
|
|
|
wait_for_edge_text_since() {
|
|
local text="$1"
|
|
local start_line="$2"
|
|
local msg="$3"
|
|
local timeout="${4:-$EVENT_TIMEOUT}"
|
|
local deadline
|
|
local slice
|
|
|
|
deadline=$((SECONDS + timeout))
|
|
slice="$TMP_DIR/edge_wait_slice"
|
|
|
|
while true; do
|
|
tail -n +"$start_line" "$EDGE_OUT" > "$slice" || true
|
|
if grep -qF "$text" "$slice"; then
|
|
return 0
|
|
fi
|
|
ensure_smoke_processes_running "$msg"
|
|
if (( SECONDS >= deadline )); then
|
|
echo "[e2e] FAIL: timed out waiting for $msg (text: '$text', timeout: ${timeout}s)"
|
|
echo "=== EDGE OUTPUT ==="
|
|
cat "$EDGE_OUT"
|
|
echo "=== NODE OUTPUT ==="
|
|
cat "$NODE_OUT"
|
|
exit 1
|
|
fi
|
|
sleep 0.5
|
|
done
|
|
}
|
|
|
|
send_cmd() {
|
|
LAST_CMD_START_LINE=$(($(wc -l < "$EDGE_OUT" | tr -d ' ') + 1))
|
|
echo "[e2e] > $1"
|
|
echo "$1" >&3
|
|
sleep "$COMMAND_SETTLE_SECONDS"
|
|
}
|
|
|
|
wait_for_node_registration
|
|
send_cmd "/nodes"
|
|
wait_for_edge_text_since "test-node (test-node)" "$LAST_CMD_START_LINE" "/nodes output"
|
|
send_cmd "/capabilities"
|
|
wait_for_edge_text_since "[node0-capabilities]" "$LAST_CMD_START_LINE" "/capabilities output"
|
|
wait_for_edge_text_since "targets = ${TARGET}" "$LAST_CMD_START_LINE" "/capabilities targets"
|
|
send_cmd "/transport"
|
|
wait_for_edge_text_since "[node0-transport]" "$LAST_CMD_START_LINE" "/transport output"
|
|
wait_for_edge_text_since "adapter = cli" "$LAST_CMD_START_LINE" "/transport adapter"
|
|
wait_for_edge_text_since "connected = true" "$LAST_CMD_START_LINE" "/transport connected status"
|
|
wait_for_edge_text_since "state = connected" "$LAST_CMD_START_LINE" "/transport state"
|
|
wait_for_edge_text_since "node_id = test-node" "$LAST_CMD_START_LINE" "/transport node id"
|
|
wait_for_edge_text_since "session_id = default" "$LAST_CMD_START_LINE" "/transport session id"
|
|
wait_for_edge_text_since "target = ${TARGET}" "$LAST_CMD_START_LINE" "/transport target"
|
|
send_cmd "$FIRST_PROMPT"
|
|
wait_for_edge_pattern_since "\\[node0-evt\\] start run_id=" "$LAST_CMD_START_LINE" "foreground run 1 start" "$RUN_TIMEOUT"
|
|
wait_for_edge_pattern_since "\\[node0-msg\\].*${FIRST_EXPECTED}" "$LAST_CMD_START_LINE" "foreground run 1 node message" "$RUN_TIMEOUT"
|
|
wait_for_edge_pattern_since "\\[node0-evt\\] complete run_id=" "$LAST_CMD_START_LINE" "foreground run 1 completion" "$RUN_TIMEOUT"
|
|
send_cmd "$SECOND_PROMPT"
|
|
wait_for_edge_pattern_since "\\[node0-evt\\] start run_id=" "$LAST_CMD_START_LINE" "foreground run 2 start" "$RUN_TIMEOUT"
|
|
wait_for_edge_pattern_since "\\[node0-msg\\].*${SECOND_EXPECTED}" "$LAST_CMD_START_LINE" "foreground run 2 node message" "$RUN_TIMEOUT"
|
|
wait_for_edge_pattern_since "\\[node0-evt\\] complete run_id=" "$LAST_CMD_START_LINE" "foreground run 2 completion" "$RUN_TIMEOUT"
|
|
send_cmd "/session session2"
|
|
wait_for_edge_pattern_since "session .*session2" "$LAST_CMD_START_LINE" "/session acknowledgement"
|
|
send_cmd "/background on"
|
|
wait_for_edge_pattern_since "background .*on" "$LAST_CMD_START_LINE" "/background on acknowledgement"
|
|
send_cmd "$BACKGROUND_PROMPT"
|
|
wait_for_edge_pattern_since "\\[node0-evt\\] start run_id=.*session=session2.*background=true" "$LAST_CMD_START_LINE" "background run start" "$RUN_TIMEOUT"
|
|
wait_for_edge_pattern_since "\\[node0-msg\\].*${BACKGROUND_EXPECTED}" "$LAST_CMD_START_LINE" "background run node message" "$RUN_TIMEOUT"
|
|
wait_for_edge_pattern_since "\\[node0-evt\\] complete run_id=" "$LAST_CMD_START_LINE" "background run completion" "$RUN_TIMEOUT"
|
|
send_cmd "/background off"
|
|
wait_for_edge_pattern_since "background .*off" "$LAST_CMD_START_LINE" "/background off acknowledgement"
|
|
send_cmd "/sessions"
|
|
wait_for_edge_text_since "[node0-sessions]" "$LAST_CMD_START_LINE" "/sessions output"
|
|
|
|
if [ "$IS_PERSISTENT" -eq 1 ]; then
|
|
send_cmd "/terminate-session"
|
|
wait_for_edge_text_since "terminated session" "$LAST_CMD_START_LINE" "/terminate-session acknowledgement"
|
|
fi
|
|
|
|
if [ "$HAS_STATUS" -eq 1 ]; then
|
|
send_cmd "/status"
|
|
wait_for_edge_text_since "[node0-status]" "$LAST_CMD_START_LINE" "/status output" "$STATUS_TIMEOUT"
|
|
fi
|
|
|
|
if [ "${IOP_E2E_RECONNECT:-0}" -eq 1 ]; then
|
|
echo "[e2e] reconnect test enabled, stopping node..."
|
|
# Update start line baseline before stopping node to ignore prior registration
|
|
LAST_CMD_START_LINE=$(($(wc -l < "$EDGE_OUT" | tr -d ' ') + 1))
|
|
|
|
kill_process_tree "$NODE_PID"
|
|
sleep 2
|
|
|
|
echo "[e2e] restarting node..."
|
|
echo "=== NODE RESTARTED ===" >> "$NODE_OUT"
|
|
IOP_NODE_CONFIG="$NODE_CONFIG" "$REPO_ROOT/scripts/dev/node.sh" >> "$NODE_OUT" 2>&1 &
|
|
NODE_PID=$!
|
|
|
|
wait_for_edge_pattern_since '\[node0-evt\] connected reason="registered"' "$LAST_CMD_START_LINE" "node re-registration"
|
|
|
|
echo "[e2e] sending 4th command after reconnect..."
|
|
send_cmd "$FOURTH_PROMPT"
|
|
wait_for_edge_pattern_since "\\[node0-evt\\] start run_id=" "$LAST_CMD_START_LINE" "foreground run 4 start" "$RUN_TIMEOUT"
|
|
wait_for_edge_pattern_since "\\[node0-msg\\].*${FOURTH_EXPECTED}" "$LAST_CMD_START_LINE" "foreground run 4 node message" "$RUN_TIMEOUT"
|
|
wait_for_edge_pattern_since "\\[node0-evt\\] complete run_id=" "$LAST_CMD_START_LINE" "foreground run 4 completion" "$RUN_TIMEOUT"
|
|
fi
|
|
|
|
IDLE_FAIL=0
|
|
IDLE_MARKER_HITS=""
|
|
if [ "$IDLE_SECONDS" -gt 0 ]; then
|
|
EDGE_BASELINE=$(wc -l < "$EDGE_OUT" | tr -d ' ')
|
|
NODE_BASELINE=$(wc -l < "$NODE_OUT" | tr -d ' ')
|
|
echo "[e2e] idle monitor: sleeping ${IDLE_SECONDS}s (edge baseline=${EDGE_BASELINE} node baseline=${NODE_BASELINE})"
|
|
sleep "$IDLE_SECONDS"
|
|
IDLE_PATTERN='transport_closed|heartbeat_timeout|node unregistered|disconnected from edge|transport_close_reason'
|
|
EDGE_IDLE_TAIL="$TMP_DIR/edge_idle_tail"
|
|
NODE_IDLE_TAIL="$TMP_DIR/node_idle_tail"
|
|
tail -n +"$((EDGE_BASELINE + 1))" "$EDGE_OUT" > "$EDGE_IDLE_TAIL" || true
|
|
tail -n +"$((NODE_BASELINE + 1))" "$NODE_OUT" > "$NODE_IDLE_TAIL" || true
|
|
if grep -E "$IDLE_PATTERN" "$EDGE_IDLE_TAIL" >/dev/null 2>&1; then
|
|
IDLE_FAIL=1
|
|
IDLE_MARKER_HITS+=$'\n--- EDGE idle window markers ---\n'
|
|
IDLE_MARKER_HITS+=$(grep -E "$IDLE_PATTERN" "$EDGE_IDLE_TAIL")
|
|
fi
|
|
if grep -E "$IDLE_PATTERN" "$NODE_IDLE_TAIL" >/dev/null 2>&1; then
|
|
IDLE_FAIL=1
|
|
IDLE_MARKER_HITS+=$'\n--- NODE idle window markers ---\n'
|
|
IDLE_MARKER_HITS+=$(grep -E "$IDLE_PATTERN" "$NODE_IDLE_TAIL")
|
|
fi
|
|
fi
|
|
|
|
send_cmd "/exit"
|
|
exec 3>&-
|
|
|
|
wait $EDGE_PID || true
|
|
kill_process_tree "$NODE_PID"
|
|
|
|
echo "=== EDGE OUTPUT ==="
|
|
cat "$EDGE_OUT"
|
|
echo "=== NODE OUTPUT ==="
|
|
cat "$NODE_OUT"
|
|
echo "==================="
|
|
|
|
FAIL=0
|
|
check_grep() {
|
|
local pattern="$1"
|
|
local file="$2"
|
|
local msg="$3"
|
|
if ! grep -q "$pattern" "$file"; then
|
|
echo "[e2e] FAIL: $msg (pattern: '$pattern')"
|
|
FAIL=1
|
|
fi
|
|
}
|
|
|
|
check_no_grep() {
|
|
local pattern="$1"
|
|
local file="$2"
|
|
local msg="$3"
|
|
if grep -q "$pattern" "$file"; then
|
|
echo "[e2e] FAIL: $msg (found pattern: '$pattern')"
|
|
grep "$pattern" "$file"
|
|
FAIL=1
|
|
fi
|
|
}
|
|
|
|
check_sessions_output() {
|
|
if grep -q "sessions:" "$EDGE_OUT"; then
|
|
return
|
|
fi
|
|
if grep -q "count = 0" "$EDGE_OUT" && grep -q "sessions = " "$EDGE_OUT"; then
|
|
return
|
|
fi
|
|
echo "[e2e] FAIL: /sessions output did not render grouped sessions or an empty session list"
|
|
FAIL=1
|
|
}
|
|
|
|
check_fail_markers() {
|
|
local file="$1"
|
|
# Strict failure detection for system/console errors
|
|
# We catch panics, unexpected exits, startup failures, and run-level error events.
|
|
if grep -i -E "panic:|unexpected|startup failed|cancel error|node reported error|does not support|adapter .* not found|no session|error run_id=|\[node-event\] error|\[[^]]+-evt\] error" "$file" >/dev/null; then
|
|
echo "[e2e] FAIL: detected error marker in $file"
|
|
grep -i -E "panic:|unexpected|startup failed|cancel error|node reported error|does not support|adapter .* not found|no session|error run_id=|\[node-event\] error|\[[^]]+-evt\] error" "$file"
|
|
FAIL=1
|
|
fi
|
|
}
|
|
|
|
# collect_node_run_ids <node-output> <out-file>
|
|
# Extracts unique run_ids from [node-event] start lines.
|
|
# Returns 0 on success; sets FAIL and prints error on empty result.
|
|
collect_node_run_ids() {
|
|
local node_out="$1" out_file="$2"
|
|
awk '
|
|
/^\[node-event\] start run_id=/ {
|
|
run = $0
|
|
sub(/^\[node-event\] start run_id=/, "", run)
|
|
sub(/[[:space:]].*$/, "", run)
|
|
print run
|
|
}
|
|
' "$node_out" | sort -u > "$out_file"
|
|
|
|
if [ ! -s "$out_file" ]; then
|
|
echo "[e2e] FAIL: no node run ids found in node output"
|
|
FAIL=1
|
|
return 1
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
# extract_node_messages <node-output> <run-id> <out-file>
|
|
# Extracts message payloads for a single run from node output.
|
|
# Skips <empty> placeholders and non-message lines.
|
|
extract_node_messages() {
|
|
local node_out="$1" run_id="$2" out_file="$3"
|
|
awk -v want="$run_id" '
|
|
/^\[node-event\] start run_id=/ {
|
|
run = $0
|
|
sub(/^\[node-event\] start run_id=/, "", run)
|
|
sub(/[[:space:]].*$/, "", run)
|
|
in_message = 0
|
|
next
|
|
}
|
|
/^\[node-event\] complete run_id=/ {
|
|
in_message = 0
|
|
next
|
|
}
|
|
/^\[node-message\] / {
|
|
in_message = (run == want)
|
|
if (run == want) {
|
|
line = $0
|
|
sub(/^\[node-message\] /, "", line)
|
|
if (line != "<empty>") {
|
|
print line
|
|
}
|
|
}
|
|
next
|
|
}
|
|
in_message && /^\[/ {
|
|
in_message = 0
|
|
}
|
|
in_message && /^\{/ {
|
|
in_message = 0
|
|
}
|
|
in_message && $0 != "" {
|
|
print
|
|
}
|
|
' "$node_out" > "$out_file"
|
|
}
|
|
|
|
# extract_edge_run_trace <edge-output> <run-id> <out-file>
|
|
# Extracts a structured trace for a single run from edge output.
|
|
# Output format: <line_no>\t<event_type>\t<payload>
|
|
# Event types: START, MSG, COMPLETE, LATE_MSG
|
|
extract_edge_run_trace() {
|
|
local edge_out="$1" run_id="$2" out_file="$3"
|
|
awk -v want="$run_id" '
|
|
$0 ~ /\[[^]]+-evt\] start run_id=/ {
|
|
if (after_want_complete) {
|
|
after_want_complete = 0
|
|
}
|
|
run = $0
|
|
sub(/^.*\[[^]]+-evt\] start run_id=/, "", run)
|
|
sub(/[[:space:]].*$/, "", run)
|
|
current = run
|
|
if (run == want) {
|
|
print NR "\tSTART\t"
|
|
}
|
|
next
|
|
}
|
|
$0 ~ /\[[^]]+-evt\] complete run_id=/ {
|
|
run = $0
|
|
sub(/^.*\[[^]]+-evt\] complete run_id=/, "", run)
|
|
sub(/[[:space:]].*$/, "", run)
|
|
if (run == want) {
|
|
print NR "\tCOMPLETE\t"
|
|
after_want_complete = 1
|
|
}
|
|
if (current == run) {
|
|
current = ""
|
|
}
|
|
next
|
|
}
|
|
$0 ~ /\[[^]]+-msg\] / {
|
|
if (current == want) {
|
|
line = $0
|
|
sub(/^.*\[[^]]+-msg\] /, "", line)
|
|
if (line != "<empty>") {
|
|
print NR "\tMSG\t" line
|
|
}
|
|
} else if (after_want_complete) {
|
|
line = $0
|
|
sub(/^.*\[[^]]+-msg\] /, "", line)
|
|
print NR "\tLATE_MSG\t" line
|
|
}
|
|
}
|
|
' "$edge_out" > "$out_file"
|
|
}
|
|
|
|
# extract_edge_messages <trace-file> <out-file>
|
|
# Extracts only MSG payloads from an edge run trace.
|
|
extract_edge_messages() {
|
|
local trace_file="$1" out_file="$2"
|
|
awk -F '\t' '$2 == "MSG" { print $3 }' "$trace_file" > "$out_file"
|
|
}
|
|
|
|
# assert_relay_ordering <run-id> <trace-file>
|
|
# Validates that:
|
|
# - edge start event exists
|
|
# - all node messages reached edge before complete
|
|
# - no message rendered after complete
|
|
# Sets FAIL on every violation; missing start/complete also returns non-zero.
|
|
assert_relay_ordering() {
|
|
local run_id="$1" trace_file="$2"
|
|
local edge_start_line edge_complete_line edge_last_message_line edge_late_message_line
|
|
|
|
edge_start_line=$(awk -F '\t' '$2 == "START" { print $1; exit }' "$trace_file")
|
|
edge_complete_line=$(awk -F '\t' '$2 == "COMPLETE" { print $1; exit }' "$trace_file")
|
|
edge_last_message_line=$(awk -F '\t' '$2 == "MSG" { line = $1 } END { print line }' "$trace_file")
|
|
edge_late_message_line=$(awk -F '\t' '$2 == "LATE_MSG" { print $1; exit }' "$trace_file")
|
|
|
|
if [ -z "$edge_start_line" ]; then
|
|
echo "[e2e] FAIL: edge start event not found for run $run_id"
|
|
FAIL=1
|
|
return 1
|
|
fi
|
|
|
|
if [ -z "$edge_complete_line" ]; then
|
|
echo "[e2e] FAIL: edge completion not found for run $run_id"
|
|
FAIL=1
|
|
return 1
|
|
fi
|
|
|
|
if [ -z "$edge_last_message_line" ] || [ "$edge_complete_line" -le "$edge_last_message_line" ]; then
|
|
echo "[e2e] FAIL: edge complete arrived before all rendered messages for run $run_id"
|
|
echo "[e2e] complete_line=$edge_complete_line last_message_line=${edge_last_message_line:-<none>}"
|
|
FAIL=1
|
|
fi
|
|
|
|
if [ -n "$edge_late_message_line" ]; then
|
|
echo "[e2e] FAIL: edge rendered a message after complete for run $run_id"
|
|
awk -F '\t' '$2 == "LATE_MSG" { print "[e2e] late_message_line=" $1 " payload=" $3 }' "$trace_file"
|
|
FAIL=1
|
|
fi
|
|
|
|
return 0
|
|
}
|
|
|
|
# assert_message_payloads_match <run-id> <node-messages-file> <edge-messages-file>
|
|
# Validates that node-generated message payloads match edge-rendered payloads exactly.
|
|
# Returns 0 on success; sets FAIL on mismatch.
|
|
assert_message_payloads_match() {
|
|
local run_id="$1" node_msgs="$2" edge_msgs="$3"
|
|
if [ ! -s "$node_msgs" ]; then
|
|
echo "[e2e] FAIL: no node-generated message payloads found for run $run_id"
|
|
FAIL=1
|
|
return 1
|
|
fi
|
|
|
|
if [ ! -s "$edge_msgs" ]; then
|
|
echo "[e2e] FAIL: no edge-rendered message payloads found for run $run_id"
|
|
FAIL=1
|
|
return 1
|
|
fi
|
|
|
|
if ! diff -u "$node_msgs" "$edge_msgs" > "$TMP_DIR/message_diff"; then
|
|
echo "[e2e] FAIL: node message payloads differ from edge-rendered payloads for run $run_id"
|
|
cat "$TMP_DIR/message_diff"
|
|
FAIL=1
|
|
return 1
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
# check_node_messages_relayed_to_edge
|
|
# Top-level orchestration: iterates node runs and validates relay integrity.
|
|
check_node_messages_relayed_to_edge() {
|
|
local node_runs="$TMP_DIR/node_runs"
|
|
local run_id
|
|
local node_messages="$TMP_DIR/node_run_messages"
|
|
local edge_messages="$TMP_DIR/edge_run_messages"
|
|
local edge_trace="$TMP_DIR/edge_run_trace"
|
|
|
|
# Step 1: collect node run ids
|
|
if ! collect_node_run_ids "$NODE_OUT" "$node_runs"; then
|
|
return
|
|
fi
|
|
|
|
# Step 2: for each run, extract and validate relay
|
|
while IFS= read -r run_id; do
|
|
if [ -z "$run_id" ]; then
|
|
continue
|
|
fi
|
|
|
|
# Step 2a: extract node messages
|
|
extract_node_messages "$NODE_OUT" "$run_id" "$node_messages"
|
|
|
|
# Step 2b: extract edge run trace
|
|
extract_edge_run_trace "$EDGE_OUT" "$run_id" "$edge_trace"
|
|
|
|
# Step 2c: extract edge messages from trace
|
|
extract_edge_messages "$edge_trace" "$edge_messages"
|
|
|
|
# Step 2d: assert ordering (start/complete/message alignment)
|
|
assert_relay_ordering "$run_id" "$edge_trace"
|
|
|
|
# Step 2e: assert payload identity
|
|
assert_message_payloads_match "$run_id" "$node_messages" "$edge_messages"
|
|
done < "$node_runs"
|
|
}
|
|
|
|
check_grep "test-node" "$EDGE_OUT" "node registration not found"
|
|
check_grep "start run_id=" "$EDGE_OUT" "run start not found"
|
|
check_grep "complete run_id=" "$EDGE_OUT" "run completion not found"
|
|
EXPECTED_RUNS=3
|
|
if [ "${IOP_E2E_RECONNECT:-0}" -eq 1 ]; then
|
|
EXPECTED_RUNS=4
|
|
fi
|
|
if [ "$(grep -c "\\[node0-evt\\] complete run_id=" "$EDGE_OUT" || true)" -lt "$EXPECTED_RUNS" ]; then
|
|
echo "[e2e] FAIL: expected at least $EXPECTED_RUNS completed runs"
|
|
FAIL=1
|
|
fi
|
|
check_grep "\\[node0-msg\\].*${FIRST_EXPECTED}" "$EDGE_OUT" "foreground run 1 node message not found"
|
|
check_grep "\\[node0-msg\\].*${SECOND_EXPECTED}" "$EDGE_OUT" "foreground run 2 node message not found"
|
|
check_grep "\\[node0-msg\\].*${BACKGROUND_EXPECTED}" "$EDGE_OUT" "background run node message not found"
|
|
if [ "${IOP_E2E_RECONNECT:-0}" -eq 1 ]; then
|
|
check_grep "\\[node0-msg\\].*${FOURTH_EXPECTED}" "$EDGE_OUT" "foreground run 4 node message not found"
|
|
fi
|
|
if [ "$EXPECT_TAIL_MESSAGES" -eq 1 ]; then
|
|
check_grep "\\[node0-msg\\].*${FIRST_TAIL_EXPECTED}" "$EDGE_OUT" "foreground run 1 tail node message not found"
|
|
check_grep "\\[node0-msg\\].*${SECOND_TAIL_EXPECTED}" "$EDGE_OUT" "foreground run 2 tail node message not found"
|
|
check_grep "\\[node0-msg\\].*${BACKGROUND_TAIL_EXPECTED}" "$EDGE_OUT" "background run tail node message not found"
|
|
if [ "${IOP_E2E_RECONNECT:-0}" -eq 1 ]; then
|
|
check_grep "\\[node0-msg\\].*${FOURTH_TAIL_EXPECTED}" "$EDGE_OUT" "foreground run 4 tail node message not found"
|
|
fi
|
|
fi
|
|
check_no_grep "\\[node0-msg\\] <empty>" "$EDGE_OUT" "empty node message found"
|
|
check_node_messages_relayed_to_edge
|
|
|
|
check_grep "\\[node0-capabilities\\]" "$EDGE_OUT" "/capabilities output not found"
|
|
check_grep "targets = ${TARGET}" "$EDGE_OUT" "/capabilities targets not found"
|
|
check_grep "\\[node0-transport\\]" "$EDGE_OUT" "/transport output not found"
|
|
check_grep "adapter = cli" "$EDGE_OUT" "/transport adapter not found"
|
|
check_grep "connected = true" "$EDGE_OUT" "/transport connected status not found"
|
|
check_grep "state = connected" "$EDGE_OUT" "/transport state not found"
|
|
check_grep "node_id = test-node" "$EDGE_OUT" "/transport node id not found"
|
|
check_grep "session_id = default" "$EDGE_OUT" "/transport session id not found"
|
|
check_grep "target = ${TARGET}" "$EDGE_OUT" "/transport target not found"
|
|
check_grep "\\[node0-sessions\\]" "$EDGE_OUT" "/sessions output not found"
|
|
check_sessions_output
|
|
|
|
if [ "$IS_APP_SERVER" -eq 1 ]; then
|
|
# codex-app-server sessions are keyed on (target, sessionID); session2 is used by the background run.
|
|
check_grep "mode=codex-app-server target=${TARGET} session=session2" "$EDGE_OUT" "/sessions entry for session2 not found (codex-app-server)"
|
|
elif [ "$IS_PERSISTENT" -eq 1 ]; then
|
|
check_grep "mode=persistent target=${TARGET} session=session2" "$EDGE_OUT" "/sessions entry for session2 not found"
|
|
fi
|
|
|
|
if [ "$IS_PERSISTENT" -eq 1 ]; then
|
|
check_grep "terminated session" "$EDGE_OUT" "/terminate-session success not found"
|
|
fi
|
|
|
|
check_fail_markers "$EDGE_OUT"
|
|
check_fail_markers "$NODE_OUT"
|
|
|
|
if [ "$IDLE_FAIL" -eq 1 ]; then
|
|
echo "[e2e] FAIL: idle window transport close markers detected"
|
|
echo "$IDLE_MARKER_HITS"
|
|
FAIL=1
|
|
fi
|
|
|
|
REAL_PROFILE_STATUS="N/A (mock used)"
|
|
if [ "$PROFILE" != "mock" ]; then
|
|
if [ $FAIL -eq 0 ]; then
|
|
if grep -q -E "command not found|no such file" "$NODE_OUT"; then
|
|
REAL_PROFILE_STATUS="BLOCKED (command not found)"
|
|
FAIL=1
|
|
elif grep -qE "login|auth|permission|token|failed: EOF" "$NODE_OUT" && ! grep -q "complete run_id=" "$EDGE_OUT"; then
|
|
REAL_PROFILE_STATUS="BLOCKED (auth/login or startup failure)"
|
|
FAIL=1
|
|
else
|
|
REAL_PROFILE_STATUS="PASSED"
|
|
fi
|
|
else
|
|
# Try to distinguish between BLOCKED and FAILED for real profile
|
|
if grep -q -E "command not found|no such file" "$NODE_OUT"; then
|
|
REAL_PROFILE_STATUS="BLOCKED (command not found)"
|
|
elif grep -qE "login|auth|permission|token" "$NODE_OUT"; then
|
|
REAL_PROFILE_STATUS="BLOCKED (auth/login required)"
|
|
else
|
|
REAL_PROFILE_STATUS="FAILED"
|
|
fi
|
|
fi
|
|
echo "[e2e] Real profile ($PROFILE) status: $REAL_PROFILE_STATUS"
|
|
fi
|
|
|
|
if [ $FAIL -eq 1 ]; then
|
|
echo "[e2e] Auxiliary smoke test FAILED."
|
|
exit 1
|
|
fi
|
|
|
|
echo "[e2e] Auxiliary smoke test PASSED."
|
|
echo "[e2e] Completion still requires scripts/dev/edge.sh + scripts/dev/node.sh user-flow verification."
|
|
exit 0
|