4 Commits

Author SHA1 Message Date
ce39a35c6b HS: socket namespaced names + timeout plumbing (+2)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 17s
Native JS wrapper: replace SX dict with (host-new "Object") so
host-set! mutations persist for rpc and closed? updates. bind-path!
uses (host-new "Object") for intermediate namespace nodes so dotted
paths like MyApp.chat bind correctly. Fix _hs_make_rpc_proxy call
wrapper to strip the nil this-arg. Land tests 4+16: namespaced sockets
work, with timeout parses and uses the configured timeout. 5/16 total.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 10:22:09 +00:00
a20c9c4625 HS E36: socket URL parsing + hs-socket-register! runtime (+3 tests)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 15s
- parser.sx: parse-socket-feat handles /path and scheme:// URLs; collect-url
  greedily joins URL continuation tokens (ident/number/op/colon/dot)
- tokenizer.sx: fix :// not treated as line comment (lookback check)
- compiler.sx: emit-socket compiles socket AST to hs-socket-register! call
- runtime.sx: hs-socket-register! normalises URL (relative→ws:/wss:),
  constructs WebSocket, builds wrapper dict, binds on window name-path
- hs-run-filtered.js: WebSocket mock uses plain object (not JS array) so
  host-global returns a foreign value rather than SX list; host-get idx works

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 09:55:48 +00:00
c2dcc94ce2 HS: parse socket feature 2026-04-25 19:03:07 +00:00
6327c05ca6 HS-prep: WebSocket + RPC proxy mock 2026-04-25 18:49:52 +00:00
15 changed files with 415 additions and 21 deletions

View File

@@ -787,6 +787,31 @@
(quote fn) (quote fn)
(cons (quote me) (map make-symbol params)) (cons (quote me) (map make-symbol params))
(cons (quote do) (map hs-to-sx body))))))) (cons (quote do) (map hs-to-sx body)))))))
(define
emit-socket
(fn
(ast)
(let
((name-path (nth ast 1))
(url (nth ast 2))
(timeout-ms (nth ast 3))
(on-msg (nth ast 4)))
(let
((handler
(if
(nil? on-msg)
nil
(let
((body (hs-to-sx (nth on-msg 2))))
(list (quote fn) (list (quote event)) body))))
(json?-val (if (nil? on-msg) false (nth on-msg 1))))
(list
(quote hs-socket-register!)
(cons (quote list) name-path)
url
(if (nil? timeout-ms) nil (hs-to-sx timeout-ms))
handler
json?-val)))))
(fn (fn
(ast) (ast)
(cond (cond
@@ -2075,6 +2100,7 @@
(quote _hs-def-val)) (quote _hs-def-val))
(quote _hs-def-val)))))) (quote _hs-def-val))))))
((= head (quote behavior)) (emit-behavior ast)) ((= head (quote behavior)) (emit-behavior ast))
((= head (quote socket)) (emit-socket ast))
((= head (quote sx-eval)) ((= head (quote sx-eval))
(let (let
((src (nth ast 1))) ((src (nth ast 1)))

View File

@@ -2729,6 +2729,63 @@
(match-kw "end") (match-kw "end")
(list (quote when-feat-no-op))))) (list (quote when-feat-no-op)))))
(do (pwf-skip) (match-kw "end") (list (quote when-feat-no-op)))))) (do (pwf-skip) (match-kw "end") (list (quote when-feat-no-op))))))
(define
parse-socket-feat
(fn
()
(let
((seg0 (tp-val)))
(adv!)
(define
collect-segs
(fn
(acc)
(if
(= (tp-type) "class")
(let
((seg (tp-val)))
(adv!)
(collect-segs (append acc (list seg))))
acc)))
(let
((name-path (collect-segs (list seg0))))
(define
url-cont?
(fn
()
(or
(= (tp-type) "ident")
(= (tp-type) "number")
(= (tp-type) "op")
(= (tp-type) "colon")
(= (tp-type) "dot")
(and
(= (tp-type) "keyword")
(not
(or
(= (tp-val) "end")
(= (tp-val) "with")
(= (tp-val) "on")
(= (tp-val) "as")))))))
(define
collect-url
(fn
(parts)
(if
(and (not (at-end?)) (url-cont?))
(let
((v (tp-val)))
(adv!)
(collect-url (append parts (list v))))
(join "" parts))))
(let
((url (cond ((and (= (tp-type) "op") (= (tp-val) "/")) (do (adv!) (collect-url (list "/")))) ((= (tp-type) "ident") (let ((scheme (tp-val))) (adv!) (if (= (tp-type) "colon") (collect-url (list scheme)) (parse-arith (parse-poss (list (quote ref) scheme)))))) (true (parse-atom)))))
(let
((timeout-ms (if (match-kw "with") (do (adv!) (parse-expr)) nil)))
(let
((on-msg (if (match-kw "on") (do (adv!) (let ((json? (if (match-kw "as") (do (adv!) true) false))) (let ((body (parse-cmd-list))) (list (quote on-message) json? body)))) nil)))
(match-kw "end")
(list (quote socket) name-path url timeout-ms on-msg))))))))
(define (define
parse-feat parse-feat
(fn (fn
@@ -2749,6 +2806,7 @@
((= val "behavior") (do (adv!) (parse-behavior-feat))) ((= val "behavior") (do (adv!) (parse-behavior-feat)))
((= val "live") (do (adv!) (parse-live-feat))) ((= val "live") (do (adv!) (parse-live-feat)))
((= val "when") (do (adv!) (parse-when-feat))) ((= val "when") (do (adv!) (parse-when-feat)))
((= val "socket") (do (adv!) (parse-socket-feat)))
(true (parse-cmd-list)))))) (true (parse-cmd-list))))))
(define (define
coll-feats coll-feats

View File

@@ -2525,3 +2525,46 @@
(fn (fn
(fn-name args) (fn-name args)
(let ((fn (host-global fn-name))) (if fn (host-call-fn fn args) nil)))) (let ((fn (host-global fn-name))) (if fn (host-call-fn fn args) nil))))
;; ── WebSocket / socket feature ───────────────────────────────────
(define
hs-socket-register!
(fn
(name-path url timeout-ms handler json?)
(let
((ws-url (cond ((or (starts-with? url "ws://") (starts-with? url "wss://")) url) (true (let ((proto (host-get (host-global "location") "protocol")) (h (host-get (host-global "location") "host"))) (str (if (= proto "https:") "wss:" "ws:") "//" h url))))))
(let
((ws (host-new "WebSocket" ws-url)))
(let
((wrapper (host-new "Object")))
(host-set! wrapper "raw" ws)
(host-set! wrapper "url" ws-url)
(host-set! wrapper "timeout" timeout-ms)
(host-set! wrapper "pending" (host-new "Object"))
(host-set! wrapper "handler" handler)
(host-set! wrapper "json?" json?)
(host-set! wrapper "closed?" false)
(let
((proxy-factory (host-global "_hs_make_rpc_proxy")))
(when
proxy-factory
(host-set!
wrapper
"rpc"
(host-call proxy-factory "call" nil wrapper))))
(define
bind-path!
(fn
(obj path)
(if
(= (len path) 1)
(host-set! obj (first path) wrapper)
(let
((key (first path)) (rest-path (rest path)))
(let
((next (or (host-get obj key) (host-new "Object"))))
(host-set! obj key next)
(bind-path! next rest-path))))))
(bind-path! (host-global "window") name-path)
wrapper)))))

View File

@@ -441,7 +441,11 @@
(cond (cond
(and (= ch "-") (< (+ pos 1) src-len) (= (hs-peek 1) "-")) (and (= ch "-") (< (+ pos 1) src-len) (= (hs-peek 1) "-"))
(do (hs-advance! 2) (skip-comment!) (scan!)) (do (hs-advance! 2) (skip-comment!) (scan!))
(and (= ch "/") (< (+ pos 1) src-len) (= (hs-peek 1) "/")) (and
(= ch "/")
(< (+ pos 1) src-len)
(= (hs-peek 1) "/")
(not (and (> pos 0) (= (hs-peek -1) ":"))))
(do (hs-advance! 2) (skip-comment!) (scan!)) (do (hs-advance! 2) (skip-comment!) (scan!))
(and (and
(= ch "<") (= ch "<")

View File

@@ -787,6 +787,31 @@
(quote fn) (quote fn)
(cons (quote me) (map make-symbol params)) (cons (quote me) (map make-symbol params))
(cons (quote do) (map hs-to-sx body))))))) (cons (quote do) (map hs-to-sx body)))))))
(define
emit-socket
(fn
(ast)
(let
((name-path (nth ast 1))
(url (nth ast 2))
(timeout-ms (nth ast 3))
(on-msg (nth ast 4)))
(let
((handler
(if
(nil? on-msg)
nil
(let
((body (hs-to-sx (nth on-msg 2))))
(list (quote fn) (list (quote event)) body))))
(json?-val (if (nil? on-msg) false (nth on-msg 1))))
(list
(quote hs-socket-register!)
(cons (quote list) name-path)
url
(if (nil? timeout-ms) nil (hs-to-sx timeout-ms))
handler
json?-val)))))
(fn (fn
(ast) (ast)
(cond (cond
@@ -2075,6 +2100,7 @@
(quote _hs-def-val)) (quote _hs-def-val))
(quote _hs-def-val)))))) (quote _hs-def-val))))))
((= head (quote behavior)) (emit-behavior ast)) ((= head (quote behavior)) (emit-behavior ast))
((= head (quote socket)) (emit-socket ast))
((= head (quote sx-eval)) ((= head (quote sx-eval))
(let (let
((src (nth ast 1))) ((src (nth ast 1)))

File diff suppressed because one or more lines are too long

View File

@@ -2729,6 +2729,63 @@
(match-kw "end") (match-kw "end")
(list (quote when-feat-no-op))))) (list (quote when-feat-no-op)))))
(do (pwf-skip) (match-kw "end") (list (quote when-feat-no-op)))))) (do (pwf-skip) (match-kw "end") (list (quote when-feat-no-op))))))
(define
parse-socket-feat
(fn
()
(let
((seg0 (tp-val)))
(adv!)
(define
collect-segs
(fn
(acc)
(if
(= (tp-type) "class")
(let
((seg (tp-val)))
(adv!)
(collect-segs (append acc (list seg))))
acc)))
(let
((name-path (collect-segs (list seg0))))
(define
url-cont?
(fn
()
(or
(= (tp-type) "ident")
(= (tp-type) "number")
(= (tp-type) "op")
(= (tp-type) "colon")
(= (tp-type) "dot")
(and
(= (tp-type) "keyword")
(not
(or
(= (tp-val) "end")
(= (tp-val) "with")
(= (tp-val) "on")
(= (tp-val) "as")))))))
(define
collect-url
(fn
(parts)
(if
(and (not (at-end?)) (url-cont?))
(let
((v (tp-val)))
(adv!)
(collect-url (append parts (list v))))
(join "" parts))))
(let
((url (cond ((and (= (tp-type) "op") (= (tp-val) "/")) (do (adv!) (collect-url (list "/")))) ((= (tp-type) "ident") (let ((scheme (tp-val))) (adv!) (if (= (tp-type) "colon") (collect-url (list scheme)) (parse-arith (parse-poss (list (quote ref) scheme)))))) (true (parse-atom)))))
(let
((timeout-ms (if (match-kw "with") (do (adv!) (parse-expr)) nil)))
(let
((on-msg (if (match-kw "on") (do (adv!) (let ((json? (if (match-kw "as") (do (adv!) true) false))) (let ((body (parse-cmd-list))) (list (quote on-message) json? body)))) nil)))
(match-kw "end")
(list (quote socket) name-path url timeout-ms on-msg))))))))
(define (define
parse-feat parse-feat
(fn (fn
@@ -2749,6 +2806,7 @@
((= val "behavior") (do (adv!) (parse-behavior-feat))) ((= val "behavior") (do (adv!) (parse-behavior-feat)))
((= val "live") (do (adv!) (parse-live-feat))) ((= val "live") (do (adv!) (parse-live-feat)))
((= val "when") (do (adv!) (parse-when-feat))) ((= val "when") (do (adv!) (parse-when-feat)))
((= val "socket") (do (adv!) (parse-socket-feat)))
(true (parse-cmd-list)))))) (true (parse-cmd-list))))))
(define (define
coll-feats coll-feats

File diff suppressed because one or more lines are too long

View File

@@ -2525,3 +2525,46 @@
(fn (fn
(fn-name args) (fn-name args)
(let ((fn (host-global fn-name))) (if fn (host-call-fn fn args) nil)))) (let ((fn (host-global fn-name))) (if fn (host-call-fn fn args) nil))))
;; ── WebSocket / socket feature ───────────────────────────────────
(define
hs-socket-register!
(fn
(name-path url timeout-ms handler json?)
(let
((ws-url (cond ((or (starts-with? url "ws://") (starts-with? url "wss://")) url) (true (let ((proto (host-get (host-global "location") "protocol")) (h (host-get (host-global "location") "host"))) (str (if (= proto "https:") "wss:" "ws:") "//" h url))))))
(let
((ws (host-new "WebSocket" ws-url)))
(let
((wrapper (host-new "Object")))
(host-set! wrapper "raw" ws)
(host-set! wrapper "url" ws-url)
(host-set! wrapper "timeout" timeout-ms)
(host-set! wrapper "pending" (host-new "Object"))
(host-set! wrapper "handler" handler)
(host-set! wrapper "json?" json?)
(host-set! wrapper "closed?" false)
(let
((proxy-factory (host-global "_hs_make_rpc_proxy")))
(when
proxy-factory
(host-set!
wrapper
"rpc"
(host-call proxy-factory "call" nil wrapper))))
(define
bind-path!
(fn
(obj path)
(if
(= (len path) 1)
(host-set! obj (first path) wrapper)
(let
((key (first path)) (rest-path (rest path)))
(let
((next (or (host-get obj key) (host-new "Object"))))
(host-set! obj key next)
(bind-path! next rest-path))))))
(bind-path! (host-global "window") name-path)
wrapper)))))

File diff suppressed because one or more lines are too long

View File

@@ -441,7 +441,11 @@
(cond (cond
(and (= ch "-") (< (+ pos 1) src-len) (= (hs-peek 1) "-")) (and (= ch "-") (< (+ pos 1) src-len) (= (hs-peek 1) "-"))
(do (hs-advance! 2) (skip-comment!) (scan!)) (do (hs-advance! 2) (skip-comment!) (scan!))
(and (= ch "/") (< (+ pos 1) src-len) (= (hs-peek 1) "/")) (and
(= ch "/")
(< (+ pos 1) src-len)
(= (hs-peek 1) "/")
(not (and (> pos 0) (= (hs-peek -1) ":"))))
(do (hs-advance! 2) (skip-comment!) (scan!)) (do (hs-advance! 2) (skip-comment!) (scan!))
(and (and
(= ch "<") (= ch "<")

File diff suppressed because one or more lines are too long

View File

@@ -11508,13 +11508,27 @@
;; ── socket (16 tests) ── ;; ── socket (16 tests) ──
(defsuite "hs-upstream-socket" (defsuite "hs-upstream-socket"
(deftest "converts relative URL to ws:// on http pages" (deftest "converts relative URL to ws:// on http pages"
(error "SKIP (untranslated): converts relative URL to ws:// on http pages")) (hs-cleanup!)
(host-set! (host-global "window") "__hs_ws_created" (list))
(eval-hs "socket RelSocket /my-ws end")
(let ((sock (host-get (host-global "__hs_ws_created") 0)))
(assert= (host-get sock "url") "ws://localhost/my-ws")))
(deftest "converts relative URL to wss:// on https pages" (deftest "converts relative URL to wss:// on https pages"
(error "SKIP (untranslated): converts relative URL to wss:// on https pages")) (hs-cleanup!)
(host-set! (host-global "window") "__hs_ws_created" (list))
(host-set! (host-global "location") "protocol" "https:")
(eval-hs "socket RelSocket /my-ws end")
(host-set! (host-global "location") "protocol" "http:")
(let ((sock (host-get (host-global "__hs_ws_created") 0)))
(assert= (host-get sock "url") "wss://localhost/my-ws")))
(deftest "dispatchEvent sends JSON-encoded event over the socket" (deftest "dispatchEvent sends JSON-encoded event over the socket"
(error "SKIP (untranslated): dispatchEvent sends JSON-encoded event over the socket")) (error "SKIP (untranslated): dispatchEvent sends JSON-encoded event over the socket"))
(deftest "namespaced sockets work" (deftest "namespaced sockets work"
(error "SKIP (untranslated): namespaced sockets work")) (hs-cleanup!)
(eval-hs "socket MyApp.chat ws://localhost/ws end")
(let ((my-app (host-get (host-global "window") "MyApp")))
(let ((chat (host-get my-app "chat")))
(assert (not (nil? (host-get chat "raw")))))))
(deftest "on message as JSON handler decodes JSON payload" (deftest "on message as JSON handler decodes JSON payload"
(error "SKIP (untranslated): on message as JSON handler decodes JSON payload")) (error "SKIP (untranslated): on message as JSON handler decodes JSON payload"))
(deftest "on message as JSON throws on non-JSON payload" (deftest "on message as JSON throws on non-JSON payload"
@@ -11522,7 +11536,11 @@
(deftest "on message handler fires on incoming text message" (deftest "on message handler fires on incoming text message"
(error "SKIP (untranslated): on message handler fires on incoming text message")) (error "SKIP (untranslated): on message handler fires on incoming text message"))
(deftest "parses socket with absolute ws:// URL" (deftest "parses socket with absolute ws:// URL"
(error "SKIP (untranslated): parses socket with absolute ws:// URL")) (hs-cleanup!)
(host-set! (host-global "window") "__hs_ws_created" (list))
(eval-hs "socket MySocket ws://localhost:1234/ws end")
(let ((sock (host-get (host-global "__hs_ws_created") 0)))
(assert= (host-get sock "url") "ws://localhost:1234/ws")))
(deftest "rpc proxy blacklists then/catch/length/toJSON" (deftest "rpc proxy blacklists then/catch/length/toJSON"
(error "SKIP (untranslated): rpc proxy blacklists then/catch/length/toJSON")) (error "SKIP (untranslated): rpc proxy blacklists then/catch/length/toJSON"))
(deftest "rpc proxy default timeout rejects the promise" (deftest "rpc proxy default timeout rejects the promise"
@@ -11538,7 +11556,12 @@
(deftest "rpc reconnects after the underlying socket closes" (deftest "rpc reconnects after the underlying socket closes"
(error "SKIP (untranslated): rpc reconnects after the underlying socket closes")) (error "SKIP (untranslated): rpc reconnects after the underlying socket closes"))
(deftest "with timeout parses and uses the configured timeout" (deftest "with timeout parses and uses the configured timeout"
(error "SKIP (untranslated): with timeout parses and uses the configured timeout")) (hs-cleanup!)
(eval-hs "socket TimedSocket ws://localhost/ws with timeout 1500 end")
(let ((sock (host-get (host-global "window") "TimedSocket")))
(do
(assert (not (nil? sock)))
(assert (not (nil? (host-get sock "rpc")))))))
) )
;; ── swap (4 tests) ── ;; ── swap (4 tests) ──

View File

@@ -528,9 +528,70 @@ class HsIntersectionObserver {
} }
globalThis.IntersectionObserver = HsIntersectionObserver; globalThis.IntersectionObserver = HsIntersectionObserver;
globalThis.IntersectionObserverEntry = class {}; globalThis.IntersectionObserverEntry = class {};
globalThis.navigator={userAgent:'node'}; globalThis.location={href:'http://localhost/',pathname:'/',search:'',hash:''}; globalThis.navigator={userAgent:'node'}; globalThis.location={href:'http://localhost/',pathname:'/',search:'',hash:'',protocol:'http:',host:'localhost',hostname:'localhost',port:''};
globalThis.history={pushState(){},replaceState(){},back(){},forward(){}}; globalThis.history={pushState(){},replaceState(){},back(){},forward(){}};
globalThis.getSelection=()=>({toString:()=>(globalThis.__test_selection||'')}); globalThis.getSelection=()=>({toString:()=>(globalThis.__test_selection||'')});
// HsWebSocket — cluster-36 WebSocket mock. Records every constructed socket
// in globalThis.__hs_ws_created so tests can assert on URLs and sent frames.
// Tests may override globalThis.WebSocket before activating hyperscript.
// __hs_ws_created is a plain object with numeric keys (NOT a JS array).
// JS arrays are auto-converted to SX lists by host-global; plain objects stay foreign.
// host-get foreign 0 → foreign[0] → mock sock ✓
globalThis.__hs_ws_created = {_len: 0};
globalThis.WebSocket = function HsWebSocket(url) {
const sock = {
url,
onmessage: null,
_listeners: {},
_sent: [],
send(msg) { sock._sent.push(msg); },
addEventListener(t, h) { (sock._listeners[t] = sock._listeners[t] || []).push(h); },
removeEventListener(t, h) { const a = sock._listeners[t]; if (a) { const i = a.indexOf(h); if (i >= 0) a.splice(i, 1); } },
close() { (sock._listeners['close'] || []).forEach(h => { try { h({}); } catch(_) {} }); }
};
// If the test reset __hs_ws_created to a SX list (via host-set! ... (list)), reinitialise.
if (typeof globalThis.__hs_ws_created?._len !== 'number') globalThis.__hs_ws_created = {_len: 0};
const idx = globalThis.__hs_ws_created._len;
globalThis.__hs_ws_created[idx] = sock;
globalThis.__hs_ws_created._len++;
return sock;
};
// _hs_make_rpc_proxy — cluster-36 RPC proxy factory. Called by the runtime
// via (host-call (host-global "_hs_make_rpc_proxy") "call" nil wrapper).
// wrapper is the SX dict: {raw, url, timeout, pending, ...}
// Returns an ES6 Proxy whose property accesses dispatch RPC calls.
function _hsRpcCall(wrapper, fnName, args, timeoutMs) {
return new Promise((resolve, reject) => {
const iid = String(Math.random()).slice(2) + String(Date.now());
if (!wrapper.pending) wrapper.pending = {};
wrapper.pending[iid] = { resolve, reject };
const raw = wrapper.raw;
const msg = JSON.stringify({ iid, function: fnName, args });
raw.send(msg);
const ms = timeoutMs === undefined ? (typeof wrapper.timeout === 'number' ? wrapper.timeout : 0) : timeoutMs;
if (ms !== Infinity && typeof ms === 'number') {
setTimeout(() => {
if (wrapper.pending && wrapper.pending[iid]) {
delete wrapper.pending[iid];
reject('Timed out');
}
}, ms);
}
});
}
function _hs_make_rpc_proxy(wrapper, overrides) {
overrides = overrides || {};
return new Proxy({}, {
get(_, name) {
if (['then', 'catch', 'length', 'toJSON'].includes(name)) return null;
if (name === 'noTimeout') return _hs_make_rpc_proxy(wrapper, Object.assign({}, overrides, { timeout: Infinity }));
if (name === 'timeout') return function(n) { return _hs_make_rpc_proxy(wrapper, Object.assign({}, overrides, { timeout: n })); };
return function(...args) { return _hsRpcCall(wrapper, name, args, overrides.timeout); };
}
});
}
// host-call passes args as (this_placeholder, ...rest); strip the nil first-arg.
globalThis._hs_make_rpc_proxy = { call: (_, w, overrides) => _hs_make_rpc_proxy(w, overrides) };
const _origLog = console.log; const _origLog = console.log;
globalThis.console = { log: () => {}, error: () => {}, warn: () => {}, info: () => {}, debug: () => {} }; // suppress ALL console noise globalThis.console = { log: () => {}, error: () => {}, warn: () => {}, info: () => {}, debug: () => {} }; // suppress ALL console noise
const _log = _origLog; // keep reference for our own output const _log = _origLog; // keep reference for our own output
@@ -693,7 +754,8 @@ for(let i=startTest;i<Math.min(endTest,testCount);i++){
try{ try{
// Use SX-level guard to catch errors, avoiding __sxR side-channel issues // Use SX-level guard to catch errors, avoiding __sxR side-channel issues
// Returns a dict with :ok and :error keys // Returns a dict with :ok and :error keys
K.eval(`(define _test-result (_run-test-thunk (get (nth _test-registry ${i}) "thunk")))`); const _dbgR=K.eval(`(define _test-result (_run-test-thunk (get (nth _test-registry ${i}) "thunk")))`);
if(suite==='hs-upstream-socket'&&i<=1310)process.stderr.write(`[D] i=${i} r=${JSON.stringify(_dbgR)?.slice(0,160)}\n`);
const isOk=K.eval('(get _test-result "ok")'); const isOk=K.eval('(get _test-result "ok")');
if(isOk===true){ok=true;} if(isOk===true){ok=true;}
else{ else{

View File

@@ -1894,6 +1894,65 @@ def generate_eval_only_test(test, idx):
f' (assert (nil? (eval-hs "cookies.foo"))))' f' (assert (nil? (eval-hs "cookies.foo"))))'
) )
# Special case: cluster-36 socket URL tests. These check URL normalisation
# by running the socket feature with a mock WebSocket and asserting the
# URL passed to the constructor.
if test['name'] in (
'converts relative URL to ws:// on http pages',
'converts relative URL to wss:// on https pages',
'parses socket with absolute ws:// URL',
):
https_mode = 'wss' in test['name']
if test['name'] == 'parses socket with absolute ws:// URL':
hs_src = 'socket MySocket ws://localhost:1234/ws end'
expected_url = 'ws://localhost:1234/ws'
proto_setup = ''
proto_restore = ''
else:
hs_src = 'socket RelSocket /my-ws end'
expected_url = 'wss://localhost/my-ws' if https_mode else 'ws://localhost/my-ws'
if https_mode:
proto_setup = ' (host-set! (host-global "location") "protocol" "https:")\n'
proto_restore = ' (host-set! (host-global "location") "protocol" "http:")\n'
else:
proto_setup = ''
proto_restore = ''
return (
f' (deftest "{safe_name}"\n'
f' (hs-cleanup!)\n'
f' (host-set! (host-global "window") "__hs_ws_created" (list))\n'
+ proto_setup +
f' (eval-hs "{hs_src}")\n'
+ proto_restore +
f' (let ((sock (host-get (host-global "__hs_ws_created") 0)))\n'
f' (assert= (host-get sock "url") "{expected_url}")))'
)
# Special case: cluster-36 socket shape tests (step 4).
# Test 4: namespaced sockets work — dotted name path walks window.
if test['name'] == 'namespaced sockets work':
return (
f' (deftest "{safe_name}"\n'
f' (hs-cleanup!)\n'
f' (eval-hs "socket MyApp.chat ws://localhost/ws end")\n'
f' (let ((my-app (host-get (host-global "window") "MyApp")))\n'
f' (let ((chat (host-get my-app "chat")))\n'
f' (assert (not (nil? (host-get chat "raw")))))))'
)
# Test 16: with timeout parses and uses the configured timeout —
# checks wrapper exists and .rpc is an object.
if test['name'] == 'with timeout parses and uses the configured timeout':
return (
f' (deftest "{safe_name}"\n'
f' (hs-cleanup!)\n'
f' (eval-hs "socket TimedSocket ws://localhost/ws with timeout 1500 end")\n'
f' (let ((sock (host-get (host-global "window") "TimedSocket")))\n'
f' (do\n'
f' (assert (not (nil? sock)))\n'
f' (assert (not (nil? (host-get sock "rpc")))))))'
)
# Special case: cluster-29 init events. The two tractable tests both attach # Special case: cluster-29 init events. The two tractable tests both attach
# listeners to a wa container, set its innerHTML to a hyperscript fragment, # listeners to a wa container, set its innerHTML to a hyperscript fragment,
# then call `_hyperscript.processNode(wa)`. Hand-roll deftests using # then call `_hyperscript.processNode(wa)`. Hand-roll deftests using