Files
windmill/backend/windmill-runtime-nativets/src/smoke_tests.rs
T
Ruben FiszelandClaude Opus 5.5 93e81ec189 [ee] chore: upgrade native runtime to current deno crates and v8 150 (#11657)
* chore: upgrade the native runtime to the current deno crates and v8 150

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: keep data and blob fetches working under no_network

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: pin the simdutf v8 archive and keep telemetry on http exporters

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: force the http otlp exporter for native telemetry and pin v8

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 19:54:57 +00:00

846 lines
36 KiB
Rust

//! Opt-in smoke tests for the nativets V8 runtime.
//!
//! Exercise the deno_core / deno_ast / swc surface (TypeScript transpile,
//! fetch, timers, URL, structuredClone, error propagation, concurrent
//! isolates, large payload roundtrip) that the standard worker-level
//! nativets tests in `backend/tests/worker.rs` don't reach — those tests
//! validate value passing through the job queue, but not the JS API
//! surface a deno_core bump would actually move.
//!
//! These tests are `#[ignore]`'d so the regular `cargo test` flow doesn't
//! pay their cost (each spawns a V8 isolate; some hit the network). Run
//! when changing the `deno_core` / `deno_ast` / `deno_runtime` / `swc_*`
//! pins in `backend/Cargo.toml`:
//!
//! cargo test -p windmill-runtime-nativets smoke -- --ignored
//!
//! Tests prefixed `smoke_net_` hit the public internet (httpbin.org,
//! example.com) and will fail if the runner has no egress. Skip them
//! locally with `cargo test -p windmill-runtime-nativets smoke -- --ignored --skip smoke_net_`.
use crate::{transpile_ts, NativeAnnotation, PrewarmedIsolate, PrewarmedResult};
/// Compile a TS snippet, run it through a fresh isolate with the given
/// positional args, and return the isolate's result + captured logs.
async fn run_ts(ts: &str, arg_names: &[&str], args: serde_json::Value) -> PrewarmedResult {
let js = transpile_ts(ts.to_string()).expect("transpile_ts failed");
let ann = NativeAnnotation::default();
let arg_names: Vec<String> = arg_names.iter().map(|s| s.to_string()).collect();
let mut iso = PrewarmedIsolate::spawn(String::new(), js, ann, arg_names, None);
iso.wait_ready().await.expect("isolate failed to pre-warm");
iso.start_execution(args.to_string())
.wait()
.await
.expect("isolate execution panicked")
}
fn unwrap_value(r: &PrewarmedResult) -> serde_json::Value {
let raw = r.result.as_ref().expect("script returned an error");
serde_json::from_str(raw.get()).expect("result not valid JSON")
}
// -----------------------------------------------------------------------------
// Local (no network) — these still need V8 / deno_core ops to be wired.
// -----------------------------------------------------------------------------
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[ignore = "deno_core upgrade smoke; run with --ignored"]
async fn smoke_basic_value_passing() {
let ts = r#"
export async function main(x: number): Promise<number> {
return x + 1;
}
"#;
let r = run_ts(ts, &["x"], serde_json::json!({"x": 41})).await;
assert_eq!(unwrap_value(&r), serde_json::json!(42));
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[ignore = "deno_core upgrade smoke; run with --ignored"]
async fn smoke_missing_optional_arg_uses_default() {
// A missing optional arg must arrive as `undefined`, not `null`, so the
// parameter default applies. With `null`, slice(0, null) -> [] -> length 0.
let ts = r#"
export async function main(limit = 50): Promise<number> {
return [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].slice(0, limit).length;
}
"#;
let r = run_ts(ts, &["limit"], serde_json::json!({})).await;
assert_eq!(unwrap_value(&r), serde_json::json!(10));
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[ignore = "deno_core upgrade smoke; run with --ignored"]
async fn smoke_explicit_null_arg_is_preserved() {
// An explicitly-provided JSON null must stay null (distinct from a missing
// arg), so the default does NOT apply.
let ts = r#"
export async function main(x: number | null = 7): Promise<string> {
return x === null ? "null" : String(x);
}
"#;
let r = run_ts(ts, &["x"], serde_json::json!({ "x": null })).await;
assert_eq!(unwrap_value(&r), serde_json::json!("null"));
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[ignore = "deno_core upgrade smoke; run with --ignored"]
async fn smoke_transpile_enum_and_union() {
// Enums + discriminated union + as-cast exercise the swc_ecma_ast +
// swc_ecma_parser TS-syntax paths the bare value tests don't.
let ts = r#"
enum Direction { Up = "U", Down = "D" }
type Msg = { kind: "move"; dir: Direction } | { kind: "stop" };
export async function main(): Promise<string> {
const msgs: Msg[] = [
{ kind: "move", dir: Direction.Up },
{ kind: "stop" },
{ kind: "move", dir: Direction.Down },
];
return msgs.map(m => m.kind === "move" ? m.dir : "_").join(",");
}
"#;
let r = run_ts(ts, &[], serde_json::json!({})).await;
assert_eq!(unwrap_value(&r), serde_json::json!("U,_,D"));
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[ignore = "deno_core upgrade smoke; run with --ignored"]
async fn smoke_set_timeout_and_promise_all() {
// setTimeout lives in deno_web; Promise.all hits the V8 microtask
// queue. A bump that breaks timer-op registration or microtask drain
// would surface here (script would hang or return wrong order).
let ts = r#"
export async function main(): Promise<number[]> {
const delays = [40, 10, 20, 30];
return await Promise.all(delays.map(d =>
new Promise<number>(resolve => setTimeout(() => resolve(d), d))
));
}
"#;
let r = run_ts(ts, &[], serde_json::json!({})).await;
// Promise.all preserves input order regardless of resolution order.
assert_eq!(unwrap_value(&r), serde_json::json!([40, 10, 20, 30]));
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[ignore = "deno_core upgrade smoke; run with --ignored"]
async fn smoke_url_and_searchparams() {
// deno_url surface: URL ctor, URLSearchParams parsing + iteration.
let ts = r#"
export async function main(): Promise<{ host: string; pairs: [string, string][] }> {
const u = new URL("https://example.com:8443/path?b=2&a=1&a=3");
const pairs: [string, string][] = [];
for (const [k, v] of u.searchParams) pairs.push([k, v]);
return { host: u.host, pairs };
}
"#;
let r = run_ts(ts, &[], serde_json::json!({})).await;
assert_eq!(
unwrap_value(&r),
serde_json::json!({
"host": "example.com:8443",
"pairs": [["b", "2"], ["a", "1"], ["a", "3"]],
}),
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[ignore = "deno_core upgrade smoke; run with --ignored"]
async fn smoke_web_blob_btoa_atob() {
// deno_web surface: Blob, atob/btoa. (`structuredClone` and the rest of
// the wired web globals are covered by `smoke_web_globals_are_wired`.)
let ts = r#"
export async function main(): Promise<{ b64: string; round_trip: string; size: number }> {
const blob = new Blob(["hello"], { type: "text/plain" });
const b64 = btoa("hello");
const round_trip = atob(b64);
return { b64, round_trip, size: blob.size };
}
"#;
let r = run_ts(ts, &[], serde_json::json!({})).await;
assert_eq!(
unwrap_value(&r),
serde_json::json!({
"b64": "aGVsbG8=",
"round_trip": "hello",
"size": 5,
}),
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[ignore = "deno_core upgrade smoke; run with --ignored"]
async fn smoke_large_payload_roundtrip() {
// ~512 KB string in and out — exercises arg encoding + result
// serialization through the deno_core <-> host op boundary at sizes
// an op-table change could break.
let big_in: String = "a".repeat(512 * 1024);
let ts = r#"
export async function main(s: string): Promise<{ in_len: number; out: string }> {
if (typeof s !== "string") throw new Error(`expected string, got ${typeof s}`);
return { in_len: s.length, out: "b".repeat(512 * 1024) };
}
"#;
let r = run_ts(ts, &["s"], serde_json::json!({"s": big_in})).await;
let v = unwrap_value(&r);
assert_eq!(v.get("in_len").and_then(|x| x.as_u64()), Some(512 * 1024));
let out_len = v
.get("out")
.and_then(|x| x.as_str())
.map(|s| s.len())
.unwrap_or(0);
assert_eq!(out_len, 512 * 1024);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[ignore = "deno_core upgrade smoke; run with --ignored"]
async fn smoke_error_propagation_with_message() {
// Throwing a typed Error must surface as PrewarmedResult::Err with
// the original message. A deno_core bump that changes the host-side
// error wrapping would lose this contract.
let ts = r#"
export async function main(): Promise<void> {
throw new Error("nativets_smoke_marker_xyz");
}
"#;
let r = run_ts(ts, &[], serde_json::json!({})).await;
let err = r.result.expect_err("expected script to fail");
assert!(
err.contains("nativets_smoke_marker_xyz"),
"thrown error message did not reach result: {err}",
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
#[ignore = "deno_core upgrade smoke; run with --ignored"]
async fn smoke_concurrent_isolates() {
// Spawn N isolates in parallel from the same tokio runtime. Each
// PrewarmedIsolate uses spawn_blocking + a fresh V8 isolate.
// Catches isolate-setup races (V8_ISOLATE_CREATE_LOCK ordering) and
// any per-isolate state that a deno_core bump could break under
// concurrency.
let ts = r#"
export async function main(i: number): Promise<number> {
return i * 10;
}
"#;
let js = transpile_ts(ts.to_string()).expect("transpile_ts failed");
const N: i64 = 8;
let mut handles = Vec::with_capacity(N as usize);
for i in 0..N {
let js = js.clone();
let h = tokio::spawn(async move {
let ann = NativeAnnotation::default();
let mut iso =
PrewarmedIsolate::spawn(String::new(), js, ann, vec!["i".to_string()], None);
iso.wait_ready().await.expect("pre-warm failed");
let res = iso
.start_execution(serde_json::json!({"i": i}).to_string())
.wait()
.await
.expect("isolate panicked");
res.result.expect("script errored")
});
handles.push(h);
}
let mut got: Vec<i64> = Vec::with_capacity(N as usize);
for h in handles {
let raw = h.await.expect("join failed");
let v: serde_json::Value = serde_json::from_str(raw.get()).expect("not JSON");
got.push(v.as_i64().unwrap_or(-1));
}
got.sort();
let expected: Vec<i64> = (0..N).map(|i| i * 10).collect();
assert_eq!(got, expected);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[ignore = "deno_core upgrade smoke; run with --ignored"]
async fn smoke_text_encoder_decoder() {
// TextEncoder/TextDecoder are wired from deno_web's 08_text_encoding.
// The "€" (U+20AC) is a 3-byte UTF-8 sequence, so this asserts the
// multi-byte encode → decode round-trip (not just ASCII) survives the
// deno_core op boundary.
let ts = r#"
export async function main(): Promise<{ bytes: number[]; round_trip: string }> {
const enc = new TextEncoder();
const dec = new TextDecoder();
const bytes = enc.encode("a€b");
return { bytes: Array.from(bytes), round_trip: dec.decode(bytes) };
}
"#;
let r = run_ts(ts, &[], serde_json::json!({})).await;
assert_eq!(
unwrap_value(&r),
serde_json::json!({
// "a" = 0x61, "€" = 0xE2 0x82 0xAC, "b" = 0x62
"bytes": [0x61, 0xE2, 0x82, 0xAC, 0x62],
"round_trip": "a€b",
}),
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[ignore = "deno_core upgrade smoke; run with --ignored"]
async fn smoke_web_globals_are_wired() {
// Guard against a wrong export name silently leaving a global unwired:
// assert each web-platform global we assign in runtime.js is actually
// defined. If a deno_web/deno_url export is renamed on a future bump,
// the corresponding `globalThis.X = mod.X` becomes `undefined` and this
// test flips it to false.
let ts = r#"
export async function main(): Promise<Record<string, boolean>> {
const names = [
"DOMException",
"TextEncoder", "TextDecoder", "TextEncoderStream", "TextDecoderStream",
"File",
"Event", "EventTarget", "CustomEvent",
"MessageEvent", "CloseEvent", "ErrorEvent", "reportError",
"ReadableStream", "WritableStream", "TransformStream",
"ReadableStreamDefaultReader", "ReadableStreamBYOBReader",
"ReadableStreamDefaultController", "ReadableByteStreamController",
"ReadableStreamBYOBRequest", "WritableStreamDefaultWriter",
"WritableStreamDefaultController", "TransformStreamDefaultController",
"ByteLengthQueuingStrategy", "CountQueuingStrategy",
"Performance", "PerformanceEntry", "PerformanceMark", "PerformanceMeasure",
"URLPattern",
"CompressionStream", "DecompressionStream",
"MessageChannel", "MessagePort",
"structuredClone", "performance",
];
const out: Record<string, boolean> = {};
for (const n of names) out[n] = typeof (globalThis as any)[n] !== "undefined";
return out;
}
"#;
let r = run_ts(ts, &[], serde_json::json!({})).await;
let v = unwrap_value(&r);
let obj = v.as_object().expect("expected an object result");
let unwired: Vec<&String> = obj
.iter()
.filter(|(_, defined)| defined.as_bool() != Some(true))
.map(|(name, _)| name)
.collect();
assert!(
unwired.is_empty(),
"these globals were not wired: {unwired:?}"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[ignore = "deno_core upgrade smoke; run with --ignored"]
async fn smoke_structured_clone_and_performance() {
// structuredClone is the spec function (from 13_message_port.js): it
// deep-clones and accepts an options bag. performance.now() must be finite,
// and performance.timeOrigin must be seeded per isolate (via the Rust-side
// __wmInitPerIsolate call) so that `timeOrigin + now()` tracks wall-clock
// time rather than being NaN.
let ts = r#"
export async function main(): Promise<{ deep_equal: boolean; source_unchanged: boolean; now_ok: boolean; origin_ok: boolean }> {
const src = { a: 1, nested: { b: [2, 3] } };
const copy = structuredClone(src);
copy.nested.b.push(4);
const deep_equal = JSON.stringify(copy) === JSON.stringify({ a: 1, nested: { b: [2, 3, 4] } });
// Mutating the clone must not touch the source (proves a real deep clone).
const source_unchanged = src.nested.b.length === 2;
const now_ok = Number.isFinite(performance.now());
// timeOrigin must be a finite epoch-ms value, and timeOrigin + now() must
// land within a few seconds of Date.now() (guards the per-isolate seeding).
const origin = performance.timeOrigin;
const origin_ok = Number.isFinite(origin) && Math.abs(origin + performance.now() - Date.now()) < 5000;
return { deep_equal, source_unchanged, now_ok, origin_ok };
}
"#;
let r = run_ts(ts, &[], serde_json::json!({})).await;
assert_eq!(
unwrap_value(&r),
serde_json::json!({ "deep_equal": true, "source_unchanged": true, "now_ok": true, "origin_ok": true }),
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[ignore = "deno_core upgrade smoke; run with --ignored"]
async fn smoke_web_globals_construct() {
// `smoke_web_globals_are_wired` only checks the constructors are defined.
// Several are backed by deno_web ops (compression, message-port, URL
// pattern parsing) that a future bump could move out from under the
// still-defined constructor — it would pass the presence check but throw
// at `new`. Actually construct those here so that regression surfaces.
let ts = r#"
export async function main(): Promise<{ pathname: boolean; channel: boolean; gzip_ok: boolean }> {
const pat = new URLPattern({ pathname: "/books/:id" });
const pathname = pat.test("https://example.com/books/42");
const chan = new MessageChannel();
const channel = chan.port1 instanceof MessagePort && chan.port2 instanceof MessagePort;
// Round-trip "hi" through gzip compression then decompression.
const compressed = new Blob(["hi"]).stream().pipeThrough(new CompressionStream("gzip"));
const restored = compressed.pipeThrough(new DecompressionStream("gzip"));
const bytes = new Uint8Array(await new Response(restored).arrayBuffer());
const gzip_ok = new TextDecoder().decode(bytes) === "hi";
return { pathname, channel, gzip_ok };
}
"#;
let r = run_ts(ts, &[], serde_json::json!({})).await;
assert_eq!(
unwrap_value(&r),
serde_json::json!({ "pathname": true, "channel": true, "gzip_ok": true }),
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[ignore = "deno_core upgrade smoke; run with --ignored"]
async fn smoke_readable_stream_roundtrip() {
// ReadableStream + TextEncoderStream/TextDecoderStream: pipe an encode
// stream through and read chunks back. Exercises the streams surface
// (06_streams.js) that `res.body instanceof ReadableStream` relies on.
let ts = r#"
export async function main(): Promise<{ is_readable: boolean; text: string }> {
const rs = new ReadableStream<string>({
start(controller) {
controller.enqueue("hello ");
controller.enqueue("stream");
controller.close();
},
});
const is_readable = rs instanceof ReadableStream;
const decoded = rs
.pipeThrough(new TextEncoderStream())
.pipeThrough(new TextDecoderStream());
let text = "";
for await (const chunk of decoded) text += chunk;
return { is_readable, text };
}
"#;
let r = run_ts(ts, &[], serde_json::json!({})).await;
assert_eq!(
unwrap_value(&r),
serde_json::json!({ "is_readable": true, "text": "hello stream" }),
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[ignore = "deno_core upgrade smoke; run with --ignored"]
async fn smoke_web_crypto() {
// deno_crypto surface: the Web Crypto globals (`crypto`, `crypto.subtle`)
// that bring nativets to parity with the bun runner. Covers all three
// op families: getRandomValues (sync fill), randomUUID (RNG + formatting),
// and subtle.digest (async op returning an ArrayBuffer). The SHA-256 of
// "abc" is a fixed NIST vector, so a broken digest op fails the assert
// rather than silently returning garbage.
let ts = r#"
export async function main(): Promise<{ uuid: string; nonzero: boolean; sha256: string }> {
const uuid = crypto.randomUUID();
const buf = new Uint8Array(16);
crypto.getRandomValues(buf);
// A 16-byte CSPRNG fill returning all zeros is astronomically unlikely;
// a no-op/stub getRandomValues would leave the array zeroed.
const nonzero = buf.some((b) => b !== 0);
// "abc" as raw bytes — this test targets deno_crypto, so it avoids
// depending on deno_web's text encoding.
const data = new Uint8Array([0x61, 0x62, 0x63]);
const digest = await crypto.subtle.digest("SHA-256", data);
const sha256 = Array.from(new Uint8Array(digest))
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
// `crypto` is installed as an accessor; polyfill-style assignment must
// still work rather than throw for want of a setter.
(globalThis as any).crypto = globalThis.crypto;
return { uuid, nonzero, sha256 };
}
"#;
let r = run_ts(ts, &[], serde_json::json!({})).await;
let v = unwrap_value(&r);
// UUIDv4 shape: 8-4-4-4-12 hex, version nibble 4, variant nibble 8/9/a/b.
let uuid = v.get("uuid").and_then(|x| x.as_str()).unwrap_or("");
let re =
regex::Regex::new(r"^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$")
.unwrap();
assert!(
re.is_match(uuid),
"randomUUID did not match UUIDv4 shape: {uuid:?}"
);
assert_eq!(
v.get("nonzero"),
Some(&serde_json::json!(true)),
"getRandomValues left the buffer all zeros",
);
// Known SHA-256("abc") vector.
assert_eq!(
v.get("sha256").and_then(|x| x.as_str()),
Some("ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"),
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[ignore = "deno_core upgrade smoke; run with --ignored"]
async fn smoke_web_globals_edge_cases() {
// Broad functional sweep of every wired global — not just "defined" but
// "actually works". A constructor can be present yet throw at `new`/on call
// (an op moved by a deno bump, or a global dependency like DOMException not
// wired), which a presence check misses. Each check returns a bool; anything
// that isn't "ok" is reported in the failure message.
let ts = r#"
export async function main(): Promise<Record<string, string>> {
const out: Record<string, string> = {};
async function check(name: string, fn: () => any) {
try { out[name] = (await fn()) ? "ok" : "FAIL"; }
catch (e: any) { out[name] = "ERR: " + (e && e.message ? e.message : String(e)); }
}
// --- DOMException + AbortController/AbortSignal (needs the DOMException global) ---
await check("DOMException.construct", () => {
const e = new DOMException("nope", "AbortError");
return e.name === "AbortError" && e.message === "nope" && e instanceof DOMException;
});
await check("AbortController.abort_default_reason", () => {
const ac = new AbortController();
let fired = false;
ac.signal.addEventListener("abort", () => { fired = true; });
ac.abort(); // constructs a default DOMException("...", "AbortError")
return fired && ac.signal.aborted === true && ac.signal.reason?.name === "AbortError";
});
await check("AbortSignal.timeout", async () => {
const sig = AbortSignal.timeout(5);
await new Promise((r) => setTimeout(r, 30));
return sig.aborted === true && sig.reason?.name === "TimeoutError";
});
// --- TextEncoder / TextDecoder ---
await check("TextEncoder.encodeInto", () => {
const buf = new Uint8Array(3);
const { read, written } = new TextEncoder().encodeInto("abc", buf);
return read === 3 && written === 3 && buf[0] === 0x61;
});
await check("TextDecoder.utf16le", () =>
new TextDecoder("utf-16le").decode(new Uint8Array([0x41, 0x00])) === "A");
await check("TextDecoder.fatal_throws", () => {
try { new TextDecoder("utf-8", { fatal: true }).decode(new Uint8Array([0xff])); return false; }
catch { return true; }
});
// --- File ---
await check("File.props_and_read", async () => {
const f = new File(["hi"], "a.txt", { type: "text/plain", lastModified: 123 });
return f.name === "a.txt" && f.type === "text/plain" && f.lastModified === 123
&& f.size === 2 && (await f.text()) === "hi" && f instanceof Blob;
});
// --- Events ---
await check("EventTarget.dispatch_fires", () => {
const et = new EventTarget();
let got = "";
et.addEventListener("ping", (e: any) => { got = e.detail; });
et.dispatchEvent(new CustomEvent("ping", { detail: "pong" }));
return got === "pong";
});
// (A throwing EventTarget listener and reportError both surface the error
// asynchronously as an unhandled exception — matching bun, which exits
// non-zero — so they fail the script rather than a `check`; the dedicated
// smoke_eventtarget_throwing_listener_reports_original test covers that the
// ORIGINAL error is surfaced, not a masking one.)
await check("MessageEvent.data", () => new MessageEvent("m", { data: 42 }).data === 42);
await check("CloseEvent.code_reason", () => {
const e = new CloseEvent("close", { code: 1000, reason: "bye" });
return e.code === 1000 && e.reason === "bye";
});
await check("ErrorEvent.message", () => new ErrorEvent("error", { message: "boom" }).message === "boom");
// --- Streams ---
await check("ReadableStream.tee", async () => {
const rs = new ReadableStream<number>({ start(c) { c.enqueue(1); c.enqueue(2); c.close(); } });
const [a, b] = rs.tee();
const ra: number[] = []; for await (const x of a) ra.push(x);
const rb: number[] = []; for await (const x of b) rb.push(x);
return JSON.stringify(ra) === "[1,2]" && JSON.stringify(rb) === "[1,2]";
});
await check("ReadableStream.reader_cancel", async () => {
const rs = new ReadableStream<string>({ start(c) { c.enqueue("x"); } });
const rd = rs.getReader();
const { value } = await rd.read();
await rd.cancel();
return value === "x";
});
await check("WritableStream.write_close", async () => {
const chunks: string[] = [];
const ws = new WritableStream<string>({ write(c) { chunks.push(c); } });
const w = ws.getWriter();
await w.write("a"); await w.write("b"); await w.close();
return JSON.stringify(chunks) === '["a","b"]';
});
await check("TransformStream.identity", async () => {
const t = new TransformStream<string, string>();
const w = t.writable.getWriter(); w.write("hello"); w.close();
let o = ""; for await (const c of t.readable) o += c;
return o === "hello";
});
await check("Stream reader/controller instanceof globals", async () => {
// The reader/writer/controller constructors are exposed as globals for
// instanceof checks (bun parity). Obtain real instances and verify.
let ctrlOk = false;
const rs = new ReadableStream({
start(c: any) { ctrlOk = c instanceof ReadableStreamDefaultController; c.close(); },
});
const reader = rs.getReader();
const readerOk = reader instanceof ReadableStreamDefaultReader;
const ws = new WritableStream();
const writerOk = ws.getWriter() instanceof WritableStreamDefaultWriter;
return ctrlOk && readerOk && writerOk;
});
await check("ByteLengthQueuingStrategy", () => {
const s = new ByteLengthQueuingStrategy({ highWaterMark: 16 });
return s.highWaterMark === 16 && typeof s.size === "function";
});
await check("CountQueuingStrategy.in_stream", async () => {
const s = new CountQueuingStrategy({ highWaterMark: 1 });
const rs = new ReadableStream<number>({ start(c) { c.enqueue(7); c.close(); } }, s);
const rd = rs.getReader();
return (await rd.read()).value === 7 && s.highWaterMark === 1;
});
// --- URLPattern ---
await check("URLPattern.exec_groups", () => {
const p = new URLPattern({ pathname: "/users/:id" });
const m = p.exec("https://x.com/users/42");
return p.test("https://x.com/users/42") && m?.pathname.groups.id === "42";
});
// --- Compression (all three formats) ---
async function roundtrip(fmt: string): Promise<string> {
const comp = new Blob(["hello compression"]).stream().pipeThrough(new CompressionStream(fmt as any));
const decomp = comp.pipeThrough(new DecompressionStream(fmt as any));
return new TextDecoder().decode(new Uint8Array(await new Response(decomp).arrayBuffer()));
}
await check("Compression.gzip", async () => (await roundtrip("gzip")) === "hello compression");
await check("Compression.deflate", async () => (await roundtrip("deflate")) === "hello compression");
await check("Compression.deflate_raw", async () => (await roundtrip("deflate-raw")) === "hello compression");
// --- structuredClone edge cases ---
await check("structuredClone.Map", () => {
const c = structuredClone(new Map([["k", 1]]));
return c instanceof Map && c.get("k") === 1;
});
await check("structuredClone.Set", () => {
const c = structuredClone(new Set([1, 2]));
return c instanceof Set && c.has(2);
});
await check("structuredClone.Date", () => {
const c = structuredClone(new Date(0));
return c instanceof Date && c.getTime() === 0;
});
await check("structuredClone.TypedArray", () => {
const c = structuredClone(new Uint8Array([1, 2, 3]));
return c instanceof Uint8Array && c[1] === 2;
});
await check("structuredClone.circular", () => {
const o: any = {}; o.self = o;
const c = structuredClone(o);
return c.self === c;
});
await check("structuredClone.rejects_function", () => {
try { structuredClone(() => {}); return false; } catch { return true; }
});
await check("structuredClone.options_bag", () => {
const c = structuredClone({ a: 1 }, { transfer: [] });
return c.a === 1;
});
// --- performance ---
await check("performance.now_monotonic", () => {
const a = performance.now(); const b = performance.now();
return Number.isFinite(a) && b >= a;
});
await check("performance.mark_measure", () => {
performance.mark("m1");
performance.measure("meas", "m1");
return performance.getEntriesByType("measure").some((e: any) => e.name === "meas");
});
await check("performance.constructor_globals", () => {
// The Performance/PerformanceEntry/Mark/Measure constructors are exposed
// as globals (bun parity) for instanceof checks against real entries.
const mark = performance.mark("m2");
const meas = performance.measure("meas2", "m2");
return performance instanceof Performance
&& mark instanceof PerformanceMark && mark instanceof PerformanceEntry
&& meas instanceof PerformanceMeasure && meas instanceof PerformanceEntry;
});
await check("performance.toJSON_origin", () => {
const j: any = performance.toJSON();
return Number.isFinite(j.timeOrigin);
});
// --- MessageChannel / MessagePort (async delivery, guarded by timeout) ---
await check("MessagePort.postMessage", async () => {
const mc = new MessageChannel();
const got = await Promise.race([
new Promise<string>((res) => {
mc.port2.onmessage = (e: any) => res(JSON.stringify(e.data));
mc.port1.postMessage({ hello: "world" });
}),
new Promise<string>((res) => setTimeout(() => res("TIMEOUT"), 3000)),
]);
return got === '{"hello":"world"}';
});
// --- Web Crypto works alongside the other wired globals ---
await check("crypto.getRandomValues", () => {
const buf = new Uint8Array(16);
crypto.getRandomValues(buf);
return buf.some((b) => b !== 0);
});
await check("crypto.subtle.digest", async () => {
const d = await crypto.subtle.digest("SHA-256", new Uint8Array([0x61, 0x62, 0x63]));
return new Uint8Array(d)[0] === 0xba; // SHA-256("abc") starts with 0xba
});
return out;
}
"#;
let r = run_ts(ts, &[], serde_json::json!({})).await;
let v = unwrap_value(&r);
let obj = v.as_object().expect("expected an object result");
let failures: Vec<String> = obj
.iter()
.filter(|(_, val)| val.as_str() != Some("ok"))
.map(|(name, val)| format!("{name} => {}", val.as_str().unwrap_or("?")))
.collect();
assert!(
failures.is_empty(),
"edge-case checks that did not pass ({} of {}):\n{}",
failures.len(),
obj.len(),
failures.join("\n"),
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[ignore = "deno_core upgrade smoke; run with --ignored"]
async fn smoke_eventtarget_throwing_listener_reports_original() {
// Invariant: globalThis is a functional EventTarget, so deno_web's
// reportException has a valid saved global dispatch target. An uncaught
// EventTarget-listener error is therefore surfaced with its original message
// as an async unhandled exception (matching bun, which exits non-zero), not
// replaced by a masking "Illegal invocation"/undefined-reference error.
let ts = r#"
export async function main(): Promise<void> {
const et = new EventTarget();
et.addEventListener("boom", () => { throw new Error("wm_listener_marker"); });
et.dispatchEvent(new Event("boom"));
// Give the async unhandled-exception report a tick to fire.
await new Promise((r) => setTimeout(r, 20));
}
"#;
let r = run_ts(ts, &[], serde_json::json!({})).await;
let err = r
.result
.expect_err("a throwing listener should surface an error");
assert!(
err.contains("wm_listener_marker"),
"the original listener error was lost: {err}",
);
assert!(
!err.contains("Illegal invocation") && !err.to_lowercase().contains("undefined"),
"dispatchEvent surfaced a masking error instead of the original: {err}",
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[ignore = "deno_core upgrade smoke; run with --ignored"]
async fn smoke_report_error_both_call_forms() {
// reportError must work both unqualified and as a property of globalThis.
// The property form goes through a receiver check (`this === globalThis_`),
// which only passes because globalThis is the saved EventTarget reference;
// otherwise it throws "Illegal invocation". Both forms report the error as an
// async unhandled exception (matching bun), so the script errors with the
// original message rather than a masking one.
for (label, call) in [
("bare", "reportError(new Error(\"wm_report_marker\"));"),
(
"property",
"globalThis.reportError(new Error(\"wm_report_marker\"));",
),
] {
let ts = format!(
r#"
export async function main(): Promise<void> {{
{call}
await new Promise((r) => setTimeout(r, 20));
}}
"#
);
let r = run_ts(&ts, &[], serde_json::json!({})).await;
let err = r
.result
.expect_err(&format!("{label} reportError should surface an error"));
assert!(
err.contains("wm_report_marker"),
"{label} reportError lost the original error: {err}",
);
assert!(
!err.contains("Illegal invocation"),
"{label} reportError threw Illegal invocation: {err}",
);
}
}
// -----------------------------------------------------------------------------
// Network — actually exercise deno_fetch end-to-end. Skip in air-gapped CI
// with `--skip smoke_net_`.
// -----------------------------------------------------------------------------
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[ignore = "deno_core upgrade smoke (network); run with --ignored"]
async fn smoke_net_fetch_example_com() {
// example.com is one of the most stable hosts on the internet and
// returns a tiny known-text body, so we can both assert "fetch works"
// and "the response body parses correctly through deno_fetch".
let ts = r#"
export async function main(): Promise<{ status: number; has_marker: boolean }> {
const r = await fetch("https://example.com/");
const body = await r.text();
return { status: r.status, has_marker: body.includes("Example Domain") };
}
"#;
let r = run_ts(ts, &[], serde_json::json!({})).await;
let v = unwrap_value(&r);
assert_eq!(v.get("status").and_then(|x| x.as_u64()), Some(200));
assert_eq!(v.get("has_marker"), Some(&serde_json::json!(true)));
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[ignore = "deno_core upgrade smoke (network); run with --ignored"]
async fn smoke_net_fetch_json_and_headers() {
// httpbin.org/anything echoes request metadata back as JSON, so we
// can verify: deno_fetch sends custom headers, parses JSON response,
// and propagates query params end-to-end.
let ts = r#"
export async function main(): Promise<{ ua: string; arg: string }> {
const r = await fetch("https://httpbin.org/anything?nativets=ok", {
headers: { "x-windmill-smoke": "1" },
});
if (!r.ok) throw new Error(`status ${r.status}`);
const j: any = await r.json();
return {
ua: j.headers["X-Windmill-Smoke"] ?? "",
arg: j.args.nativets ?? "",
};
}
"#;
let r = run_ts(ts, &[], serde_json::json!({})).await;
let v = unwrap_value(&r);
assert_eq!(v.get("ua").and_then(|x| x.as_str()), Some("1"));
assert_eq!(v.get("arg").and_then(|x| x.as_str()), Some("ok"));
}