test: add endpoint coverage for healthz, metrics, estimate, demo Retry-After
Some checks failed
CI / Typecheck & Lint (pull_request) Failing after 0s
Some checks failed
CI / Typecheck & Lint (pull_request) Failing after 0s
Add four new testkit tests (T37–T40) covering self-serve endpoints: - T37: GET /api/healthz extended fields (uptime_s, jobs_total) - T38: GET /api/metrics full snapshot verification - T39: GET /api/estimate cost-preview endpoint - T40: Demo 429 Retry-After header (RFC 7231) Also adds Retry-After header to the demo rate-limit 429 response. Fixes #45 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -43,6 +43,7 @@ router.get("/demo", async (req: Request, res: Response) => {
|
||||
if (!allowed) {
|
||||
const secsUntilReset = Math.ceil((resetAt - Date.now()) / 1000);
|
||||
logger.warn("demo rate limited", { ip, retry_after_s: secsUntilReset });
|
||||
res.setHeader("Retry-After", String(secsUntilReset));
|
||||
res.status(429).json({
|
||||
error: `Rate limit exceeded. Try again in ${secsUntilReset}s (5 requests per hour per IP).`,
|
||||
});
|
||||
|
||||
@@ -29,6 +29,10 @@ const router = Router();
|
||||
* Guarded on stubMode=true; polls until state=provisioning|ready (20 s timeout).
|
||||
* - T24 ADDED: costLedger completeness after job completion — 8 fields, honest-accounting
|
||||
* invariant (actualAmountSats ≤ workAmountSats), refundState enum check.
|
||||
* - T37 ADDED: GET /api/healthz extended — verifies uptime_s and jobs_total fields (observability).
|
||||
* - T38 ADDED: GET /api/metrics — full snapshot field verification, zero prior coverage.
|
||||
* - T39 ADDED: GET /api/estimate — cost-preview endpoint, estimatedSats + model + tokens.
|
||||
* - T40 ADDED: Demo 429 Retry-After header — RFC 7231 compliance on rate-limited response.
|
||||
*/
|
||||
router.get("/testkit", (req: Request, res: Response) => {
|
||||
const proto =
|
||||
@@ -1092,27 +1096,117 @@ NODESCRIPT
|
||||
fi
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Test 37 — Healthz extended fields (uptime_s, jobs_total)
|
||||
# T1 checks status=ok; this test verifies the observability fields are present
|
||||
# and have sensible types (uptime_s >= 0, jobs_total >= 0).
|
||||
# ---------------------------------------------------------------------------
|
||||
sep "Test 37 — Healthz extended fields (uptime_s, jobs_total)"
|
||||
T37_RES=\$(curl -s -w "\\n%{http_code}" "\$BASE/api/healthz")
|
||||
T37_BODY=\$(body_of "\$T37_RES"); T37_CODE=\$(code_of "\$T37_RES")
|
||||
T37_UPTIME=\$(echo "\$T37_BODY" | jq '.uptime_s' 2>/dev/null || echo "")
|
||||
T37_JOBS=\$(echo "\$T37_BODY" | jq '.jobs_total' 2>/dev/null || echo "")
|
||||
if [[ "\$T37_CODE" == "200" ]] && \\
|
||||
[[ "\$T37_UPTIME" =~ ^[0-9]+$ ]] && \\
|
||||
[[ "\$T37_JOBS" =~ ^[0-9]+$ ]]; then
|
||||
note PASS "uptime_s=\$T37_UPTIME jobs_total=\$T37_JOBS"
|
||||
PASS=\$((PASS+1))
|
||||
else
|
||||
note FAIL "code=\$T37_CODE uptime_s='\$T37_UPTIME' jobs_total='\$T37_JOBS' body=\$T37_BODY"
|
||||
FAIL=\$((FAIL+1))
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Test 38 — Metrics endpoint (GET /api/metrics)
|
||||
# Verifies all top-level metric sections are present and well-typed:
|
||||
# uptime_s (int), jobs.total (int), jobs.by_state (object),
|
||||
# invoices.conversion_rate (number|null), earnings.total_sats (int),
|
||||
# latency (object).
|
||||
# ---------------------------------------------------------------------------
|
||||
sep "Test 38 — Metrics endpoint"
|
||||
T38_RES=\$(curl -s -w "\\n%{http_code}" "\$BASE/api/metrics")
|
||||
T38_BODY=\$(body_of "\$T38_RES"); T38_CODE=\$(code_of "\$T38_RES")
|
||||
T38_UPTIME=\$(echo "\$T38_BODY" | jq '.uptime_s' 2>/dev/null || echo "")
|
||||
T38_JOBS_TOTAL=\$(echo "\$T38_BODY" | jq '.jobs.total' 2>/dev/null || echo "")
|
||||
T38_BY_STATE=\$(echo "\$T38_BODY" | jq '.jobs.by_state | type' 2>/dev/null || echo "")
|
||||
T38_CONV_RATE=\$(echo "\$T38_BODY" | jq '.invoices.conversion_rate' 2>/dev/null || echo "")
|
||||
T38_SATS=\$(echo "\$T38_BODY" | jq '.earnings.total_sats' 2>/dev/null || echo "")
|
||||
T38_LATENCY=\$(echo "\$T38_BODY" | jq '.latency | type' 2>/dev/null || echo "")
|
||||
if [[ "\$T38_CODE" == "200" ]] && \\
|
||||
[[ "\$T38_UPTIME" =~ ^[0-9]+$ ]] && \\
|
||||
[[ "\$T38_JOBS_TOTAL" =~ ^[0-9]+$ ]] && \\
|
||||
[[ "\$T38_BY_STATE" == '"object"' ]] && \\
|
||||
[[ "\$T38_CONV_RATE" != "" ]] && \\
|
||||
[[ "\$T38_SATS" =~ ^[0-9]+$ ]] && \\
|
||||
[[ "\$T38_LATENCY" == '"object"' ]]; then
|
||||
note PASS "uptime_s=\$T38_UPTIME jobs_total=\$T38_JOBS_TOTAL sats_earned=\$T38_SATS"
|
||||
PASS=\$((PASS+1))
|
||||
else
|
||||
note FAIL "code=\$T38_CODE body=\$T38_BODY"
|
||||
FAIL=\$((FAIL+1))
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Test 39 — Estimate endpoint (GET /api/estimate?request=hello)
|
||||
# Verifies the cost-preview endpoint returns estimatedSats, model, and
|
||||
# token estimates. No payment required, no job created.
|
||||
# ---------------------------------------------------------------------------
|
||||
sep "Test 39 — Estimate endpoint"
|
||||
T39_RES=\$(curl -s -w "\\n%{http_code}" "\$BASE/api/estimate?request=hello")
|
||||
T39_BODY=\$(body_of "\$T39_RES"); T39_CODE=\$(code_of "\$T39_RES")
|
||||
T39_SATS=\$(echo "\$T39_BODY" | jq '.estimatedSats' 2>/dev/null || echo "")
|
||||
T39_MODEL=\$(echo "\$T39_BODY" | jq -r '.tokenEstimate.model' 2>/dev/null || echo "")
|
||||
T39_INPUT=\$(echo "\$T39_BODY" | jq '.tokenEstimate.inputTokens' 2>/dev/null || echo "")
|
||||
T39_OUTPUT=\$(echo "\$T39_BODY" | jq '.tokenEstimate.outputTokens' 2>/dev/null || echo "")
|
||||
T39_IDENTITY=\$(echo "\$T39_BODY" | jq '.identity | type' 2>/dev/null || echo "")
|
||||
T39_POOL=\$(echo "\$T39_BODY" | jq '.pool | type' 2>/dev/null || echo "")
|
||||
if [[ "\$T39_CODE" == "200" ]] && \\
|
||||
[[ "\$T39_SATS" =~ ^[0-9]+$ ]] && \\
|
||||
[[ -n "\$T39_MODEL" && "\$T39_MODEL" != "null" ]] && \\
|
||||
[[ "\$T39_INPUT" =~ ^[0-9]+$ ]] && \\
|
||||
[[ "\$T39_OUTPUT" =~ ^[0-9]+$ ]] && \\
|
||||
[[ "\$T39_IDENTITY" == '"object"' ]] && \\
|
||||
[[ "\$T39_POOL" == '"object"' ]]; then
|
||||
note PASS "estimatedSats=\$T39_SATS model=\$T39_MODEL input=\$T39_INPUT output=\$T39_OUTPUT"
|
||||
PASS=\$((PASS+1))
|
||||
else
|
||||
note FAIL "code=\$T39_CODE body=\$T39_BODY"
|
||||
FAIL=\$((FAIL+1))
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Test 40 — Demo 429 includes Retry-After header
|
||||
# By this point the rate-limit window is exhausted. Verify the 429 response
|
||||
# includes a numeric Retry-After header (RFC 7231 §7.1.3).
|
||||
# ---------------------------------------------------------------------------
|
||||
sep "Test 40 — Demo 429 includes Retry-After header"
|
||||
T40_OUT=\$(curl -si "\$BASE/api/demo?request=retry+after+probe" 2>/dev/null || true)
|
||||
T40_CODE=\$(echo "\$T40_OUT" | grep -i "^HTTP/" | tail -1 | awk '{print \$2}')
|
||||
T40_RETRY=\$(echo "\$T40_OUT" | grep -i "^Retry-After:" | head -1 | tr -d '\\r' | awk '{print \$2}')
|
||||
if [[ "\$T40_CODE" == "429" ]] && [[ "\$T40_RETRY" =~ ^[0-9]+$ ]]; then
|
||||
note PASS "HTTP 429 with Retry-After=\$T40_RETRY"
|
||||
PASS=\$((PASS+1))
|
||||
else
|
||||
note FAIL "code=\$T40_CODE Retry-After='\$T40_RETRY'"
|
||||
FAIL=\$((FAIL+1))
|
||||
fi
|
||||
|
||||
# ===========================================================================
|
||||
# FUTURE STUBS — placeholders for upcoming tasks (do not affect PASS/FAIL)
|
||||
# ===========================================================================
|
||||
# These are bash comments only. They document planned tests so future tasks
|
||||
# can implement them with the correct numbering context.
|
||||
#
|
||||
# FUTURE T37: GET /api/estimate returns cost preview
|
||||
# GET \$BASE/api/estimate?request=<text>
|
||||
# Assert HTTP 200, estimatedSats is a positive integer
|
||||
# Assert model, inputTokens, outputTokens are present
|
||||
#
|
||||
# FUTURE T38: Anonymous job always hits Lightning gate
|
||||
# FUTURE T41: Anonymous job always hits Lightning gate
|
||||
# Create anonymous job, poll to awaiting_work_payment
|
||||
# Assert response.free_tier is absent or false in all poll responses
|
||||
#
|
||||
# FUTURE T39: Nostr-identified trusted identity → free response
|
||||
# FUTURE T42: Nostr-identified trusted identity → free response
|
||||
# Requires identity with trust_score >= 50 (trusted tier) and daily budget not exhausted
|
||||
# Submit request with identity token
|
||||
# Assert HTTP 200, response.free_tier == true, no invoice created
|
||||
#
|
||||
# FUTURE T40: Timmy initiates a zap
|
||||
# FUTURE T43: Timmy initiates a zap
|
||||
# POST to /api/identity/me/tip (or similar)
|
||||
# Assert Timmy initiates a Lightning outbound payment to caller's LNURL
|
||||
|
||||
|
||||
Reference in New Issue
Block a user