;; ==========================================================================
;; test-types.sx — Tests for the SX gradual type system
;;
;; Requires: test-framework.sx loaded first.
;; Modules tested: types.sx (subtype?, infer-type, check-component, etc.)
;;
;; Platform functions required (beyond test framework):
;;   All type system functions from types.sx must be loaded.
;;   test-prim-types — a dict of primitive return types for testing.
;; ==========================================================================


;; --------------------------------------------------------------------------
;; Subtype checking
;; --------------------------------------------------------------------------

(defsuite "subtype-basics"
  (deftest "any accepts everything"
    (assert-true (subtype? "number" "any"))
    (assert-true (subtype? "string" "any"))
    (assert-true (subtype? "nil" "any"))
    (assert-true (subtype? "boolean" "any"))
    (assert-true (subtype? "any" "any")))

  (deftest "never is subtype of everything"
    (assert-true (subtype? "never" "number"))
    (assert-true (subtype? "never" "string"))
    (assert-true (subtype? "never" "any"))
    (assert-true (subtype? "never" "nil")))

  (deftest "identical types"
    (assert-true (subtype? "number" "number"))
    (assert-true (subtype? "string" "string"))
    (assert-true (subtype? "boolean" "boolean"))
    (assert-true (subtype? "nil" "nil")))

  (deftest "different base types are not subtypes"
    (assert-false (subtype? "number" "string"))
    (assert-false (subtype? "string" "number"))
    (assert-false (subtype? "boolean" "number"))
    (assert-false (subtype? "string" "boolean")))

  (deftest "any is not subtype of specific type"
    (assert-false (subtype? "any" "number"))
    (assert-false (subtype? "any" "string"))))


(defsuite "subtype-nullable"
  (deftest "nil is subtype of nullable types"
    (assert-true (subtype? "nil" "string?"))
    (assert-true (subtype? "nil" "number?"))
    (assert-true (subtype? "nil" "dict?"))
    (assert-true (subtype? "nil" "boolean?")))

  (deftest "base is subtype of its nullable"
    (assert-true (subtype? "string" "string?"))
    (assert-true (subtype? "number" "number?"))
    (assert-true (subtype? "dict" "dict?")))

  (deftest "nullable is not subtype of base"
    (assert-false (subtype? "string?" "string"))
    (assert-false (subtype? "number?" "number")))

  (deftest "different nullable types are not subtypes"
    (assert-false (subtype? "number" "string?"))
    (assert-false (subtype? "string" "number?"))))


(defsuite "subtype-unions"
  (deftest "member is subtype of union"
    (assert-true (subtype? "number" (list "or" "number" "string")))
    (assert-true (subtype? "string" (list "or" "number" "string"))))

  (deftest "non-member is not subtype of union"
    (assert-false (subtype? "boolean" (list "or" "number" "string"))))

  (deftest "union is subtype if all members are"
    (assert-true (subtype? (list "or" "number" "string")
                           (list "or" "number" "string" "boolean")))
    (assert-true (subtype? (list "or" "number" "string") "any")))

  (deftest "union is not subtype if any member is not"
    (assert-false (subtype? (list "or" "number" "string") "number"))))


(defsuite "subtype-list-of"
  (deftest "list-of covariance"
    (assert-true (subtype? (list "list-of" "number") (list "list-of" "number")))
    (assert-true (subtype? (list "list-of" "number") (list "list-of" "any"))))

  (deftest "list-of is subtype of list"
    (assert-true (subtype? (list "list-of" "number") "list")))

  (deftest "list is subtype of list-of any"
    (assert-true (subtype? "list" (list "list-of" "any")))))


;; --------------------------------------------------------------------------
;; Type union
;; --------------------------------------------------------------------------

(defsuite "type-union"
  (deftest "same types"
    (assert-equal "number" (type-union "number" "number"))
    (assert-equal "string" (type-union "string" "string")))

  (deftest "any absorbs"
    (assert-equal "any" (type-union "any" "number"))
    (assert-equal "any" (type-union "number" "any")))

  (deftest "never is identity"
    (assert-equal "number" (type-union "never" "number"))
    (assert-equal "string" (type-union "string" "never")))

  (deftest "nil + base creates nullable"
    (assert-equal "string?" (type-union "nil" "string"))
    (assert-equal "number?" (type-union "number" "nil")))

  (deftest "subtype collapses"
    (assert-equal "string?" (type-union "string" "string?"))
    (assert-equal "string?" (type-union "string?" "string")))

  (deftest "incompatible creates union"
    (let ((result (type-union "number" "string")))
      (assert-true (= (type-of result) "list"))
      (assert-equal "or" (first result))
      (assert-true (contains? result "number"))
      (assert-true (contains? result "string")))))


;; --------------------------------------------------------------------------
;; Type narrowing
;; --------------------------------------------------------------------------

(defsuite "type-narrowing"
  (deftest "nil? narrows to nil in then branch"
    (let ((result (narrow-type "string?" "nil?")))
      (assert-equal "nil" (first result))
      (assert-equal "string" (nth result 1))))

  (deftest "nil? narrows any stays any"
    (let ((result (narrow-type "any" "nil?")))
      (assert-equal "nil" (first result))
      (assert-equal "any" (nth result 1))))

  (deftest "string? narrows to string in then branch"
    (let ((result (narrow-type "any" "string?")))
      (assert-equal "string" (first result))
      ;; else branch — can't narrow any
      (assert-equal "any" (nth result 1))))

  (deftest "nil? on nil type narrows to never in else"
    (let ((result (narrow-type "nil" "nil?")))
      (assert-equal "nil" (first result))
      (assert-equal "never" (nth result 1)))))


;; --------------------------------------------------------------------------
;; Type inference
;; --------------------------------------------------------------------------

(defsuite "infer-literals"
  (deftest "number literal"
    (assert-equal "number" (infer-type 42 (dict) (test-prim-types))))

  (deftest "string literal"
    (assert-equal "string" (infer-type "hello" (dict) (test-prim-types))))

  (deftest "boolean literal"
    (assert-equal "boolean" (infer-type true (dict) (test-prim-types))))

  (deftest "nil"
    (assert-equal "nil" (infer-type nil (dict) (test-prim-types)))))


(defsuite "infer-calls"
  (deftest "known primitive return type"
    ;; (+ 1 2) → number
    (let ((expr (sx-parse "(+ 1 2)")))
      (assert-equal "number"
        (infer-type (first expr) (dict) (test-prim-types)))))

  (deftest "str returns string"
    (let ((expr (sx-parse "(str 1 2)")))
      (assert-equal "string"
        (infer-type (first expr) (dict) (test-prim-types)))))

  (deftest "comparison returns boolean"
    (let ((expr (sx-parse "(= 1 2)")))
      (assert-equal "boolean"
        (infer-type (first expr) (dict) (test-prim-types)))))

  (deftest "component call returns element"
    (let ((expr (sx-parse "(~card :title \"hi\")")))
      (assert-equal "element"
        (infer-type (first expr) (dict) (test-prim-types)))))

  (deftest "unknown function returns any"
    (let ((expr (sx-parse "(unknown-fn 1 2)")))
      (assert-equal "any"
        (infer-type (first expr) (dict) (test-prim-types))))))


(defsuite "infer-special-forms"
  (deftest "if produces union of branches"
    (let ((expr (sx-parse "(if true 42 \"hello\")")))
      (let ((t (infer-type (first expr) (dict) (test-prim-types))))
        ;; number | string — should be a union
        (assert-true (or (equal? t (list "or" "number" "string"))
                         (= t "any"))))))

  (deftest "if with no else includes nil"
    (let ((expr (sx-parse "(if true 42)")))
      (let ((t (infer-type (first expr) (dict) (test-prim-types))))
        (assert-equal "number?" t))))

  (deftest "when includes nil"
    (let ((expr (sx-parse "(when true 42)")))
      (let ((t (infer-type (first expr) (dict) (test-prim-types))))
        (assert-equal "number?" t))))

  (deftest "do returns last type"
    (let ((expr (sx-parse "(do 1 2 \"hello\")")))
      (assert-equal "string"
        (infer-type (first expr) (dict) (test-prim-types)))))

  (deftest "let infers binding types"
    (let ((expr (sx-parse "(let ((x 42)) x)")))
      (assert-equal "number"
        (infer-type (first expr) (dict) (test-prim-types)))))

  (deftest "lambda returns lambda"
    (let ((expr (sx-parse "(fn (x) (+ x 1))")))
      (assert-equal "lambda"
        (infer-type (first expr) (dict) (test-prim-types))))))


;; --------------------------------------------------------------------------
;; Component call checking
;; --------------------------------------------------------------------------

(defsuite "check-component-calls"
  (deftest "type mismatch produces error"
    ;; Create a component with typed params, then check a bad call
    (let ((env (test-env)))
      ;; Define a typed component
      (do
        (define dummy-env env)
        (defcomp ~typed-card (&key title price) (div title price))
        (component-set-param-types! ~typed-card
          {:title "string" :price "number"}))
      ;; Check a call with wrong type
      (let ((diagnostics
              (check-component-call "~typed-card" ~typed-card
                (rest (first (sx-parse "(~typed-card :title 42 :price \"bad\")")))
                (dict) (test-prim-types))))
        (assert-true (> (len diagnostics) 0))
        (assert-equal "error" (dict-get (first diagnostics) "level")))))

  (deftest "correct call produces no errors"
    (let ((env (test-env)))
      (do
        (define dummy-env env)
        (defcomp ~ok-card (&key title price) (div title price))
        (component-set-param-types! ~ok-card
          {:title "string" :price "number"}))
      (let ((diagnostics
              (check-component-call "~ok-card" ~ok-card
                (rest (first (sx-parse "(~ok-card :title \"hi\" :price 42)")))
                (dict) (test-prim-types))))
        (assert-equal 0 (len diagnostics)))))

  (deftest "unknown kwarg produces warning"
    (let ((env (test-env)))
      (do
        (define dummy-env env)
        (defcomp ~warn-card (&key title) (div title))
        (component-set-param-types! ~warn-card
          {:title "string"}))
      (let ((diagnostics
              (check-component-call "~warn-card" ~warn-card
                (rest (first (sx-parse "(~warn-card :title \"hi\" :colour \"red\")")))
                (dict) (test-prim-types))))
        (assert-true (> (len diagnostics) 0))
        (assert-equal "warning" (dict-get (first diagnostics) "level"))))))


;; --------------------------------------------------------------------------
;; Annotation syntax: (name :as type) in defcomp params
;; --------------------------------------------------------------------------

(defsuite "typed-defcomp"
  (deftest "typed params are parsed and stored"
    (let ((env (test-env)))
      (defcomp ~typed-widget (&key (title :as string) (count :as number)) (div title count))
      (let ((pt (component-param-types ~typed-widget)))
        (assert-true (not (nil? pt)))
        (assert-equal "string" (dict-get pt "title"))
        (assert-equal "number" (dict-get pt "count")))))

  (deftest "mixed typed and untyped params"
    (let ((env (test-env)))
      (defcomp ~mixed-widget (&key (title :as string) subtitle) (div title subtitle))
      (let ((pt (component-param-types ~mixed-widget)))
        (assert-true (not (nil? pt)))
        (assert-equal "string" (dict-get pt "title"))
        ;; subtitle has no annotation — should not be in param-types
        (assert-false (has-key? pt "subtitle")))))

  (deftest "untyped defcomp has nil param-types"
    (let ((env (test-env)))
      (defcomp ~plain-widget (&key title subtitle) (div title subtitle))
      (assert-true (nil? (component-param-types ~plain-widget)))))

  (deftest "typed component catches type error on call"
    (let ((env (test-env)))
      (defcomp ~strict-card (&key (title :as string) (price :as number)) (div title price))
      ;; Call with wrong types
      (let ((diagnostics
              (check-component-call "~strict-card" ~strict-card
                (rest (first (sx-parse "(~strict-card :title 42 :price \"bad\")")))
                (dict) (test-prim-types))))
        ;; Should have errors for both wrong-type args
        (assert-true (>= (len diagnostics) 1))
        (assert-equal "error" (dict-get (first diagnostics) "level")))))

  (deftest "typed component passes correct call"
    (let ((env (test-env)))
      (defcomp ~ok-widget (&key (name :as string) (age :as number)) (div name age))
      (let ((diagnostics
              (check-component-call "~ok-widget" ~ok-widget
                (rest (first (sx-parse "(~ok-widget :name \"Alice\" :age 30)")))
                (dict) (test-prim-types))))
        (assert-equal 0 (len diagnostics)))))

  (deftest "nullable type accepts nil"
    (let ((env (test-env)))
      (defcomp ~nullable-widget (&key (title :as string) (subtitle :as string?)) (div title subtitle))
      ;; Passing nil for nullable param should be fine
      (let ((diagnostics
              (check-component-call "~nullable-widget" ~nullable-widget
                (rest (first (sx-parse "(~nullable-widget :title \"hi\" :subtitle nil)")))
                (dict) (test-prim-types))))
        (assert-equal 0 (len diagnostics))))))


;; --------------------------------------------------------------------------
;; Primitive call checking (Phase 5)
;; --------------------------------------------------------------------------

(defsuite "check-primitive-calls"
  (deftest "correct types produce no errors"
    (let ((ppt (test-prim-param-types)))
      (let ((diagnostics
              (check-primitive-call "+" (rest (first (sx-parse "(+ 1 2 3)")))
                (dict) (test-prim-types) ppt nil)))
        (assert-equal 0 (len diagnostics)))))

  (deftest "string arg to numeric primitive produces error"
    (let ((ppt (test-prim-param-types)))
      (let ((diagnostics
              (check-primitive-call "+" (rest (first (sx-parse "(+ 1 \"hello\")")))
                (dict) (test-prim-types) ppt nil)))
        (assert-true (> (len diagnostics) 0))
        (assert-equal "error" (get (first diagnostics) "level")))))

  (deftest "number arg to string primitive produces error"
    (let ((ppt (test-prim-param-types)))
      (let ((diagnostics
              (check-primitive-call "upper" (rest (first (sx-parse "(upper 42)")))
                (dict) (test-prim-types) ppt nil)))
        (assert-true (> (len diagnostics) 0))
        (assert-equal "error" (get (first diagnostics) "level")))))

  (deftest "positional and rest params both checked"
    ;; (- "bad" 1) — first positional arg is string, expects number
    (let ((ppt (test-prim-param-types)))
      (let ((diagnostics
              (check-primitive-call "-" (rest (first (sx-parse "(- \"bad\" 1)")))
                (dict) (test-prim-types) ppt nil)))
        (assert-true (> (len diagnostics) 0)))))

  (deftest "dict arg to keys is valid"
    (let ((ppt (test-prim-param-types)))
      (let ((diagnostics
              (check-primitive-call "keys" (rest (first (sx-parse "(keys {:a 1})")))
                (dict) (test-prim-types) ppt nil)))
        (assert-equal 0 (len diagnostics)))))

  (deftest "number arg to keys produces error"
    (let ((ppt (test-prim-param-types)))
      (let ((diagnostics
              (check-primitive-call "keys" (rest (first (sx-parse "(keys 42)")))
                (dict) (test-prim-types) ppt nil)))
        (assert-true (> (len diagnostics) 0)))))

  (deftest "variable with known type passes check"
    ;; Variable n is known to be number in type-env
    (let ((ppt (test-prim-param-types))
          (tenv {"n" "number"}))
      (let ((diagnostics
              (check-primitive-call "inc" (rest (first (sx-parse "(inc n)")))
                tenv (test-prim-types) ppt nil)))
        (assert-equal 0 (len diagnostics)))))

  (deftest "variable with wrong type fails check"
    ;; Variable s is known to be string in type-env
    (let ((ppt (test-prim-param-types))
          (tenv {"s" "string"}))
      (let ((diagnostics
              (check-primitive-call "inc" (rest (first (sx-parse "(inc s)")))
                tenv (test-prim-types) ppt nil)))
        (assert-true (> (len diagnostics) 0)))))

  (deftest "any-typed variable skips check"
    ;; Variable x has type any — should not produce errors
    (let ((ppt (test-prim-param-types))
          (tenv {"x" "any"}))
      (let ((diagnostics
              (check-primitive-call "upper" (rest (first (sx-parse "(upper x)")))
                tenv (test-prim-types) ppt nil)))
        (assert-equal 0 (len diagnostics)))))

  (deftest "body-walk catches primitive errors in component"
    ;; Manually build a component and check it via check-body-walk directly
    (let ((ppt (test-prim-param-types))
          (body (first (sx-parse "(div (+ name 1))")))
          (type-env {"name" "string"})
          (diagnostics (list)))
      (check-body-walk body "~bad-math" type-env (test-prim-types) ppt (test-env) diagnostics nil nil)
      (assert-true (> (len diagnostics) 0))
      (assert-equal "error" (get (first diagnostics) "level")))))


;; --------------------------------------------------------------------------
;; deftype — type aliases
;; --------------------------------------------------------------------------

(defsuite "deftype-alias"
  (deftest "simple alias resolves"
    (let ((registry {"price" {:name "price" :params () :body "number"}}))
      (assert-equal "number" (resolve-type "price" registry))))

  (deftest "alias chain resolves"
    (let ((registry {"price" {:name "price" :params () :body "number"}
                     "cost" {:name "cost" :params () :body "price"}}))
      (assert-equal "number" (resolve-type "cost" registry))))

  (deftest "unknown type passes through"
    (let ((registry {"price" {:name "price" :params () :body "number"}}))
      (assert-equal "string" (resolve-type "string" registry))))

  (deftest "subtype-resolved? works through alias"
    (let ((registry {"price" {:name "price" :params () :body "number"}}))
      (assert-true (subtype-resolved? "price" "number" registry))
      (assert-true (subtype-resolved? "number" "price" registry)))))


;; --------------------------------------------------------------------------
;; deftype — union types
;; --------------------------------------------------------------------------

(defsuite "deftype-union"
  (deftest "union resolves"
    (let ((registry {"status" {:name "status" :params (list) :body (list "or" "string" "number")}}))
      (let ((resolved (resolve-type "status" registry)))
        (assert-true (= (type-of resolved) "list"))
        (assert-equal "or" (first resolved)))))

  (deftest "subtype through named union"
    (let ((registry {"status" {:name "status" :params (list) :body (list "or" "string" "number")}}))
      (assert-true (subtype-resolved? "string" "status" registry))
      (assert-true (subtype-resolved? "number" "status" registry))
      (assert-false (subtype-resolved? "boolean" "status" registry)))))


;; --------------------------------------------------------------------------
;; deftype — record types
;; --------------------------------------------------------------------------

(defsuite "deftype-record"
  (deftest "record resolves to dict"
    (let ((registry {"card-props" {:name "card-props" :params ()
                     :body {"title" "string" "price" "number"}}}))
      (let ((resolved (resolve-type "card-props" registry)))
        (assert-equal "dict" (type-of resolved))
        (assert-equal "string" (get resolved "title"))
        (assert-equal "number" (get resolved "price")))))

  (deftest "record structural subtyping"
    (let ((registry {"card-props" {:name "card-props" :params ()
                     :body {"title" "string" "price" "number"}}
                     "titled" {:name "titled" :params ()
                     :body {"title" "string"}}}))
      ;; card-props has title+price, titled has just title
      ;; card-props <: titled (has all required fields)
      (assert-true (subtype-resolved? "card-props" "titled" registry))))

  (deftest "get infers field type from record"
    (let ((registry {"card-props" {:name "card-props" :params (list)
                     :body {"title" "string" "price" "number"}}})
          (type-env {"d" "card-props"})
          (expr (first (sx-parse "(get d :title)"))))
      (assert-equal "string"
        (infer-type expr type-env (test-prim-types) registry)))))


;; --------------------------------------------------------------------------
;; deftype — parameterized types
;; --------------------------------------------------------------------------

(defsuite "deftype-parameterized"
  (deftest "maybe instantiation"
    (let ((registry {"maybe" {:name "maybe" :params (list "a")
                     :body (list "or" "a" "nil")}}))
      (let ((resolved (resolve-type (list "maybe" "string") registry)))
        (assert-true (= (type-of resolved) "list"))
        (assert-equal "or" (first resolved))
        (assert-true (contains? resolved "string"))
        (assert-true (contains? resolved "nil")))))

  (deftest "subtype through parameterized type"
    (let ((registry {"maybe" {:name "maybe" :params (list "a")
                     :body (list "or" "a" "nil")}}))
      (assert-true (subtype-resolved? "string" (list "maybe" "string") registry))
      (assert-true (subtype-resolved? "nil" (list "maybe" "string") registry))
      (assert-false (subtype-resolved? "number" (list "maybe" "string") registry))))

  (deftest "substitute-type-vars works"
    (let ((result (substitute-type-vars (list "or" "a" "nil") (list "a") (list "number"))))
      (assert-equal "or" (first result))
      (assert-true (contains? result "number"))
      (assert-true (contains? result "nil")))))


;; --------------------------------------------------------------------------
;; defeffect — effect basics
;; --------------------------------------------------------------------------

(defsuite "defeffect-basics"
  (deftest "get-effects returns nil for unannotated"
    (let ((anns {"fetch" ("io")}))
      (assert-true (nil? (get-effects "unknown" anns)))))

  (deftest "get-effects returns effects for annotated"
    (let ((anns {"fetch" ("io")}))
      (assert-equal (list "io") (get-effects "fetch" anns))))

  (deftest "nil annotations returns nil"
    (assert-true (nil? (get-effects "anything" nil)))))


;; --------------------------------------------------------------------------
;; defeffect — effect checking
;; --------------------------------------------------------------------------

(defsuite "effect-checking"
  (deftest "pure cannot call io"
    (let ((anns {"~pure-comp" () "fetch" ("io")}))
      (let ((diagnostics (check-effect-call "fetch" (list) anns "~pure-comp")))
        (assert-true (> (len diagnostics) 0))
        (assert-equal "error" (get (first diagnostics) "level")))))

  (deftest "io context allows io"
    (let ((anns {"~io-comp" ("io") "fetch" ("io")}))
      (let ((diagnostics (check-effect-call "fetch" (list "io") anns "~io-comp")))
        (assert-equal 0 (len diagnostics)))))

  (deftest "unannotated caller allows everything"
    (let ((anns {"fetch" ("io")}))
      (let ((diagnostics (check-effect-call "fetch" nil anns "~unknown")))
        (assert-equal 0 (len diagnostics)))))

  (deftest "unannotated callee skips check"
    (let ((anns {"~pure-comp" ()}))
      (let ((diagnostics (check-effect-call "unknown-fn" (list) anns "~pure-comp")))
        (assert-equal 0 (len diagnostics))))))


;; --------------------------------------------------------------------------
;; defeffect — subset checking
;; --------------------------------------------------------------------------

(defsuite "effect-subset"
  (deftest "empty is subset of anything"
    (assert-true (effects-subset? (list) (list "io")))
    (assert-true (effects-subset? (list) (list))))

  (deftest "io is subset of io"
    (assert-true (effects-subset? (list "io") (list "io" "async"))))

  (deftest "io is not subset of pure"
    (assert-false (effects-subset? (list "io") (list))))

  (deftest "nil callee skips check"
    (assert-true (effects-subset? nil (list))))

  (deftest "nil caller allows all"
    (assert-true (effects-subset? (list "io") nil))))


;; --------------------------------------------------------------------------
;; build-effect-annotations
;; --------------------------------------------------------------------------

(defsuite "build-effect-annotations"
  (deftest "builds annotations from io declarations"
    (let ((decls (list {"name" "fetch"} {"name" "save!"}))
          (anns (build-effect-annotations decls)))
      (assert-equal (list "io") (get anns "fetch"))
      (assert-equal (list "io") (get anns "save!"))))

  (deftest "skips entries without name"
    (let ((decls (list {"name" "fetch"} {"other" "x"}))
          (anns (build-effect-annotations decls)))
      (assert-true (has-key? anns "fetch"))
      (assert-false (has-key? anns "other"))))

  (deftest "empty declarations produce empty dict"
    (let ((anns (build-effect-annotations (list))))
      (assert-equal 0 (len (keys anns))))))


;; --------------------------------------------------------------------------
;; check-component-effects
;; --------------------------------------------------------------------------

(defsuite "check-component-effects"
  (deftest "pure component calling io produces diagnostic"
    ;; Define component in a local env so check-component-effects can find it
    (let ((e (env-extend (test-env))))
      (eval-expr-cek (sx-parse-one "(defcomp ~eff-pure-card () :effects [] (div (fetch \"url\")))") e)
      (let ((anns {"~eff-pure-card" () "fetch" ("io")})
            (diagnostics (check-component-effects "~eff-pure-card" e anns)))
        (assert-true (> (len diagnostics) 0)))))

  (deftest "io component calling io produces no diagnostic"
    (let ((e (env-extend (test-env))))
      (eval-expr-cek (sx-parse-one "(defcomp ~eff-io-card () :effects [io] (div (fetch \"url\")))") e)
      (let ((anns {"~eff-io-card" ("io") "fetch" ("io")})
            (diagnostics (check-component-effects "~eff-io-card" e anns)))
        (assert-equal 0 (len diagnostics)))))

  (deftest "unannotated component skips check"
    (let ((e (env-extend (test-env))))
      (eval-expr-cek (sx-parse-one "(defcomp ~eff-unannot-card () (div (fetch \"url\")))") e)
      (let ((anns {"fetch" ("io")})
            (diagnostics (check-component-effects "~eff-unannot-card" e anns)))
        (assert-equal 0 (len diagnostics))))))
