diff --git a/artifacts/api-server/src/routes/testkit.ts b/artifacts/api-server/src/routes/testkit.ts index ee67f86..f21b9d7 100644 --- a/artifacts/api-server/src/routes/testkit.ts +++ b/artifacts/api-server/src/routes/testkit.ts @@ -23,6 +23,10 @@ const router = Router(); * - T20 ADDED: POST /api/jobs/:id/refund guards — financial endpoint, zero prior coverage. * - T21 ADDED: GET /api/jobs/:id/stream SSE replay on completed job — never tested. * - T22 ADDED: GET /api/sessions/:id unknown ID → 404 — never tested. + * - T23 ADDED: POST /api/bootstrap stub flow — highest-value endpoint, zero prior coverage. + * 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. */ router.get("/testkit", (req: Request, res: Response) => { const proto = @@ -644,6 +648,97 @@ else FAIL=\$((FAIL+1)) fi +# --------------------------------------------------------------------------- +# Test 23 — Bootstrap: create → stub-pay → poll provisioning state +# Highest-value paid feature (10,000 sats default) — previously zero coverage. +# Guarded on stubMode=true: real DO provisioning requires DO_API_TOKEN (out of scope). +# Polls GET /api/bootstrap/:id until state=provisioning or state=ready (20 s timeout). +# --------------------------------------------------------------------------- +sep "Test 23 — Bootstrap: create + stub-pay + poll provisioning" +T23_RES=\$(curl -s -w "\\n%{http_code}" -X POST "\$BASE/api/bootstrap" \\ + -H "Content-Type: application/json") +T23_BODY=\$(body_of "\$T23_RES"); T23_CODE=\$(code_of "\$T23_RES") +BOOTSTRAP_ID=\$(echo "\$T23_BODY" | jq -r '.bootstrapJobId' 2>/dev/null || echo "") +T23_STUB=\$(echo "\$T23_BODY" | jq -r '.stubMode' 2>/dev/null || echo "false") +BOOTSTRAP_HASH=\$(echo "\$T23_BODY" | jq -r '.invoice.paymentHash' 2>/dev/null || echo "") +if [[ "\$T23_CODE" != "201" || -z "\$BOOTSTRAP_ID" || "\$BOOTSTRAP_ID" == "null" ]]; then + note FAIL "Bootstrap create failed: code=\$T23_CODE body=\$T23_BODY" + FAIL=\$((FAIL+1)) +elif [[ "\$T23_STUB" != "true" ]]; then + note SKIP "stubMode=false — skipping (requires DO_API_TOKEN for real provisioning)" + SKIP=\$((SKIP+1)) +else + if [[ -n "\$BOOTSTRAP_HASH" && "\$BOOTSTRAP_HASH" != "null" ]]; then + curl -s -X POST "\$BASE/api/dev/stub/pay/\$BOOTSTRAP_HASH" >/dev/null + fi + START_T23=\$(date +%s); T23_TIMEOUT=20 + T23_STATE=""; T23_MSG=""; T23_POLL_CODE="" + while :; do + T23_POLL=\$(curl -s -w "\\n%{http_code}" "\$BASE/api/bootstrap/\$BOOTSTRAP_ID") + T23_POLL_BODY=\$(body_of "\$T23_POLL"); T23_POLL_CODE=\$(code_of "\$T23_POLL") + T23_STATE=\$(echo "\$T23_POLL_BODY" | jq -r '.state' 2>/dev/null || echo "") + T23_MSG=\$(echo "\$T23_POLL_BODY" | jq -r '.message' 2>/dev/null || echo "") + NOW_T23=\$(date +%s); ELAPSED_T23=\$((NOW_T23 - START_T23)) + if [[ "\$T23_STATE" == "provisioning" || "\$T23_STATE" == "ready" ]]; then break; fi + if (( ELAPSED_T23 > T23_TIMEOUT )); then break; fi + sleep 2 + done + if [[ "\$T23_POLL_CODE" == "200" \\ + && ("\$T23_STATE" == "provisioning" || "\$T23_STATE" == "ready") \\ + && -n "\$T23_MSG" && "\$T23_MSG" != "null" ]]; then + note PASS "state=\$T23_STATE in \$ELAPSED_T23 s, message present, bootstrapJobId=\$BOOTSTRAP_ID" + PASS=\$((PASS+1)) + else + note FAIL "code=\$T23_POLL_CODE state=\$T23_STATE elapsed=\${ELAPSED_T23}s body=\$T23_POLL_BODY" + FAIL=\$((FAIL+1)) + fi +fi + +# --------------------------------------------------------------------------- +# Test 24 — Cost ledger completeness after job completion +# Uses the completed JOB_ID from T6 (guarded on STATE_T6=complete). +# Verifies 8 cost-ledger fields are non-null: +# actualInputTokens, actualOutputTokens, totalTokens, actualCostUsd, +# actualAmountSats, workAmountSats, refundAmountSats, refundState. +# Honest-accounting invariant: actualAmountSats <= workAmountSats. +# refundState must be one of: not_applicable | pending | paid. +# --------------------------------------------------------------------------- +sep "Test 24 — Cost ledger completeness (job completed)" +if [[ -n "\$JOB_ID" && "\$STATE_T6" == "complete" ]]; then + T24_RES=\$(curl -s -w "\\n%{http_code}" "\$BASE/api/jobs/\$JOB_ID") + T24_BODY=\$(body_of "\$T24_RES"); T24_CODE=\$(code_of "\$T24_RES") + T24_LEDGER=\$(echo "\$T24_BODY" | jq '.costLedger' 2>/dev/null || echo "null") + T24_REFUND_STATE=\$(echo "\$T24_LEDGER" | jq -r '.refundState' 2>/dev/null || echo "") + T24_FIELDS_OK=true + for field in actualInputTokens actualOutputTokens totalTokens actualCostUsd actualAmountSats workAmountSats refundAmountSats refundState; do + VAL=\$(echo "\$T24_LEDGER" | jq -r ".\$field" 2>/dev/null || echo "MISSING") + [[ -z "\$VAL" || "\$VAL" == "null" ]] && T24_FIELDS_OK=false || true + done + T24_INV_OK=\$(echo "\$T24_LEDGER" | jq ' + (.actualAmountSats != null) and + (.workAmountSats != null) and + (.actualAmountSats <= .workAmountSats) and + (.refundAmountSats >= 0) + ' 2>/dev/null || echo "false") + if [[ "\$T24_CODE" == "200" \\ + && "\$T24_LEDGER" != "null" \\ + && "\$T24_FIELDS_OK" == "true" \\ + && "\$T24_INV_OK" == "true" \\ + && "\$T24_REFUND_STATE" =~ ^(not_applicable|pending|paid)\$ ]]; then + T24_ACTUAL=\$(echo "\$T24_LEDGER" | jq -r '.actualAmountSats' 2>/dev/null || echo "?") + T24_WORK=\$(echo "\$T24_LEDGER" | jq -r '.workAmountSats' 2>/dev/null || echo "?") + T24_REFUND=\$(echo "\$T24_LEDGER" | jq -r '.refundAmountSats' 2>/dev/null || echo "?") + note PASS "8 fields non-null, actualAmountSats(\$T24_ACTUAL)<=workAmountSats(\$T24_WORK), refundAmountSats=\$T24_REFUND, refundState=\$T24_REFUND_STATE" + PASS=\$((PASS+1)) + else + note FAIL "code=\$T24_CODE ledger=\$T24_LEDGER fields_ok=\$T24_FIELDS_OK inv_ok=\$T24_INV_OK refundState=\$T24_REFUND_STATE" + FAIL=\$((FAIL+1)) + fi +else + note SKIP "Skipping — job not complete (STATE_T6=\${STATE_T6:-not_set}) or no JOB_ID" + SKIP=\$((SKIP+1)) +fi + # --------------------------------------------------------------------------- # Summary # ---------------------------------------------------------------------------