Commita27ab968Recorded5 Aug 2026Repositoryapiary

Act on the adversarial review: confirmation must wait for evidence, not for absence

Message

An adversarial pass over the previous commit found a HIGH that would have shipped worse behaviour than the change fixes, plus a regression guard that covered a branch no caller can reach. Both are the author's own blind spots, found by someone else, which is the point.

THE HIGH. Confirmation waited for its token to LEAVE the outstanding list. A token leaves for two very different reasons: the peer answered it, or we abandoned the socket -- enclave-conn-pings-clear!, called from trigger-reconnect! and enclave-disconnect. Absence conflates them. So a reconnect triggered DURING the wait -- a server restart, an RST, a keepalive timeout -- cleared the token, and on its next wakeup the waiter read that as proof and returned Broadcast to <channel> for a message written to a socket being abandoned at that moment. The exact false positive this whole change exists to remove, reintroduced inside the fix for it.

Confirmation now waits for POSITIVE evidence: ping-ack!, which runs only from the PONG handler on an exact token match, records the token in a separate bounded acked-pings list, and the waiter watches that. Abandoning a socket clears the outstanding list and touches nothing else, so it can no longer be mistaken for an answer.

Two more, from the same pass:

  • Nothing bound the confirming PING to the socket the message went out on. Callers now capture irc at write time and confirmation returns 'link-changed rather than interpreting a PONG that arrived on a different connection.
  • enclave-bridge-send-react! still returned a bare "Reacted ๐ŸŽ‰ to ... in #chan" with no confirmation at all -- the defect surviving one function over, in the same file, because attention was on the two paths named in the brief.

And two smaller ones:

  • Every non-confirmed outcome rendered "the server did not answer within 3s", including outcomes where no PING was sent and no budget was ever spent. The verb was honest and the diagnosis was invented, which is the same disease one clause along. Reasons are now distinct: 'unconfirmed, 'no-evidence, 'link-down, 'link-changed, 'not-ready.
  • "Apiary keepalive PING out" was logged even when the write raised, in exactly the DEBUG log the harness experiments parse.

THE VACUOUS GUARD. The blocker-2 regression test drove enclave-post-multiline with '(), which returns 'sent-empty early. It did fail against the pre-fix source, so it looked real. But split-lines returns (list "") for empty text, so no caller can ever reach that branch: the guard covered a dead path, and re-adding the touch to the single-line branch -- the path every real post takes -- left the whole suite green. It now drives the paths real posts take, and its remaining limit (a touch added AFTER the write is invisible here, because the write raises first) is written down next to it along with which gate does cover that.

Hardening from the same review: a conn whose inbound clock was never seeded now counts as idle rather than as "not yet due". Every conn built here seeds it, but the constructor is exported with a #f default, and a future construction site that forgot would otherwise disable the keepalive silently and for ever.

Changed
 src/apiary/enclave.sgl | 214 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------------------------------
 src/apiary/tools.sgl   |  69 ++++++++++++++++++++++++++++++++++++++++++++-----------
 test/test-enclave.sgl  |  92 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------
 test/test-tools.sgl    |  20 +++++++++++++++-
 4 files changed, 300 insertions(+), 95 deletions(-)
Diff
src/apiary/enclave.sglmodified
@@ -89,6 +89,8 @@
89
enclave-conn-ping-add!
90
enclave-conn-ping-ack!
91
enclave-conn-ping-waiting?
+92
enclave-conn-ping-acked?
+93
enclave-conn-acked-pings
94
enclave-conn-pings-clear!
95
enclave-conn-ping-outstanding?
96
enclave-conn-oldest-ping-ts
@@ -257,6 +259,13 @@
259
;; always measured from the oldest, so that is structurally
260
;; impossible rather than merely avoided by care.
261
(pending-pings default: '() mutable: #t)
+262
;; Tokens the peer has ANSWERED, most recent first, capped at
+263
;; `+max-acked-pings+`. Separate from the list above because
+264
;; leaving that list is ambiguous โ€” an entry goes away either
+265
;; because the peer answered it or because we abandoned the
+266
;; socket โ€” and delivery confirmation must only ever act on the
+267
;; first of those. See `enclave-conn-ping-acked?`.
+268
(acked-pings default: '() mutable: #t)
269
;; Throttle the keepalive sweep โ€” the event loop ticks every
270
;; 50ms but the keepalive logic only needs to run every few
271
;; seconds, so we record when we last ran the sweep and skip
@@ -362,6 +371,9 @@
371
;; Hard cap on outstanding PINGs so a burst of posts inside one
372
;; confirmation budget cannot grow the list without bound.
373
(define +max-outstanding-pings+ 4)
+374
;; How many answered tokens to remember. They only need to
+375
;; outlive the confirmation wait that is watching for them.
+376
(define +max-acked-pings+ 8)
377
(define +reconnect-backoff-min+ 1.0)
378
(define +reconnect-backoff-max+ 60.0)
379
;; sigil-irc completes its handshake (TCP connect โ†’ CAP LS โ†’
@@ -1016,6 +1028,43 @@
1028
(: enclave-conn? string? -> boolean?)
1029
(and (assoc token (enclave-conn-pending-pings conn)) #t))
1030
+1031
;;; #t when THIS token was actually ANSWERED by the peer.
+1032
;;;
+1033
;;; This is not the negation of `ping-waiting?`, and the
+1034
;;; difference is the whole reason it exists. An entry leaves the
+1035
;;; outstanding list for two very different reasons: the peer
+1036
;;; answered it, or we gave up on the socket
+1037
;;; (`enclave-conn-pings-clear!`, called from `trigger-reconnect!`
+1038
;;; and `enclave-disconnect`). Absence conflates them.
+1039
;;;
+1040
;;; An earlier version of `enclave-confirm-delivery!` waited for
+1041
;;; absence, which meant a reconnect triggered DURING the wait
+1042
;;; cleared the token and the waiter read that as proof of
+1043
;;; delivery โ€” reporting `Broadcast to <channel>` for a message
+1044
;;; written to a socket that was at that moment being abandoned.
+1045
;;; That is the exact false positive this whole change exists to
+1046
;;; remove, reintroduced inside the fix. Found by adversarial
+1047
;;; review, not by its author.
+1048
;;;
+1049
;;; So confirmation waits for POSITIVE evidence. Only `ping-ack!`
+1050
;;; โ€” which runs solely from the PONG handler, on an exact token
+1051
;;; match โ€” records here.
+1052
(define (enclave-conn-ping-acked? conn token)
+1053
(: enclave-conn? string? -> boolean?)
+1054
(and (member-string? token (enclave-conn-acked-pings conn)) #t))
+1055
+1056
;;; Record an answered token, keeping the list short. It only
+1057
;;; needs to outlive the confirmation wait watching for it.
+1058
(define (enclave-conn-note-acked! conn token)
+1059
(: enclave-conn? string? -> any?)
+1060
(set-enclave-conn-acked-pings!
+1061
conn (let loop ((xs (cons token (enclave-conn-acked-pings conn)))
+1062
(n +max-acked-pings+)
+1063
(acc '()))
+1064
(cond
+1065
((or (null? xs) (= n 0)) (reverse acc))
+1066
(else (loop (cdr xs) (- n 1) (cons (car xs) acc)))))))
+1067
1068
;;; Epoch second of the OLDEST unanswered PING, or #f if none.
1069
;;; The keepalive measures its PONG timeout from this, so a
1070
;;; newer PING can never postpone an older one's deadline.
@@ -1038,6 +1087,7 @@
1087
(cond
1088
((not (assoc token entries)) #f)
1089
(else
+1090
(enclave-conn-note-acked! conn token)
1091
(set-enclave-conn-pending-pings!
1092
conn (filter (lambda (e) (not (equal? (car e) token)))
1093
entries))
@@ -1472,12 +1522,22 @@
1522
sent-sec-ago: (number->string (- now oldest-ping)))
1523
(trigger-reconnect! conn))
1524
;; Nothing in flight and no INBOUND traffic for longer
โˆ’1475
;; than the idle threshold โ€” emit a fresh PING. Note
โˆ’1476
;; this is deliberately blind to how much WE have sent:
โˆ’1477
;; our own writes say nothing about the peer.
+1525
;; than the idle threshold โ€” emit a fresh PING.
+1526
;;
+1527
;; Deliberately blind to how much WE have sent: our own
+1528
;; writes say nothing about the peer.
+1529
;;
+1530
;; A conn whose clock has never been set counts as idle
+1531
;; rather than as "not yet due". Every conn built in this
+1532
;; module seeds it at connect, but the constructor is
+1533
;; exported and its default is #f, so a second
+1534
;; construction site that forgot to seed it would
+1535
;; otherwise disable the keepalive silently and for ever
+1536
;; โ€” a worse version of the defect this file just
+1537
;; removed.
1538
((and (not oldest-ping)
โˆ’1479
last-in
โˆ’1480
(> (- now last-in) +keepalive-idle-threshold+))
+1539
(or (not last-in)
+1540
(> (- now last-in) +keepalive-idle-threshold+)))
1541
(emit-keepalive-ping! conn now))
1542
(else #f))))))
1543
@@ -1508,12 +1568,18 @@
1568
(irc-send-raw irc
1569
(string-append "PING :" token "\r\n"))
1570
(set! sent? #t))
โˆ’1511
;; Token never logged at INFO โ€” defense-in-depth so a
โˆ’1512
;; misbehaved peer reading log tail can't forge a PONG.
โˆ’1513
(log-debug "Apiary keepalive PING out"
โˆ’1514
token-prefix: (safe-token-prefix token)
โˆ’1515
outstanding: (number->string
โˆ’1516
(length (enclave-conn-pending-pings conn))))
+1571
;; Only claim a PING went out when the write returned. The
+1572
;; guard above falls through to here, so an unconditional log
+1573
;; line would report "PING out" for a send that raised โ€” in
+1574
;; exactly the DEBUG log the harness experiments parse.
+1575
;;
+1576
;; Token never logged at INFO โ€” defence in depth so a
+1577
;; misbehaved peer reading a log tail can't forge a PONG.
+1578
(when sent?
+1579
(log-debug "Apiary keepalive PING out"
+1580
token-prefix: (safe-token-prefix token)
+1581
outstanding: (number->string
+1582
(length (enclave-conn-pending-pings conn)))))
1583
(and sent? token)))
1584
1585
;;; Keepalive's own PING. Thin wrapper so the sweep reads as
@@ -2004,60 +2070,80 @@
2070
;;; keepalive. A busy bot now reaches its PONG timeout from the
2071
;;; moment it posted into a dead link, instead of never reaching
2072
;;; it at all.
โˆ’2007
(define (enclave-confirm-delivery! conn)
โˆ’2008
(: enclave-conn? -> symbol?)
+2073
(define (enclave-confirm-delivery! conn irc-at-write)
+2074
(: enclave-conn? any? -> symbol?)
2075
(let ((overdue-ts (enclave-conn-oldest-ping-ts conn)))
โˆ’2010
(cond
โˆ’2011
((enclave-conn-shutdown? conn) 'not-ready)
โˆ’2012
((enclave-conn-reconnecting? conn) 'unconfirmed)
โˆ’2013
;; Guard 2 โ€” we already know. Checked BEFORE `ready?` on
โˆ’2014
;; purpose: an unanswered PING older than the budget settles
โˆ’2015
;; the question whatever the session state says, and putting
โˆ’2016
;; it first means the guard is reachable (and therefore
โˆ’2017
;; testable) without a live irc-connection.
โˆ’2018
((and overdue-ts
โˆ’2019
(> (- (current-second) overdue-ts) +post-confirm-timeout+))
โˆ’2020
(log-debug "Apiary post-confirm skipped โ€” ping already overdue"
โˆ’2021
overdue-sec: (number->string
โˆ’2022
(- (current-second) overdue-ts)))
โˆ’2023
'unconfirmed)
โˆ’2024
((not (enclave-conn-ready? conn)) 'unconfirmed)
โˆ’2025
(else
โˆ’2026
(let ((now (current-second)))
โˆ’2027
(cond
โˆ’2028
;; Burst backstop: a flood of posts inside one budget
โˆ’2029
;; must not grow the list without bound.
โˆ’2030
((>= (length (enclave-conn-pending-pings conn))
โˆ’2031
+max-outstanding-pings+)
โˆ’2032
(log-debug "Apiary post-confirm skipped โ€” too many pings outstanding")
โˆ’2033
'unconfirmed)
โˆ’2034
(else
โˆ’2035
(let ((token (send-ping! conn now)))
โˆ’2036
(cond
โˆ’2037
((not token) 'unconfirmed)
โˆ’2038
(else
โˆ’2039
(let ((deadline (+ (current-second)
โˆ’2040
+post-confirm-timeout+)))
โˆ’2041
(let wait ()
โˆ’2042
(cond
โˆ’2043
((not (enclave-conn-ping-waiting? conn token))
โˆ’2044
(log-debug "Apiary post-confirm confirmed"
โˆ’2045
elapsed-ms:
โˆ’2046
(number->string
โˆ’2047
(round (* 1000 (- (current-second)
โˆ’2048
now)))))
โˆ’2049
'confirmed)
โˆ’2050
((> (current-second) deadline)
โˆ’2051
(log-warn "Apiary post-confirm UNCONFIRMED โ€” no PONG within budget"
โˆ’2052
budget-sec:
โˆ’2053
(number->string +post-confirm-timeout+))
โˆ’2054
'unconfirmed)
โˆ’2055
(else
โˆ’2056
;; Yields to the cooperative scheduler so
โˆ’2057
;; the event-loop goroutine can read the
โˆ’2058
;; socket and ack our token.
โˆ’2059
(sleep +post-confirm-poll+)
โˆ’2060
(wait)))))))))))))))
+2076
(cond
+2077
((enclave-conn-shutdown? conn) 'not-ready)
+2078
((enclave-conn-reconnecting? conn) 'link-down)
+2079
;; The socket we wrote to is no longer the socket we have.
+2080
;; A PONG on the NEW connection would say nothing about a
+2081
;; message written to the OLD one, so refuse to interpret
+2082
;; it. An `irc-at-write` of #f opts out, for callers that
+2083
;; do not track which socket they wrote to.
+2084
((and irc-at-write
+2085
(not (eq? irc-at-write (enclave-conn-irc conn))))
+2086
'link-changed)
+2087
;; We already know: a PING has gone unanswered for longer
+2088
;; than the budget. Checked BEFORE `ready?` on purpose โ€” an
+2089
;; overdue PING settles the question whatever the session
+2090
;; state says, and putting it first means the guard is
+2091
;; reachable, and therefore testable, without a live
+2092
;; irc-connection.
+2093
((and overdue-ts
+2094
(> (- (current-second) overdue-ts) +post-confirm-timeout+))
+2095
(log-debug "Apiary post-confirm skipped โ€” ping already overdue"
+2096
overdue-sec: (number->string
+2097
(- (current-second) overdue-ts)))
+2098
'no-evidence)
+2099
((not (enclave-conn-ready? conn)) 'link-down)
+2100
;; Burst backstop: a flood of posts inside one budget must
+2101
;; not grow the outstanding list without bound.
+2102
((>= (length (enclave-conn-pending-pings conn))
+2103
+max-outstanding-pings+)
+2104
(log-debug "Apiary post-confirm skipped โ€” too many pings outstanding")
+2105
'no-evidence)
+2106
(else
+2107
(let* ((now (current-second))
+2108
(token (send-ping! conn now)))
+2109
(cond
+2110
((not token) 'link-down)
+2111
(else
+2112
(let ((deadline (+ (current-second) +post-confirm-timeout+)))
+2113
(let wait ()
+2114
(cond
+2115
;; POSITIVE evidence only. NOT "the token left
+2116
;; the outstanding list" โ€” see
+2117
;; `enclave-conn-ping-acked?` for why that is a
+2118
;; different and much weaker claim.
+2119
((enclave-conn-ping-acked? conn token)
+2120
(log-debug "Apiary post-confirm confirmed"
+2121
elapsed-ms:
+2122
(number->string
+2123
(round (* 1000 (- (current-second) now)))))
+2124
'confirmed)
+2125
;; Give up early once the link is known bad.
+2126
;; Belt and braces on top of the acked-token
+2127
;; check: neither alone is sufficient, because a
+2128
;; flag can be set and cleared again between two
+2129
;; wakeups.
+2130
((or (enclave-conn-shutdown? conn)
+2131
(enclave-conn-reconnecting? conn))
+2132
'link-down)
+2133
((and irc-at-write
+2134
(not (eq? irc-at-write (enclave-conn-irc conn))))
+2135
'link-changed)
+2136
((> (current-second) deadline)
+2137
(log-warn "Apiary post-confirm UNCONFIRMED โ€” no PONG within budget"
+2138
budget-sec:
+2139
(number->string +post-confirm-timeout+))
+2140
'unconfirmed)
+2141
(else
+2142
;; Yields to the cooperative scheduler so the
+2143
;; event-loop goroutine can read the socket and
+2144
;; ack our token.
+2145
(sleep +post-confirm-poll+)
+2146
(wait))))))))))))
2147
2148
;;; Send a PRIVMSG to a channel (or any target). Always uses the
2149
;;; `:trailing` form for the body so single-token messages like
src/apiary/tools.sglmodified
@@ -860,12 +860,18 @@
860
;; and don't re-prefix.
861
(let* ((lines (apply-mention-to-first-line
862
mention (split-lines text)))
+863
;; Capture WHICH socket the message goes out on. If a
+864
;; reconnect swaps it before confirmation completes, a
+865
;; PONG on the new connection says nothing about a
+866
;; message written to the old one, and confirmation
+867
;; refuses to interpret it.
+868
(irc-at-write (enclave-conn-irc conn))
869
(outcome (enclave-post-multiline conn primary lines))
870
(style (post-outcome-label outcome (length lines)))
871
;; Ask the peer to prove it took those bytes BEFORE we
872
;; describe them as posted. See
873
;; `enclave-confirm-delivery!`.
โˆ’868
(confirmed (enclave-confirm-delivery! conn)))
+874
(confirmed (enclave-confirm-delivery! conn irc-at-write)))
875
(cond
876
((and (string? mention) (> (string-length mention) 0))
877
(qualify-post-result
@@ -896,6 +902,32 @@
902
;;; reasonably wait instead of repeating itself โ€” a new false
903
;;; claim in the opposite direction, which is the same defect
904
;;; this change exists to remove.
+905
;;;
+906
;;; The REASON has to match too. An earlier version rendered "the
+907
;;; server did not answer within 3s" for every non-confirmed
+908
;;; outcome, including ones where no PING was sent and no budget
+909
;;; was ever spent. The verb was honest and the diagnosis was
+910
;;; invented, which is the same disease one clause along.
+911
(define (unconfirmed-reason confirmed)
+912
(: symbol? -> string?)
+913
(case confirmed
+914
((unconfirmed)
+915
(string-append "the server did not answer within "
+916
(number->string +post-confirm-timeout+)
+917
"s"))
+918
((no-evidence)
+919
(string-append "an earlier message has been unanswered for "
+920
"longer than "
+921
(number->string +post-confirm-timeout+)
+922
"s, so the link is already unresponsive"))
+923
((link-down)
+924
"the connection is down or re-establishing")
+925
((link-changed)
+926
"the connection was replaced before delivery could be checked")
+927
((not-ready)
+928
"the bridge is shutting down")
+929
(else "delivery could not be checked")))
+930
931
(define (qualify-post-result confirmed ok-text unconfirmed-text)
932
(: symbol? string? string? -> string?)
933
(case confirmed
@@ -903,11 +935,10 @@
935
(else
936
(string-append
937
unconfirmed-text
โˆ’906
" โ€” UNCONFIRMED: the server did not answer within "
โˆ’907
(number->string +post-confirm-timeout+)
โˆ’908
"s, so the link may be dead and this message may not have "
โˆ’909
"been delivered. Re-send once the bridge reports "
โˆ’910
"connected; nothing is holding a copy."))))
+938
" โ€” UNCONFIRMED: " (unconfirmed-reason confirmed)
+939
", so this message may not have been delivered. Re-send "
+940
"once the bridge reports connected; nothing is holding a "
+941
"copy."))))
942
943
;; Translate enclave-post-multiline's outcome symbol into a
944
;; tool-call status string. The string ships back to the agent
@@ -959,9 +990,10 @@
990
normalized
991
(markdown->irc normalized)))
992
(lines (split-lines formatted))
+993
(irc-at-write (enclave-conn-irc conn))
994
(outcome (enclave-post-multiline conn nick lines))
995
(style (post-outcome-label outcome (length lines)))
โˆ’964
(confirmed (enclave-confirm-delivery! conn)))
+996
(confirmed (enclave-confirm-delivery! conn irc-at-write)))
997
(qualify-post-result
998
confirmed
999
(string-append "DM to " nick ": " style)
@@ -983,12 +1015,23 @@
1015
((or (not (string? emoji)) (string=? emoji ""))
1016
"Error: emoji required")
1017
(else
โˆ’986
(enclave-post-react conn target msgid emoji)
โˆ’987
(let ((short-len (if (< (string-length msgid) 8)
โˆ’988
(string-length msgid) 8)))
โˆ’989
(string-append "Reacted " emoji
โˆ’990
" to " (substring msgid 0 short-len)
โˆ’991
"โ€ฆ in " target))))))
+1018
;; "Reacted" is a delivery claim exactly like "Broadcast",
+1019
;; so it gets the same evidence requirement. Left
+1020
;; unqualified this would have been the defect the rest of
+1021
;; this change removes, surviving one function over.
+1022
(let ((irc-at-write (enclave-conn-irc conn)))
+1023
(enclave-post-react conn target msgid emoji)
+1024
(let ((confirmed (enclave-confirm-delivery! conn irc-at-write))
+1025
(short-len (if (< (string-length msgid) 8)
+1026
(string-length msgid) 8)))
+1027
(qualify-post-result
+1028
confirmed
+1029
(string-append "Reacted " emoji
+1030
" to " (substring msgid 0 short-len)
+1031
"โ€ฆ in " target)
+1032
(string-append "Wrote reaction " emoji
+1033
" to " (substring msgid 0 short-len)
+1034
"โ€ฆ in " target))))))))
1035
1036
;; ============================================================
1037
;; Internal: result formatters
test/test-enclave.sglmodified
@@ -166,20 +166,42 @@
166
167
;; ---- the idle clock is INBOUND-ONLY -------------------------
168
;;
โˆ’169
;; This is the blocker-2 regression guard and it is deliberately
โˆ’170
;; aimed at the PRODUCT, not at a mirror of it.
+169
;; The blocker-2 regression guard, aimed at the PRODUCT rather than
+170
;; at a mirror of it.
171
;;
โˆ’172
;; `enclave-post-multiline` with no lines returns 'sent-empty
โˆ’173
;; without touching the irc connection at all, so it runs fine on
โˆ’174
;; a bare conn. The old code refreshed the activity clock at the
โˆ’175
;; TOP of that procedure, before the empty check โ€” so this test
โˆ’176
;; FAILS against the pre-fix source and passes after. A test that
โˆ’177
;; could not distinguish the two would be worth nothing here.
+172
;; READ THIS BEFORE TRUSTING IT โ€” the first version of this test
+173
;; was VACUOUS and an adversarial reviewer proved it. It drove
+174
;; `enclave-post-multiline` with `'()`, which returns 'sent-empty
+175
;; early. That does fail against the pre-fix source, so it looked
+176
;; like a real gate. But `split-lines` returns `(list "")` for
+177
;; empty text, so NO CALLER CAN EVER REACH the empty-list path: the
+178
;; guard covered a dead branch, and re-adding the touch to the
+179
;; single-line branch โ€” the path every real post takes โ€” left all
+180
;; tests green.
+181
;;
+182
;; These drive the paths real posts take. There is no irc here, so
+183
;; the write raises and is swallowed; what is being asserted is
+184
;; that nothing refreshed the clock ON THE WAY TO the write, which
+185
;; is where the removed calls lived in every one of these three
+186
;; procedures.
+187
;;
+188
;; Its limit, stated so nobody over-reads it: a touch added AFTER
+189
;; the write would not be caught here, because the write raises
+190
;; first. The gate for that is the harness experiment
+191
;; `test/harness/exp_chatty_vs_silent.py`, which runs against a
+192
;; live server and is the only thing that can see it.
193
(test "an outbound post does NOT refresh the idle clock"
โˆ’179
(let ((c (enclave-conn irc: #f config: (enclave-config))))
โˆ’180
(set-enclave-conn-last-inbound-ts! c 100)
โˆ’181
(assert-equal 'sent-empty (enclave-post-multiline c "#chan" '()))
โˆ’182
(assert-equal 100 (enclave-conn-last-inbound-ts c))))
+194
(for-each
+195
(lambda (post!)
+196
(let ((c (enclave-conn irc: #f config: (enclave-config))))
+197
(set-enclave-conn-last-inbound-ts! c 100)
+198
(guard (e (else #f)) (post! c))
+199
(assert-equal 100 (enclave-conn-last-inbound-ts c))))
+200
(list (lambda (c) (enclave-post c "#chan" "hello there"))
+201
(lambda (c) (enclave-post-multiline c "#chan" '("hello there")))
+202
(lambda (c) (enclave-post-multiline c "#chan"
+203
'("one two" "three four")))
+204
(lambda (c) (enclave-post-react c "#chan" "mid1" ":+1:")))))
205
206
(test "keepalive-tick: idle conn under threshold does NOT PING"
207
;; No outstanding PING, but inbound traffic was only 30s ago โ€”
@@ -308,12 +330,12 @@
330
(test "confirm-delivery! on a shut-down conn is 'not-ready"
331
(let ((c (enclave-conn irc: #f config: (enclave-config))))
332
(set-enclave-conn-shutdown?! c #t)
โˆ’311
(assert-equal 'not-ready (enclave-confirm-delivery! c))))
+333
(assert-equal 'not-ready (enclave-confirm-delivery! c #f))))
334
โˆ’313
(test "confirm-delivery! while reconnecting is 'unconfirmed"
+335
(test "confirm-delivery! while reconnecting is 'link-down"
336
(let ((c (enclave-conn irc: #f config: (enclave-config))))
337
(set-enclave-conn-reconnecting?! c #t)
โˆ’316
(assert-equal 'unconfirmed (enclave-confirm-delivery! c))))
+338
(assert-equal 'link-down (enclave-confirm-delivery! c #f))))
339
340
(test "confirm-delivery! answers immediately when a ping is already overdue"
341
;; Guard 2, scored through the real entry point.
@@ -333,8 +355,44 @@
355
(enclave-conn-ping-add! c "apk-overdue"
356
(- (current-second)
357
(+ +post-confirm-timeout+ 1)))
โˆ’336
(assert-equal 'unconfirmed (enclave-confirm-delivery! c))
โˆ’337
(assert-equal 1 (length (enclave-conn-pending-pings c))))))
+358
(assert-equal 'no-evidence (enclave-confirm-delivery! c #f))
+359
(assert-equal 1 (length (enclave-conn-pending-pings c)))))
+360
+361
(test "confirm-delivery! refuses to read a PONG from a REPLACED socket"
+362
;; The socket-identity guard. A reconnect can swap `irc` between
+363
;; the write and the confirmation, and a PONG arriving on the NEW
+364
;; connection says nothing about a message written to the OLD
+365
;; one. Passing an `irc-at-write` that no longer matches must be
+366
;; refused outright rather than confirmed.
+367
(let ((c (enclave-conn irc: 'socket-b config: (enclave-config))))
+368
(assert-equal 'link-changed (enclave-confirm-delivery! c 'socket-a))))
+369
+370
(test "an abandoned ping is NOT evidence of delivery"
+371
;; The HIGH an adversarial review found. A token leaves the
+372
;; outstanding list for two very different reasons: the peer
+373
;; answered it, or we gave up on the socket. Confirmation must
+374
;; act only on the first.
+375
;;
+376
;; enclave-disconnect clears the outstanding list without any
+377
;; PONG ever arriving. If confirmation keyed on absence โ€” as it
+378
;; originally did โ€” this would read as proof of delivery.
+379
(let ((c (enclave-conn irc: #f config: (enclave-config))))
+380
(enclave-conn-ping-add! c "apk-abandoned" (current-second))
+381
(assert-true (enclave-conn-ping-waiting? c "apk-abandoned"))
+382
(enclave-conn-pings-clear! c)
+383
(assert-false (enclave-conn-ping-waiting? c "apk-abandoned"))
+384
(assert-false (enclave-conn-ping-acked? c "apk-abandoned"))))
+385
+386
(test "an ANSWERED ping is evidence, and only for its own token"
+387
;; The positive control for the test above: the same predicate
+388
;; must report #t when the peer really did answer, or "not acked"
+389
;; would pass for free and prove nothing.
+390
(let ((c (enclave-conn irc: #f config: (enclave-config))))
+391
(enclave-conn-ping-add! c "apk-answered" (current-second))
+392
(enclave-conn-ping-add! c "apk-other" (current-second))
+393
(assert-true (enclave-conn-ping-ack! c "apk-answered"))
+394
(assert-true (enclave-conn-ping-acked? c "apk-answered"))
+395
(assert-false (enclave-conn-ping-acked? c "apk-other")))))
396
397
398
test/test-tools.sglmodified
@@ -643,4 +643,22 @@
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")))))
+646
(assert-false (string-contains? out "ueued"))))
+647
+648
(test "the UNCONFIRMED reason matches why it was unconfirmed"
+649
;; An earlier version claimed "the server did not answer within
+650
;; 3s" for every outcome, including ones where no PING was sent
+651
;; and no budget was ever spent. The verb was honest and the
+652
;; 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 "already 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 delivery
+663
(for-each (lambda (s) (assert-false (string-contains? s "ok")))
+664
(list budget stale down swapped)))))