Skip to content

Commit eea7834

Browse files
test(integration): harden docker compose webhook e2e
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
1 parent ae918ff commit eea7834

2 files changed

Lines changed: 111 additions & 14 deletions

File tree

scripts/integration-test-helpers_test.sh

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,21 @@ test_assert_mcp_ok_rejects_jsonrpc_error() {
107107
assert_fails "JSON-RPC error response" assert_mcp_ok "bad_tool" "$resp"
108108
}
109109

110+
test_assert_mcp_ok_reports_jsonrpc_error_without_python_nameerror() {
111+
local resp output
112+
resp='{"error":{"code":-32603,"message":"boom"}}'
113+
output=$(assert_mcp_ok "bad_tool" "$resp" 2>&1 || true)
114+
115+
[[ "$output" == *"JSON-RPC error:"* ]] || {
116+
echo "expected JSON-RPC error output, got: $output" >&2
117+
exit 1
118+
}
119+
[[ "$output" != *"NameError"* ]] || {
120+
echo "did not expect Python NameError in output: $output" >&2
121+
exit 1
122+
}
123+
}
124+
110125
test_assert_mcp_ok_rejects_result_is_error() {
111126
local resp
112127
resp='{"result":{"isError":true,"content":[{"type":"text","text":"boom"}]}}'
@@ -125,6 +140,14 @@ test_mcp_init_failure_requires_explicit_debug_override() {
125140
mcp_session_required ""
126141
}
127142

143+
test_mcp_log_fallback_allowed_defaults_false() {
144+
assert_fails "log fallback should be disabled by default" mcp_log_fallback_allowed
145+
CCG_E2E_ALLOW_MCP_LOG_FALLBACK=0
146+
assert_fails "log fallback should reject zero value" mcp_log_fallback_allowed
147+
CCG_E2E_ALLOW_MCP_LOG_FALLBACK=1
148+
mcp_log_fallback_allowed
149+
}
150+
128151
test_extract_mcp_session_id_reads_header_case_insensitively() {
129152
local session
130153
session=$(extract_mcp_session_id $'HTTP/1.1 200 OK\r\nMcp-Session-Id: abc-123\r\n\r\n{}')
@@ -152,6 +175,40 @@ test_log_fallback_detects_webhook_failure() {
152175
webhook_logs_failed "$logs" "sample-go"
153176
}
154177

178+
test_wait_for_webhook_sync_requires_mcp_nodes_when_fallback_disabled() {
179+
graph_nodes_for_workspace() { return 1; }
180+
compose() { printf '%s\n' 'time=... webhook sync completed workspace=sample-go nodes=4'; }
181+
webhook_logs_completed() { return 0; }
182+
webhook_logs_failed() { return 1; }
183+
sleep() { SECONDS=$((SECONDS + 2)); }
184+
185+
assert_fails "log-only webhook sync should fail without explicit fallback" wait_for_webhook_sync "sample-go" 1 "sample-go"
186+
}
187+
188+
test_wait_for_webhook_sync_allows_log_success_when_fallback_enabled() {
189+
graph_nodes_for_workspace() { return 1; }
190+
compose() { printf '%s\n' 'time=... webhook sync completed workspace=sample-go nodes=4'; }
191+
webhook_logs_completed() { return 0; }
192+
webhook_logs_failed() { return 1; }
193+
sleep() { SECONDS=$((SECONDS + 2)); }
194+
CCG_E2E_ALLOW_MCP_LOG_FALLBACK=1
195+
196+
wait_for_webhook_sync "sample-go" 1 "sample-go"
197+
}
198+
199+
test_start_integration_stack_runs_migrate_before_starting_ccg() {
200+
local -a compose_calls=() wait_calls=()
201+
info() { :; }
202+
compose() { compose_calls+=("$*"); }
203+
wait_for_postgres() { wait_calls+=("postgres:$1:$2"); }
204+
wait_for_health() { wait_calls+=("health:$1:$2:$3"); }
205+
206+
start_integration_stack
207+
208+
assert_equals $'up -d postgres gitea\nbuild ccg\nrun --rm --no-deps ccg migrate\nup -d ccg' "$(printf '%s\n' "${compose_calls[@]}")"
209+
assert_equals $'postgres:30:PostgreSQL\nhealth:http://localhost:3000/api/v1/version:30:Gitea\nhealth:http://localhost:18080/ready:30:ccg' "$(printf '%s\n' "${wait_calls[@]}")"
210+
}
211+
155212
test_helper_env_isolation_mutates_globals() {
156213
KEEP_CONTAINERS=1
157214
COMPOSE_CMD="false"
@@ -178,13 +235,18 @@ run_test test_compose_does_not_eval_command_string
178235
run_test test_mcp_text_extracts_content
179236
run_test test_assert_mcp_ok_rejects_malformed_json
180237
run_test test_assert_mcp_ok_rejects_jsonrpc_error
238+
run_test test_assert_mcp_ok_reports_jsonrpc_error_without_python_nameerror
181239
run_test test_assert_mcp_ok_rejects_result_is_error
182240
run_test test_assert_mcp_contains_rejects_missing_content_text
183241
run_test test_mcp_init_failure_requires_explicit_debug_override
242+
run_test test_mcp_log_fallback_allowed_defaults_false
184243
run_test test_extract_mcp_session_id_reads_header_case_insensitively
185244
run_test test_log_fallback_detects_webhook_completion
186245
run_test test_log_fallback_matches_without_pipefail_sigpipe_risk
187246
run_test test_log_fallback_detects_webhook_failure
247+
run_test test_wait_for_webhook_sync_requires_mcp_nodes_when_fallback_disabled
248+
run_test test_wait_for_webhook_sync_allows_log_success_when_fallback_enabled
249+
run_test test_start_integration_stack_runs_migrate_before_starting_ccg
188250
run_test test_helper_env_isolation_mutates_globals
189251
run_test test_helper_env_isolation_observes_defaults
190252

scripts/integration-test.sh

Lines changed: 49 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,42 @@ wait_for_health() {
9090
fail "$label did not become healthy after $((max * 2))s"
9191
}
9292

93+
wait_for_postgres() {
94+
local max="$1" label="${2:-PostgreSQL}"
95+
for i in $(seq 1 "$max"); do
96+
if compose exec -T postgres pg_isready -U ccg -d ccg_integration >/dev/null 2>&1; then
97+
info "$label is healthy"
98+
return 0
99+
fi
100+
echo -n "."
101+
sleep 2
102+
done
103+
fail "$label did not become healthy after $((max * 2))s"
104+
}
105+
106+
start_integration_stack() {
107+
info "Starting base services..."
108+
compose up -d postgres gitea
109+
110+
info "Building ccg image..."
111+
compose build ccg
112+
113+
info "Waiting for PostgreSQL..."
114+
wait_for_postgres 30 "PostgreSQL"
115+
116+
info "Waiting for Gitea..."
117+
wait_for_health "$GITEA_URL/api/v1/version" 30 "Gitea"
118+
119+
info "Running ccg migrations..."
120+
compose run --rm --no-deps ccg migrate
121+
122+
info "Starting ccg..."
123+
compose up -d ccg
124+
125+
info "Waiting for ccg..."
126+
wait_for_health "$CCG_URL/ready" 30 "ccg"
127+
}
128+
93129
api() {
94130
local method="$1" path="$2"; shift 2
95131
curl -fsS -X "$method" "${GITEA_URL}${path}" \
@@ -147,7 +183,7 @@ if not isinstance(r, dict):
147183
print("response is not a JSON object", file=sys.stderr)
148184
sys.exit(2)
149185
if r.get("error") is not None:
150-
print(f"JSON-RPC error: {r.get('error')}", file=sys.stderr)
186+
print("JSON-RPC error:", r.get("error"), file=sys.stderr)
151187
sys.exit(3)
152188
result = r.get("result")
153189
if not isinstance(result, dict):
@@ -225,6 +261,10 @@ mcp_session_required() {
225261
[ -n "$session" ] || [ "${CCG_E2E_ALLOW_MCP_LOG_FALLBACK:-0}" = "1" ]
226262
}
227263

264+
mcp_log_fallback_allowed() {
265+
[ "${CCG_E2E_ALLOW_MCP_LOG_FALLBACK:-0}" = "1" ]
266+
}
267+
228268
extract_last_webhook_node_count() {
229269
local logs="$1"
230270
printf '%s' "$logs" | python3 -c '
@@ -267,7 +307,7 @@ wait_for_webhook_sync() {
267307
if webhook_logs_failed "$logs" "$workspace"; then
268308
fail "ccg build failed for ${label}. See ${ARTIFACT_DIR}/ccg.log after cleanup. Recent logs:\n$(printf '%s\n' "$logs" | tail -20)"
269309
fi
270-
if webhook_logs_completed "$logs" "$workspace"; then
310+
if mcp_log_fallback_allowed && webhook_logs_completed "$logs" "$workspace"; then
271311
info "${label} webhook sync observed via logs"
272312
return 0
273313
fi
@@ -279,14 +319,8 @@ wait_for_webhook_sync() {
279319
run_integration_test() {
280320

281321
# ── Phase 1: Start containers ──
282-
info "Starting containers..."
283-
compose up -d --build
284-
285-
info "Waiting for Gitea..."
286-
wait_for_health "$GITEA_URL/api/v1/version" 30 "Gitea"
287-
288-
info "Waiting for ccg..."
289-
wait_for_health "$CCG_URL/ready" 30 "ccg"
322+
info "Starting containers..."
323+
start_integration_stack
290324

291325
# ── Phase 2: Create Gitea admin user ──
292326
info "Creating Gitea admin user..."
@@ -398,7 +432,7 @@ else
398432
-H "Content-Type: application/json" \
399433
-H "Authorization: Bearer ${MCP_BEARER_TOKEN}" \
400434
-H "Mcp-Session-Id: ${MCP_SESSION}" \
401-
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"list_graph_stats","arguments":{}}}' 2>/dev/null) || STATS_RESP=""
435+
-d "{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"tools/call\",\"params\":{\"name\":\"list_graph_stats\",\"arguments\":{\"workspace\":\"${REPO_NAME}\"}}}" 2>/dev/null) || STATS_RESP=""
402436

403437
if [ -z "$STATS_RESP" ]; then
404438
fail "MCP tools/call returned empty response"
@@ -817,16 +851,17 @@ fi
817851

818852
# ── Summary ──
819853
TOOLS_TESTED=0
854+
TOTAL_MCP_TOOLS=33
820855
if [ -n "$MCP_SESSION" ]; then
821-
TOOLS_TESTED=29
856+
TOOLS_TESTED=27
822857
fi
823858

824859
echo ""
825860
echo -e "${GREEN}════════════════════════════════════════════════════════${NC}"
826861
if [ -n "$MCP_SESSION" ]; then
827-
echo -e "${GREEN} Integration test PASSED — ${TOOLS_TESTED}/29 MCP tools verified${NC}"
862+
echo -e "${GREEN} Integration test PASSED — ${TOOLS_TESTED}/${TOTAL_MCP_TOOLS} MCP tools verified${NC}"
828863
else
829-
echo -e "${YELLOW} Integration webhook smoke completed — 0/29 MCP tools verified (local debug override)${NC}"
864+
echo -e "${YELLOW} Integration webhook smoke completed — 0/${TOTAL_MCP_TOOLS} MCP tools verified (local debug override)${NC}"
830865
fi
831866
echo -e "${GREEN}════════════════════════════════════════════════════════${NC}"
832867
echo ""

0 commit comments

Comments
 (0)