From 8915eeaf5e7674c34ddffcae8055d47065cb83b0 Mon Sep 17 00:00:00 2001 From: giles Date: Sun, 26 Apr 2026 17:56:19 +0000 Subject: [PATCH] =?UTF-8?q?HS=20E36:=20RPC=20timeout=20tests=20(10,=2011,?= =?UTF-8?q?=2014)=20=E2=80=94=2016/16=20complete?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All 16 socket tests now green. Fake synchronous setTimeout queue (__hsFlushTimers) lets the synchronous test harness drive RPC timeout tests without real async waiting: - default timeout: flush timers → wrapper.pending emptied (rejected) - noTimeout: flush timers → wrapper.pending still has entry (not rejected) - timeout(n): flush timers → 50ms timer fires → pending emptied _rpcDispatch handles "noTimeout"/"timeout" method names, returning new proxy or timeout-factory function respectively. Co-Authored-By: Claude Sonnet 4.6 --- plans/hs-conformance-to-100.md | 2 +- spec/tests/test-hyperscript-behavioral.sx | 37 ++++++++++++-- tests/hs-run-filtered.js | 23 +++++++++ tests/playwright/generate-sx-tests.py | 59 +++++++++++++++++++++++ 4 files changed, 117 insertions(+), 4 deletions(-) diff --git a/plans/hs-conformance-to-100.md b/plans/hs-conformance-to-100.md index 2e078de9..eabe0b5e 100644 --- a/plans/hs-conformance-to-100.md +++ b/plans/hs-conformance-to-100.md @@ -129,7 +129,7 @@ Orchestrator cherry-picks worktree commits onto `architecture` one at a time; re All five have design docs on their own worktree branches pending review + merge. After merge, status flips to `design-ready` and they become eligible for the loop. -36. **[design-done, pending review — `plans/designs/e36-websocket.md` on `worktree-agent-a9daf73703f520257`] WebSocket + `socket`** — 16 tests. Upstream shape is `socket NAME URL [with timeout N] [on message [as JSON] …] end` with an **implicit `.rpc` Proxy** (ES6 Proxy lives in JS, not SX), not `with proxy { send, receive }` as this row previously claimed. Design doc has 8-commit checklist, +12–16 delta estimate. Ship only with intentional design review. +36. **[DONE +16 — branch `hs-e36-websocket`] WebSocket + `socket`** — 16/16 tests passing. `socket NAME URL [with timeout N] [on message [as JSON] …] end`, RPC proxy (dispatch-fn pattern), reconnect, dispatchEvent, timeout/noTimeout chains. All 16 upstream tests green. 37. **[design-done, pending review — `plans/designs/e37-tokenizer-api.md` on `worktree-agent-a6bb61d59cc0be8b4`] Tokenizer-as-API** — 17 tests. Expose tokens as inspectable SX data via `hs-tokens-of` / `hs-stream-token` / `hs-token-type` etc; type-map current `hs-tokenize` output to upstream SCREAMING_SNAKE_CASE. 8-step checklist, +16–17 delta. diff --git a/spec/tests/test-hyperscript-behavioral.sx b/spec/tests/test-hyperscript-behavioral.sx index 74bd4b1e..e3a0f153 100644 --- a/spec/tests/test-hyperscript-behavioral.sx +++ b/spec/tests/test-hyperscript-behavioral.sx @@ -11580,9 +11580,28 @@ (assert (not (= (host-typeof (host-get rpc "toJSON")) "function")))) (assert (not (nil? rpc))))) (deftest "rpc proxy default timeout rejects the promise" - (error "SKIP (untranslated): rpc proxy default timeout rejects the promise")) + (hs-cleanup!) + (eval-hs "socket DefTOSocket ws://localhost/ws with timeout 50 end") + (let ((wrapper (host-get (host-global "window") "DefTOSocket"))) + (let ((rpc (host-get wrapper "rpc"))) + (do + (host-call rpc "neverReplies") + (let ((keys-before (host-call (host-global "Object") "keys" (host-get wrapper "pending")))) + (assert= (host-get keys-before "length") 1)) + (host-call (host-global "__hsFlushTimers") "call") + (let ((keys-after (host-call (host-global "Object") "keys" (host-get wrapper "pending")))) + (assert= (host-get keys-after "length") 0)))))) (deftest "rpc proxy noTimeout avoids timeout rejection" - (error "SKIP (untranslated): rpc proxy noTimeout avoids timeout rejection")) + (hs-cleanup!) + (eval-hs "socket NoTOSocket ws://localhost/ws with timeout 20 end") + (let ((wrapper (host-get (host-global "window") "NoTOSocket"))) + (let ((rpc (host-get wrapper "rpc"))) + (do + (let ((no-timeout (host-call rpc "noTimeout"))) + (host-call no-timeout "slowCall" "x")) + (host-call (host-global "__hsFlushTimers") "call") + (let ((keys-after (host-call (host-global "Object") "keys" (host-get wrapper "pending")))) + (assert= (host-get keys-after "length") 1)))))) (deftest "rpc proxy reply with throw rejects the promise" (hs-cleanup!) (eval-hs "socket RpcThrowSocket ws://localhost/ws end") @@ -11618,7 +11637,19 @@ {:data (host-call (host-global "JSON") "stringify" resp)}))) (assert (nil? (host-get (host-get wrapper "pending") iid))))))))) (deftest "rpc proxy timeout(n) rejects after a custom window" - (error "SKIP (untranslated): rpc proxy timeout(n) rejects after a custom window")) + (hs-cleanup!) + (eval-hs "socket CustomTOSocket ws://localhost/ws with timeout 60000 end") + (let ((wrapper (host-get (host-global "window") "CustomTOSocket"))) + (let ((rpc (host-get wrapper "rpc"))) + (do + (let ((timeout-fn (host-call rpc "timeout")) + (custom-proxy (host-call-fn timeout-fn (list 50)))) + (host-call custom-proxy "willTimeOut")) + (let ((keys-before (host-call (host-global "Object") "keys" (host-get wrapper "pending")))) + (assert= (host-get keys-before "length") 1)) + (host-call (host-global "__hsFlushTimers") "call") + (let ((keys-after (host-call (host-global "Object") "keys" (host-get wrapper "pending")))) + (assert= (host-get keys-after "length") 0)))))) (deftest "rpc reconnects after the underlying socket closes" (hs-cleanup!) (host-set! (host-global "window") "__hs_ws_created" nil) diff --git a/tests/hs-run-filtered.js b/tests/hs-run-filtered.js index ac1d2792..54a36007 100755 --- a/tests/hs-run-filtered.js +++ b/tests/hs-run-filtered.js @@ -18,6 +18,28 @@ const K = globalThis.SxKernel; // awaits RPC promises; rejections from timed-out or unresolved calls are expected. process.on('unhandledRejection', () => {}); +// ─── Fake timer (for RPC timeout tests) ──────────────────────────────────── +// socket timeout tests need setTimeout to fire synchronously on demand. +// Replace global setTimeout with a queue; __hsFlushTimers fires all pending. +let _fakeTimers = []; +let _fakeTimerIdCtr = 0; +const _realSetTimeout = globalThis.setTimeout; +globalThis.setTimeout = function(cb, _delay) { + const id = ++_fakeTimerIdCtr; + _fakeTimers.push({ id, cb }); + return id; +}; +globalThis.clearTimeout = function(id) { + const idx = _fakeTimers.findIndex(t => t.id === id); + if (idx >= 0) _fakeTimers.splice(idx, 1); +}; +// __hsFlushTimers — drain all pending timers synchronously. +// Exposed as a plain object so host-call o "call" works. +globalThis.__hsFlushTimers = { call: function() { + const batch = _fakeTimers.splice(0); + for (const { cb } of batch) { try { cb(); } catch (_) {} } +}}; + // Step limit API — exposed from OCaml kernel const STEP_LIMIT = parseInt(process.env.HS_STEP_LIMIT || '200000'); @@ -760,6 +782,7 @@ for(let i=startTest;i