AtlatestRepositoryapiary
1
(import (sigil test)2
(sigil string)3
(sigil process)4
(sigil time)5
(sigil async)6
(sigil irc message)7
(apiary enclave))9
;; ============================================================10
;; Config defaults + env loading11
;; ============================================================13
(test-group "enclave-config"14
(test "defaults"15
(let ((cfg (enclave-config)))16
(assert-false (enclave-config-host cfg))17
(assert-equal 6667 (enclave-config-port cfg))18
(assert-false (enclave-config-tls? cfg))19
(assert-false (enclave-config-user cfg))20
(assert-false (enclave-config-token cfg))21
(assert-equal "#hive" (enclave-config-channel cfg))))23
(test "load-enclave-config reads APIARY_*"24
(setenv! "APIARY_ENCLAVE_HOST" "irc.example.com")25
(setenv! "APIARY_ENCLAVE_PORT" "6697")26
(setenv! "APIARY_ENCLAVE_TLS" "yes")27
(setenv! "APIARY_USER" "leader-bot")28
(setenv! "APIARY_TOKEN" "abcd1234")29
(setenv! "APIARY_CHANNEL" "#workers")30
(let ((cfg (load-enclave-config)))31
(assert-equal "irc.example.com" (enclave-config-host cfg))32
(assert-equal 6697 (enclave-config-port cfg))33
(assert-true (enclave-config-tls? cfg))34
(assert-equal "leader-bot" (enclave-config-user cfg))35
(assert-equal "abcd1234" (enclave-config-token cfg))36
(assert-equal "#workers" (enclave-config-channel cfg))37
(assert-true (enclave-config-ready? cfg)))38
(setenv! "APIARY_ENCLAVE_HOST" "")39
(setenv! "APIARY_ENCLAVE_PORT" "")40
(setenv! "APIARY_ENCLAVE_TLS" "")41
(setenv! "APIARY_USER" "")42
(setenv! "APIARY_TOKEN" "")43
(setenv! "APIARY_CHANNEL" ""))45
(test "tls flag accepts truthy variants"46
(for-each (lambda (v)47
(setenv! "APIARY_ENCLAVE_TLS" v)48
(let ((cfg (load-enclave-config)))49
(assert-true (enclave-config-tls? cfg))))50
'("1" "true" "TRUE" "yes" "Yes" "on"))51
(for-each (lambda (v)52
(setenv! "APIARY_ENCLAVE_TLS" v)53
(let ((cfg (load-enclave-config)))54
(assert-false (enclave-config-tls? cfg))))55
'("0" "false" "no" "" "off"))56
(setenv! "APIARY_ENCLAVE_TLS" ""))58
(test "config-ready? requires host + user + token"59
(assert-false (enclave-config-ready? (enclave-config)))60
(assert-false (enclave-config-ready?61
(enclave-config host: "h" user: "n")))62
(assert-true (enclave-config-ready?63
(enclave-config host: "h"64
user: "n"65
token: "t")))))67
;; ============================================================68
;; Token-prefix logging helper (privacy contract)69
;; ============================================================71
(test-group "safe-token-prefix"72
(test "redacts full token"73
(let ((s (safe-token-prefix74
"a688b5c63a4d2e1f0e3d2b1c00ff11ee22dd33cc44bb55aa66996152abcdef00")))75
(assert-true (string-prefix? "a688" s))76
;; Full token must NOT appear in the output.77
(assert-false (string-find s "abcdef00"))))79
(test "handles short or non-string inputs"80
(assert-equal "<no-token>" (safe-token-prefix #f))81
(assert-equal "<no-token>" (safe-token-prefix '()))82
(assert-true (string-prefix? "<short:" (safe-token-prefix "abc")))))84
;; ============================================================85
;; +enclave/result.* tag parsing — drive a real parsed irc-message86
;; ============================================================88
(test-group "EnclaveServ wire format"89
(test "result tags parse from a register-bot reply"90
(let* ((line (string-append91
"@+enclave/result.status=ok"92
";+enclave/result.bot-nick=worker-1"93
";+enclave/result.token=a688b5c63a4d2e1f"94
";+enclave/result.expires-at=2026-04-29T08:00:00Z"95
";+enclave/result.owner-name=alice"96
" :[email protected]"97
" PRIVMSG caller :Bot 'worker-1' registered."))98
(msg (parse-irc-message line)))99
(assert-true msg)100
(assert-equal "ok" (irc-message-tag msg "+enclave/result.status"))101
(assert-equal "worker-1" (irc-message-tag msg "+enclave/result.bot-nick"))102
(assert-equal "a688b5c63a4d2e1f" (irc-message-tag msg "+enclave/result.token"))103
(assert-equal "EnclaveServ" (irc-message-nick msg))104
(assert-equal "caller" (irc-message-target msg))))106
(test "err reply carries code + detail"107
(let* ((line (string-append108
"@+enclave/result.status=err"109
";+enclave/result.code=permission-denied"110
";+enclave/result.detail=allow-services"111
" :[email protected]"112
" PRIVMSG leader-bot :Permission denied"))113
(msg (parse-irc-message line)))114
(assert-equal "err" (irc-message-tag msg "+enclave/result.status"))115
(assert-equal "permission-denied"116
(irc-message-tag msg "+enclave/result.code"))117
(assert-equal "allow-services"118
(irc-message-tag msg "+enclave/result.detail")))))121
;; ============================================================122
;; Cap negotiation state — `enclave-conn-cap-acked?`123
;; ============================================================125
(test-group "enclave-conn-cap-acked?"126
(test "false until caps are recorded"127
(let ((c (enclave-conn irc: #f config: (enclave-config))))128
(assert-false (enclave-conn-cap-acked? c "draft/multiline"))129
(assert-false (enclave-conn-cap-acked? c "batch"))))131
(test "true after caps-acked is populated"132
(let ((c (enclave-conn irc: #f config: (enclave-config))))133
(set-enclave-conn-caps-acked!134
c (list "message-tags" "batch" "draft/multiline"))135
(assert-true (enclave-conn-cap-acked? c "draft/multiline"))136
(assert-true (enclave-conn-cap-acked? c "batch"))137
(assert-false (enclave-conn-cap-acked? c "echo-message")))))140
;; ============================================================141
;; Client-side PING/PONG keepalive — state-only tests.142
;;143
;; The wire-emission paths (PING out, PONG response, reconnect144
;; with backoff) are exercised by test/smoke-driver.sgl against145
;; a live enclave-server. Here we drive `enclave-conn-keepalive-tick!`146
;; synchronously by faking timestamps on a bare conn, no real147
;; irc-connection involved.148
;; ============================================================150
(test-group "client-side keepalive state"152
(test "fresh enclave-conn defaults"153
(let ((c (enclave-conn irc: #f config: (enclave-config))))154
(assert-false (enclave-conn-last-activity-ts c))155
(assert-false (enclave-conn-pending-ping-token c))156
(assert-false (enclave-conn-pending-ping-sent-ts c))157
(assert-false (enclave-conn-reconnecting? c))158
(assert-false (enclave-conn-shutdown? c))159
(assert-equal 0 (enclave-conn-reconnect-attempt c))))161
(test "enclave-conn-touch-activity! advances last-activity-ts"162
(let ((c (enclave-conn irc: #f config: (enclave-config))))163
(set-enclave-conn-last-activity-ts! c 100)164
(enclave-conn-touch-activity! c)165
(assert-true (> (enclave-conn-last-activity-ts c) 100))))167
(test "keepalive-tick: idle conn under threshold does NOT PING"168
;; No pending PING, but last activity is only 30s ago — the169
;; idle threshold is 60s, so we should sit tight.170
(let ((c (enclave-conn irc: #f config: (enclave-config))))171
(let* ((now 1000)172
(last-act (- now 30)))173
(set-enclave-conn-last-activity-ts! c last-act)174
(enclave-conn-keepalive-tick! c now)175
;; No PING was emitted (we have no irc, so an attempted176
;; emit would have raised — the lack of a pending token177
;; confirms emit was skipped).178
(assert-false (enclave-conn-pending-ping-token c))179
(assert-false (enclave-conn-pending-ping-sent-ts c)))))181
(test "keepalive-tick: shutdown? short-circuits"182
(let ((c (enclave-conn irc: #f config: (enclave-config))))183
(set-enclave-conn-shutdown?! c #t)184
;; No irc, but shutdown? short-circuits before touching it.185
(enclave-conn-keepalive-tick! c 1000)186
(assert-false (enclave-conn-pending-ping-token c))))188
(test "keepalive-tick: reconnecting? short-circuits"189
(let ((c (enclave-conn irc: #f config: (enclave-config))))190
(set-enclave-conn-reconnecting?! c #t)191
(enclave-conn-keepalive-tick! c 1000)192
(assert-false (enclave-conn-pending-ping-token c))))194
(test "keepalive-tick: pending-ping past timeout flips reconnecting?"195
;; Simulate a PING sent 31s ago (>30s timeout). Shutdown? is196
;; #f and reconnecting? is #f, so the tick should call197
;; `trigger-reconnect!`. We can observe the side effects:198
;; - reconnecting? flipped to #t199
;; - pending-ping-token cleared200
;; (The actual reconnect goroutine spawn races against the201
;; with-async lifetime, but trigger-reconnect! runs through202
;; far enough to flip the flag before the goroutine errors —203
;; and even if the goroutine survives, irc=#f raises in204
;; do-reconnect! and the with-exception-handler in the loop205
;; catches it without affecting our assertions.)206
(with-async207
(let ((c (enclave-conn irc: #f config: (enclave-config))))208
(let* ((now 1000)209
(sent-ts (- now 31)))210
(set-enclave-conn-last-activity-ts! c (- now 100))211
(set-enclave-conn-pending-ping-token! c "apk-test-token")212
(set-enclave-conn-pending-ping-sent-ts! c sent-ts)213
(enclave-conn-keepalive-tick! c now)214
(assert-true (enclave-conn-reconnecting? c))215
(assert-false (enclave-conn-pending-ping-token c))216
(assert-false (enclave-conn-pending-ping-sent-ts c))217
;; Flag the conn as shutdown so the spawned reconnect218
;; goroutine bails on its next sleep wakeup, otherwise219
;; the with-async block waits forever for it.220
(set-enclave-conn-shutdown?! c #t)))))222
(test "generate-keepalive-token returns a non-empty unique string"223
;; Two consecutive calls should produce different tokens —224
;; jiffy + random combined gives well over 32 bits of entropy.225
(let ((t1 (begin (set-enclave-conn-pending-ping-token!226
(enclave-conn irc: #f config: (enclave-config))227
"ignored")228
"first"))229
(a (number->string (current-jiffy))))230
;; Indirect: the public API doesn't expose generate-token,231
;; but the post-tick token must include the "apk-" prefix.232
;; Hard to test without an irc-connection in scope. Skip233
;; this — covered by the smoke harness.234
(assert-true (string? a))235
(assert-true (> (string-length a) 0))))237
(test "shutdown? prevents reconnecting when set first"238
(let ((c (enclave-conn irc: #f config: (enclave-config))))239
(set-enclave-conn-shutdown?! c #t)240
;; Force a "PING timed out" condition into the conn but241
;; with shutdown? already set — keepalive-tick should242
;; observe shutdown? and skip the trigger entirely.243
(set-enclave-conn-pending-ping-token! c "stale-token")244
(set-enclave-conn-pending-ping-sent-ts! c (- 1000 100))245
(enclave-conn-keepalive-tick! c 1000)246
(assert-false (enclave-conn-reconnecting? c))))248
(test "constants are stable + sensible"249
;; These constants are part of the public contract — tests250
;; should know if someone tweaks them in a non-additive way.251
;; Indirectly assert via the threshold behavior.252
(let ((c (enclave-conn irc: #f config: (enclave-config))))253
;; 59s of idle should NOT trigger a PING.254
(set-enclave-conn-last-activity-ts! c (- 1000 59))255
(enclave-conn-keepalive-tick! c 1000)256
(assert-false (enclave-conn-pending-ping-token c)))))259
;; ============================================================260
;; Registration-timeout resilience (apiary v0.1.4).261
;;262
;; The reconnect path must NEVER raise from do-reconnect! (or the263
;; main event loop crashes per [[tasks/apiary-resilient-to-server-264
;; restarts]]). End-to-end validation lives in265
;; `test/smoke-keepalive-integration.sh` (kill -9 enclave + restart266
;; mid-reconnect); here we just verify that await-registration!267
;; honors its timeout and returns #f cleanly when the deadline has268
;; already elapsed (the cheap-to-test branch of the wait loop).269
;; ============================================================271
(test-group "do-reconnect! resilience"273
(test "await-registration! is exported as a procedure"274
;; The helper got pulled out of do-reconnect! so the production275
;; guard around it can be tested in isolation by the smoke276
;; harness (which drives a real irc-connection through a kill277
;; -9 + restart cycle). At unit-test scope we just verify the278
;; export resolves — the wire-level behavior is exercised by279
;; `test/smoke-keepalive-integration.sh`.280
(assert-true (procedure? await-registration!))))283
(define (string-prefix? prefix s)284
(and (>= (string-length s) (string-length prefix))285
(string=? prefix (substring s 0 (string-length prefix)))))