Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 35s
Durable booking path alongside in-memory: ev/book-occ!, ev/cancel-occ!, ev/roster-occ, ev/seats-left-occ (capacity from scheduled event); ev/free-p?, ev/next-free-p, ev/conflicts-p derive availability by replaying persist booking streams. Reordered conformance preloads. 120/120 green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
272 lines
8.7 KiB
Plaintext
272 lines
8.7 KiB
Plaintext
;; lib/events/tests/api.sx — public events facade (schedule/agenda/free/book).
|
|
|
|
(define ev-api-pass 0)
|
|
(define ev-api-fail 0)
|
|
(define ev-api-failures (list))
|
|
|
|
(define
|
|
ev-api-check!
|
|
(fn
|
|
(name got expected)
|
|
(if
|
|
(= got expected)
|
|
(set! ev-api-pass (+ ev-api-pass 1))
|
|
(do
|
|
(set! ev-api-fail (+ ev-api-fail 1))
|
|
(append!
|
|
ev-api-failures
|
|
(str name "\n expected: " expected "\n got: " got))))))
|
|
|
|
;; A store with a weekly yoga class (Mon+Wed 18:00, 60m, 4 occurrences).
|
|
(define
|
|
ev-api-store
|
|
(fn
|
|
()
|
|
(ev/schedule
|
|
(ev/empty)
|
|
(quote yoga)
|
|
(ev-dt 2026 6 1 18 0)
|
|
60
|
|
{:freq :weekly :count 4 :byday (list 0 2)}
|
|
20)))
|
|
|
|
(define
|
|
ev-api-run-all!
|
|
(fn
|
|
()
|
|
(let
|
|
((s0 (ev-api-store)))
|
|
(let
|
|
((occs (ev/agenda s0 (ev-date 2026 6 1) (ev-date 2026 7 1))))
|
|
(let
|
|
((s1 (ev/book (ev/book s0 (quote nia) (ev-occ-key (first occs))) (quote nia) (ev-occ-key (first (rest occs))))))
|
|
(do
|
|
(ev-api-check!
|
|
"agenda expands weekly class to four occurrences"
|
|
(map (fn (o) (ev-dt->civil (get o :start))) occs)
|
|
(list
|
|
(list 2026 6 1)
|
|
(list 2026 6 3)
|
|
(list 2026 6 8)
|
|
(list 2026 6 10)))
|
|
(ev-api-check!
|
|
"empty store has empty agenda"
|
|
(ev/agenda
|
|
(ev/empty)
|
|
(ev-date 2026 6 1)
|
|
(ev-date 2026 7 1))
|
|
(list))
|
|
(ev-api-check!
|
|
"max duration reflects scheduled events"
|
|
(ev/store-max-duration s0)
|
|
60)
|
|
(ev-api-check!
|
|
"max duration of empty store is zero"
|
|
(ev/store-max-duration (ev/empty))
|
|
0)
|
|
(ev-api-check!
|
|
"event-by-id finds the scheduled event"
|
|
(get (ev/event-by-id s0 (quote yoga)) :capacity)
|
|
20)
|
|
(ev-api-check!
|
|
"event-by-id is nil for unknown id"
|
|
(ev/event-by-id s0 (quote nope))
|
|
nil)
|
|
(ev-api-check!
|
|
"agenda-for lists only booked occurrences"
|
|
(map
|
|
(fn (o) (ev-dt->civil (get o :start)))
|
|
(ev/agenda-for
|
|
s1
|
|
(quote nia)
|
|
(ev-date 2026 6 1)
|
|
(ev-date 2026 7 1)))
|
|
(list
|
|
(list 2026 6 1)
|
|
(list 2026 6 3)))
|
|
(ev-api-check!
|
|
"agenda-for empty for unbooked actor"
|
|
(ev/agenda-for
|
|
s1
|
|
(quote zed)
|
|
(ev-date 2026 6 1)
|
|
(ev-date 2026 7 1))
|
|
(list))
|
|
(ev-api-check!
|
|
"free? false during a booked occurrence"
|
|
(ev/free?
|
|
s1
|
|
(quote nia)
|
|
(ev-dt 2026 6 1 18 30)
|
|
(ev-dt 2026 6 1 19 0))
|
|
false)
|
|
(ev-api-check!
|
|
"free? true in an open window"
|
|
(ev/free?
|
|
s1
|
|
(quote nia)
|
|
(ev-dt 2026 6 1 9 0)
|
|
(ev-dt 2026 6 1 10 0))
|
|
true)
|
|
(ev-api-check!
|
|
"free? half-open at occurrence end"
|
|
(ev/free?
|
|
s1
|
|
(quote nia)
|
|
(ev-dt 2026 6 1 19 0)
|
|
(ev-dt 2026 6 1 20 0))
|
|
true)
|
|
(ev-api-check!
|
|
"free? true for an actor who booked nothing"
|
|
(ev/free?
|
|
s1
|
|
(quote zed)
|
|
(ev-dt 2026 6 1 18 0)
|
|
(ev-dt 2026 6 1 19 0))
|
|
true)
|
|
(ev-api-check!
|
|
"next-free skips the booked slot to the hour after"
|
|
(ev-dt-tod
|
|
(ev/next-free
|
|
s1
|
|
(quote nia)
|
|
(ev-dt
|
|
2026
|
|
6
|
|
1
|
|
18
|
|
0)
|
|
60
|
|
(ev-dt
|
|
2026
|
|
6
|
|
1
|
|
23
|
|
0)))
|
|
(* 19 60))
|
|
(ev-api-check!
|
|
"next-free returns `after` when already open"
|
|
(ev/next-free
|
|
s1
|
|
(quote nia)
|
|
(ev-dt 2026 6 1 9 0)
|
|
60
|
|
(ev-dt 2026 6 1 18 0))
|
|
(ev-dt 2026 6 1 9 0))
|
|
(ev-api-check!
|
|
"no conflict among disjoint bookings"
|
|
(ev/has-conflict?
|
|
s1
|
|
(quote nia)
|
|
(ev-date 2026 6 1)
|
|
(ev-date 2026 7 1))
|
|
false)
|
|
(let
|
|
((sc (ev/book (ev/schedule s1 (quote talk) (ev-dt 2026 6 1 18 30) 60 nil 5) (quote nia) (ev-occ-key (ev-occ (quote talk) (ev-dt 2026 6 1 18 30) 60)))))
|
|
(ev-api-check!
|
|
"overlapping second booking creates a conflict"
|
|
(ev/has-conflict?
|
|
sc
|
|
(quote nia)
|
|
(ev-date 2026 6 1)
|
|
(ev-date 2026 7 1))
|
|
true))
|
|
(let
|
|
((b (persist/open)) (occ1 (first occs)))
|
|
(do
|
|
(let
|
|
((sp (ev/schedule (ev/empty) (quote clinic) (ev-dt 2026 6 5 9 0) 30 nil 2)))
|
|
(let
|
|
((occ (ev-occ (quote clinic) (ev-dt 2026 6 5 9 0) 30)))
|
|
(do
|
|
(ev-api-check!
|
|
"durable book returns booked"
|
|
(get (ev/book-occ! b sp (quote a) occ) :status)
|
|
:booked)
|
|
(ev/book-occ! b sp (quote c) occ)
|
|
(ev-api-check!
|
|
"durable book past capacity is full"
|
|
(get (ev/book-occ! b sp (quote d) occ) :status)
|
|
:full)
|
|
(ev-api-check!
|
|
"durable roster reflects persisted bookings"
|
|
(ev/roster-occ b occ)
|
|
(list (quote a) (quote c)))
|
|
(ev-api-check!
|
|
"durable seats-left honours capacity"
|
|
(ev/seats-left-occ b sp occ)
|
|
0)
|
|
(ev-api-check!
|
|
"persist free? false during a durable booking"
|
|
(ev/free-p?
|
|
b
|
|
sp
|
|
(quote a)
|
|
(ev-dt
|
|
2026
|
|
6
|
|
5
|
|
9
|
|
10)
|
|
(ev-dt
|
|
2026
|
|
6
|
|
5
|
|
9
|
|
20))
|
|
false)
|
|
(ev-api-check!
|
|
"persist free? true in an open window"
|
|
(ev/free-p?
|
|
b
|
|
sp
|
|
(quote a)
|
|
(ev-dt
|
|
2026
|
|
6
|
|
5
|
|
10
|
|
0)
|
|
(ev-dt
|
|
2026
|
|
6
|
|
5
|
|
10
|
|
30))
|
|
true)
|
|
(ev/cancel-occ! b sp (quote a) occ)
|
|
(ev-api-check!
|
|
"durable cancel frees a seat"
|
|
(ev/seats-left-occ b sp occ)
|
|
1)
|
|
(ev-api-check!
|
|
"persist free? true after cancellation"
|
|
(ev/free-p?
|
|
b
|
|
sp
|
|
(quote a)
|
|
(ev-dt
|
|
2026
|
|
6
|
|
5
|
|
9
|
|
10)
|
|
(ev-dt
|
|
2026
|
|
6
|
|
5
|
|
9
|
|
20))
|
|
true))))))))))))
|
|
|
|
(define
|
|
ev-api-tests-run!
|
|
(fn
|
|
()
|
|
(do
|
|
(set! ev-api-pass 0)
|
|
(set! ev-api-fail 0)
|
|
(set! ev-api-failures (list))
|
|
(ev-api-run-all!)
|
|
{:failures ev-api-failures :total (+ ev-api-pass ev-api-fail) :passed ev-api-pass :failed ev-api-fail})))
|