Files
rose-ash/shared/sx/ref/test-router.sx
giles b23e81730c SX URL algebra: relative resolution, keyword ops, ! special forms
Extends router.sx with the full SX URL algebra — structural navigation
(.slug, .., ...), keyword set/delta (.:page.4, .:page.+1), bare-dot
shorthand, and ! special form parsing (!source, !inspect, !diff, !search,
!raw, !json). All pure SX spec, bootstrapped to both Python and JS.

Fixes: index-of -1/nil portability (_index-of-safe wrapper), variadic
(+ a b c) transpilation bug (use nested binary +). Includes 115 passing
tests covering all operations. Also: "The" strapline and essay title.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 18:31:21 +00:00

709 lines
26 KiB
Plaintext

;; ==========================================================================
;; test-router.sx — Tests for client-side route matching & SX URL algebra
;;
;; Requires: test-framework.sx loaded first.
;; Modules tested: router.sx
;;
;; No additional platform functions needed — router.sx is pure.
;; ==========================================================================
;; --------------------------------------------------------------------------
;; split-path-segments
;; --------------------------------------------------------------------------
(defsuite "split-path-segments"
(deftest "root path"
(assert-equal (list) (split-path-segments "/")))
(deftest "single segment"
(assert-equal (list "docs") (split-path-segments "/docs")))
(deftest "multiple segments"
(assert-equal (list "docs" "hello") (split-path-segments "/docs/hello")))
(deftest "trailing slash stripped"
(assert-equal (list "docs") (split-path-segments "/docs/")))
(deftest "deep path"
(assert-equal (list "a" "b" "c" "d") (split-path-segments "/a/b/c/d"))))
;; --------------------------------------------------------------------------
;; parse-route-pattern
;; --------------------------------------------------------------------------
(defsuite "parse-route-pattern"
(deftest "static pattern"
(let ((segs (parse-route-pattern "/docs/intro")))
(assert-length 2 segs)
(assert-equal "literal" (get (first segs) "type"))
(assert-equal "docs" (get (first segs) "value"))
(assert-equal "literal" (get (nth segs 1) "type"))
(assert-equal "intro" (get (nth segs 1) "value"))))
(deftest "pattern with param"
(let ((segs (parse-route-pattern "/docs/<slug>")))
(assert-length 2 segs)
(assert-equal "literal" (get (first segs) "type"))
(assert-equal "docs" (get (first segs) "value"))
(assert-equal "param" (get (nth segs 1) "type"))
(assert-equal "slug" (get (nth segs 1) "value"))))
(deftest "multiple params"
(let ((segs (parse-route-pattern "/users/<uid>/posts/<pid>")))
(assert-length 4 segs)
(assert-equal "param" (get (nth segs 1) "type"))
(assert-equal "uid" (get (nth segs 1) "value"))
(assert-equal "param" (get (nth segs 3) "type"))
(assert-equal "pid" (get (nth segs 3) "value"))))
(deftest "root pattern"
(assert-equal (list) (parse-route-pattern "/"))))
;; --------------------------------------------------------------------------
;; match-route
;; --------------------------------------------------------------------------
(defsuite "match-route"
(deftest "exact match returns empty params"
(let ((result (match-route "/docs/intro" "/docs/intro")))
(assert-true (not (nil? result)))
(assert-length 0 (keys result))))
(deftest "param match extracts value"
(let ((result (match-route "/docs/hello" "/docs/<slug>")))
(assert-true (not (nil? result)))
(assert-equal "hello" (get result "slug"))))
(deftest "no match returns nil"
(assert-nil (match-route "/docs/hello" "/essays/<slug>"))
(assert-nil (match-route "/docs" "/docs/<slug>")))
(deftest "segment count mismatch returns nil"
(assert-nil (match-route "/a/b/c" "/a/<b>"))
(assert-nil (match-route "/a" "/a/b")))
(deftest "root matches root"
(let ((result (match-route "/" "/")))
(assert-true (not (nil? result)))))
(deftest "multiple params extracted"
(let ((result (match-route "/users/42/posts/99" "/users/<uid>/posts/<pid>")))
(assert-true (not (nil? result)))
(assert-equal "42" (get result "uid"))
(assert-equal "99" (get result "pid")))))
;; --------------------------------------------------------------------------
;; find-matching-route
;; --------------------------------------------------------------------------
(defsuite "find-matching-route"
(deftest "finds first matching route"
(let ((routes (list
{:pattern "/docs/" :parsed (parse-route-pattern "/docs/") :name "docs-index"}
{:pattern "/docs/<slug>" :parsed (parse-route-pattern "/docs/<slug>") :name "docs-page"})))
(let ((result (find-matching-route "/docs/hello" routes)))
(assert-true (not (nil? result)))
(assert-equal "docs-page" (get result "name"))
(assert-equal "hello" (get (get result "params") "slug")))))
(deftest "returns nil for no match"
(let ((routes (list
{:pattern "/docs/<slug>" :parsed (parse-route-pattern "/docs/<slug>") :name "docs-page"})))
(assert-nil (find-matching-route "/essays/hello" routes))))
(deftest "matches exact routes before param routes"
(let ((routes (list
{:pattern "/docs/" :parsed (parse-route-pattern "/docs/") :name "docs-index"}
{:pattern "/docs/<slug>" :parsed (parse-route-pattern "/docs/<slug>") :name "docs-page"})))
(let ((result (find-matching-route "/docs/" routes)))
(assert-true (not (nil? result)))
(assert-equal "docs-index" (get result "name")))))
(deftest "propagates stream flag from route"
(let ((routes (list
{:pattern "/demo/streaming"
:parsed (parse-route-pattern "/demo/streaming")
:name "streaming-demo"
:stream true
:has-data true})))
(let ((result (find-matching-route "/demo/streaming" routes)))
(assert-true (not (nil? result)))
(assert-equal true (get result "stream"))
(assert-equal true (get result "has-data")))))
(deftest "non-streaming route has no stream flag"
(let ((routes (list
{:pattern "/about"
:parsed (parse-route-pattern "/about")
:name "about"
:has-data false})))
(let ((result (find-matching-route "/about" routes)))
(assert-true (not (nil? result)))
(assert-nil (get result "stream"))))))
;; --------------------------------------------------------------------------
;; sx-url-to-path — SX expression URL → old-style path
;; --------------------------------------------------------------------------
(defsuite "sx-url-to-path"
(deftest "simple two-level"
(assert-equal "/language/docs/introduction"
(sx-url-to-path "/(language.(doc.introduction))")))
(deftest "deep nesting"
(assert-equal "/geography/hypermedia/reference/attributes"
(sx-url-to-path "/(geography.(hypermedia.(reference.attributes)))")))
(deftest "section index"
(assert-equal "/language"
(sx-url-to-path "/(language)")))
(deftest "function name mapping — doc to docs"
(assert-equal "/language/docs/getting-started"
(sx-url-to-path "/(language.(doc.getting-started))")))
(deftest "function name mapping — spec to specs"
(assert-equal "/language/specs/core"
(sx-url-to-path "/(language.(spec.core))")))
(deftest "function name mapping — example to examples"
(assert-equal "/geography/hypermedia/examples/click-to-load"
(sx-url-to-path "/(geography.(hypermedia.(example.click-to-load)))")))
(deftest "function name mapping — essay to essays"
(assert-equal "/etc/essays/sx-sucks"
(sx-url-to-path "/(etc.(essay.sx-sucks))")))
(deftest "function name mapping — plan to plans"
(assert-equal "/etc/plans/spec-explorer"
(sx-url-to-path "/(etc.(plan.spec-explorer))")))
(deftest "function name mapping — test to testing"
(assert-equal "/language/testing/eval"
(sx-url-to-path "/(language.(test.eval))")))
(deftest "function name mapping — bootstrapper to bootstrappers"
(assert-equal "/language/bootstrappers/python"
(sx-url-to-path "/(language.(bootstrapper.python))")))
(deftest "function name mapping — protocol to protocols"
(assert-equal "/applications/protocols/wire-format"
(sx-url-to-path "/(applications.(protocol.wire-format))")))
(deftest "function name mapping — reference-detail to reference"
(assert-equal "/geography/hypermedia/reference/attributes"
(sx-url-to-path "/(geography.(hypermedia.(reference-detail.attributes)))")))
(deftest "non-SX URL returns nil"
(assert-nil (sx-url-to-path "/language/docs/introduction"))
(assert-nil (sx-url-to-path "https://example.com"))))
;; --------------------------------------------------------------------------
;; find-matching-route with SX URLs
;; --------------------------------------------------------------------------
(defsuite "find-matching-route-sx-urls"
(deftest "SX URL auto-converts for matching"
(let ((routes (list
{:pattern "/language/docs/<slug>"
:parsed (parse-route-pattern "/language/docs/<slug>")
:name "docs-page"})))
(let ((result (find-matching-route "/(language.(doc.introduction))" routes)))
(assert-true (not (nil? result)))
(assert-equal "docs-page" (get result "name"))
(assert-equal "introduction" (get (get result "params") "slug"))))))
;; ==========================================================================
;; SX URL Resolution — Structural Navigation
;; ==========================================================================
(defsuite "relative-sx-url?"
(deftest "paren-form relative"
(assert-true (relative-sx-url? "(.slug)"))
(assert-true (relative-sx-url? "(..)"))
(assert-true (relative-sx-url? "(..reactive.demo)")))
(deftest "bare-dot relative"
(assert-true (relative-sx-url? ".slug"))
(assert-true (relative-sx-url? ".."))
(assert-true (relative-sx-url? "..."))
(assert-true (relative-sx-url? ".:page.4")))
(deftest "absolute URLs are not relative"
(assert-false (relative-sx-url? "/(language.(doc.intro))"))
(assert-false (relative-sx-url? "/"))
(assert-false (relative-sx-url? "/language/docs/intro")))
(deftest "special form URLs are not relative"
(assert-false (relative-sx-url? "/(!source.(~essay))"))))
;; --------------------------------------------------------------------------
;; Structural: append at current level (1 dot)
;; --------------------------------------------------------------------------
(defsuite "resolve-relative-url: append (.slug)"
(deftest "append to deep URL"
(assert-equal "/(geography.(hypermedia.(example.progress-bar)))"
(resolve-relative-url
"/(geography.(hypermedia.(example)))"
"(.progress-bar)")))
(deftest "append to single-level URL"
(assert-equal "/(language.intro)"
(resolve-relative-url "/(language)" "(.intro)")))
(deftest "append with multi-token body"
(assert-equal "/(geography.(hypermedia.(example.progress-bar.v2)))"
(resolve-relative-url
"/(geography.(hypermedia.(example)))"
"(.progress-bar.v2)")))
(deftest "bare-dot shorthand"
(assert-equal "/(geography.(hypermedia.(example.progress-bar)))"
(resolve-relative-url
"/(geography.(hypermedia.(example)))"
".progress-bar"))))
;; --------------------------------------------------------------------------
;; Structural: go up one level (2 dots)
;; --------------------------------------------------------------------------
(defsuite "resolve-relative-url: up one (..slug)"
(deftest "sibling call"
(assert-equal "/(geography.(hypermedia.(reactive.demo)))"
(resolve-relative-url
"/(geography.(hypermedia.(example)))"
"(..reactive.demo)")))
(deftest "just go up — no new content"
(assert-equal "/(geography.(hypermedia))"
(resolve-relative-url
"/(geography.(hypermedia.(example)))"
"(..)")))
(deftest "bare-dot shorthand for up"
(assert-equal "/(geography.(hypermedia))"
(resolve-relative-url
"/(geography.(hypermedia.(example)))"
"..")))
(deftest "up from two-level URL"
(assert-equal "/(language)"
(resolve-relative-url "/(language.(doc))" "(..)")))
(deftest "up from single-level pops to root"
(assert-equal "/"
(resolve-relative-url "/(language)" "(..)"))))
;; --------------------------------------------------------------------------
;; Structural: go up two levels (3 dots)
;; --------------------------------------------------------------------------
(defsuite "resolve-relative-url: up two (...slug)"
(deftest "up two and push"
(assert-equal "/(geography.(marshes))"
(resolve-relative-url
"/(geography.(hypermedia.(example)))"
"(...marshes)")))
(deftest "just up two — no content"
(assert-equal "/(geography)"
(resolve-relative-url
"/(geography.(hypermedia.(example)))"
"(...)")))
(deftest "bare-dot shorthand for up two"
(assert-equal "/(geography)"
(resolve-relative-url
"/(geography.(hypermedia.(example)))"
"...")))
(deftest "up two from two-level pops to root"
(assert-equal "/"
(resolve-relative-url "/(language.(doc))" "(...)")))
(deftest "up two and push from deep URL"
;; 4-level URL, ... = 3 dots = pop 2 levels → at hypermedia level
(assert-equal "/(geography.(hypermedia.(reactive.demo)))"
(resolve-relative-url
"/(geography.(hypermedia.(reference.(attributes))))"
"(...reactive.demo)"))))
;; --------------------------------------------------------------------------
;; Structural: up N levels (N+1 dots)
;; --------------------------------------------------------------------------
(defsuite "resolve-relative-url: up N"
(deftest "up three levels (4 dots) from 4-level URL"
;; 4-level URL, .... = 4 dots = pop 3 levels → at geography level
(assert-equal "/(geography)"
(resolve-relative-url
"/(geography.(hypermedia.(reference.(attributes))))"
"(....)")))
(deftest "up three and push from 4-level URL"
;; 4 dots = pop 3 → at geography, then push new-section
(assert-equal "/(geography.(new-section))"
(resolve-relative-url
"/(geography.(hypermedia.(reference.(attributes))))"
"(....new-section)")))
(deftest "up four levels (5 dots) pops to root"
(assert-equal "/"
(resolve-relative-url
"/(geography.(hypermedia.(reference.(attributes))))"
"(.....)"))))
;; --------------------------------------------------------------------------
;; Structural: current (1 dot, no body) = no-op
;; --------------------------------------------------------------------------
(defsuite "resolve-relative-url: current level no-op"
(deftest "dot with no body is identity"
;; (.): dots=1, body="" → no positional, no keywords → current unchanged
(assert-equal "/(language.(doc.intro))"
(resolve-relative-url "/(language.(doc.intro))" "(.)")))
(deftest "bare dot shorthand"
(assert-equal "/(language.(doc.intro))"
(resolve-relative-url "/(language.(doc.intro))" "."))))
;; ==========================================================================
;; SX URL Resolution — Keyword Operations
;; ==========================================================================
;; --------------------------------------------------------------------------
;; Keyword set: absolute value
;; --------------------------------------------------------------------------
(defsuite "resolve-relative-url: keyword set"
(deftest "set keyword on URL without keywords"
(assert-equal "/(language.(spec.(explore.signals.:page.4)))"
(resolve-relative-url
"/(language.(spec.(explore.signals)))"
"(.:page.4)")))
(deftest "replace existing keyword"
(assert-equal "/(language.(spec.(explore.signals.:page.4)))"
(resolve-relative-url
"/(language.(spec.(explore.signals.:page.3)))"
"(.:page.4)")))
(deftest "set keyword with bare-dot shorthand"
(assert-equal "/(language.(spec.(explore.signals.:page.4)))"
(resolve-relative-url
"/(language.(spec.(explore.signals.:page.3)))"
".:page.4")))
(deftest "set keyword on single-level URL"
(assert-equal "/(language.:page.1)"
(resolve-relative-url "/(language)" "(.:page.1)")))
(deftest "set multiple keywords"
(assert-equal "/(language.(spec.(explore.signals.:page.4.:section.batch)))"
(resolve-relative-url
"/(language.(spec.(explore.signals.:page.3)))"
"(.:page.4.:section.batch)")))
(deftest "add new keyword preserving existing"
(assert-equal "/(language.(spec.(explore.signals.:page.3.:section.batch)))"
(resolve-relative-url
"/(language.(spec.(explore.signals.:page.3)))"
"(.:section.batch)"))))
;; --------------------------------------------------------------------------
;; Keyword delta: +N / -N
;; --------------------------------------------------------------------------
(defsuite "resolve-relative-url: keyword delta"
(deftest "increment by 1"
(assert-equal "/(language.(spec.(explore.signals.:page.4)))"
(resolve-relative-url
"/(language.(spec.(explore.signals.:page.3)))"
"(.:page.+1)")))
(deftest "decrement by 1"
(assert-equal "/(language.(spec.(explore.signals.:page.2)))"
(resolve-relative-url
"/(language.(spec.(explore.signals.:page.3)))"
"(.:page.-1)")))
(deftest "increment by larger amount"
(assert-equal "/(language.(spec.(explore.signals.:page.13)))"
(resolve-relative-url
"/(language.(spec.(explore.signals.:page.3)))"
"(.:page.+10)")))
(deftest "delta with bare-dot shorthand"
(assert-equal "/(language.(spec.(explore.signals.:page.4)))"
(resolve-relative-url
"/(language.(spec.(explore.signals.:page.3)))"
".:page.+1")))
(deftest "delta on missing keyword uses literal"
;; If :page doesn't exist, +1 is used as-is (not numeric delta)
(assert-equal "/(language.(spec.(explore.signals.:page.+1)))"
(resolve-relative-url
"/(language.(spec.(explore.signals)))"
"(.:page.+1)"))))
;; --------------------------------------------------------------------------
;; Composed: structural + keyword
;; --------------------------------------------------------------------------
(defsuite "resolve-relative-url: composed structural + keyword"
(deftest "append slug + set keyword"
(assert-equal "/(language.(spec.(explore.signals.batch.:page.1)))"
(resolve-relative-url
"/(language.(spec.(explore.signals)))"
"(.batch.:page.1)")))
(deftest "sibling + set keyword"
(assert-equal "/(language.(spec.(eval.:page.1)))"
(resolve-relative-url
"/(language.(spec.(explore.signals.:page.3)))"
"(..eval.:page.1)")))
(deftest "up two + set keyword"
(assert-equal "/(geography.(reactive.demo.:page.1))"
(resolve-relative-url
"/(geography.(hypermedia.(example.progress-bar)))"
"(...reactive.demo.:page.1)")))
(deftest "bare-dot composed"
(assert-equal "/(language.(spec.(eval.:page.1)))"
(resolve-relative-url
"/(language.(spec.(explore.signals.:page.3)))"
"..eval.:page.1"))))
;; ==========================================================================
;; SX URL Parsing — parse-sx-url
;; ==========================================================================
(defsuite "parse-sx-url"
(deftest "home URL"
(let ((parsed (parse-sx-url "/")))
(assert-equal "home" (get parsed "type"))
(assert-equal "/" (get parsed "raw"))))
(deftest "absolute SX URL"
(let ((parsed (parse-sx-url "/(language.(doc.intro))")))
(assert-equal "absolute" (get parsed "type"))))
(deftest "relative paren-form"
(let ((parsed (parse-sx-url "(.slug)")))
(assert-equal "relative" (get parsed "type"))))
(deftest "relative bare-dot"
(let ((parsed (parse-sx-url ".slug")))
(assert-equal "relative" (get parsed "type"))))
(deftest "relative double-dot"
(let ((parsed (parse-sx-url "..")))
(assert-equal "relative" (get parsed "type"))))
(deftest "direct component"
(let ((parsed (parse-sx-url "/(~essay-sx-sucks)")))
(assert-equal "direct-component" (get parsed "type"))
(assert-equal "~essay-sx-sucks" (get parsed "name"))))
(deftest "old-style path"
(let ((parsed (parse-sx-url "/language/docs/intro")))
(assert-equal "path" (get parsed "type")))))
;; ==========================================================================
;; URL Special Forms (! prefix)
;; ==========================================================================
(defsuite "url-special-form?"
(deftest "known special forms"
(assert-true (url-special-form? "!source"))
(assert-true (url-special-form? "!inspect"))
(assert-true (url-special-form? "!diff"))
(assert-true (url-special-form? "!search"))
(assert-true (url-special-form? "!raw"))
(assert-true (url-special-form? "!json")))
(deftest "unknown bang-prefix is not a special form"
(assert-false (url-special-form? "!unknown"))
(assert-false (url-special-form? "!foo")))
(deftest "non-bang names are not special forms"
(assert-false (url-special-form? "source"))
(assert-false (url-special-form? "language"))
(assert-false (url-special-form? "~essay"))))
(defsuite "parse-sx-url: special forms"
(deftest "source special form"
(let ((parsed (parse-sx-url "/(!source.(~essay-sx-sucks))")))
(assert-equal "special-form" (get parsed "type"))
(assert-equal "!source" (get parsed "form"))
(assert-equal "(~essay-sx-sucks)" (get parsed "inner"))))
(deftest "inspect special form"
(let ((parsed (parse-sx-url "/(!inspect.(language.(doc.primitives)))")))
(assert-equal "special-form" (get parsed "type"))
(assert-equal "!inspect" (get parsed "form"))
(assert-equal "(language.(doc.primitives))" (get parsed "inner"))))
(deftest "diff special form with two args"
(let ((parsed (parse-sx-url "/(!diff.(language.(spec.signals)).(language.(spec.eval)))")))
(assert-equal "special-form" (get parsed "type"))
(assert-equal "!diff" (get parsed "form"))
(assert-equal "(language.(spec.signals)).(language.(spec.eval))" (get parsed "inner"))))
(deftest "raw special form"
(let ((parsed (parse-sx-url "/(!raw.(~some-component))")))
(assert-equal "special-form" (get parsed "type"))
(assert-equal "!raw" (get parsed "form"))
(assert-equal "(~some-component)" (get parsed "inner"))))
(deftest "json special form"
(let ((parsed (parse-sx-url "/(!json.(language.(doc.primitives)))")))
(assert-equal "special-form" (get parsed "type"))
(assert-equal "!json" (get parsed "form"))
(assert-equal "(language.(doc.primitives))" (get parsed "inner")))))
(defsuite "url-special-form-name"
(deftest "extracts form name"
(assert-equal "!source"
(url-special-form-name "/(!source.(~essay))")))
(deftest "returns nil for non-special-form"
(assert-nil (url-special-form-name "/(language.(doc.intro))"))
(assert-nil (url-special-form-name "/"))
(assert-nil (url-special-form-name "(.slug)"))))
(defsuite "url-special-form-inner"
(deftest "extracts inner expression"
(assert-equal "(~essay)"
(url-special-form-inner "/(!source.(~essay))")))
(deftest "extracts multi-arg inner"
(assert-equal "(a).(b)"
(url-special-form-inner "/(!diff.(a).(b))")))
(deftest "returns nil for non-special-form"
(assert-nil (url-special-form-inner "/(language.(doc.intro))"))))
;; ==========================================================================
;; Internal helpers — additional edge cases
;; ==========================================================================
(defsuite "internal: _pop-sx-url-level"
(deftest "pop three-level"
(assert-equal "/(a.(b))"
(_pop-sx-url-level "/(a.(b.(c)))")))
(deftest "pop two-level"
(assert-equal "/(a)"
(_pop-sx-url-level "/(a.(b))")))
(deftest "pop single-level to root"
(assert-equal "/"
(_pop-sx-url-level "/(a)")))
(deftest "pop root stays root"
(assert-equal "/"
(_pop-sx-url-level "/"))))
(defsuite "internal: _extract-innermost"
(deftest "single-level URL"
(let ((parts (_extract-innermost "/(language)")))
(assert-equal "/(" (get parts "before"))
(assert-equal "language" (get parts "content"))
(assert-equal ")" (get parts "suffix"))))
(deftest "two-level URL"
(let ((parts (_extract-innermost "/(language.(doc.intro))")))
(assert-equal "/(language.(" (get parts "before"))
(assert-equal "doc.intro" (get parts "content"))
(assert-equal "))" (get parts "suffix"))))
(deftest "three-level URL with keywords"
(let ((parts (_extract-innermost "/(a.(b.(c.d.:page.3)))")))
(assert-equal "/(a.(b.(" (get parts "before"))
(assert-equal "c.d.:page.3" (get parts "content"))
(assert-equal ")))" (get parts "suffix")))))
(defsuite "internal: _find-keyword-value"
(deftest "finds keyword"
(assert-equal "3"
(_find-keyword-value "explore.signals.:page.3" ":page")))
(deftest "returns nil when not found"
(assert-nil (_find-keyword-value "explore.signals" ":page")))
(deftest "finds among multiple keywords"
(assert-equal "batch"
(_find-keyword-value "explore.signals.:page.3.:section.batch" ":section"))))
(defsuite "internal: _set-keyword-in-content"
(deftest "replace existing"
(assert-equal "a.b.:page.4"
(_set-keyword-in-content "a.b.:page.3" ":page" "4")))
(deftest "append when missing"
(assert-equal "a.b.:page.1"
(_set-keyword-in-content "a.b" ":page" "1")))
(deftest "replace with multiple keywords present"
(assert-equal "a.:page.4.:section.batch"
(_set-keyword-in-content "a.:page.3.:section.batch" ":page" "4"))))
(defsuite "internal: _is-delta-value?"
(deftest "positive delta"
(assert-true (_is-delta-value? "+1"))
(assert-true (_is-delta-value? "+10")))
(deftest "negative delta"
(assert-true (_is-delta-value? "-1"))
(assert-true (_is-delta-value? "-10")))
(deftest "bare minus is not delta"
(assert-false (_is-delta-value? "-")))
(deftest "bare plus is not delta"
(assert-false (_is-delta-value? "+")))
(deftest "plain number is not delta"
(assert-false (_is-delta-value? "3"))
(assert-false (_is-delta-value? "0")))
(deftest "empty string is not delta"
(assert-false (_is-delta-value? ""))))
(defsuite "internal: _apply-delta"
(deftest "increment"
(assert-equal "4" (_apply-delta "3" "+1")))
(deftest "decrement"
(assert-equal "2" (_apply-delta "3" "-1")))
(deftest "large increment"
(assert-equal "13" (_apply-delta "3" "+10")))
(deftest "non-numeric current falls back"
(assert-equal "+1" (_apply-delta "abc" "+1"))))