HS E36: RPC timeout tests (10, 11, 14) — 16/16 complete
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 16s

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 <noreply@anthropic.com>
This commit is contained in:
2026-04-26 17:56:19 +00:00
parent de493e41d8
commit 8915eeaf5e
4 changed files with 117 additions and 4 deletions

View File

@@ -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, +1216 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, +1617 delta.

View File

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

View File

@@ -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<Math.min(endTest,testCount);i++){
globalThis.__hsMutationRegistry.length = 0;
globalThis.__hsMutationActive = false;
globalThis.__currentHsTestName = name;
_fakeTimers = []; // reset timer queue between tests
// Enable step limit for timeout protection
setStepLimit(STEP_LIMIT);

View File

@@ -2096,6 +2096,65 @@ def generate_eval_only_test(test, idx):
f' (assert= (host-get (host-global "__hs_ws_created") "_len") 2)))))'
)
# Test 10: rpc proxy default timeout rejects the promise.
# With a socket created using `with timeout 50`, calling rpc.neverReplies()
# enqueues a fake setTimeout. After flushing timers, wrapper.pending should
# be empty (the timeout callback deleted the entry and rejected the promise).
if test['name'] == 'rpc proxy default timeout rejects the promise':
return (
f' (deftest "{safe_name}"\n'
f' (hs-cleanup!)\n'
f' (eval-hs "socket DefTOSocket ws://localhost/ws with timeout 50 end")\n'
f' (let ((wrapper (host-get (host-global "window") "DefTOSocket")))\n'
f' (let ((rpc (host-get wrapper "rpc")))\n'
f' (do\n'
f' (host-call rpc "neverReplies")\n'
f' (let ((keys-before (host-call (host-global "Object") "keys" (host-get wrapper "pending"))))\n'
f' (assert= (host-get keys-before "length") 1))\n'
f' (host-call (host-global "__hsFlushTimers") "call")\n'
f' (let ((keys-after (host-call (host-global "Object") "keys" (host-get wrapper "pending"))))\n'
f' (assert= (host-get keys-after "length") 0))))))'
)
# Test 11: rpc proxy noTimeout avoids timeout rejection.
# rpc.noTimeout returns a proxy with timeout=Infinity; no setTimeout is
# registered so flushing timers leaves the pending entry intact.
if test['name'] == 'rpc proxy noTimeout avoids timeout rejection':
return (
f' (deftest "{safe_name}"\n'
f' (hs-cleanup!)\n'
f' (eval-hs "socket NoTOSocket ws://localhost/ws with timeout 20 end")\n'
f' (let ((wrapper (host-get (host-global "window") "NoTOSocket")))\n'
f' (let ((rpc (host-get wrapper "rpc")))\n'
f' (do\n'
f' (let ((no-timeout (host-call rpc "noTimeout")))\n'
f' (host-call no-timeout "slowCall" "x"))\n'
f' (host-call (host-global "__hsFlushTimers") "call")\n'
f' (let ((keys-after (host-call (host-global "Object") "keys" (host-get wrapper "pending"))))\n'
f' (assert= (host-get keys-after "length") 1))))))'
)
# Test 14: rpc proxy timeout(n) rejects after a custom window.
# rpc.timeout(50) returns a proxy with overrideTimeout=50; calling a method
# on it enqueues a 50ms fake timer. After flushing, pending is empty.
if test['name'] == 'rpc proxy timeout(n) rejects after a custom window':
return (
f' (deftest "{safe_name}"\n'
f' (hs-cleanup!)\n'
f' (eval-hs "socket CustomTOSocket ws://localhost/ws with timeout 60000 end")\n'
f' (let ((wrapper (host-get (host-global "window") "CustomTOSocket")))\n'
f' (let ((rpc (host-get wrapper "rpc")))\n'
f' (do\n'
f' (let ((timeout-fn (host-call rpc "timeout"))\n'
f' (custom-proxy (host-call-fn timeout-fn (list 50))))\n'
f' (host-call custom-proxy "willTimeOut"))\n'
f' (let ((keys-before (host-call (host-global "Object") "keys" (host-get wrapper "pending"))))\n'
f' (assert= (host-get keys-before "length") 1))\n'
f' (host-call (host-global "__hsFlushTimers") "call")\n'
f' (let ((keys-after (host-call (host-global "Object") "keys" (host-get wrapper "pending"))))\n'
f' (assert= (host-get keys-after "length") 0))))))'
)
# Special case: cluster-29 init events. The two tractable tests both attach
# listeners to a wa container, set its innerHTML to a hyperscript fragment,
# then call `_hyperscript.processNode(wa)`. Hand-roll deftests using