From c7bb5de5e6181c5a4711362dae807001d062517a Mon Sep 17 00:00:00 2001 From: alexpaynex <55271826-alexpaynex@users.noreply.replit.com> Date: Thu, 19 Mar 2026 21:09:50 +0000 Subject: [PATCH] =?UTF-8?q?task/35:=20Testkit=20T25=E2=80=93T36=20?= =?UTF-8?q?=E2=80=94=20Nostr=20identity=20+=20trust=20engine=20coverage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 12 new tests added to artifacts/api-server/src/routes/testkit.ts Inserted before the Summary block, after T24 (cost ledger). T25 — POST /identity/challenge: HTTP 200, nonce=64-char hex, expiresAt=ISO T26 — POST /identity/verify {}: HTTP 400, non-empty error T27 — POST /identity/verify fake nonce: HTTP 401, error contains "Nonce not found" (uses a plausible-looking event structure to hit the nonce check, not the signature check — tests the right layer) T28 — GET /identity/me no header: HTTP 401, error contains "Missing" T29 — GET /identity/me invalid token: HTTP 401 (Invalid/expired wording) T30 — POST /sessions bad X-Nostr-Token: HTTP 401, "Invalid or expired", no sessionId T31 — POST /jobs bad X-Nostr-Token: HTTP 401, "Invalid or expired" T32 — POST /sessions anonymous: HTTP 201, trust_tier="anonymous"; captures T32_SESSION_ID T33 — POST /jobs anonymous: HTTP 201, trust_tier="anonymous"; captures T33_JOB_ID T34 — GET /jobs/:id (using T33_JOB_ID): HTTP 200, trust_tier non-null and "anonymous" T35 — GET /sessions/:id (using T32_SESSION_ID): HTTP 200, trust_tier="anonymous" T36 — Full challenge→sign→verify E2E: inline node CJS script generates ephemeral secp256k1 keypair via nostr-tools CJS bundle, POSTs challenge, signs kind=27235 event with finalizeEvent(), verifies → nostr_token, GETs /identity/me, asserts tier=new, interactionCount=0, pubkey matches. Guard: SKIP if node not in PATH or script fails. ## nostr-tools import strategy nostr-tools v2 is ESM-only. CJS workaround: the package ships a CJS bundle at lib/cjs/index.js. T36 uses require() with the absolute path to that bundle. Falls back to bare require('nostr-tools') for portability, exits with code 1 if neither works — bash guard catches this and marks T36 SKIP (not FAIL). ## Stubs T37–T40 added as bash block comments after T36 Format: `# FUTURE T3N: ` so they are grepped easily. Covers: GET /api/estimate (cost preview), anonymous Lightning gate, trusted free tier, Timmy-initiates-zap. Does not affect PASS/FAIL totals. ## TIMMY_TEST_PLAN.md updated New "Nostr identity + trust engine (tests 25–36)" section added to the test table. ## TypeScript: 0 errors. All 12 tests smoke-tested individually against localhost:8080. T25-T35: all correct HTTP status codes and JSON fields verified via curl. T36: full E2E verified — tier=new, icount=0, pubkey matches /identity/me response. --- TIMMY_TEST_PLAN.md | 17 ++ artifacts/api-server/src/routes/testkit.ts | 332 +++++++++++++++++++++ 2 files changed, 349 insertions(+) diff --git a/TIMMY_TEST_PLAN.md b/TIMMY_TEST_PLAN.md index bf2f171..d4e5d51 100644 --- a/TIMMY_TEST_PLAN.md +++ b/TIMMY_TEST_PLAN.md @@ -57,6 +57,23 @@ Requirements: `curl`, `bash`, `jq` — nothing else. | 15 | Request without macaroon | Same endpoint, no `Authorization` header → HTTP 401 | | 16 | Topup invoice creation | `POST /api/sessions/:id/topup {"amount_sats":500}` with macaroon → HTTP 200, `topup.paymentRequest` present, `topup.amountSats=500` | +### Nostr identity + trust engine (tests 25–36) + +| # | Name | What it checks | +|---|------|----------------| +| 25 | Challenge nonce | `POST /api/identity/challenge` → HTTP 200, `nonce` is 64-char hex, `expiresAt` is ISO in future | +| 26 | Verify: missing event | `POST /api/identity/verify {}` → HTTP 400, non-empty `error` | +| 27 | Verify: unknown nonce | `POST /api/identity/verify` with fake nonce in content → HTTP 401, `error` contains "Nonce not found" | +| 28 | Me: no token | `GET /api/identity/me` without header → HTTP 401, `error` contains "Missing" | +| 29 | Me: invalid token | `GET /api/identity/me` with `X-Nostr-Token: totally.invalid.token` → HTTP 401 | +| 30 | Sessions: bogus token | `POST /api/sessions` with `X-Nostr-Token: badtoken` → HTTP 401, no `sessionId` in response | +| 31 | Jobs: bogus token | `POST /api/jobs` with `X-Nostr-Token: badtoken` → HTTP 401 | +| 32 | Sessions anonymous tier | `POST /api/sessions` (no token) → HTTP 201, `trust_tier == "anonymous"` | +| 33 | Jobs anonymous tier | `POST /api/jobs` (no token) → HTTP 201, `trust_tier == "anonymous"` | +| 34 | GET jobs/:id includes tier | `GET /api/jobs/:id` → HTTP 200, `trust_tier` non-null (anonymous job → `"anonymous"`) | +| 35 | GET sessions/:id includes tier | `GET /api/sessions/:id` → HTTP 200, `trust_tier == "anonymous"` | +| 36 | Full challenge→sign→verify | Inline node script: generate keypair, challenge, sign kind=27235 event, verify → token; GET /identity/me → tier=new, pubkey matches | + --- ## Architecture notes for reviewers diff --git a/artifacts/api-server/src/routes/testkit.ts b/artifacts/api-server/src/routes/testkit.ts index 546c81c..35d9111 100644 --- a/artifacts/api-server/src/routes/testkit.ts +++ b/artifacts/api-server/src/routes/testkit.ts @@ -745,6 +745,338 @@ else SKIP=\$((SKIP+1)) fi +# =========================================================================== +# T25–T36 Nostr identity + trust engine +# =========================================================================== + +# --------------------------------------------------------------------------- +# T25 — POST /identity/challenge returns valid nonce +# --------------------------------------------------------------------------- +sep "Test 25 — POST /identity/challenge returns valid nonce" +T25_RES=\$(curl -s -w "\\n%{http_code}" -X POST "\$BASE/api/identity/challenge") +T25_BODY=\$(body_of "\$T25_RES"); T25_CODE=\$(code_of "\$T25_RES") +T25_NONCE=\$(echo "\$T25_BODY" | jq -r '.nonce' 2>/dev/null || echo "") +T25_EXP=\$(echo "\$T25_BODY" | jq -r '.expiresAt' 2>/dev/null || echo "") +T25_NONCE_OK=false; T25_EXP_OK=false +[[ "\$T25_NONCE" =~ ^[0-9a-f]{64}\$ ]] && T25_NONCE_OK=true || true +[[ "\$T25_EXP" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}T ]] && T25_EXP_OK=true || true +if [[ "\$T25_CODE" == "200" && "\$T25_NONCE_OK" == "true" && "\$T25_EXP_OK" == "true" ]]; then + note PASS "HTTP 200, nonce=64-char-hex, expiresAt=ISO" + PASS=\$((PASS+1)) +else + note FAIL "code=\$T25_CODE nonce='\$T25_NONCE' expiresAt='\$T25_EXP'" + FAIL=\$((FAIL+1)) +fi + +# --------------------------------------------------------------------------- +# T26 — POST /identity/verify with missing event body → 400 +# --------------------------------------------------------------------------- +sep "Test 26 — POST /identity/verify missing event → 400" +T26_RES=\$(curl -s -w "\\n%{http_code}" -X POST "\$BASE/api/identity/verify" \\ + -H "Content-Type: application/json" -d '{}') +T26_BODY=\$(body_of "\$T26_RES"); T26_CODE=\$(code_of "\$T26_RES") +T26_ERR=\$(echo "\$T26_BODY" | jq -r '.error' 2>/dev/null || echo "") +if [[ "\$T26_CODE" == "400" && -n "\$T26_ERR" ]]; then + note PASS "HTTP 400, error: \${T26_ERR:0:80}" + PASS=\$((PASS+1)) +else + note FAIL "code=\$T26_CODE body=\$T26_BODY" + FAIL=\$((FAIL+1)) +fi + +# --------------------------------------------------------------------------- +# T27 — POST /identity/verify with unknown nonce → 401 "Nonce not found" +# Uses a real-looking (structurally plausible) event with a fake nonce. +# --------------------------------------------------------------------------- +sep "Test 27 — POST /identity/verify unknown nonce → 401" +T27_RES=\$(curl -s -w "\\n%{http_code}" -X POST "\$BASE/api/identity/verify" \\ + -H "Content-Type: application/json" \\ + -d '{"event":{"pubkey":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","content":"0000000000000000000000000000000000000000000000000000000000000000","kind":27235,"tags":[],"created_at":1700000000,"id":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","sig":"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"}}') +T27_BODY=\$(body_of "\$T27_RES"); T27_CODE=\$(code_of "\$T27_RES") +T27_ERR=\$(echo "\$T27_BODY" | jq -r '.error' 2>/dev/null || echo "") +if [[ "\$T27_CODE" == "401" && "\$T27_ERR" == *"Nonce not found"* ]]; then + note PASS "HTTP 401, error contains 'Nonce not found'" + PASS=\$((PASS+1)) +else + note FAIL "code=\$T27_CODE err='\$T27_ERR' body=\$T27_BODY" + FAIL=\$((FAIL+1)) +fi + +# --------------------------------------------------------------------------- +# T28 — GET /identity/me with no X-Nostr-Token header → 401 "Missing" +# --------------------------------------------------------------------------- +sep "Test 28 — GET /identity/me without token → 401" +T28_RES=\$(curl -s -w "\\n%{http_code}" "\$BASE/api/identity/me") +T28_BODY=\$(body_of "\$T28_RES"); T28_CODE=\$(code_of "\$T28_RES") +T28_ERR=\$(echo "\$T28_BODY" | jq -r '.error' 2>/dev/null || echo "") +if [[ "\$T28_CODE" == "401" && "\$T28_ERR" == *"Missing"* ]]; then + note PASS "HTTP 401, error contains 'Missing'" + PASS=\$((PASS+1)) +else + note FAIL "code=\$T28_CODE err='\$T28_ERR'" + FAIL=\$((FAIL+1)) +fi + +# --------------------------------------------------------------------------- +# T29 — GET /identity/me with invalid X-Nostr-Token → 401 +# --------------------------------------------------------------------------- +sep "Test 29 — GET /identity/me with invalid token → 401" +T29_RES=\$(curl -s -w "\\n%{http_code}" "\$BASE/api/identity/me" \\ + -H "X-Nostr-Token: totally.invalid.token") +T29_BODY=\$(body_of "\$T29_RES"); T29_CODE=\$(code_of "\$T29_RES") +T29_ERR=\$(echo "\$T29_BODY" | jq -r '.error' 2>/dev/null || echo "") +if [[ "\$T29_CODE" == "401" && ( "\$T29_ERR" == *"Invalid"* || "\$T29_ERR" == *"expired"* ) ]]; then + note PASS "HTTP 401, error: \${T29_ERR:0:80}" + PASS=\$((PASS+1)) +else + note FAIL "code=\$T29_CODE err='\$T29_ERR'" + FAIL=\$((FAIL+1)) +fi + +# --------------------------------------------------------------------------- +# T30 — POST /sessions with bogus X-Nostr-Token → 401 +# --------------------------------------------------------------------------- +sep "Test 30 — POST /sessions with bad token → 401" +T30_RES=\$(curl -s -w "\\n%{http_code}" -X POST "\$BASE/api/sessions" \\ + -H "Content-Type: application/json" \\ + -H "X-Nostr-Token: badtoken" \\ + -d '{"amount_sats":200}') +T30_BODY=\$(body_of "\$T30_RES"); T30_CODE=\$(code_of "\$T30_RES") +T30_ERR=\$(echo "\$T30_BODY" | jq -r '.error' 2>/dev/null || echo "") +T30_SESSION=\$(echo "\$T30_BODY" | jq -r '.sessionId' 2>/dev/null || echo "") +if [[ "\$T30_CODE" == "401" && "\$T30_ERR" == *"Invalid or expired"* && -z "\$T30_SESSION" ]]; then + note PASS "HTTP 401, error contains 'Invalid or expired', no sessionId created" + PASS=\$((PASS+1)) +else + note FAIL "code=\$T30_CODE err='\$T30_ERR' sessionId='\$T30_SESSION'" + FAIL=\$((FAIL+1)) +fi + +# --------------------------------------------------------------------------- +# T31 — POST /jobs with bogus X-Nostr-Token → 401 +# --------------------------------------------------------------------------- +sep "Test 31 — POST /jobs with bad token → 401" +T31_RES=\$(curl -s -w "\\n%{http_code}" -X POST "\$BASE/api/jobs" \\ + -H "Content-Type: application/json" \\ + -H "X-Nostr-Token: badtoken" \\ + -d '{"request":"What is Bitcoin?"}') +T31_BODY=\$(body_of "\$T31_RES"); T31_CODE=\$(code_of "\$T31_RES") +T31_ERR=\$(echo "\$T31_BODY" | jq -r '.error' 2>/dev/null || echo "") +if [[ "\$T31_CODE" == "401" && "\$T31_ERR" == *"Invalid or expired"* ]]; then + note PASS "HTTP 401, error contains 'Invalid or expired'" + PASS=\$((PASS+1)) +else + note FAIL "code=\$T31_CODE err='\$T31_ERR' body=\$T31_BODY" + FAIL=\$((FAIL+1)) +fi + +# --------------------------------------------------------------------------- +# T32 — POST /sessions (anonymous) includes trust_tier = "anonymous" +# Capture T32_SESSION_ID for reuse in T35. +# --------------------------------------------------------------------------- +sep "Test 32 — POST /sessions anonymous → trust_tier=anonymous" +T32_RES=\$(curl -s -w "\\n%{http_code}" -X POST "\$BASE/api/sessions" \\ + -H "Content-Type: application/json" \\ + -d '{"amount_sats":200}') +T32_BODY=\$(body_of "\$T32_RES"); T32_CODE=\$(code_of "\$T32_RES") +T32_SESSION_ID=\$(echo "\$T32_BODY" | jq -r '.sessionId' 2>/dev/null || echo "") +T32_TIER=\$(echo "\$T32_BODY" | jq -r '.trust_tier' 2>/dev/null || echo "") +if [[ "\$T32_CODE" == "201" && "\$T32_TIER" == "anonymous" && -n "\$T32_SESSION_ID" ]]; then + note PASS "HTTP 201, trust_tier=anonymous, sessionId=\${T32_SESSION_ID:0:8}..." + PASS=\$((PASS+1)) +else + note FAIL "code=\$T32_CODE trust_tier='\$T32_TIER' body=\$T32_BODY" + FAIL=\$((FAIL+1)) +fi + +# --------------------------------------------------------------------------- +# T33 — POST /jobs (anonymous) includes trust_tier = "anonymous" +# Capture T33_JOB_ID for reuse in T34. +# --------------------------------------------------------------------------- +sep "Test 33 — POST /jobs anonymous → trust_tier=anonymous" +T33_RES=\$(curl -s -w "\\n%{http_code}" -X POST "\$BASE/api/jobs" \\ + -H "Content-Type: application/json" \\ + -d '{"request":"T33 trust tier probe"}') +T33_BODY=\$(body_of "\$T33_RES"); T33_CODE=\$(code_of "\$T33_RES") +T33_JOB_ID=\$(echo "\$T33_BODY" | jq -r '.jobId' 2>/dev/null || echo "") +T33_TIER=\$(echo "\$T33_BODY" | jq -r '.trust_tier' 2>/dev/null || echo "") +if [[ "\$T33_CODE" == "201" && "\$T33_TIER" == "anonymous" && -n "\$T33_JOB_ID" ]]; then + note PASS "HTTP 201, trust_tier=anonymous, jobId=\${T33_JOB_ID:0:8}..." + PASS=\$((PASS+1)) +else + note FAIL "code=\$T33_CODE trust_tier='\$T33_TIER' body=\$T33_BODY" + FAIL=\$((FAIL+1)) +fi + +# --------------------------------------------------------------------------- +# T34 — GET /jobs/:id always includes trust_tier (uses T33_JOB_ID) +# --------------------------------------------------------------------------- +sep "Test 34 — GET /jobs/:id always includes trust_tier" +if [[ -n "\$T33_JOB_ID" && "\$T33_JOB_ID" != "null" ]]; then + T34_RES=\$(curl -s -w "\\n%{http_code}" "\$BASE/api/jobs/\$T33_JOB_ID") + T34_BODY=\$(body_of "\$T34_RES"); T34_CODE=\$(code_of "\$T34_RES") + T34_TIER=\$(echo "\$T34_BODY" | jq -r '.trust_tier' 2>/dev/null || echo "") + if [[ "\$T34_CODE" == "200" && -n "\$T34_TIER" && "\$T34_TIER" != "null" && "\$T34_TIER" == "anonymous" ]]; then + note PASS "HTTP 200, trust_tier=\$T34_TIER (anonymous job)" + PASS=\$((PASS+1)) + else + note FAIL "code=\$T34_CODE trust_tier='\$T34_TIER' body=\$T34_BODY" + FAIL=\$((FAIL+1)) + fi +else + note SKIP "No T33_JOB_ID available — skipping" + SKIP=\$((SKIP+1)) +fi + +# --------------------------------------------------------------------------- +# T35 — GET /sessions/:id always includes trust_tier (uses T32_SESSION_ID) +# --------------------------------------------------------------------------- +sep "Test 35 — GET /sessions/:id always includes trust_tier" +if [[ -n "\$T32_SESSION_ID" && "\$T32_SESSION_ID" != "null" ]]; then + T35_RES=\$(curl -s -w "\\n%{http_code}" "\$BASE/api/sessions/\$T32_SESSION_ID") + T35_BODY=\$(body_of "\$T35_RES"); T35_CODE=\$(code_of "\$T35_RES") + T35_TIER=\$(echo "\$T35_BODY" | jq -r '.trust_tier' 2>/dev/null || echo "") + if [[ "\$T35_CODE" == "200" && "\$T35_TIER" == "anonymous" ]]; then + note PASS "HTTP 200, trust_tier=\$T35_TIER" + PASS=\$((PASS+1)) + else + note FAIL "code=\$T35_CODE trust_tier='\$T35_TIER' body=\$T35_BODY" + FAIL=\$((FAIL+1)) + fi +else + note SKIP "No T32_SESSION_ID available — skipping" + SKIP=\$((SKIP+1)) +fi + +# --------------------------------------------------------------------------- +# T36 — Full challenge → sign → verify end-to-end flow (inline node script) +# Uses nostr-tools CJS bundle (node_modules absolute path for portability). +# Guards on node availability and script success; SKIP if either fails. +# --------------------------------------------------------------------------- +sep "Test 36 — Full challenge → sign → verify (Nostr identity flow)" +T36_SKIP=false +if ! command -v node >/dev/null 2>&1; then + note SKIP "node not found in PATH — skipping T36" + SKIP=\$((SKIP+1)) + T36_SKIP=true +fi + +if [[ "\$T36_SKIP" == "false" ]]; then + # Write a temp CommonJS script so we can import nostr-tools via require() + # with an absolute path to the CJS bundle. Falls back to SKIP on any failure. + T36_TMPFILE=\$(mktemp /tmp/nostr_t36_XXXXXX.cjs) + cat > "\$T36_TMPFILE" << 'NODESCRIPT' +'use strict'; +const https = require('https'); +const http = require('http'); +const BASE = process.argv[2]; + +// Try the absolute CJS bundle path first (dev/Replit), then bare module name. +let nt; +const NOSTR_CJS = '/home/runner/workspace/artifacts/api-server/node_modules/nostr-tools/lib/cjs/index.js'; +try { nt = require(NOSTR_CJS); } catch { try { nt = require('nostr-tools'); } catch { process.stderr.write('nostr-tools not importable\n'); process.exit(1); } } +const { generateSecretKey, getPublicKey, finalizeEvent } = nt; + +function request(url, opts, body) { + return new Promise((resolve, reject) => { + const u = new URL(url); + const mod = u.protocol === 'https:' ? https : http; + const req = mod.request(u, opts, (res) => { + let data = ''; + res.on('data', c => data += c); + res.on('end', () => resolve({ status: res.statusCode, body: data })); + }); + req.on('error', reject); + if (body) req.write(body); + req.end(); + }); +} + +async function main() { + // 1. Generate ephemeral keypair + const sk = generateSecretKey(); + const pubkey = getPublicKey(sk); + + // 2. Get challenge nonce + const chalRes = await request(BASE + '/api/identity/challenge', { method: 'POST', headers: { 'Content-Type': 'application/json' } }, '{}'); + if (chalRes.status !== 200) { process.stderr.write('challenge failed: ' + chalRes.status + '\n'); process.exit(1); } + const { nonce } = JSON.parse(chalRes.body); + + // 3. Build and sign Nostr event (kind=27235, content=nonce) + const event = finalizeEvent({ kind: 27235, content: nonce, tags: [], created_at: Math.floor(Date.now() / 1000) }, sk); + + // 4. POST /identity/verify + const verRes = await request(BASE + '/api/identity/verify', + { method: 'POST', headers: { 'Content-Type': 'application/json' } }, + JSON.stringify({ event })); + if (verRes.status !== 200) { process.stderr.write('verify failed: ' + verRes.status + ' ' + verRes.body + '\n'); process.exit(1); } + const verBody = JSON.parse(verRes.body); + const token = verBody.nostr_token; + const tier = verBody.trust && verBody.trust.tier; + const icount = verBody.trust && verBody.trust.interactionCount; + + // 5. GET /identity/me + const meRes = await request(BASE + '/api/identity/me', + { method: 'GET', headers: { 'X-Nostr-Token': token } }); + if (meRes.status !== 200) { process.stderr.write('identity/me failed: ' + meRes.status + '\n'); process.exit(1); } + const meBody = JSON.parse(meRes.body); + const meTier = meBody.trust && meBody.trust.tier; + const mePubkey = meBody.pubkey; + + // Print result as single JSON line + process.stdout.write(JSON.stringify({ pubkey, tier, icount, meTier, mePubkey }) + '\n'); +} +main().catch(err => { process.stderr.write(String(err) + '\n'); process.exit(1); }); +NODESCRIPT + + T36_OUT=\$(node "\$T36_TMPFILE" "\$BASE" 2>/dev/null) + T36_EXIT=\$? + rm -f "\$T36_TMPFILE" + + if [[ \$T36_EXIT -ne 0 || -z "\$T36_OUT" ]]; then + note SKIP "node script failed (nostr-tools unavailable or network error)" + SKIP=\$((SKIP+1)) + else + T36_TIER=\$(echo "\$T36_OUT" | jq -r '.tier' 2>/dev/null || echo "") + T36_ICOUNT=\$(echo "\$T36_OUT" | jq -r '.icount' 2>/dev/null || echo "") + T36_METIER=\$(echo "\$T36_OUT" | jq -r '.meTier' 2>/dev/null || echo "") + T36_PK=\$(echo "\$T36_OUT" | jq -r '.pubkey' 2>/dev/null || echo "") + T36_MEPK=\$(echo "\$T36_OUT" | jq -r '.mePubkey' 2>/dev/null || echo "") + if [[ "\$T36_TIER" == "new" && "\$T36_ICOUNT" == "0" \\ + && "\$T36_METIER" == "new" && "\$T36_PK" == "\$T36_MEPK" && -n "\$T36_PK" ]]; then + note PASS "challenge→sign→verify OK: tier=\$T36_TIER icount=\$T36_ICOUNT identity/me pubkey matches" + PASS=\$((PASS+1)) + else + note FAIL "tier=\$T36_TIER icount=\$T36_ICOUNT meTier=\$T36_METIER pkMatch=\$([[ \$T36_PK == \$T36_MEPK ]] && echo yes || echo no)" + FAIL=\$((FAIL+1)) + fi + fi +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 (Task #27 — cost routing): GET /api/estimate returns cost preview +# GET \$BASE/api/estimate?request= +# Assert HTTP 200, estimatedSats is a positive integer +# Assert model, inputTokens, outputTokens are present +# +# FUTURE T38 (Task #27 — cost routing): 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 (Task #27 — free tier): 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 (Task #29 — Timmy as peer): Timmy initiates a zap +# POST to /api/identity/me/tip (or similar) +# Assert Timmy initiates a Lightning outbound payment to caller's LNURL + # --------------------------------------------------------------------------- # Summary # ---------------------------------------------------------------------------