diff --git a/shared/sx/ref/test-orchestration.sx b/shared/sx/ref/test-orchestration.sx new file mode 100644 index 0000000..ceecdbf --- /dev/null +++ b/shared/sx/ref/test-orchestration.sx @@ -0,0 +1,170 @@ +;; ========================================================================== +;; test-orchestration.sx — Tests for orchestration.sx Phase 7c + 7d +;; +;; Requires: test-framework.sx loaded first. +;; Platform functions mocked by test runner: +;; now-ms, log-info, log-warn, execute-action, try-rerender-page +;; ========================================================================== + + +;; -------------------------------------------------------------------------- +;; 1. page-data-cache — basic cache operations +;; -------------------------------------------------------------------------- + +(defsuite "page-data-cache" + + (deftest "cache-key bare page name" + (assert-equal "my-page" (page-data-cache-key "my-page" nil))) + + (deftest "cache-key with params" + (let ((key (page-data-cache-key "my-page" {"id" "42"}))) + (assert-equal "my-page:id=42" key))) + + (deftest "cache-set then get" + (let ((key "test-cache-1")) + (page-data-cache-set key {"items" (list 1 2 3)}) + (let ((result (page-data-cache-get key))) + (assert-equal (list 1 2 3) (get result "items"))))) + + (deftest "cache miss returns nil" + (assert-nil (page-data-cache-get "nonexistent-key")))) + + +;; -------------------------------------------------------------------------- +;; 2. optimistic-cache-update — predicted mutation with snapshot +;; -------------------------------------------------------------------------- + +(defsuite "optimistic-cache-update" + + (deftest "applies mutator to cached data" + (let ((key "opt-test-1")) + ;; Seed the cache + (page-data-cache-set key {"count" 10}) + ;; Apply optimistic mutation + (let ((predicted (optimistic-cache-update key + (fn (data) (merge data {"count" 11}))))) + (assert-equal 11 (get predicted "count"))))) + + (deftest "updates cache with prediction" + (let ((key "opt-test-2")) + (page-data-cache-set key {"count" 5}) + (optimistic-cache-update key (fn (data) (merge data {"count" 6}))) + ;; Cache now has predicted value + (let ((cached (page-data-cache-get key))) + (assert-equal 6 (get cached "count"))))) + + (deftest "returns nil when no cached data" + (let ((result (optimistic-cache-update "no-such-key" + (fn (data) (merge data {"x" 1}))))) + (assert-nil result)))) + + +;; -------------------------------------------------------------------------- +;; 3. optimistic-cache-revert — restore from snapshot +;; -------------------------------------------------------------------------- + +(defsuite "optimistic-cache-revert" + + (deftest "reverts to original data" + (let ((key "revert-test-1")) + (page-data-cache-set key {"count" 10}) + (optimistic-cache-update key (fn (data) (merge data {"count" 99}))) + ;; Cache now has 99 + (assert-equal 99 (get (page-data-cache-get key) "count")) + ;; Revert + (let ((restored (optimistic-cache-revert key))) + (assert-equal 10 (get restored "count")) + ;; Cache is back to original + (assert-equal 10 (get (page-data-cache-get key) "count"))))) + + (deftest "returns nil when no snapshot" + (assert-nil (optimistic-cache-revert "never-mutated")))) + + +;; -------------------------------------------------------------------------- +;; 4. optimistic-cache-confirm — discard snapshot +;; -------------------------------------------------------------------------- + +(defsuite "optimistic-cache-confirm" + + (deftest "confirm clears snapshot" + (let ((key "confirm-test-1")) + (page-data-cache-set key {"val" "a"}) + (optimistic-cache-update key (fn (data) (merge data {"val" "b"}))) + ;; Confirm — accepts the optimistic value + (optimistic-cache-confirm key) + ;; Revert should now return nil (no snapshot) + (assert-nil (optimistic-cache-revert key)) + ;; Cache still has optimistic value + (assert-equal "b" (get (page-data-cache-get key) "val"))))) + + +;; -------------------------------------------------------------------------- +;; 5. offline-is-online? / offline-set-online! — connectivity tracking +;; -------------------------------------------------------------------------- + +(defsuite "offline-connectivity" + + (deftest "initially online" + (assert-true (offline-is-online?))) + + (deftest "set offline" + (offline-set-online! false) + (assert-false (offline-is-online?))) + + (deftest "set back online" + (offline-set-online! true) + (assert-true (offline-is-online?)))) + + +;; -------------------------------------------------------------------------- +;; 6. offline-queue-mutation — queue entries when offline +;; -------------------------------------------------------------------------- + +(defsuite "offline-queue-mutation" + + (deftest "queues an entry" + ;; Seed cache so optimistic update works + (let ((key (page-data-cache-key "notes" nil))) + (page-data-cache-set key {"items" (list "a" "b")}) + (let ((entry (offline-queue-mutation "add-note" + {"text" "c"} + "notes" nil + (fn (data) (merge data {"items" (list "a" "b" "c")}))))) + (assert-equal "add-note" (get entry "action")) + (assert-equal "pending" (get entry "status"))))) + + (deftest "pending count increases" + ;; Previous test queued 1 entry; count should be >= 1 + (assert-true (> (offline-pending-count) 0)))) + + +;; -------------------------------------------------------------------------- +;; 7. offline-aware-mutation — routes by connectivity +;; -------------------------------------------------------------------------- + +(defsuite "offline-aware-mutation" + + (deftest "when online calls submit-mutation path" + (offline-set-online! true) + (let ((key (page-data-cache-key "test-page" nil))) + (page-data-cache-set key {"v" 1}) + ;; This will trigger execute-action (mocked) which calls success cb + (let ((status nil)) + (offline-aware-mutation "test-page" nil "do-thing" {"x" 1} + (fn (data) (merge data {"v" 2})) + (fn (s) (set! status s))) + ;; Mock execute-action calls success immediately + (assert-equal "confirmed" status)))) + + (deftest "when offline queues mutation" + (offline-set-online! false) + (let ((key (page-data-cache-key "test-page-2" nil))) + (page-data-cache-set key {"v" 1}) + (let ((status nil)) + (offline-aware-mutation "test-page-2" nil "do-thing" {"x" 1} + (fn (data) (merge data {"v" 2})) + (fn (s) (set! status s))) + (assert-equal "queued" status))) + ;; Clean up: go back online + (offline-set-online! true))) diff --git a/shared/sx/tests/run.py b/shared/sx/tests/run.py index 63ac207..37b5983 100644 --- a/shared/sx/tests/run.py +++ b/shared/sx/tests/run.py @@ -143,6 +143,7 @@ SPECS = { "render": {"file": "test-render.sx", "needs": ["render-html"]}, "deps": {"file": "test-deps.sx", "needs": []}, "engine": {"file": "test-engine.sx", "needs": []}, + "orchestration": {"file": "test-orchestration.sx", "needs": []}, } REF_DIR = os.path.join(_HERE, "..", "ref") @@ -333,6 +334,65 @@ def _load_engine_from_bootstrap(env): eval_file("engine.sx", env) +def _load_orchestration(env): + """Load orchestration.sx with mocked platform functions for testing. + + Orchestration defines many browser-wiring functions (DOM, fetch, etc.) + but the Phase 7c/7d tests only exercise the cache, optimistic, and + offline functions. Lambda bodies referencing DOM/fetch are never called, + so we only need to mock the functions actually invoked by the tests: + now-ms, log-info, log-warn, execute-action, try-rerender-page. + """ + _mock_ts = [1000] # mutable so mock can advance time + + def _mock_now_ms(): + return _mock_ts[0] + + def _noop(*_a, **_kw): + return NIL + + def _mock_execute_action(action, payload, on_success, on_error): + """Mock: immediately calls on_success with payload as 'server truth'.""" + on_success(payload) + return NIL + + def _dict_delete(d, k): + if isinstance(d, dict) and k in d: + del d[k] + return NIL + + env["now-ms"] = _mock_now_ms + env["log-info"] = _noop + env["log-warn"] = _noop + env["execute-action"] = _mock_execute_action + env["try-rerender-page"] = _noop + env["persist-offline-data"] = _noop + env["retrieve-offline-data"] = lambda: NIL + env["dict-delete!"] = _dict_delete + # DOM / browser stubs (never called by tests, but referenced in lambdas + # that the evaluator might try to resolve at call time) + for stub in [ + "try-parse-json", "dom-dispatch", "dom-query-selector", + "dom-get-attribute", "dom-set-attribute", "dom-set-text-content", + "dom-append", "dom-insert-html-adjacent", "dom-remove", + "dom-outer-html", "dom-inner-html", "dom-create-element", + "dom-set-inner-html", "dom-morph", "dom-get-tag", + "dom-query-selector-all", "dom-add-event-listener", + "dom-set-timeout", "dom-prevent-default", "dom-closest", + "dom-matches", "dom-get-id", "dom-set-id", "dom-form-data", + "dom-is-form", "browser-location-href", "browser-push-state", + "browser-replace-state", "sx-hydrate-elements", "render-to-dom", + "hoist-head-elements-full", "url-pathname", + ]: + if stub not in env: + env[stub] = _noop + + # Load engine.sx first (orchestration depends on it) + _load_engine_from_bootstrap(env) + # Load orchestration.sx + eval_file("orchestration.sx", env) + + def _load_forms_from_bootstrap(env): """Load forms functions (including streaming protocol) from sx_ref.py.""" try: @@ -395,6 +455,8 @@ def main(): _load_deps_from_bootstrap(env) if spec_name == "engine": _load_engine_from_bootstrap(env) + if spec_name == "orchestration": + _load_orchestration(env) print(f"# --- {spec_name} ---") eval_file(spec["file"], env) diff --git a/sx/sx/nav-data.sx b/sx/sx/nav-data.sx index 03a06f7..f445051 100644 --- a/sx/sx/nav-data.sx +++ b/sx/sx/nav-data.sx @@ -126,6 +126,7 @@ (dict :label "Renderer" :href "/testing/render") (dict :label "Dependencies" :href "/testing/deps") (dict :label "Engine" :href "/testing/engine") + (dict :label "Orchestration" :href "/testing/orchestration") (dict :label "Runners" :href "/testing/runners"))) (define isomorphism-nav-items (list diff --git a/sx/sxc/pages/docs.sx b/sx/sxc/pages/docs.sx index 63286ef..283823c 100644 --- a/sx/sxc/pages/docs.sx +++ b/sx/sxc/pages/docs.sx @@ -648,6 +648,7 @@ "render" (run-modular-tests "render") "deps" (run-modular-tests "deps") "engine" (run-modular-tests "engine") + "orchestration" (run-modular-tests "orchestration") :else (dict)) :content (case slug "eval" (~testing-spec-content @@ -692,6 +693,13 @@ :spec-source spec-source :framework-source framework-source :server-results server-results) + "orchestration" (~testing-spec-content + :spec-name "orchestration" + :spec-title "Orchestration Tests" + :spec-desc "17 tests covering Phase 7c+7d orchestration — page data cache, optimistic cache update/revert/confirm, offline connectivity, offline queue mutation, and offline-aware routing." + :spec-source spec-source + :framework-source framework-source + :server-results server-results) "runners" (~testing-runners-content) :else (~testing-overview-content :server-results server-results)))