AtlatestRepositoryapiary
1
(import (sigil test)2
(sigil string)3
(sigil dotenv)4
(sigil process)5
(sigil mcp server)6
(sigil mcp protocol)7
(apiary markdown-irc)8
;; for `enclave-config`, which `enclave-bridge-run!` takes9
(apiary enclave)10
(apiary tools))12
;; A literal backslash-n as it actually arrives from an LLM caller:13
;; the two characters #\\ and #\n, NOT a real newline.14
(define lit-nl (string #\\ #\n))16
;; Drive a tools/list JSON-RPC request through the server and read17
;; the entries the response advertises. This is the same path the18
;; MCP client hits during init — proves clients see the full tool19
;; surface even when the bridge hasn't connected.20
(define (advertised-tool-names server)21
(let* ((req (jsonrpc-request id: 1 method: "tools/list" params: '()))22
(resp (mcp-server-handle-message server req))23
(result (jsonrpc-response-result resp))24
(tools (or (assoc-ref 'tools result) '())))25
(map (lambda (t) (assoc-ref 'name t)) tools)))27
;; ============================================================28
;; Bridge state defaults29
;; ============================================================31
(test-group "enclave-bridge-state"32
(test "defaults"33
(let ((s (make-enclave-bridge-state)))34
(assert-false (enclave-bridge-state-conn s))35
(assert-false (enclave-bridge-state-config s))36
(assert-equal 'leader (enclave-bridge-state-mode s))37
(assert-false (enclave-bridge-state-owner-nick s))38
(assert-false (enclave-bridge-state-reports-to s))39
(assert-equal '() (enclave-bridge-state-subordinates s))40
(assert-equal '() (enclave-bridge-state-listened-peers s)))))42
;; ============================================================43
;; Mention syntax parser44
;; ============================================================46
(test-group "parse-mention-prefix"47
(test "extracts nick from @-prefixed mention"48
(call-with-values49
(lambda () (parse-mention-prefix "@alice: hello there"))50
(lambda (addressee body)51
(assert-equal "alice" addressee)52
(assert-equal "hello there" body))))54
(test "tolerates dashes and underscores in nick"55
(call-with-values56
(lambda () (parse-mention-prefix "@worker-bot_3: ack"))57
(lambda (addressee body)58
(assert-equal "worker-bot_3" addressee)59
(assert-equal "ack" body))))61
(test "rejects bare nick:body when my-nick not provided (legacy mode)"62
;; Without my-nick context, only the legacy @-prefixed form is63
;; recognized — bare nick:body falls through to broadcast.64
(call-with-values65
(lambda () (parse-mention-prefix "alice: hello"))66
(lambda (addressee body)67
(assert-false addressee)68
(assert-equal "alice: hello" body))))70
(test "rejects unprefixed text"71
(call-with-values72
(lambda () (parse-mention-prefix "no prefix here"))73
(lambda (addressee body)74
(assert-false addressee)75
(assert-equal "no prefix here" body))))77
(test "rejects @ alone"78
(call-with-values79
(lambda () (parse-mention-prefix "@"))80
(lambda (addressee body)81
(assert-false addressee)82
(assert-equal "@" body))))84
(test "rejects @: with no nick"85
(call-with-values86
(lambda () (parse-mention-prefix "@: empty"))87
(lambda (addressee body)88
(assert-false addressee)89
(assert-equal "@: empty" body))))91
(test "rejects colon without trailing space"92
(call-with-values93
(lambda () (parse-mention-prefix "@alice:no-space"))94
(lambda (addressee body)95
(assert-false addressee)96
(assert-equal "@alice:no-space" body))))98
(test "rejects nick with invalid chars"99
(call-with-values100
(lambda () (parse-mention-prefix "@[admin]: text"))101
(lambda (addressee body)102
(assert-false addressee)103
(assert-equal "@[admin]: text" body))))105
(test "tolerates colons in body (no ambiguity)"106
(call-with-values107
(lambda () (parse-mention-prefix "@bee-3: STARTED: phase 1"))108
(lambda (addressee body)109
(assert-equal "bee-3" addressee)110
(assert-equal "STARTED: phase 1" body))))112
(test "handles non-string input"113
(call-with-values114
(lambda () (parse-mention-prefix #f))115
(lambda (addressee body)116
(assert-false addressee)117
(assert-false body)))))119
;; ============================================================120
;; IRC-native bare mention parsing (v0.1.6+)121
;;122
;; With my-nick passed, parse-mention-prefix recognizes the IRC-native123
;; bare `<my-nick>: <body>` form. Tokens that don't match my-nick fall124
;; through to broadcast — apiary doesn't track channel membership, so125
;; the my-nick gate is the load-bearing safety net against126
;; misclassifying prose like "note: write this down" as a mention.127
;; ============================================================129
(test-group "parse-mention-prefix (IRC-native bare form)"130
(test "bare nick:body is mention when nick equals my-nick"131
(call-with-values132
(lambda () (parse-mention-prefix "quinn: ping" "quinn"))133
(lambda (addressee body)134
(assert-equal "quinn" addressee)135
(assert-equal "ping" body))))137
(test "bare match is case-insensitive"138
(call-with-values139
(lambda () (parse-mention-prefix "Quinn: PING" "quinn"))140
(lambda (addressee body)141
(assert-equal "quinn" addressee)142
(assert-equal "PING" body))))144
(test "bare nick:body is broadcast when nick != my-nick"145
(call-with-values146
(lambda () (parse-mention-prefix "alice: hello" "quinn"))147
(lambda (addressee body)148
(assert-false addressee)149
(assert-equal "alice: hello" body))))151
(test "broadcast: prose-shaped nick:body (not actually a mention)"152
(call-with-values153
(lambda () (parse-mention-prefix "note: write this down" "quinn"))154
(lambda (addressee body)155
(assert-false addressee)156
(assert-equal "note: write this down" body))))158
(test "broadcast: token has whitespace (foo bar: ...)"159
(call-with-values160
(lambda () (parse-mention-prefix "foo bar: hello" "foo"))161
(lambda (addressee body)162
(assert-false addressee)163
(assert-equal "foo bar: hello" body))))165
(test "broadcast: no space after colon (key:value)"166
(call-with-values167
(lambda () (parse-mention-prefix "quinn:value" "quinn"))168
(lambda (addressee body)169
(assert-false addressee)170
(assert-equal "quinn:value" body))))172
(test "broadcast: mid-line nick: pattern is not a mention"173
(call-with-values174
(lambda () (parse-mention-prefix "Hello, quinn: how are you?" "quinn"))175
(lambda (addressee body)176
(assert-false addressee)177
(assert-equal "Hello, quinn: how are you?" body))))179
(test "legacy @<nick>: form still parsed for any nick (deprecation)"180
;; During the v0.2.x deprecation window, legacy @-prefixed mentions181
;; continue to parse for any token regardless of my-nick. Removed182
;; in v0.3.0 cleanup.183
(call-with-values184
(lambda () (parse-mention-prefix "@bee-3: ack" "quinn"))185
(lambda (addressee body)186
(assert-equal "bee-3" addressee)187
(assert-equal "ack" body))))189
(test "bare body may be empty"190
(call-with-values191
(lambda () (parse-mention-prefix "quinn: " "quinn"))192
(lambda (addressee body)193
(assert-equal "quinn" addressee)194
(assert-equal "" body)))))196
;; ============================================================197
;; Mention formatter (v0.1.6: bare nick: form, no @ prefix)198
;; ============================================================200
(test-group "format-mention"201
(test "wraps body with bare <nick>: prefix (no @)"202
(assert-equal "bee-3: please proceed"203
(format-mention "bee-3" "please proceed")))205
(test "round-trips through parse-mention-prefix when my-nick matches"206
(call-with-values207
(lambda () (parse-mention-prefix208
(format-mention "worker-7" "ack")209
"worker-7"))210
(lambda (addressee body)211
(assert-equal "worker-7" addressee)212
(assert-equal "ack" body)))))214
;; ============================================================215
;; Literal backslash-n normalization (outbound newline fix)216
;;217
;; LLM callers frequently pass a literal "\n" (two chars: #\\ #\n)218
;; where they mean a line break. Without normalization the literal219
;; never triggers the per-line split, and — on the DM path — the220
;; markdown->mIRC escape pass then strips the backslash, leaving a221
;; stray "n" where the break belonged (observed 2026-07-08:222
;; "...marketing.nnTrashed..."). normalize-literal-newlines runs at223
;; both outbound entry points BEFORE any markdown translation or line224
;; split, so send-message (DM) and send-channel both benefit.225
;; ============================================================227
(test-group "normalize-literal-newlines"228
(test "converts a literal backslash-n to a real newline"229
(assert-equal "a\nb"230
(normalize-literal-newlines (string-append "a" lit-nl "b"))))232
(test "a real newline is left untouched (no double-conversion)"233
(assert-equal "a\nb"234
(normalize-literal-newlines "a\nb")))236
(test "converts every literal occurrence"237
(assert-equal "one\ntwo\nthree"238
(normalize-literal-newlines239
(string-append "one" lit-nl "two" lit-nl "three"))))241
(test "no literal present: string returned unchanged"242
(assert-equal "no breaks here"243
(normalize-literal-newlines "no breaks here")))245
(test "a lone backslash (not followed by n) is preserved"246
(assert-equal (string #\\ #\x)247
(normalize-literal-newlines (string #\\ #\x))))249
(test "non-string input passes through"250
(assert-false (normalize-literal-newlines #f)))252
;; End-to-end shape checks — these mirror what the two outbound253
;; helpers do to the text before handing it to enclave-post-multiline.255
(test "send-channel path: normalized literal splits into multiple lines"256
;; enclave-bridge-send-channel! normalizes, then splits on real "\n".257
(let* ((raw (string-append "first" lit-nl "second"))258
(lines (string-split (normalize-literal-newlines raw) "\n")))259
(assert-equal 2 (length lines))260
(assert-equal "first" (car lines))261
(assert-equal "second" (cadr lines))))263
(test "send-message (DM) path: normalize before markdown, then split"264
;; enclave-bridge-send-dm! normalizes, runs markdown->irc, then265
;; splits. Before the fix the markdown escape pass ate the266
;; backslash and the whole message stayed a single "firstnsecond"267
;; line. After it, two lines survive.268
(let* ((raw (string-append "first" lit-nl "second"))269
(rendered (markdown->irc (normalize-literal-newlines raw)))270
(lines (string-split rendered "\n")))271
(assert-equal 2 (length lines))272
(assert-equal "first" (car lines))273
(assert-equal "second" (cadr lines))))275
(test "regression: without normalization the DM path collapses to one line"276
;; Documents the bug: feeding the raw literal straight to277
;; markdown->irc yields a single line whose break became a bare "n".278
(let* ((raw (string-append "first" lit-nl "second"))279
(rendered (markdown->irc raw))280
(lines (string-split rendered "\n")))281
(assert-equal 1 (length lines))282
(assert-equal "firstnsecond" (car lines)))))284
;; ============================================================285
;; Trusted-set predicate286
;; ============================================================288
;; ============================================================289
;; register-apiary-tools! — capability declaration290
;;291
;; Registration is gated on APIARY_MODE alone, not on292
;; enclave-config-ready or on a live bridge connection. The MCP293
;; tools/list response at init must expose the full mode-appropriate294
;; tool set so clients with `tools.listChanged: false` see the right295
;; surface from the first reply. Tool *calls* that need a live296
;; bridge handle that runtime concern themselves.297
;; ============================================================299
(test-group "register-apiary-tools! mode-driven registration"300
(test "leader mode registers all 9 tools, no live bridge required"301
(let* ((server (mcp-server name: "x" version: "1.0"))302
(state (make-enclave-bridge-state)))303
(set-enclave-bridge-state-mode! state 'leader)304
(register-apiary-tools! server state)305
(let ((tool-names (advertised-tool-names server)))306
(assert-equal 9 (length tool-names))307
(assert-true (member "spawn-worker" tool-names))308
(assert-true (member "revoke-worker" tool-names))309
(assert-true (member "rotate-token" tool-names))310
(assert-true (member "list-members" tool-names))311
(assert-true (member "send-channel" tool-names))312
(assert-true (member "send-message" tool-names))313
(assert-true (member "send-react" tool-names))314
(assert-true (member "listen-peer" tool-names))315
(assert-true (member "unlisten-peer" tool-names)))))317
(test "worker mode registers only the 4 worker tools"318
(let* ((server (mcp-server name: "x" version: "1.0"))319
(state (make-enclave-bridge-state)))320
(set-enclave-bridge-state-mode! state 'worker)321
(register-apiary-tools! server state)322
(let ((tool-names (advertised-tool-names server)))323
(assert-equal 4 (length tool-names))324
(assert-true (member "send-channel" tool-names))325
(assert-true (member "send-react" tool-names))326
(assert-true (member "listen-peer" tool-names))327
(assert-true (member "unlisten-peer" tool-names))328
;; Leader-only tools must NOT appear in worker mode.329
(assert-false (member "spawn-worker" tool-names))330
(assert-false (member "send-message" tool-names))331
(assert-false (member "rotate-token" tool-names)))))333
(test "registration is independent of enclave-config / bridge state"334
;; No config set, no conn — registration should still succeed335
;; and the full leader tool set should appear.336
(let* ((server (mcp-server name: "x" version: "1.0"))337
(state (make-enclave-bridge-state)))338
(set-enclave-bridge-state-mode! state 'leader)339
(assert-false (enclave-bridge-state-config state))340
(assert-false (enclave-bridge-state-conn state))341
(register-apiary-tools! server state)342
(assert-equal 9 (length (advertised-tool-names server))))))344
(test-group "tool-call error path before bridge connects"345
(test "send-channel returns informative error when bridge isn't connected"346
(let ((state (make-enclave-bridge-state)))347
;; No conn set — direct call into the helper exercises the348
;; same path the registered tool's lambda hits.349
(let ((result (enclave-bridge-send-channel! state #f "hello")))350
(assert-true (string? result))351
(assert-true (string-contains? result "not connected"))))))353
;; ============================================================354
;; Bug A — spawn-worker groups defaulting355
;;356
;; or-empty-env (caller-arg env-name) is the resolution shape used357
;; by spawn-worker for its `groups` argument: explicit non-empty358
;; arg wins, else the env var, else #f. Empty string is treated359
;; as "not provided" (the same as #f) so an MCP caller passing360
;; `groups: ""` falls back to the env default rather than361
;; sending an explicit no-group request.362
;; ============================================================364
(test-group "or-empty-env (groups defaulting helper)"365
(test "non-empty arg wins over env"366
(setenv! "APIARY_TEST_GROUP_AB" "from-env")367
(assert-equal "from-arg"368
(or-empty-env "from-arg" "APIARY_TEST_GROUP_AB"))369
(setenv! "APIARY_TEST_GROUP_AB" ""))371
(test "missing arg falls back to env"372
(setenv! "APIARY_TEST_GROUP_AC" "ops-workers")373
(assert-equal "ops-workers"374
(or-empty-env #f "APIARY_TEST_GROUP_AC"))375
(setenv! "APIARY_TEST_GROUP_AC" ""))377
(test "empty-string arg falls back to env (NOT treated as explicit)"378
(setenv! "APIARY_TEST_GROUP_AD" "ops-workers")379
(assert-equal "ops-workers"380
(or-empty-env "" "APIARY_TEST_GROUP_AD"))381
(setenv! "APIARY_TEST_GROUP_AD" ""))383
(test "missing arg + unset env returns #f"384
;; Use a never-touched name so it's reliably unset.385
(assert-false (or-empty-env #f "APIARY_TEST_GROUP_NEVER_SET")))387
(test "missing arg + empty-string env returns #f"388
(setenv! "APIARY_TEST_GROUP_AF" "")389
(assert-false (or-empty-env #f "APIARY_TEST_GROUP_AF")))391
(test "non-string arg falls back to env"392
(setenv! "APIARY_TEST_GROUP_AG" "fallback")393
(assert-equal "fallback"394
(or-empty-env 42 "APIARY_TEST_GROUP_AG"))395
(setenv! "APIARY_TEST_GROUP_AG" "")))397
(test-group "env-or-false"398
(test "returns env value when set and non-empty"399
(setenv! "APIARY_TEST_EOB" "yes")400
(assert-equal "yes" (env-or-false "APIARY_TEST_EOB"))401
(setenv! "APIARY_TEST_EOB" ""))403
(test "returns #f when unset"404
(assert-false (env-or-false "APIARY_TEST_EOB_NEVER_SET")))406
(test "returns #f when set to empty string"407
(setenv! "APIARY_TEST_EOB_EMPTY" "")408
(assert-false (env-or-false "APIARY_TEST_EOB_EMPTY"))))410
(test-group "trusted-sender?"411
(test "owner is trusted (case-insensitive)"412
(let ((s (make-enclave-bridge-state)))413
(set-enclave-bridge-state-owner-nick! s "daviwil")414
(assert-true (trusted-sender? s "daviwil"))415
(assert-true (trusted-sender? s "DAVIWIL"))416
(assert-true (trusted-sender? s "Daviwil"))417
(assert-false (trusted-sender? s "stranger"))))419
(test "reports-to is trusted"420
(let ((s (make-enclave-bridge-state)))421
(set-enclave-bridge-state-reports-to! s "quinn")422
(assert-true (trusted-sender? s "quinn"))423
(assert-true (trusted-sender? s "QUINN"))424
(assert-false (trusted-sender? s "other"))))426
(test "subordinates are trusted"427
(let ((s (make-enclave-bridge-state)))428
(set-enclave-bridge-state-subordinates!429
s (list "worker-1" "worker-2"))430
(assert-true (trusted-sender? s "worker-1"))431
(assert-true (trusted-sender? s "WORKER-2"))432
(assert-false (trusted-sender? s "worker-3"))))434
(test "listened-peers are trusted"435
(let ((s (make-enclave-bridge-state)))436
(set-enclave-bridge-state-listened-peers!437
s (list "bee-3" "bee-7"))438
(assert-true (trusted-sender? s "bee-3"))439
(assert-true (trusted-sender? s "BEE-7"))440
(assert-false (trusted-sender? s "bee-99"))))442
(test "non-string sender returns #f"443
(let ((s (make-enclave-bridge-state)))444
(set-enclave-bridge-state-owner-nick! s "daviwil")445
(assert-false (trusted-sender? s #f))446
(assert-false (trusted-sender? s '()))))448
(test "empty trusted set rejects everyone"449
(let ((s (make-enclave-bridge-state)))450
(assert-false (trusted-sender? s "anyone"))451
(assert-false (trusted-sender? s "owner")))))454
;; ============================================================455
;; Tool-facing link status456
;;457
;; The defect: every tool call that could not reach the channel458
;; returned the string "Error: enclave bridge not connected",459
;; whether the bridge was two seconds from succeeding or would460
;; never connect again for the life of the process. Those are461
;; different facts and a caller has to be able to act on the462
;; difference — a process that will never recover reading as a463
;; transient blip is what let an outage look survivable for a day464
;; and a half.465
;; ============================================================467
(define (contains? haystack needle)468
(and (string? haystack)469
(>= (string-length haystack) (string-length needle))470
(let loop ((i 0))471
(cond472
((> (+ i (string-length needle)) (string-length haystack)) #f)473
((string=? needle (substring haystack i474
(+ i (string-length needle))))475
#t)476
(else (loop (+ i 1)))))))478
(test-group "bridge-unavailable-message"480
(test "terminal says it will NOT connect and asks for operator action"481
(let ((s (make-enclave-bridge-state)))482
(set-enclave-bridge-state-link-state! s 'terminal)483
(set-enclave-bridge-state-link-detail! s "incomplete configuration")484
(let ((msg (bridge-unavailable-message s)))485
(assert-true (contains? msg "will NOT connect"))486
(assert-true (contains? msg "incomplete configuration"))487
;; It must NOT imply a retry that is not happening.488
(assert-false (contains? msg "retrying")))))490
(test "retrying names the attempt, the last failure and the next try"491
(let ((s (make-enclave-bridge-state)))492
(set-enclave-bridge-state-link-state! s 'connecting)493
(set-enclave-bridge-state-link-detail!494
s "connect timed out: connection timed out at 10.0.0.1:6697")495
(set-enclave-bridge-state-link-attempt! s 4)496
(set-enclave-bridge-state-link-next-delay! s 8)497
(let ((msg (bridge-unavailable-message s)))498
(assert-true (contains? msg "retrying"))499
(assert-true (contains? msg "attempt 4"))500
(assert-true (contains? msg "connect timed out"))501
(assert-true (contains? msg "next attempt in 8s"))502
;; And it must NOT claim the situation is hopeless.503
(assert-false (contains? msg "will NOT connect")))))505
(test "terminal and retrying are DISTINGUISHABLE"506
;; The single property the whole change exists to provide. Asserted507
;; directly so it cannot regress into two differently-worded508
;; strings that a caller still cannot act on differently.509
(let ((terminal (make-enclave-bridge-state))510
(retrying (make-enclave-bridge-state)))511
(set-enclave-bridge-state-link-state! terminal 'terminal)512
(set-enclave-bridge-state-link-detail! terminal "same detail")513
(set-enclave-bridge-state-link-state! retrying 'connecting)514
(set-enclave-bridge-state-link-detail! retrying "same detail")515
(assert-false (string=? (bridge-unavailable-message terminal)516
(bridge-unavailable-message retrying)))))518
;; ----------------------------------------------------------------519
;; The tests above exercise the FORMATTER only: they write520
;; link-detail through a setter and then assert the string renders521
;; it. Every one of them passed while the real plumbing was522
;; overwriting the classified failure with the literal word523
;; "reconnecting" before each backoff sleep — a bug an adversarial524
;; reviewer found and these tests structurally could not.525
;;526
;; A test that writes a value through a setter and then asserts the527
;; formatter rendered it is testing the formatter. The tests below528
;; drive `set-link-status!` in the SEQUENCE the connect machinery529
;; drives it, which is where the defect lived.530
;; ----------------------------------------------------------------532
(test "a scheduled attempt does NOT erase the last classified failure"533
;; The exact regression. Sequence per retry iteration is:534
;; report 'connecting (attempt scheduled, no new diagnosis)535
;; ... sleep, up to 60s ...536
;; attempt537
;; report the classified outcome538
;; A tool call landing in the sleep — which is most of them, since539
;; the sleep dominates the attempt by an order of magnitude — must540
;; still see the real reason.541
(let ((s (make-enclave-bridge-state)))542
;; A real failure lands.543
(set-link-status! s 'connecting544
"connect timed out: 10.0.0.1:6697" 3 8)545
;; Next iteration schedules an attempt with NO new diagnosis.546
(set-link-status! s 'connecting #f 4 16)547
(let ((msg (bridge-unavailable-message s)))548
(assert-true (contains? msg "connect timed out"))549
(assert-false (contains? msg "last failure: reconnecting"))550
;; The freshly-scheduled attempt/delay DO update.551
(assert-true (contains? msg "attempt 4"))552
(assert-true (contains? msg "next attempt in 16s")))))554
(test "a successful connect leaves no failure text behind"555
;; link-detail is only ever rendered as "last failure: <detail>",556
;; so writing a success string into it produces "last failure:557
;; connected" the next time the session drops.558
(let ((s (make-enclave-bridge-state)))559
(set-link-status! s 'connecting "connect timed out: host:6697" 3 8)560
(set-link-status! s 'connected "" 0 #f)561
;; Session drops; nothing has reported in yet.562
(let ((msg (bridge-unavailable-message s)))563
(assert-false (contains? msg "last failure: connected"))564
(assert-false (contains? msg "connect timed out"))565
;; And it must be honest about how long detection can take.566
(assert-true (contains? msg "90s")))))568
(test "a fresh state defaults to idle, and idle reads as retrying"569
;; 'idle means the connect goroutine has not reported yet, which570
;; is a "not yet" rather than a "never" — reporting it as terminal571
;; would be the same lie in the other direction.572
(let ((s (make-enclave-bridge-state)))573
(assert-equal 'idle (enclave-bridge-state-link-state s))574
(assert-true (contains? (bridge-unavailable-message s)575
"retrying")))))578
;; ============================================================579
;; The start-up retry loop580
;;581
;; This was the largest new control-flow addition in the change and582
;; had NO coverage at all — not in this suite and not in the smoke583
;; harness, which calls `enclave-bridge-start!` rather than584
;; `enclave-bridge-run!`. Found by adversarial review, not by me.585
;;586
;; A loop whose only exit is success cannot be tested by letting it587
;; run, so both terminating paths are what get asserted here.588
;; ============================================================590
(test-group "enclave-bridge-run!"592
(test "an unconfigured bridge is TERMINAL, and does not spin"593
;; No configuration is arriving later in this process's life, so594
;; retrying would burn the scheduler forever on something that595
;; cannot succeed AND would report "retrying" while doing it.596
(let* ((s (make-enclave-bridge-state))597
(result (enclave-bridge-run! s #f (enclave-config)598
'leader #f #f)))599
(assert-false result)600
(assert-equal 'terminal (enclave-bridge-state-link-state s))601
(let ((msg (bridge-unavailable-message s)))602
(assert-true (contains? msg "will NOT connect"))603
(assert-false (contains? msg "retrying")))))605
(test "a stop request ends the loop instead of connecting anyway"606
;; Without this the loop's only exit is success, so a process asked607
;; to shut down mid-outage would keep retrying and could establish608
;; a connection after the request to stop. The config here is609
;; deliberately COMPLETE, so 'terminal cannot be reached by the610
;; unconfigured path above — the stop flag is the only way out, and611
;; a regression that ignored it would hang this test rather than612
;; failing it quietly.613
(let ((s (make-enclave-bridge-state)))614
(set-enclave-bridge-state-stop?! s #t)615
(let ((result (enclave-bridge-run!616
s #f617
(enclave-config host: "127.0.0.1" port: 1618
user: "n" token: "t")619
'leader #f #f)))620
(assert-false result)621
(assert-equal 'terminal (enclave-bridge-state-link-state s))))))624
;; ============================================================625
;; Post-result vocabulary626
;; ============================================================628
(test-group "post-result vocabulary"630
(test "qualify-post-result says nothing about delivery when unconfirmed"631
;; The tool-facing vocabulary is part of the contract. The632
;; confirmed rendering may claim delivery; the unconfirmed one633
;; must not, and specifically must not say "queued" — nothing634
;; holds a copy, so a caller told that would wait instead of635
;; re-sending.636
(assert-equal "Broadcast to #chan (1 line)"637
(qualify-post-result 'confirmed638
"Broadcast to #chan (1 line)"639
"Wrote to #chan (1 line)"))640
(let ((out (qualify-post-result 'unconfirmed641
"Broadcast to #chan (1 line)"642
"Wrote to #chan (1 line)")))643
(assert-true (string-contains? out "UNCONFIRMED"))644
(assert-true (string-contains? out "Wrote to #chan"))645
(assert-false (string-contains? out "Broadcast"))646
(assert-false (string-contains? out "ueued"))))648
(test "the UNCONFIRMED reason matches why it was unconfirmed"649
;; An earlier version claimed "the server did not answer within650
;; 3s" for every outcome, including ones where no PING was sent651
;; and no budget was ever spent. The verb was honest and the652
;; diagnosis was invented.653
(let ((budget (qualify-post-result 'unconfirmed "ok" "wrote"))654
(stale (qualify-post-result 'no-evidence "ok" "wrote"))655
(down (qualify-post-result 'link-down "ok" "wrote"))656
(swapped (qualify-post-result 'link-changed "ok" "wrote")))657
(assert-true (string-contains? budget "did not answer within"))658
(assert-false (string-contains? stale "did not answer within"))659
(assert-true (string-contains? stale "link is unresponsive"))660
(assert-true (string-contains? down "down or re-establishing"))661
(assert-true (string-contains? swapped "was replaced"))662
;; whatever the reason, none of them claims delivery663
(for-each (lambda (s) (assert-false (string-contains? s "ok")))664
(list budget stale down swapped))665
;; and none of them stutters: the reason is a noun phrase and666
;; the consequence is its own sentence, so no "..., so X, so667
;; Y ..." can reach the agent that reads this.668
(for-each (lambda (s) (assert-false (string-contains? s ", so ")))669
(list budget stale down swapped)))))