;; lib/flow/tests/control.sx — Phase 2: control flow + error handling. (define flow-ctl-pass 0) (define flow-ctl-fail 0) (define flow-ctl-fails (list)) (define flow-ctl-test (fn (name actual expected) (if (= actual expected) (set! flow-ctl-pass (+ flow-ctl-pass 1)) (begin (set! flow-ctl-fail (+ flow-ctl-fail 1)) (append! flow-ctl-fails {:name name :expected expected :actual actual}))))) (define flow-c (fn (src) (flow-run src))) (define flow-cs (fn (src) (get (flow-run src) :scm-string))) ;; ── branch ────────────────────────────────────────────────────── (flow-ctl-test "branch: true selects then" (flow-c "(flow/start (branch (lambda (x) (> x 0)) (lambda (x) (* x 100)) (lambda (x) (- 0 x))) 5)") 500) (flow-ctl-test "branch: false selects else" (flow-c "(flow/start (branch (lambda (x) (> x 0)) (lambda (x) (* x 100)) (lambda (x) (- 0 x))) -3)") 3) (flow-ctl-test "branch: predicate sees the threaded input" (flow-c "(flow/start (sequence (lambda (x) (+ x 1)) (branch (lambda (x) (> x 3)) (flow-const 100) (flow-const 0))) 3)") 100) (flow-ctl-test "branch: branches are full nodes (sequence inside)" (flow-c "(flow/start (branch (lambda (x) (< x 10)) (sequence (lambda (x) (+ x 1)) (lambda (x) (* x 2))) (flow-const 0)) 4)") 10) (flow-ctl-test "branch: nested branch (3-way sign)" (flow-c "(defflow sign (branch (lambda (x) (> x 0)) (flow-const 1) (branch (lambda (x) (< x 0)) (flow-const -1) (flow-const 0)))) (list (flow/start sign 7) (flow/start sign -7) (flow/start sign 0))") (list 1 -1 0)) (flow-ctl-test "branch: publish-shaped approval gate" (flow-cs "(defflow publish (branch (lambda (post) (>= (string-length post) 3)) (lambda (post) (string-append post \" [published]\")) (lambda (post) (string-append post \" [rejected]\")))) (flow/start publish \"ok\")") "ok [rejected]") ;; ── error model — explicit (fail reason) values ───────────────── (flow-ctl-test "fail: failed? is true for a failure value" (flow-c "(failed? (fail 404))") true) (flow-ctl-test "fail: fail-reason extracts the reason" (flow-c "(fail-reason (fail 404))") 404) (flow-ctl-test "fail: failed? is false for a plain value" (flow-c "(failed? 7)") false) (flow-ctl-test "fail: failed? is false for an ordinary list" (flow-c "(failed? (list 1 2 3))") false) (flow-ctl-test "fail: a node may emit a failure as data" (flow-c "(defflow validate (lambda (s) (if (>= (string-length s) 3) s (fail (quote too-short))))) (failed? (flow/start validate \"hi\"))") true) (flow-ctl-test "fail: failure flows downstream, branch recovers" (flow-c "(defflow guarded (sequence (lambda (s) (if (>= (string-length s) 3) (string-length s) (fail (quote too-short)))) (branch failed? (lambda (f) (list (quote recovered) (fail-reason f))) (lambda (n) (list (quote ok) n))))) (flow/start guarded \"hi\")") (list "recovered" "too-short")) ;; ── try-catch — reify raised exceptions ───────────────────────── (flow-ctl-test "try-catch: no exception returns node result" (flow-c "(flow/start (try-catch (lambda (x) (* x 2)) (lambda (e) -1)) 5)") 10) (flow-ctl-test "try-catch: handler runs on raise" (flow-c "(flow/start (try-catch (lambda (x) (raise (quote boom))) (flow-const 99)) 1)") 99) (flow-ctl-test "try-catch: handler receives the reified error" (flow-c "(flow/start (try-catch (lambda (x) (raise 42)) (lambda (e) e)) 0)") 42) (flow-ctl-test "try-catch: catches exception from deep inside a sequence" (flow-c "(flow/start (try-catch (sequence (lambda (x) (+ x 1)) (lambda (x) (raise (quote deep)))) (flow-const -99)) 5)") -99) (flow-ctl-test "try-catch: handler may convert to a failure value" (flow-c "(failed? (flow/start (try-catch (lambda (x) (raise (quote bad))) (lambda (e) (fail e))) 0))") true) (flow-ctl-test "try-catch: composes — recover then continue" (flow-c "(flow/start (sequence (try-catch (lambda (x) (raise (quote x))) (flow-const 10)) (lambda (n) (* n 5))) 0)") 50) ;; ── retry — re-run on raised exceptions ───────────────────────── (flow-ctl-test "retry: succeeds after transient failures" (flow-c "(define ctr 0) (defflow flaky (lambda (x) (set! ctr (+ ctr 1)) (if (< ctr 3) (raise (quote nope)) (* x 10)))) (list (flow/start (retry 5 flaky) 7) ctr)") (list 70 3)) (flow-ctl-test "retry: exhausted re-raises (caught by try-catch)" (flow-c "(flow/start (try-catch (retry 2 (lambda (x) (raise (quote always)))) (flow-const (quote gaveup))) 0)") "gaveup") (flow-ctl-test "retry: n=1 means a single attempt" (flow-c "(define ctr 0) (flow/start (try-catch (retry 1 (lambda (x) (set! ctr (+ ctr 1)) (raise (quote bad)))) (lambda (e) ctr)) 0)") 1) (flow-ctl-test "retry: success on first attempt does not re-run" (flow-c "(define ctr 0) (flow/start (sequence (retry 5 (lambda (x) (set! ctr (+ ctr 1)) (* x 2))) (lambda (n) ctr)) 21)") 1) (flow-ctl-test "retry: does not retry explicit failure values" (flow-c "(define ctr 0) (failed? (flow/start (retry 5 (lambda (x) (set! ctr (+ ctr 1)) (fail (quote bad)))) 0))") true) (flow-ctl-test "retry: failure-value path runs node exactly once" (flow-c "(define ctr 0) (flow/start (sequence (retry 5 (lambda (x) (set! ctr (+ ctr 1)) (fail (quote bad)))) (lambda (f) ctr)) 0)") 1) ;; ── timeout — cooperative step budget ─────────────────────────── (flow-ctl-test "timeout: work within budget completes" (flow-c "(define (cd n) (if (<= n 0) 99 (begin (tick) (cd (- n 1))))) (flow/start (try-catch (timeout 10 (lambda (x) (cd x))) (flow-const (quote timed-out))) 5)") 99) (flow-ctl-test "timeout: work exceeding budget raises flow-timeout" (flow-c "(define (cd n) (if (<= n 0) 99 (begin (tick) (cd (- n 1))))) (flow/start (try-catch (timeout 10 (lambda (x) (cd x))) (flow-const (quote timed-out))) 20)") "timed-out") (flow-ctl-test "timeout: exact budget boundary completes" (flow-c "(define (cd n) (if (<= n 0) 99 (begin (tick) (cd (- n 1))))) (flow/start (try-catch (timeout 5 (lambda (x) (cd x))) (flow-const (quote timed-out))) 5)") 99) (flow-ctl-test "timeout: one tick over the budget raises" (flow-c "(define (cd n) (if (<= n 0) 99 (begin (tick) (cd (- n 1))))) (flow/start (try-catch (timeout 5 (lambda (x) (cd x))) (flow-const (quote timed-out))) 6)") "timed-out") (flow-ctl-test "timeout: the raised error is identifiable" (flow-c "(define (cd n) (if (<= n 0) 99 (begin (tick) (cd (- n 1))))) (flow/start (try-catch (timeout 2 (lambda (x) (cd x))) (lambda (e) e)) 9)") "flow-timeout") (flow-ctl-test "timeout: a node that never ticks is unbounded" (flow-c "(flow/start (timeout 0 (lambda (x) (* x 2))) 5)") 10) (flow-ctl-test "timeout: budget is restored across sequential timeouts" (flow-c "(define (cd n) (if (<= n 0) 1 (begin (tick) (cd (- n 1))))) (flow/start (sequence (timeout 4 (lambda (x) (cd x))) (timeout 4 (lambda (x) (cd 3))) (lambda (x) (begin (tick) (+ x 100)))) 3)") 101) (define flow-ctl-tests-run! (fn () {:total (+ flow-ctl-pass flow-ctl-fail) :passed flow-ctl-pass :failed flow-ctl-fail :fails flow-ctl-fails}))