;; lib/flow/tests/suspend.sx — Phase 3: suspend / resume / cancel (deterministic replay). (define flow-sus-pass 0) (define flow-sus-fail 0) (define flow-sus-fails (list)) (define flow-sus-test (fn (name actual expected) (if (= actual expected) (set! flow-sus-pass (+ flow-sus-pass 1)) (begin (set! flow-sus-fail (+ flow-sus-fail 1)) (append! flow-sus-fails {:name name :expected expected :actual actual}))))) (define flow-s (fn (src) (flow-run src))) ;; ── flow/start ────────────────────────────────────────────────── (flow-sus-test "start: non-suspending flow returns the raw result" (flow-s "(flow/start (lambda (x) (* x 2)) 5)") 10) (flow-sus-test "start: a suspending flow returns a flow-suspended state" (flow-s "(defflow w (sequence (lambda (x) (+ x 1)) (lambda (g) (suspend (quote await))) (lambda (c) c))) (car (flow/start w 10))") "flow-suspended") (flow-sus-test "start: suspended state carries a numeric id" (flow-s "(defflow w (lambda (x) (suspend (quote await)))) (car (cdr (flow/start w 10)))") 1) (flow-sus-test "start: suspended state carries the suspend tag" (flow-s "(defflow w (lambda (x) (suspend (quote await)))) (car (cdr (cdr (flow/start w 10))))") "await") ;; ── flow/resume ───────────────────────────────────────────────── (flow-sus-test "resume: injects the value and completes" (flow-s "(defflow w (sequence (lambda (x) (+ x 1)) (lambda (g) (suspend (quote await))) (lambda (c) (list (quote done) c)))) (define s (flow/start w 10)) (flow/resume (car (cdr s)) 777)") (list "done" 777)) (flow-sus-test "resume: injected value threads into the next node" (flow-s "(defflow w (sequence (lambda (x) (suspend (quote v))) (lambda (n) (* n 3)))) (define s (flow/start w 0)) (flow/resume (car (cdr s)) 14)") 42) (flow-sus-test "resume: replays earlier suspends (recompute is deterministic)" (flow-s "(define runs 0) (defflow w (sequence (lambda (x) (begin (set! runs (+ runs 1)) (+ x 1))) (lambda (g) (suspend (quote await))) (lambda (c) c))) (define s (flow/start w 10)) (flow/resume (car (cdr s)) 99) runs") 2) ;; ── multi-step suspension ─────────────────────────────────────── (flow-sus-test "multi: first resume suspends at the next tag" (flow-s "(defflow two (sequence (lambda (x) (suspend (quote a))) (lambda (x) (suspend (quote b))) (lambda (x) (list (quote end) x)))) (define s (flow/start two 0)) (define s2 (flow/resume (car (cdr s)) 100)) (car (cdr (cdr s2)))") "b") (flow-sus-test "multi: second resume completes with the latest value" (flow-s "(defflow two (sequence (lambda (x) (suspend (quote a))) (lambda (x) (suspend (quote b))) (lambda (x) (list (quote end) x)))) (define id (car (cdr (flow/start two 0)))) (flow/resume id 100) (flow/resume id 200)") (list "end" 200)) ;; ── error / lifecycle guards ──────────────────────────────────── (flow-sus-test "resume: completed flow cannot be resumed again" (flow-s "(defflow w (lambda (x) (suspend (quote q)))) (define id (car (cdr (flow/start w 0)))) (flow/resume id 1) (flow/resume id 2)") (list "flow-error" "not-suspended")) (flow-sus-test "resume: unknown id errors" (flow-s "(flow/resume 999 1)") (list "flow-error" "no-such-flow")) ;; ── flow/cancel ───────────────────────────────────────────────── (flow-sus-test "cancel: returns a flow-cancelled state" (flow-s "(defflow w (lambda (x) (suspend (quote q)))) (define id (car (cdr (flow/start w 0)))) (flow/cancel id)") (list "flow-cancelled" 1)) (flow-sus-test "cancel: a cancelled flow cannot be resumed (stale resume rejected)" (flow-s "(defflow w (lambda (x) (suspend (quote q)))) (define id (car (cdr (flow/start w 0)))) (flow/cancel id) (flow/resume id 5)") (list "flow-error" "not-suspended")) (flow-sus-test "cancel: unknown id errors" (flow-s "(flow/cancel 999)") (list "flow-error" "no-such-flow")) ;; ── composition ───────────────────────────────────────────────── (flow-sus-test "suspend inside a branch arm" (flow-s "(defflow gate (branch (lambda (x) (> x 0)) (lambda (x) (suspend (quote approve))) (flow-const (quote rejected)))) (define s (flow/start gate 5)) (flow/resume (car (cdr s)) (quote approved))") "approved") (flow-sus-test "two independent runs get independent ids" (flow-s "(defflow w (lambda (x) (suspend (quote q)))) (list (car (cdr (flow/start w 0))) (car (cdr (flow/start w 0))))") (list 1 2)) (flow-sus-test "suspend reason may be a structured value" (flow-s "(defflow w (lambda (x) (suspend (list (quote needs) (quote approval))))) (car (cdr (cdr (flow/start w 0))))") (list "needs" "approval")) (define flow-sus-tests-run! (fn () {:total (+ flow-sus-pass flow-sus-fail) :passed flow-sus-pass :failed flow-sus-fail :fails flow-sus-fails}))