Skip to content

Commit e20c494

Browse files
debug
1 parent 0281c73 commit e20c494

File tree

8 files changed

+881
-16
lines changed

8 files changed

+881
-16
lines changed

results.json

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,25 @@
33
"passed": 13,
44
"failed": 0,
55
"total": 13,
6-
"suite_duration_ms": 4572,
7-
"generated_at": "2025-10-06T04:16:19Z"
6+
"suite_duration_ms": 4465,
7+
"generated_at": "2025-10-06T04:22:24Z"
88
},
99
"tests": [
10-
{"name": help\ option\ prints\ usage, "status": pass, "duration_ms": 10},
11-
{"name": missing\ docker\ binary\ aborts, "status": pass, "duration_ms": 31},
12-
{"name": missing\ compose\ file\ aborts, "status": pass, "duration_ms": 31},
13-
{"name": full\ deployment\ flow\ succeeds, "status": pass, "duration_ms": 180},
14-
{"name": skip\ flags\ suppress\ actions, "status": pass, "duration_ms": 140},
15-
{"name": parallel\ build\ retry\ works, "status": pass, "duration_ms": 148},
16-
{"name": smoke\ test\ issues\ admin\ request, "status": pass, "duration_ms": 138},
17-
{"name": health\ check\ failure\ propagates, "status": pass, "duration_ms": 1435},
18-
{"name": setup\ failure\ aborts\ deployment, "status": pass, "duration_ms": 156},
19-
{"name": health\ retries\ counted\ \(locator\ delayed\), "status": pass, "duration_ms": 216},
20-
{"name": smoke\ test\ failure\ propagates, "status": pass, "duration_ms": 1562},
21-
{"name": idempotent\ second\ run\ without\ down, "status": pass, "duration_ms": 315},
22-
{"name": quick\ runtime\ with\ skips, "status": pass, "duration_ms": 174}
10+
{"name": "help option prints usage", "status": "pass", "duration_ms": 10},
11+
{"name": "missing docker binary aborts", "status": "pass", "duration_ms": 32},
12+
{"name": "missing compose file aborts", "status": "pass", "duration_ms": 30},
13+
{"name": "full deployment flow succeeds", "status": "pass", "duration_ms": 151},
14+
{"name": "skip flags suppress actions", "status": "pass", "duration_ms": 136},
15+
{"name": "parallel build retry works", "status": "pass", "duration_ms": 152},
16+
{"name": "smoke test issues admin request", "status": "pass", "duration_ms": 139},
17+
{"name": "health check failure propagates", "status": "pass", "duration_ms": 1516},
18+
{"name": "setup failure aborts deployment", "status": "pass", "duration_ms": 149},
19+
{"name": "health retries counted (locator delayed)", "status": "pass", "duration_ms": 174},
20+
{"name": "smoke test failure propagates", "status": "pass", "duration_ms": 1550},
21+
{"name": "idempotent second run without down", "status": "pass", "duration_ms": 249},
22+
{"name": "quick runtime with skips", "status": "pass", "duration_ms": 148}
23+
],
24+
"api_stats": [
2325
],
2426
"api_calls": [
2527
]

scripts/test-deploy-full-stack.sh

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,16 @@ fi
130130
end_ns=$(date +%s%N)
131131
duration_ms=$(( (end_ns - start_ns)/1000000 ))
132132
printf 'curl %s duration_ms=%s exit_code=%s%s\n' "$*" "$duration_ms" "$exit_code" "$curl_extra" >>"$log_dir/curl.log"
133+
# Also emit structured JSON line for robust parsing
134+
json_extra=""
135+
if [[ -n "$curl_extra" ]]; then
136+
# extract numeric time_total if present
137+
tt=$(grep -oE 'time_total=[0-9.]+' <<<"$curl_extra" | cut -d= -f2 || true)
138+
if [[ -n "$tt" ]]; then
139+
json_extra=",\"time_total_s\":$tt"
140+
fi
141+
fi
142+
printf '{"url":"%s","duration_ms":%s,"exit_code":%s%s}\n' "${url:-UNKNOWN}" "$duration_ms" "$exit_code" "$json_extra" >>"$log_dir/curl.jsonl"
133143
exit "$exit_code"
134144
EOF
135145
chmod +x "$fixture/stubs/curl"
@@ -374,6 +384,50 @@ write_results_json() {
374384
fi
375385
done <"$p/.state/curl.log"
376386
fi
387+
# Parse structured JSON lines if present
388+
if [[ -f "$p/.state/curl.jsonl" ]]; then
389+
while IFS= read -r jline; do
390+
# Validate minimal JSON (contains url & duration_ms)
391+
if [[ "$jline" == '{'*'url'* ]]; then
392+
api_entries+=("$jline")
393+
fi
394+
done <"$p/.state/curl.jsonl"
395+
fi
396+
done
397+
398+
# Build aggregated stats per URL
399+
declare -A URL_DURS URL_FAILS URL_COUNT
400+
for entry in "${api_entries[@]}"; do
401+
url=$(grep -oE '"url":"[^"]+"' <<<"$entry" | cut -d'"' -f4)
402+
dur=$(grep -oE '"duration_ms":[0-9]+' <<<"$entry" | cut -d: -f2)
403+
ec=$(grep -oE '"exit_code":[0-9]+' <<<"$entry" | cut -d: -f2)
404+
[[ -z "$url" ]] && continue
405+
URL_DURS[$url]="${URL_DURS[$url]} $dur"
406+
URL_COUNT[$url]=$(( ${URL_COUNT[$url]:-0} + 1 ))
407+
if (( ec != 0 )); then
408+
URL_FAILS[$url]=$(( ${URL_FAILS[$url]:-0} + 1 ))
409+
fi
410+
done
411+
412+
api_stats_json=()
413+
for u in "${!URL_COUNT[@]}"; do
414+
# Compute avg, max, p95
415+
dlist=( ${URL_DURS[$u]} )
416+
sum=0; max=0
417+
for d in "${dlist[@]}"; do
418+
(( d > max )) && max=$d
419+
sum=$((sum + d))
420+
done
421+
count=${URL_COUNT[$u]}
422+
avg=$(( sum / count ))
423+
# p95 (simple: sort and pick index ceil(0.95*n)-1)
424+
sorted=($(printf '%s\n' "${dlist[@]}" | sort -n))
425+
idx=$(( (95 * count + 99) / 100 - 1 ))
426+
(( idx < 0 )) && idx=0
427+
(( idx >= count )) && idx=$((count-1))
428+
p95=${sorted[$idx]}
429+
fails=${URL_FAILS[$u]:-0}
430+
api_stats_json+=("{\"url\":\"$u\",\"count\":$count,\"failures\":$fails,\"avg_ms\":$avg,\"max_ms\":$max,\"p95_ms\":$p95}")
377431
done
378432

379433
{
@@ -388,10 +442,26 @@ write_results_json() {
388442
echo ' "tests": ['
389443
local i last=$(( ${#TEST_NAMES[@]} - 1 ))
390444
for i in "${!TEST_NAMES[@]}"; do
391-
printf ' {"name": %q, "status": %q, "duration_ms": %s}' "${TEST_NAMES[$i]}" "${TEST_STATUSES[$i]}" "${TEST_DURATIONS_MS[$i]}"
445+
# Use printf %q then convert bash escaping to JSON-friendly (remove leading/trailing quotes if present)
446+
tn=$(printf %q "${TEST_NAMES[$i]}")
447+
ts=$(printf %q "${TEST_STATUSES[$i]}")
448+
# Replace escaped spaces (\ ) with space, and escaped parentheses
449+
# Clean bash-style escapes (\ ) (\() (\)) left by %q for readability
450+
tn=${tn//\\ / }
451+
tn=${tn//\\(/(}
452+
tn=${tn//\\)/)}
453+
ts=${ts//\\ / }
454+
printf ' {"name": "%s", "status": "%s", "duration_ms": %s}' "$tn" "$ts" "${TEST_DURATIONS_MS[$i]}"
392455
if (( i < last )); then echo ','; else echo; fi
393456
done
394457
echo ' ],'
458+
echo ' "api_stats": ['
459+
local k last3=$(( ${#api_stats_json[@]} - 1 ))
460+
for k in "${!api_stats_json[@]}"; do
461+
printf ' %s' "${api_stats_json[$k]}"
462+
if (( k < last3 )); then echo ','; else echo; fi
463+
done
464+
echo ' ],'
395465
echo ' "api_calls": ['
396466
local j last2=$(( ${#api_entries[@]} - 1 ))
397467
for j in "${!api_entries[@]}"; do

services/guardian/credits_test.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package main
2+
3+
import (
4+
"encoding/json"
5+
"net/http"
6+
"net/http/httptest"
7+
"testing"
8+
)
9+
10+
// TestConsumeCreditsSuccess verifies a normal successful debit returns (true,200).
11+
func TestConsumeCreditsSuccess(t *testing.T) {
12+
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
13+
if r.Method != http.MethodPost {
14+
t.Fatalf("expected POST got %s", r.Method)
15+
}
16+
if r.URL.Path != "/credits/consume" {
17+
t.Fatalf("unexpected path %s", r.URL.Path)
18+
}
19+
_ = json.NewEncoder(w).Encode(map[string]any{"success": true})
20+
}))
21+
defer srv.Close()
22+
ok, code := consumeCredits(srv.URL, "tenantA", 5)
23+
if !ok || code != http.StatusOK {
24+
t.Fatalf("expected success 200 got ok=%v code=%d", ok, code)
25+
}
26+
}
27+
28+
// TestConsumeCreditsInsufficient verifies insufficient credits path mapping to 402.
29+
func TestConsumeCreditsInsufficient(t *testing.T) {
30+
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
31+
_ = json.NewEncoder(w).Encode(map[string]any{"success": false, "error": "Insufficient balance"})
32+
}))
33+
defer srv.Close()
34+
ok, code := consumeCredits(srv.URL, "tenantA", 10)
35+
if ok || code != http.StatusPaymentRequired {
36+
t.Fatalf("expected insufficient (false,402) got ok=%v code=%d", ok, code)
37+
}
38+
}
39+
40+
// TestConsumeCreditsServiceError verifies non-2xx status propagates service code.
41+
func TestConsumeCreditsServiceError(t *testing.T) {
42+
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
43+
w.WriteHeader(http.StatusServiceUnavailable)
44+
_ = json.NewEncoder(w).Encode(map[string]any{"success": false})
45+
}))
46+
defer srv.Close()
47+
ok, code := consumeCredits(srv.URL, "tenantB", 1)
48+
if ok || code != http.StatusServiceUnavailable {
49+
t.Fatalf("expected (false,503) got ok=%v code=%d", ok, code)
50+
}
51+
}
52+
53+
// TestConsumeCreditsBadJSON verifies decode error returns BadGateway.
54+
func TestConsumeCreditsBadJSON(t *testing.T) {
55+
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
56+
w.Write([]byte("not-json"))
57+
}))
58+
defer srv.Close()
59+
ok, code := consumeCredits(srv.URL, "tenantC", 2)
60+
if ok || code != http.StatusBadGateway {
61+
t.Fatalf("expected (false,502) got ok=%v code=%d", ok, code)
62+
}
63+
}

0 commit comments

Comments
 (0)