Add orchestration test suite: 17 tests for Phase 7c+7d

Tests cover page data cache, optimistic cache update/revert/confirm,
offline connectivity tracking, offline queue mutation, and offline-aware
routing. Registered in test runner with mocked platform functions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-08 01:53:27 +00:00
parent 5f20a16aa0
commit 7f665d874c
4 changed files with 241 additions and 0 deletions

View File

@@ -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)))

View File

@@ -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)

View File

@@ -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

View File

@@ -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)))