···109109## Safe commands
110110111111### Testing
112112-- `nu tests/repo_sync_integrity.nu` - Runs the full integration test suite using Nushell. This builds the binary, starts a temporary instance, performs a backfill against a real PDS, and verifies record integrity.
112112+- `nu tests/run_all.nu` - Runs all tests in parallel with automatically assigned free ports. Pass `--skip-creds` to skip tests requiring `.env` credentials, or `--only=[<name>...]` to run a subset.
113113+- `nu tests/repo_sync_integrity.nu` - Performs a backfill against a real PDS, and verifies record integrity compared to hydrant.
113114- `nu tests/verify_crawler.nu` - Verifies full-network crawler functionality using a mock relay.
114114-- `nu tests/throttling_test.nu` - Verifies crawler throttling logic when pending queue is full.
115115-- `nu tests/stream_test.nu` - Tests WebSocket streaming functionality. Verifies both live event streaming during backfill and historical replay with cursor.
116116-- `nu tests/authenticated_stream_test.nu` - Tests authenticated event streaming. Verifies that create, update, and delete actions on a real account are correctly streamed by Hydrant in the correct order. Requires `TEST_REPO` and `TEST_PASSWORD` in `.env`.
115115+- `nu tests/throttling.nu` - Verifies crawler throttling logic when pending queue is full.
116116+- `nu tests/stream.nu` - Tests WebSocket streaming functionality. Verifies both live event streaming during backfill and historical replay with cursor.
117117+- `nu tests/authenticated_stream.nu` - Tests authenticated event streaming. Verifies that create, update, and delete actions on a real account are correctly streamed by Hydrant in the correct order. Requires `TEST_REPO` and `TEST_PASSWORD` in `.env`.
117118- `nu tests/debug_endpoints.nu` - Tests debug/introspection endpoints (`/debug/iter`, `/debug/get`) and verifies DB content and serialization.
118118-- `nu tests/api_test.nu` - Tests management API endpoints (filter, repos, ingestion, sources).
119119-- `nu tests/repos_api_test.nu` - Tests the `/repos` API endpoints including pagination and single-repo lookup.
120120-- `nu tests/signal_filter_test.nu` - Verifies signal-based filtered indexing.
121121-- `nu tests/collection_index_test.nu` - Tests collection-indexed crawling via `listReposByCollection`.
122122-- `nu tests/backlinks_test.nu` - Tests backlinks indexing and XRPC query endpoints (requires `backlinks` feature).
119119+- `nu tests/api.nu` - Tests management API endpoints (filter, repos, ingestion, sources).
120120+- `nu tests/repos_api.nu` - Tests the `/repos` API endpoints including pagination and single-repo lookup.
121121+- `nu tests/signal_filter.nu` - Verifies signal-based filtered indexing.
122122+- `nu tests/by_collection.nu` - Tests by-collection crawling via `listReposByCollection`.
123123+- `nu tests/backlinks.nu` - Tests backlinks indexing and XRPC query endpoints (requires `backlinks` feature).
123124- `nu tests/ephemeral_gc.nu` - Tests ephemeral mode TTL expiration and event watermark cleanup.
124125125126## Rust code style
+22-58
tests/api_test.nu
tests/api.nu
···11#!/usr/bin/env nu
22use common.nu *
3344-# print a failure message, kill any running hydrant instances, and exit.
55-def fail [msg: string, ...pids: int] {
66- print $" FAILED: ($msg)"
77- for pid in $pids {
88- try { kill $pid }
99- }
1010- exit 1
1111-}
1212-134def test-crawler-sources [url: string, pid: int] {
145 print "=== test: crawler sources ==="
156···23142415 # add a relay source
2516 print " POST /crawler/sources (relay)..."
2626- let resp_add = (http post -f -e -t application/json $"($url)/crawler/sources" {
1717+ http post -f -e -t application/json $"($url)/crawler/sources" {
2718 url: "https://bsky.network",
2819 mode: "relay"
2929- })
3030- if $resp_add.status != 201 {
3131- fail $"expected 201, got ($resp_add.status)" $pid
3232- }
2020+ } | assert-status 201 "POST /crawler/sources" $pid
3321 print " ok: 201 Created"
34223523 # verify the source appears with correct fields
···49375038 # posting the same URL with a different mode replaces the existing entry
5139 print " POST /crawler/sources (should override)..."
5252- let resp_replace = (http post -f -e -t application/json $"($url)/crawler/sources" {
4040+ http post -f -e -t application/json $"($url)/crawler/sources" {
5341 url: "https://bsky.network",
5442 mode: "by_collection"
5555- })
5656- if $resp_replace.status != 201 {
5757- fail $"expected 201, got ($resp_replace.status)" $pid
5858- }
4343+ } | assert-status 201 "POST /crawler/sources override" $pid
5944 let after_replace = (http get $"($url)/crawler/sources")
6045 if ($after_replace | length) != 1 {
6146 fail $"expected 1 source after override, got ($after_replace | length)" $pid
···67526853 # remove the source
6954 print " DELETE /crawler/sources..."
7070- let resp_del = (http delete -f -e -t application/json $"($url)/crawler/sources" --data {
5555+ http delete -f -e -t application/json $"($url)/crawler/sources" --data {
7156 url: "https://bsky.network"
7272- })
7373- if $resp_del.status != 200 {
7474- fail $"expected 200, got ($resp_del.status)" $pid
7575- }
5757+ } | assert-status 200 "DELETE /crawler/sources" $pid
7658 let after_del = (http get $"($url)/crawler/sources")
7759 if ($after_del | length) != 0 {
7860 fail "expected empty list after delete" $pid
···81638264 # deleting a non-existent source returns 404
8365 print " DELETE /crawler/sources (should be 404)..."
8484- let resp_del_missing = (http delete -f -e -t application/json $"($url)/crawler/sources" --data {
6666+ http delete -f -e -t application/json $"($url)/crawler/sources" --data {
8567 url: "https://bsky.network"
8686- })
8787- if $resp_del_missing.status != 404 {
8888- fail $"expected 404, got ($resp_del_missing.status)" $pid
8989- }
6868+ } | assert-status 404 "DELETE /crawler/sources missing" $pid
9069 print " ok: 404 for non-existent source"
91709271 print "crawler source tests passed!"
···175154176155 # the task can be stopped at runtime
177156 print " deleting config source at runtime..."
178178- let resp = (http delete -f -e -t application/json $"($url)/crawler/sources" --data {
157157+ http delete -f -e -t application/json $"($url)/crawler/sources" --data {
179158 url: $crawler_url
180180- })
181181- if $resp.status != 200 {
182182- fail $"expected 200, got ($resp.status)" $instance.pid
183183- }
159159+ } | assert-status 200 "DELETE /crawler/sources runtime" $instance.pid
184160 let after_del = (http get $"($url)/crawler/sources")
185161 if ($after_del | length) != 0 {
186162 fail "expected source to be gone after runtime delete" $instance.pid
···225201226202 # add a relay source
227203 print " POST /firehose/sources..."
228228- let resp_add = (http post -f -e -t application/json $"($url)/firehose/sources" {
204204+ http post -f -e -t application/json $"($url)/firehose/sources" {
229205 url: "wss://test.bsky.network"
230230- })
231231- if $resp_add.status != 201 {
232232- fail $"expected 201, got ($resp_add.status)" $pid
233233- }
206206+ } | assert-status 201 "POST /firehose/sources" $pid
234207 print " ok: 201 Created"
235208236209 # verify it appears
···247220248221 # posting the same URL replaces the existing entry
249222 print " POST /firehose/sources (should override)..."
250250- let resp_replace = (http post -f -e -t application/json $"($url)/firehose/sources" {
223223+ http post -f -e -t application/json $"($url)/firehose/sources" {
251224 url: "wss://test.bsky.network"
252252- })
253253- if $resp_replace.status != 201 {
254254- fail $"expected 201, got ($resp_replace.status)" $pid
255255- }
225225+ } | assert-status 201 "POST /firehose/sources override" $pid
256226 let after_replace = (http get $"($url)/firehose/sources")
257227 if ($after_replace | length) != 1 {
258228 fail $"expected 1 source after override, got ($after_replace | length)" $pid
···261231262232 # remove the source
263233 print " DELETE /firehose/sources..."
264264- let resp_del = (http delete -f -e -t application/json $"($url)/firehose/sources" --data {
234234+ http delete -f -e -t application/json $"($url)/firehose/sources" --data {
265235 url: "wss://test.bsky.network"
266266- })
267267- if $resp_del.status != 200 {
268268- fail $"expected 200, got ($resp_del.status)" $pid
269269- }
236236+ } | assert-status 200 "DELETE /firehose/sources" $pid
270237 let after_del = (http get $"($url)/firehose/sources")
271238 if ($after_del | length) != 0 {
272239 fail "expected empty list after delete" $pid
···275242276243 # deleting a non-existent source returns 404
277244 print " DELETE /firehose/sources (should be 404)..."
278278- let resp_del_missing = (http delete -f -e -t application/json $"($url)/firehose/sources" --data {
245245+ http delete -f -e -t application/json $"($url)/firehose/sources" --data {
279246 url: "wss://test.bsky.network"
280280- })
281281- if $resp_del_missing.status != 404 {
282282- fail $"expected 404, got ($resp_del_missing.status)" $pid
283283- }
247247+ } | assert-status 404 "DELETE /firehose/sources missing" $pid
284248 print " ok: 404 for non-existent source"
285249286250 print "firehose source tests passed!"
287251}
288252289253def main [] {
290290- let port = 3007
254254+ let port = resolve-test-port 3007
291255 let url = $"http://localhost:($port)"
292256293257 let binary = build-hydrant
294258295295- let db = (mktemp -d -t hydrant_api_test.XXXXXX)
259259+ let db = (mktemp -d -t hydrant_api.XXXXXX)
296260 print $"db: ($db)"
297261298262 let instance = (with-env { HYDRANT_CRAWLER_URLS: "", HYDRANT_RELAY_HOSTS: "" } {
···308272 kill $instance.pid
309273 sleep 2sec
310274311311- let db_persist = (mktemp -d -t hydrant_api_test.XXXXXX)
275275+ let db_persist = (mktemp -d -t hydrant_api.XXXXXX)
312276 print $"db: ($db_persist)"
313277 test-source-persistence $binary $db_persist $port
314278315279 sleep 1sec
316280317317- let db_config = (mktemp -d -t hydrant_api_test.XXXXXX)
281281+ let db_config = (mktemp -d -t hydrant_api.XXXXXX)
318282 print $"db: ($db_config)"
319283 test-config-source-not-persisted $binary $db_config $port
320284
···3636 } catch {
3737 print "warning: failed to add repo (might already be tracked), continuing..."
3838 }
3939-4040- sleep 5sec
3939+ wait-for-backfill $url
41404241 # 5. perform actions
4342 let collection = "app.bsky.feed.post"
···91909291 # 6. verify
9392 sleep 3sec
9393+9494 print "stopping listener..."
9595 try { kill -9 $stream_pid }
9696···173173 let password = ($env_vars | get --optional TEST_PASSWORD)
174174175175 if ($did | is-empty) or ($password | is-empty) {
176176- print "error: TEST_REPO and TEST_PASSWORD must be set in .env"
177177- exit 1
176176+ print "SKIP: TEST_REPO and TEST_PASSWORD not set in .env"
177177+ exit 0
178178 }
179179180180 let pds_url = resolve-pds $did
···182182 # ensure build
183183 build-hydrant | ignore
184184185185+ let port = resolve-test-port 3005
186186+185187 print "=== running single-relay test ==="
186188 let relay1 = "wss://relay.fire.hose.cam"
187187- let success1 = run-auth-test $did $password $pds_url $relay1 3005
189189+ let success1 = run-auth-test $did $password $pds_url $relay1 $port
188190189191 print ""
190192 print "=== running multi-relay test ==="
191193 let relay_multi = "wss://relay.fire.hose.cam,wss://relay3.fr.hose.cam,wss://relay1.us-west.bsky.network,wss://relay1.us-east.bsky.network"
192192- let success2 = run-auth-test $did $password $pds_url $relay_multi 3015
194194+ let success2 = run-auth-test $did $password $pds_url $relay_multi $port
193195194196 if $success1 and $success2 {
195197 print ""
+1-1
tests/backlinks_test.nu
tests/backlinks.nu
···180180181181def main [] {
182182 let did = "did:plc:dfl62fgb7wtjj3fcbb72naae"
183183- let port = 3020
183183+ let port = resolve-test-port 3020
184184 let url = $"http://localhost:($port)"
185185 let db_path = (mktemp -d -t hydrant_backlinks_test.XXXXXX)
186186
···88use common.nu *
991010def main [] {
1111- let port = 3015
1111+ let port = resolve-test-port 3015
1212 let url = $"http://localhost:($port)"
1313 let db_path = (mktemp -d -t hydrant_collection_index_test.XXXXXX)
1414 let collection = "app.bsky.graph.starterpack"
+49-1
tests/common.nu
···11+# print a failure message, kill any running hydrant pids, and exit with code 1.
22+export def fail [msg: string, ...pids: int] {
33+ print $" FAILED: ($msg)"
44+ for pid in $pids {
55+ try { kill $pid }
66+ }
77+ exit 1
88+}
99+1010+# pipe a full http response (obtained via -f -e flags) through this to assert an expected
1111+# status code. on mismatch, prints the status, response body, kills any supplied pids, and exits.
1212+# on success, returns the response record so callers can inspect fields further.
1313+export def assert-status [expected: int, label: string, ...pids: int] {
1414+ let resp = $in
1515+ if $resp.status != $expected {
1616+ print $" ($label): expected status ($expected), got ($resp.status)"
1717+ try { print $" body: ($resp.body)" }
1818+ for pid in $pids { try { kill $pid } }
1919+ exit 1
2020+ }
2121+ $resp
2222+}
2323+2424+# resolve the api port this test instance should use.
2525+export def resolve-test-port [default: int] {
2626+ $env | get --optional HYDRANT_API_PORT | default ($default | into string) | into int
2727+}
2828+2929+# resolve the debug port this test instance should use.
3030+export def resolve-test-debug-port [default: int] {
3131+ $env | get --optional HYDRANT_DEBUG_PORT | default ($default | into string) | into int
3232+}
3333+3434+# resolve the mock relay port for tests that need one.
3535+export def resolve-test-mock-port [default: int] {
3636+ $env | get --optional HYDRANT_TEST_MOCK_PORT | default ($default | into string) | into int
3737+}
3838+3939+export def resolve-binary [default: string] {
4040+ $env | get --optional HYDRANT_BINARY | default $default
4141+}
4242+143export def load-env-file [] {
244 if (".env" | path exists) {
345 let content = (open .env)
···51935294# build the hydrant binary
5395export def build-hydrant [] {
9696+ if ($env | get --optional HYDRANT_BINARY | is-not-empty) {
9797+ return $env.HYDRANT_BINARY
9898+ }
5499 print "building hydrant..."
55100 cargo build
56101 "target/debug/hydrant"
···5810359104# build the hydrant binary with extra cargo features (space-separated string)
60105export def build-hydrant-features [features: string] {
106106+ if ($env | get --optional HYDRANT_BINARY | is-not-empty) {
107107+ return $env.HYDRANT_BINARY
108108+ }
61109 print $"building hydrant with features: ($features)..."
62110 cargo build --features $features
63111 "target/debug/hydrant"
···74122 HYDRANT_FULL_NETWORK: "false",
75123 HYDRANT_API_PORT: ($port | into string),
76124 HYDRANT_ENABLE_DEBUG: "true",
7777- HYDRANT_DEBUG_PORT: ($port + 1 | into string),
125125+ HYDRANT_DEBUG_PORT: (resolve-test-debug-port ($port + 1) | into string),
78126 HYDRANT_PLC_URL: "https://plc.gaze.systems",
79127 RUST_LOG: "debug,hyper=error,tokio=error,h2=error,tower=error,rustls=error"
80128 } | merge $hydrant_vars
+8-8
tests/debug_endpoints.nu
···3344def main [] {
55 let did = "did:web:guestbook.gaze.systems"
66- let port = 3003
77- let debug_port = $port + 1
66+ let port = resolve-test-port 3003
77+ let debug_port = resolve-test-debug-port ($port + 1)
88 let url = $"http://localhost:($port)"
99 let debug_url = $"http://localhost:($debug_port)"
1010 let db_path = (mktemp -d -t hydrant_debug_test.XXXXXX)
···7676 print "PASSED: /debug/iter on events returns JSON objects"
7777 }
78787979- # 4. Test size in /stats
8080- print "testing size in /stats"
7979+ # 4. Test sizes in /stats
8080+ print "testing sizes in /stats"
8181 let stats = http get $"($url)/stats"
8282- if ($stats.size | is-empty) {
8383- print "FAILED: /stats returned empty size"
8282+ if ($stats.sizes | is-empty) {
8383+ print "FAILED: /stats returned empty sizes"
8484 exit 1
8585 }
8686- if not ("repos" in ($stats.size | columns)) {
8787- print "FAILED: /stats missing 'repos' in size"
8686+ if not ("repos" in ($stats.sizes | columns)) {
8787+ print "FAILED: /stats missing 'repos' in sizes"
8888 exit 1
8989 }
9090 print "PASSED: /stats returns keyspace sizes"
+5-5
tests/ephemeral_gc.nu
···22use common.nu *
3344# start hydrant in ephemeral mode
55-def run-ephemeral-instance [name: string, scenario_closure: closure] {
66- let port = 3006
77- let debug_port = $port + 1
55+def run-ephemeral-instance [name: string, port: int, scenario_closure: closure] {
66+ let debug_port = resolve-test-debug-port ($port + 1)
87 let url = $"http://localhost:($port)"
98 let debug_url = $"http://localhost:($debug_port)"
109 let db_path = (mktemp -d -t hydrant_ephemeral_gc_test.XXXXXX)
···4645}
47464847def main [] {
4848+ let port = resolve-test-port 3006
4949 let repo1 = "did:web:guestbook.gaze.systems"
50505151 # verify TTL tick runs without error when no events are eligible for expiry
5252- run-ephemeral-instance "TTL tick is safe with no eligible events" { |url, debug_url|
5252+ run-ephemeral-instance "TTL tick is safe with no eligible events" $port { |url, debug_url|
5353 print $"adding repo ($repo1)..."
5454 http put -t application/json $"($url)/repos" [{ did: ($repo1) }]
5555···7171 }
72727373 # plant a past watermark, trigger the real TTL path, and verify all events and blocks are gone
7474- run-ephemeral-instance "TTL tick with past watermark deletes events and blocks" { |url, debug_url|
7474+ run-ephemeral-instance "TTL tick with past watermark deletes events and blocks" $port { |url, debug_url|
7575 print $"adding repo ($repo1)..."
7676 http put -t application/json $"($url)/repos" [{ did: ($repo1) }]
7777
+3-2
tests/repo_sync_integrity.nu
···9696def main [] {
9797 let did = "did:plc:dfl62fgb7wtjj3fcbb72naae"
9898 let pds = "https://zwsp.xyz"
9999- let port = 3001
9999+ let port = resolve-test-port 3001
100100 let url = $"http://localhost:($port)"
101101- let debug_url = $"http://127.0.0.1:($port + 1)"
101101+ let debug_port = resolve-test-debug-port ($port + 1)
102102+ let debug_url = $"http://127.0.0.1:($debug_port)"
102103 let db_path = (mktemp -d -t hydrant_test.XXXXXX)
103104104105 print $"testing backfill integrity for ($did)..."
+99
tests/repos_api.nu
···11+#!/usr/bin/env nu
22+use common.nu *
33+44+def test-repos [url: string] {
55+ print "verifying /repos pagination and filtering..."
66+77+ # 1. test limit
88+ print " testing limit=1..."
99+ let items = (http get $"($url)/repos?limit=1" | from json -o)
1010+ if ($items | length) != 1 {
1111+ fail "expected 1 item with limit=1"
1212+ }
1313+ print $" count: ($items | length)"
1414+1515+ # 2. test partition=all
1616+ print " testing partition=all..."
1717+ let all_items = (http get $"($url)/repos?partition=all" | from json -o)
1818+ print $" count: ($all_items | length)"
1919+2020+ # 3. test cursor (if we have enough items)
2121+ if ($all_items | length) > 1 {
2222+ let first_did = ($all_items | get 0).did
2323+ print $" testing cursor with did ($first_did)..."
2424+ let cursor_items = (http get $"($url)/repos?cursor=($first_did)&limit=1" | from json -o)
2525+ if ($cursor_items | length) > 0 {
2626+ let next_did = ($cursor_items | get 0).did
2727+ if $first_did == $next_did {
2828+ fail "cursor did should be excluded from results"
2929+ }
3030+ print $" next did: ($next_did)"
3131+ }
3232+ }
3333+3434+ # 4. test partition=pending
3535+ print " testing partition=pending..."
3636+ let pending_items = (http get $"($url)/repos?partition=pending" | from json -o)
3737+ print $" pending count: ($pending_items | length)"
3838+3939+ # 5. test partition=resync
4040+ print " testing partition=resync..."
4141+ let resync_items = (http get $"($url)/repos?partition=resync" | from json -o)
4242+ print $" resync count: ($resync_items | length)"
4343+4444+ print "all /repos pagination and filtering tests passed!"
4545+}
4646+4747+def test-errors [url: string] {
4848+ print "verifying /repos error handling..."
4949+5050+ # invalid DID in PUT
5151+ print " testing PUT /repos with invalid DID..."
5252+ http put -f -e -t application/json $"($url)/repos" { did: "invalid" }
5353+ | assert-status 400 "PUT /repos invalid DID"
5454+5555+ # invalid DID in DELETE
5656+ print " testing DELETE /repos with invalid DID..."
5757+ http delete -f -e -t application/json $"($url)/repos" --data { did: "invalid" }
5858+ | assert-status 400 "DELETE /repos invalid DID"
5959+6060+ # invalid cursor in GET
6161+ print " testing GET /repos with invalid cursor..."
6262+ http get -f -e $"($url)/repos?cursor=invalid"
6363+ | assert-status 400 "GET /repos invalid cursor"
6464+6565+ # invalid partition in GET
6666+ print " testing GET /repos with invalid partition..."
6767+ http get -f -e $"($url)/repos?partition=invalid"
6868+ | assert-status 400 "GET /repos invalid partition"
6969+7070+ print "all /repos error handling tests passed!"
7171+}
7272+7373+def main [] {
7474+ let port = resolve-test-port 3001
7575+ let url = $"http://localhost:($port)"
7676+ let db_path = (mktemp -d -t hydrant_repos_api.XXXXXX)
7777+7878+ print $"starting hydrant for repos API verification..."
7979+ let binary = build-hydrant
8080+ let instance = (with-env { HYDRANT_MODE: "filter" } {
8181+ start-hydrant $binary $db_path $port
8282+ })
8383+8484+ if not (wait-for-api $url) {
8585+ fail "hydrant did not start" $instance.pid
8686+ }
8787+8888+ # seed a couple of repos so pagination tests have data
8989+ let dids = [
9090+ "did:plc:dfl62fgb7wtjj3fcbb72naae"
9191+ "did:plc:q6gjnv26m4ay3m42ojvzx2m4"
9292+ ]
9393+ http put -t application/json $"($url)/repos" ($dids | each { |d| { did: $d } })
9494+9595+ test-repos $url
9696+ test-errors $url
9797+9898+ kill $instance.pid
9999+}
-141
tests/repos_api_test.nu
···11-#!/usr/bin/env nu
22-33-def test-repos [url: string] {
44- print "verifying /repos pagination and filtering..."
55-66- # 1. Test limit
77- print " testing limit=1..."
88- let items = (http get $"($url)/repos?limit=1" | from json -o)
99- let count = ($items | length)
1010- print $" count: ($count)"
1111- if $count != 1 {
1212- print " FAILED: expected 1 item"
1313- exit 1
1414- }
1515-1616- # 2. Test partition=all
1717- print " testing partition=all..."
1818- let all_items = (http get $"($url)/repos?partition=all" | from json -o)
1919- print $" count: ($all_items | length)"
2020-2121- # 3. Test cursor (if we have items)
2222- if ($all_items | length) > 1 {
2323- let first_did = ($all_items | get 0).did
2424- print $" testing cursor with did ($first_did)..."
2525- let cursor_items = (http get $"($url)/repos?cursor=($first_did)&limit=1" | from json -o)
2626- if ($cursor_items | length) > 0 {
2727- let next_did = ($cursor_items | get 0).did
2828- if $first_did == $next_did {
2929- print " FAILED: cursor did should be excluded"
3030- exit 1
3131- }
3232- print $" next did: ($next_did)"
3333- }
3434- }
3535-3636- # 4. Test partition=pending
3737- print " testing partition=pending..."
3838- let pending_items = (http get $"($url)/repos?partition=pending" | from json -o)
3939- print $" pending count: ($pending_items | length)"
4040-4141- # 5. Test partition=resync
4242- print " testing partition=resync..."
4343- let resync_items = (http get $"($url)/repos?partition=resync" | from json -o)
4444- print $" resync count: ($resync_items | length)"
4545-4646- print "all /repos pagination and filtering tests passed!"
4747-}
4848-4949-def test-errors [url: string] {
5050- print "verifying /repos error handling..."
5151-5252- # 1. Invalid DID in PUT
5353- print " testing PUT /repos with invalid DID..."
5454- let resp_put = (http put -f -e -t application/json $"($url)/repos" { did: "invalid" })
5555- if $resp_put.status != 400 {
5656- print $" FAILED: expected 400, got ($resp_put.status)"
5757- exit 1
5858- }
5959-6060- # 2. Invalid DID in DELETE
6161- print " testing DELETE /repos with invalid DID..."
6262- let resp_del = (http delete -f -e -t application/json $"($url)/repos" --data { did: "invalid" })
6363- if $resp_del.status != 400 {
6464- print $" FAILED: expected 400, got ($resp_del.status)"
6565- exit 1
6666- }
6767-6868- # 3. Invalid cursor in GET
6969- print " testing GET /repos with invalid cursor..."
7070- let resp_get_cursor = (http get -f -e $"($url)/repos?cursor=invalid")
7171- if $resp_get_cursor.status != 400 {
7272- print $" FAILED: expected 400, got ($resp_get_cursor.status)"
7373- exit 1
7474- }
7575-7676- # 4. Invalid partition in GET
7777- print " testing GET /repos with invalid partition..."
7878- let resp_get_part = (http get -f -e $"($url)/repos?partition=invalid")
7979- if $resp_get_part.status != 400 {
8080- print $" FAILED: expected 400, got ($resp_get_part.status)"
8181- exit 1
8282- }
8383-8484- print "all /repos error handling tests passed!"
8585-}
8686-8787-def main [] {
8888- let port = 3001
8989- let url = $"http://localhost:($port)"
9090- let db_path = (mktemp -d -t hydrant_api_test.XXXXXX)
9191-9292- print $"starting hydrant for API verification..."
9393- let binary = (build-hydrant)
9494- let instance = (start-hydrant $binary $db_path $port)
9595-9696- if (wait-for-api $url) {
9797- # add a few repos
9898- let dids = [
9999- "did:plc:dfl62fgb7wtjj3fcbb72naae"
100100- "did:plc:q6gjnv26m4ay3m42ojvzx2m4"
101101- ]
102102- http put -t application/json $"($url)/repos" ($dids | each { |d| { did: $d } })
103103-104104- test-repos $url
105105- test-errors $url
106106- }
107107-108108- kill $instance.pid
109109-}
110110-111111-# Helper to build hydrant
112112-def build-hydrant [] {
113113- cargo build --quiet
114114- "./target/debug/hydrant"
115115-}
116116-117117-# Helper to start hydrant
118118-def start-hydrant [binary: string, db_path: string, port: int] {
119119- let log_file = $"($db_path)/hydrant.log"
120120- let pid = (with-env {
121121- HYDRANT_DATABASE_PATH: $db_path,
122122- HYDRANT_API_PORT: ($port | into string),
123123- HYDRANT_DEBUG_PORT: (($port + 1) | into string),
124124- HYDRANT_MODE: "filter",
125125- HYDRANT_LOG_LEVEL: "info"
126126- } {
127127- sh -c $"($binary) >($log_file) 2>&1 & echo $!" | str trim | into int
128128- })
129129- { pid: $pid, log: $log_file }
130130-}
131131-132132-# Helper to wait for api
133133-def wait-for-api [url: string] {
134134- for i in 1..20 {
135135- if (try { (http get $"($url)/health") == "OK" } catch { false }) {
136136- return true
137137- }
138138- sleep 500ms
139139- }
140140- false
141141-}
+107
tests/run_all.nu
···11+#!/usr/bin/env nu
22+# run all hydrant integration tests in parallel with automatically assigned free ports.
33+#
44+# usage:
55+# nu tests/run_all.nu
66+# nu tests/run_all.nu --only [stream repos_api]
77+88+def get_free_ports [count: int] {
99+ mut chosen = []
1010+ loop {
1111+ let p = port
1212+ if ($chosen | any {$in == $p}) {
1313+ continue;
1414+ }
1515+ $chosen = $chosen | append $p
1616+ if ($chosen | length) == $count {
1717+ break
1818+ }
1919+ }
2020+ $chosen
2121+}
2222+2323+def run-test [] {
2424+ let result = (with-env {
2525+ HYDRANT_API_PORT: $in.api
2626+ HYDRANT_DEBUG_PORT: $in.debug
2727+ HYDRANT_TEST_MOCK_PORT: $in.mock
2828+ HYDRANT_BINARY: "target/debug/hydrant"
2929+ } {
3030+ ^nu $"tests/($in.name).nu" | complete
3131+ })
3232+ {
3333+ name: $in.name
3434+ success: ($result.exit_code == 0)
3535+ output: $result.stdout
3636+ stderr: $result.stderr
3737+ }
3838+}
3939+4040+def main [--only: list<string> = []] {
4141+ print "building hydrant..."
4242+ # build defaults features
4343+ cargo build
4444+ # build backlinks
4545+ cargo build --features backlinks
4646+ print ""
4747+4848+ # discover all test scripts, excluding infrastructure files
4949+ let excluded = ["common", "mock_relay", "run_all"]
5050+ let discovered = (
5151+ ls tests/*.nu
5252+ | get name
5353+ | each {path basename | str replace ".nu" ""}
5454+ | where {|name| not ($excluded | any {$in == $name})}
5555+ )
5656+5757+ let tests = if ($only | is-empty) {
5858+ $discovered
5959+ } else {
6060+ $discovered | where {|t| $only | any {$in == $t}}
6161+ }
6262+ let ports = get_free_ports (($tests | length) * 3)
6363+6464+ mut assigned = []
6565+ for test in ($tests | enumerate) {
6666+ let p = {($test | get index) * 3 + $in}
6767+ let entry = {
6868+ name: ($test | get item),
6969+ api: ($ports | get (0 | do $p)),
7070+ debug: ($ports | get (1 | do $p)),
7171+ mock: ($ports | get (2 | do $p))
7272+ }
7373+ $assigned = ($assigned | append $entry)
7474+ }
7575+7676+ let groups = {
7777+ "authenticated_stream": "event_dependent",
7878+ "signal_filter": "event_dependent",
7979+ }
8080+ let grouped = $assigned | group-by {|t| $groups | get -o $t.name | default $t.name}
8181+8282+ print $"running ($assigned | length) tests...\n"
8383+8484+ let run_group = {each {timeit -o {run-test} | {time: $in.time, ...$in.output}}};
8585+ let results = $grouped | values | par-each {do $run_group} | flatten
8686+8787+ print "\n=== results ===\n"
8888+ for r in $results {
8989+ if $r.success {
9090+ print $" PASSED ($r.name) in ($r.time)"
9191+ } else {
9292+ print $" FAILED ($r.name) in ($r.time)"
9393+ let combined = $"($r.output)\n($r.stderr)" | str trim
9494+ $combined | lines | each {print $" ($in)"}
9595+ print ""
9696+ }
9797+ }
9898+9999+ let res = $results | group-by {$in.success}
100100+ let failed = $res | get -o false | default []
101101+ let passed = $res | get -o true | default []
102102+ print $"\n($passed | length) passed, ($failed | length) failed"
103103+104104+ try { ^pkill "hydrant" }
105105+106106+ if ($failed | length) > 0 { exit 1 }
107107+}
···77 let password = ($env_vars | get --optional TEST_PASSWORD)
8899 if ($did | is-empty) or ($password | is-empty) {
1010- print "error: TEST_REPO and TEST_PASSWORD must be set in .env"
1111- exit 1
1010+ print "SKIP: TEST_REPO and TEST_PASSWORD not set in .env"
1111+ exit 0
1212 }
13131414- let port = 3011
1414+ let port = resolve-test-port 3011
1515 let url = $"http://localhost:($port)"
1616 let db_path = (mktemp -d -t hydrant_signal_test.XXXXXX)
1717···103103 }
104104 } else {
105105 print "hydrant failed to start"
106106+ if ($instance.log | path exists) {
107107+ print "--- hydrant log ---"
108108+ print (open $instance.log)
109109+ print "-------------------"
110110+ }
106111 }
107112108113 print "stopping hydrant..."
+1-1
tests/stream_test.nu
tests/stream.nu
···3344def main [] {
55 let did = "did:web:guestbook.gaze.systems"
66- let port = 3002
66+ let port = resolve-test-port 3002
77 let url = $"http://localhost:($port)"
88 let ws_url = $"ws://localhost:($port)/stream"
99 let db_path = (mktemp -d -t hydrant_stream_test.XXXXXX)
+4-8
tests/throttling_test.nu
tests/throttling.nu
···99 }
10101111 # 2. setup ports and paths
1212- let port = 3010
1313- let mock_port = 3012
1212+ let port = resolve-test-port 3010
1313+ let mock_port = resolve-test-mock-port 3012
1414 let url = $"http://localhost:($port)"
1515 let mock_url = $"http://localhost:($mock_port)"
1616 let db_path = (mktemp -d -t hydrant_throttling.XXXXXX)
···2626 | into int
2727 )
2828 print $"mock relay pid: ($mock_pid)"
2929-3030- # give mock relay a moment
3131- sleep 1sec
32293330 # 4. start hydrant with low throttling limits
3431 let binary = build-hydrant
···86838784 # now check logs for throttling message
8885 print "checking logs for throttling message..."
8989- sleep 2sec # give logging a moment
90869187 let logs = (open $log_file | str replace --all "\n" " ")
9288 if ($logs | str contains "throttling: above max pending") {
···104100 {"did": "did:web:mock4.com"}
105101 ]' $"($url)/repos"
106102107107- print "waiting for crawler to wake up (max 10s)..."
108108- sleep 15sec
103103+ print "waiting for crawler to wake up..."
104104+ sleep 1sec
109105110106 # check logs for resumption message
111107 let logs_after = (open $log_file | str replace --all "\n" " ")
+5-4
tests/verify_crawler.nu
···99 }
10101111 # 2. setup ports and paths
1212- let port = 3006
1313- let mock_port = 3008
1212+ let port = resolve-test-port 3006
1313+ let mock_port = resolve-test-mock-port 3008
1414 let url = $"http://localhost:($port)"
1515- let debug_url = $"http://localhost:($port + 1)"
1515+ let debug_port = resolve-test-debug-port ($port + 1)
1616+ let debug_url = $"http://localhost:($debug_port)"
1617 let mock_url = $"http://localhost:($mock_port)"
1718 let db_path = (mktemp -d -t hydrant_full_net.XXXXXX)
1819···4647 HYDRANT_DISABLE_BACKFILL: "true",
4748 HYDRANT_API_PORT: ($port | into string),
4849 HYDRANT_ENABLE_DEBUG: "true", # for stats checking
4949- HYDRANT_DEBUG_PORT: ($port + 1 | into string),
5050+ HYDRANT_DEBUG_PORT: (resolve-test-debug-port ($port + 1) | into string),
5051 HYDRANT_LOG_LEVEL: "debug",
5152 HYDRANT_CURSOR_SAVE_INTERVAL: "1" # faster save
5253 } {