use super::{run, run_to_completion};
use serde_json::json;
use tsrun::{
InternalModule, Interpreter, InterpreterConfig, JsString, JsValue, OrderId, OrderResponse,
RuntimeValue, StepResult, api, create_eval_internal_module, value::PropertyKey,
};
const GLOBALS_SOURCE: &str = r#"
import { order } from "tsrun:host";
// sleep(ms) - Returns Promise that resolves after delay
globalThis.sleep = async function(ms: number): Promise<void> {
await order({ type: "sleep", delay: ms });
};
// fetch(url, options?) - Returns Promise with response
globalThis.fetch = async function(url: string, options?: {
method?: string;
body?: string;
headers?: Record<string, string>;
}): Promise<any> {
return await order({
type: "fetch",
url: url,
method: options?.method || "GET",
body: options?.body,
headers: options?.headers
});
};
// readFile(path) - Returns Promise with file content
globalThis.readFile = async function(path: string): Promise<string> {
return await order({ type: "readFile", path: path });
};
// writeFile(path, content) - Returns Promise when complete
globalThis.writeFile = async function(path: string, content: string): Promise<string> {
return await order({ type: "writeFile", path: path, content: content });
};
"#;
fn create_test_interp() -> Interpreter {
let config = InterpreterConfig {
internal_modules: vec![
create_eval_internal_module(),
InternalModule::source("eval:globals", GLOBALS_SOURCE),
],
..Default::default()
};
let interp = Interpreter::with_config(config);
let gc_threshold = std::env::var("GC_THRESHOLD")
.ok()
.and_then(|s| s.parse::<usize>().ok())
.unwrap_or(1);
interp.set_gc_threshold(gc_threshold);
interp
}
fn get_string_prop(obj: &JsValue, key: &str) -> Option<String> {
if let JsValue::Object(o) = obj
&& let Some(JsValue::String(s)) = o
.borrow()
.get_property(&PropertyKey::String(JsString::from(key)))
{
return Some(s.to_string());
}
None
}
fn get_number_prop(obj: &JsValue, key: &str) -> Option<f64> {
if let JsValue::Object(o) = obj
&& let Some(JsValue::Number(n)) = o
.borrow()
.get_property(&PropertyKey::String(JsString::from(key)))
{
return Some(n);
}
None
}
#[allow(clippy::expect_used)]
fn run_with_globals(interp: &mut Interpreter, script: &str) -> StepResult {
let full_script = format!(
r#"import "eval:globals";
{}"#,
script
);
run(interp, &full_script, None).expect("eval should not fail")
}
#[test]
fn test_promise_then_callback_closure() {
let mut interp = create_test_interp();
let result = run_with_globals(
&mut interp,
r#"
import { order } from "tsrun:host";
// Create a captured variable in module scope
let captured = "initial";
// Call request to get a Promise from host, then attach .then() callback
const promise = order({ type: "getPromise" });
await promise.then(() => {
captured = "modified";
});
// Return the captured value (should be "modified" after callback ran)
captured;
"#,
);
let StepResult::Suspended { pending, .. } = result else {
panic!("Expected Suspended for request");
};
assert_eq!(pending.len(), 1);
assert_eq!(
get_string_prop(pending[0].payload.value(), "type"),
Some("getPromise".into())
);
let promise = api::create_promise(&mut interp);
interp.fulfill_orders(vec![OrderResponse {
id: pending[0].id,
result: Ok(RuntimeValue::unguarded(promise.value().clone())),
}]);
let result2 = run_to_completion(&mut interp).unwrap();
let StepResult::Suspended { .. } = result2 else {
panic!("Expected Suspended waiting for Promise resolution");
};
api::resolve_promise(
&mut interp,
&promise,
RuntimeValue::unguarded(JsValue::Undefined),
)
.unwrap();
let result3 = run_to_completion(&mut interp).unwrap();
let StepResult::Complete(value) = result3 else {
panic!("Expected Complete after Promise resolution");
};
assert_eq!(*value, JsValue::String("modified".into()));
}
#[test]
fn test_promise_then_callback_nested_closure() {
let mut interp = create_test_interp();
let result = run_with_globals(
&mut interp,
r#"
import { order } from "tsrun:host";
// Module-level variable
let moduleVar = "initial";
// Function that returns a callback capturing moduleVar
function createCallback(): () => void {
return () => {
moduleVar = "modified";
};
}
// Call createCallback to get a callback, then use it in .then()
const cb = createCallback();
const promise = order({ type: "getPromise" });
await promise.then(cb);
moduleVar;
"#,
);
let StepResult::Suspended { pending, .. } = result else {
panic!("Expected Suspended for request");
};
assert_eq!(pending.len(), 1);
let promise = api::create_promise(&mut interp);
interp.fulfill_orders(vec![OrderResponse {
id: pending[0].id,
result: Ok(RuntimeValue::unguarded(promise.value().clone())),
}]);
let result2 = run_to_completion(&mut interp).unwrap();
let StepResult::Suspended { .. } = result2 else {
panic!("Expected Suspended waiting for Promise");
};
api::resolve_promise(
&mut interp,
&promise,
RuntimeValue::unguarded(JsValue::Undefined),
)
.unwrap();
let result3 = run_to_completion(&mut interp).unwrap();
let StepResult::Complete(value) = result3 else {
panic!("Expected Complete after Promise resolution");
};
assert_eq!(*value, JsValue::String("modified".into()));
}
#[test]
fn test_cross_module_closure_simple() {
let config = InterpreterConfig {
internal_modules: vec![
create_eval_internal_module(),
InternalModule::source(
"eval:timer-module",
r#"
import { order } from "tsrun:host";
// Module-level variable
let moduleState: string = "initial";
// Function that gets a Promise from request and attaches .then() callback
// The callback captures moduleState from this module
export function runWithCallback(): Promise<void> {
const promise = order({ type: "getPromise" });
return promise.then(() => {
moduleState = "from-callback";
});
}
export function getState(): string {
return moduleState;
}
"#,
),
],
..Default::default()
};
let mut interp = Interpreter::with_config(config);
let gc_threshold = std::env::var("GC_THRESHOLD")
.ok()
.and_then(|s| s.parse::<usize>().ok())
.unwrap_or(1);
interp.set_gc_threshold(gc_threshold);
let result = run(
&mut interp,
r#"
import { runWithCallback, getState } from "eval:timer-module";
// Call the function that uses .then()
await runWithCallback();
// Check the state
getState();
"#,
None,
)
.expect("eval should work");
let StepResult::Suspended { pending, .. } = result else {
panic!("Expected Suspended for request");
};
assert_eq!(pending.len(), 1);
let promise = api::create_promise(&mut interp);
interp.fulfill_orders(vec![OrderResponse {
id: pending[0].id,
result: Ok(RuntimeValue::unguarded(promise.value().clone())),
}]);
let result2 = run_to_completion(&mut interp).unwrap();
let StepResult::Suspended { .. } = result2 else {
panic!("Expected Suspended waiting for Promise");
};
api::resolve_promise(
&mut interp,
&promise,
RuntimeValue::unguarded(JsValue::Undefined),
)
.unwrap();
let result3 = run_to_completion(&mut interp).unwrap();
let StepResult::Complete(value) = result3 else {
panic!("Expected Complete after Promise resolution");
};
assert_eq!(*value, JsValue::String("from-callback".into()));
}
#[test]
fn test_cross_module_nested_closure() {
let config = InterpreterConfig {
internal_modules: vec![
create_eval_internal_module(),
InternalModule::source(
"eval:helper-module",
r#"
import { order } from "tsrun:host";
// Module-level state
let moduleState: string = "module-initial";
// This function gets a Promise from request and attaches .then() callback that
// accesses BOTH function-local variable AND module variable
export function wrapWithThen(userCallback: () => void): Promise<void> {
const functionLocal = "function-local";
const promise = order({ type: "getPromise" });
return promise.then(() => {
// Access module variable
moduleState = "from-then";
// Access function-local variable
const combined = functionLocal + "+" + moduleState;
// Call user callback
userCallback();
});
}
export function getState(): string {
return moduleState;
}
"#,
),
],
..Default::default()
};
let mut interp = Interpreter::with_config(config);
let gc_threshold = std::env::var("GC_THRESHOLD")
.ok()
.and_then(|s| s.parse::<usize>().ok())
.unwrap_or(1);
interp.set_gc_threshold(gc_threshold);
let result = run(
&mut interp,
r#"
import { wrapWithThen, getState } from "eval:helper-module";
let userResult = "not-called";
await wrapWithThen(() => {
userResult = "user-callback-ran";
});
// Return both the module state and user result
getState() + " / " + userResult;
"#,
None,
)
.expect("eval should work");
let StepResult::Suspended { pending, .. } = result else {
panic!("Expected Suspended for request");
};
assert_eq!(pending.len(), 1);
let promise = api::create_promise(&mut interp);
interp.fulfill_orders(vec![OrderResponse {
id: pending[0].id,
result: Ok(RuntimeValue::unguarded(promise.value().clone())),
}]);
let result2 = run_to_completion(&mut interp).unwrap();
let StepResult::Suspended { .. } = result2 else {
panic!("Expected Suspended waiting for Promise");
};
api::resolve_promise(
&mut interp,
&promise,
RuntimeValue::unguarded(JsValue::Undefined),
)
.unwrap();
let result3 = run_to_completion(&mut interp).unwrap();
let StepResult::Complete(value) = result3 else {
panic!("Expected Complete after Promise resolution");
};
assert_eq!(
*value,
JsValue::String("from-then / user-callback-ran".into())
);
}
#[test]
fn test_debug_closure_gc() {
let config = InterpreterConfig {
internal_modules: vec![create_eval_internal_module()],
..Default::default()
};
let mut interp = Interpreter::with_config(config);
interp.set_gc_threshold(1);
let result = run(
&mut interp,
r#"
import { order } from "tsrun:host";
let state: string = "initial";
// Wrapper function WITH a local variable
function wrapper(): Promise<void> {
const local = "local"; // This triggers call env creation
const promise = order({ type: "getPromise" });
return promise.then(() => {
state = "modified-" + local;
});
}
await wrapper();
state;
"#,
None,
)
.expect("eval should work");
let StepResult::Suspended { pending, .. } = result else {
panic!("Expected Suspended for request");
};
let promise = api::create_promise(&mut interp);
interp.fulfill_orders(vec![OrderResponse {
id: pending[0].id,
result: Ok(RuntimeValue::unguarded(promise.value().clone())),
}]);
let result2 = run_to_completion(&mut interp).unwrap();
let StepResult::Suspended { .. } = result2 else {
panic!("Expected Suspended waiting for Promise");
};
api::resolve_promise(
&mut interp,
&promise,
RuntimeValue::unguarded(JsValue::Undefined),
)
.unwrap();
let result3 = run_to_completion(&mut interp).unwrap();
let StepResult::Complete(value) = result3 else {
panic!("Expected Complete after Promise resolution");
};
assert_eq!(*value, JsValue::String("modified-local".into()));
}
#[test]
fn test_sleep_basic() {
let mut interp = create_test_interp();
let result = run_with_globals(
&mut interp,
r#"
let result = "before";
await sleep(100);
result = "after";
result;
"#,
);
let StepResult::Suspended { pending, .. } = result else {
panic!("Expected Suspended for sleep()");
};
assert_eq!(pending.len(), 1);
let payload = pending[0].payload.value();
assert_eq!(get_string_prop(payload, "type"), Some("sleep".into()));
interp.fulfill_orders(vec![OrderResponse {
id: pending[0].id,
result: Ok(RuntimeValue::unguarded(JsValue::Undefined)),
}]);
let result2 = run_to_completion(&mut interp).unwrap();
let StepResult::Complete(value) = result2 else {
panic!("Expected Complete after sleep");
};
assert_eq!(*value, JsValue::String("after".into()));
}
#[test]
fn test_sleep_sequential() {
let mut interp = create_test_interp();
let result = run_with_globals(
&mut interp,
r#"
let count = 0;
await sleep(10);
count += 1;
await sleep(20);
count += 1;
await sleep(30);
count += 1;
count;
"#,
);
let StepResult::Suspended { pending, .. } = result else {
panic!("Expected Suspended for first sleep");
};
assert_eq!(pending.len(), 1);
interp.fulfill_orders(vec![OrderResponse {
id: pending[0].id,
result: Ok(RuntimeValue::unguarded(JsValue::Undefined)),
}]);
let result = run_to_completion(&mut interp).unwrap();
let StepResult::Suspended { pending, .. } = result else {
panic!("Expected Suspended for second sleep");
};
assert_eq!(pending.len(), 1);
interp.fulfill_orders(vec![OrderResponse {
id: pending[0].id,
result: Ok(RuntimeValue::unguarded(JsValue::Undefined)),
}]);
let result = run_to_completion(&mut interp).unwrap();
let StepResult::Suspended { pending, .. } = result else {
panic!("Expected Suspended for third sleep");
};
assert_eq!(pending.len(), 1);
interp.fulfill_orders(vec![OrderResponse {
id: pending[0].id,
result: Ok(RuntimeValue::unguarded(JsValue::Undefined)),
}]);
let result = run_to_completion(&mut interp).unwrap();
let StepResult::Complete(value) = result else {
panic!("Expected Complete after all sleeps");
};
assert_eq!(*value, JsValue::Number(3.0));
}
#[test]
fn test_fetch_get_basic() {
let mut interp = create_test_interp();
let result = run_with_globals(
&mut interp,
r#"
const response = await fetch("https://api.example.com/users/1");
response.name;
"#,
);
match result {
StepResult::Suspended { pending, .. } => {
assert_eq!(pending.len(), 1);
let payload = pending[0].payload.value();
assert_eq!(get_string_prop(payload, "type"), Some("fetch".into()));
assert_eq!(
get_string_prop(payload, "url"),
Some("https://api.example.com/users/1".into())
);
assert_eq!(get_string_prop(payload, "method"), Some("GET".into()));
let mock_response = api::create_response_object(
&mut interp,
&json!({
"id": 1,
"name": "John",
"email": "john@example.com"
}),
)
.unwrap();
let response = OrderResponse {
id: pending[0].id,
result: Ok(mock_response),
};
interp.fulfill_orders(vec![response]);
let result2 = run_to_completion(&mut interp).unwrap();
match result2 {
StepResult::Complete(value) => {
assert_eq!(*value, JsValue::String("John".into()));
}
_ => panic!("Expected Complete after fulfillment"),
}
}
_ => panic!("Expected Suspended"),
}
}
#[test]
fn test_fetch_post_with_body() {
let mut interp = create_test_interp();
let result = run_with_globals(
&mut interp,
r#"
const response = await fetch("https://api.example.com/users", {
method: "POST",
body: JSON.stringify({ name: "Jane" }),
headers: { "Content-Type": "application/json" }
});
response.id;
"#,
);
match result {
StepResult::Suspended { pending, .. } => {
assert_eq!(pending.len(), 1);
let payload = pending[0].payload.value();
assert_eq!(get_string_prop(payload, "type"), Some("fetch".into()));
assert_eq!(
get_string_prop(payload, "url"),
Some("https://api.example.com/users".into())
);
assert_eq!(get_string_prop(payload, "method"), Some("POST".into()));
assert_eq!(
get_string_prop(payload, "body"),
Some(r#"{"name":"Jane"}"#.into())
);
let mock_response =
api::create_response_object(&mut interp, &json!({ "id": 42, "name": "Jane" }))
.unwrap();
let response = OrderResponse {
id: pending[0].id,
result: Ok(mock_response),
};
interp.fulfill_orders(vec![response]);
let result2 = run_to_completion(&mut interp).unwrap();
match result2 {
StepResult::Complete(value) => {
assert_eq!(*value, JsValue::Number(42.0));
}
_ => panic!("Expected Complete after fulfillment"),
}
}
_ => panic!("Expected Suspended"),
}
}
#[test]
fn test_fetch_parallel() {
let mut interp = create_test_interp();
let result = run_with_globals(
&mut interp,
r#"
const user = await fetch("/users/1");
const posts = await fetch("/posts?userId=1");
user.name + " has " + posts.length + " posts";
"#,
);
let StepResult::Suspended { pending, .. } = result else {
panic!("Expected Suspended for first fetch");
};
assert_eq!(pending.len(), 1);
assert_eq!(
get_string_prop(pending[0].payload.value(), "url"),
Some("/users/1".into())
);
let user_response =
api::create_response_object(&mut interp, &json!({ "name": "John" })).unwrap();
interp.fulfill_orders(vec![OrderResponse {
id: pending[0].id,
result: Ok(user_response),
}]);
let result2 = run_to_completion(&mut interp).unwrap();
let StepResult::Suspended { pending, .. } = result2 else {
panic!("Expected Suspended for second fetch, got {:?}", result2);
};
assert_eq!(pending.len(), 1);
assert_eq!(
get_string_prop(pending[0].payload.value(), "url"),
Some("/posts?userId=1".into())
);
let posts_response =
api::create_response_object(&mut interp, &json!([{ "id": 1 }, { "id": 2 }, { "id": 3 }]))
.unwrap();
interp.fulfill_orders(vec![OrderResponse {
id: pending[0].id,
result: Ok(posts_response),
}]);
let result3 = run_to_completion(&mut interp).unwrap();
let StepResult::Complete(value) = result3 else {
panic!("Expected Complete after fulfillment, got {:?}", result3);
};
assert_eq!(*value, JsValue::String("John has 3 posts".into()));
}
#[test]
fn test_read_file_basic() {
let mut interp = create_test_interp();
let result = run_with_globals(
&mut interp,
r#"
const content = await readFile("/config.txt");
content;
"#,
);
match result {
StepResult::Suspended { pending, .. } => {
assert_eq!(pending.len(), 1);
let payload = pending[0].payload.value();
assert_eq!(get_string_prop(payload, "type"), Some("readFile".into()));
assert_eq!(get_string_prop(payload, "path"), Some("/config.txt".into()));
let response = OrderResponse {
id: pending[0].id,
result: Ok(RuntimeValue::unguarded(JsValue::String(
"Hello, World!".into(),
))),
};
interp.fulfill_orders(vec![response]);
let result2 = run_to_completion(&mut interp).unwrap();
match result2 {
StepResult::Complete(value) => {
assert_eq!(*value, JsValue::String("Hello, World!".into()));
}
_ => panic!("Expected Complete after fulfillment"),
}
}
_ => panic!("Expected Suspended"),
}
}
#[test]
fn test_read_file_json_parse() {
let mut interp = create_test_interp();
let result = run_with_globals(
&mut interp,
r#"
const raw = await readFile("/config.json");
const config = JSON.parse(raw);
config.database.host;
"#,
);
match result {
StepResult::Suspended { pending, .. } => {
assert_eq!(pending.len(), 1);
let json_content = r#"{"database": {"host": "localhost", "port": 5432}}"#;
let response = OrderResponse {
id: pending[0].id,
result: Ok(RuntimeValue::unguarded(JsValue::String(
json_content.into(),
))),
};
interp.fulfill_orders(vec![response]);
let result2 = run_to_completion(&mut interp).unwrap();
match result2 {
StepResult::Complete(value) => {
assert_eq!(*value, JsValue::String("localhost".into()));
}
_ => panic!("Expected Complete after fulfillment"),
}
}
_ => panic!("Expected Suspended"),
}
}
#[test]
fn test_write_file_basic() {
let mut interp = create_test_interp();
let result = run_with_globals(
&mut interp,
r#"
await writeFile("/output.txt", "Hello from TypeScript!");
"written";
"#,
);
match result {
StepResult::Suspended { pending, .. } => {
assert_eq!(pending.len(), 1);
let payload = pending[0].payload.value();
assert_eq!(get_string_prop(payload, "type"), Some("writeFile".into()));
assert_eq!(get_string_prop(payload, "path"), Some("/output.txt".into()));
assert_eq!(
get_string_prop(payload, "content"),
Some("Hello from TypeScript!".into())
);
let response = OrderResponse {
id: pending[0].id,
result: Ok(RuntimeValue::unguarded(JsValue::Undefined)),
};
interp.fulfill_orders(vec![response]);
let result2 = run_to_completion(&mut interp).unwrap();
match result2 {
StepResult::Complete(value) => {
assert_eq!(*value, JsValue::String("written".into()));
}
_ => panic!("Expected Complete after fulfillment"),
}
}
_ => panic!("Expected Suspended"),
}
}
#[test]
fn test_read_write_roundtrip() {
let mut interp = create_test_interp();
let result = run_with_globals(
&mut interp,
r#"
const data = "test data 12345";
await writeFile("/temp.txt", data);
const read = await readFile("/temp.txt");
read === data;
"#,
);
let StepResult::Suspended { pending, .. } = result else {
panic!("Expected Suspended for writeFile");
};
assert_eq!(pending.len(), 1);
assert_eq!(
get_string_prop(pending[0].payload.value(), "type"),
Some("writeFile".into())
);
let file_content = get_string_prop(pending[0].payload.value(), "content").unwrap_or_default();
assert_eq!(file_content, "test data 12345");
interp.fulfill_orders(vec![OrderResponse {
id: pending[0].id,
result: Ok(RuntimeValue::unguarded(JsValue::Undefined)),
}]);
let result = run_to_completion(&mut interp).unwrap();
let StepResult::Suspended { pending, .. } = result else {
panic!("Expected Suspended for readFile");
};
assert_eq!(pending.len(), 1);
assert_eq!(
get_string_prop(pending[0].payload.value(), "type"),
Some("readFile".into())
);
interp.fulfill_orders(vec![OrderResponse {
id: pending[0].id,
result: Ok(RuntimeValue::unguarded(JsValue::String(
file_content.into(),
))),
}]);
let result = run_to_completion(&mut interp).unwrap();
let StepResult::Complete(value) = result else {
panic!("Expected Complete after roundtrip");
};
assert_eq!(*value, JsValue::Boolean(true));
}
#[test]
fn test_config_generation_workflow() {
let mut interp = create_test_interp();
let result = run_with_globals(
&mut interp,
r#"
// 1. Read local config
const configRaw = await readFile("/app.json");
const config = JSON.parse(configRaw);
// 2. Fetch remote data
const apiData = await fetch(config.apiUrl + "/settings");
// 3. Generate manifest
const manifest = {
name: config.name,
version: config.version,
settings: apiData,
};
// 4. Write output
await writeFile("/manifest.json", JSON.stringify(manifest));
manifest.name + " v" + manifest.version;
"#,
);
let StepResult::Suspended { pending, .. } = result else {
panic!("Expected Suspended for readFile");
};
assert_eq!(pending.len(), 1);
assert_eq!(
get_string_prop(pending[0].payload.value(), "type"),
Some("readFile".into())
);
let config_json =
r#"{"name": "MyApp", "version": "1.0.0", "apiUrl": "https://api.example.com"}"#;
interp.fulfill_orders(vec![OrderResponse {
id: pending[0].id,
result: Ok(RuntimeValue::unguarded(JsValue::String(config_json.into()))),
}]);
let result = run_to_completion(&mut interp).unwrap();
let StepResult::Suspended { pending, .. } = result else {
panic!("Expected Suspended for fetch");
};
assert_eq!(pending.len(), 1);
assert_eq!(
get_string_prop(pending[0].payload.value(), "type"),
Some("fetch".into())
);
assert_eq!(
get_string_prop(pending[0].payload.value(), "url"),
Some("https://api.example.com/settings".into())
);
let api_response =
api::create_response_object(&mut interp, &json!({ "theme": "dark", "language": "en" }))
.unwrap();
interp.fulfill_orders(vec![OrderResponse {
id: pending[0].id,
result: Ok(api_response),
}]);
let result = run_to_completion(&mut interp).unwrap();
let StepResult::Suspended { pending, .. } = result else {
panic!("Expected Suspended for writeFile");
};
assert_eq!(pending.len(), 1);
assert_eq!(
get_string_prop(pending[0].payload.value(), "type"),
Some("writeFile".into())
);
assert_eq!(
get_string_prop(pending[0].payload.value(), "path"),
Some("/manifest.json".into())
);
let manifest_content = get_string_prop(pending[0].payload.value(), "content").unwrap();
assert!(manifest_content.contains("MyApp"));
assert!(manifest_content.contains("1.0.0"));
assert!(manifest_content.contains("dark"));
interp.fulfill_orders(vec![OrderResponse {
id: pending[0].id,
result: Ok(RuntimeValue::unguarded(JsValue::Undefined)),
}]);
let result = run_to_completion(&mut interp).unwrap();
let StepResult::Complete(value) = result else {
panic!("Expected Complete after workflow");
};
assert_eq!(*value, JsValue::String("MyApp v1.0.0".into()));
}
#[test]
fn test_host_create_and_resolve_promise() {
let mut interp = create_test_interp();
let host_promise = api::create_promise(&mut interp);
let result = run_with_globals(
&mut interp,
r#"
import { order } from "tsrun:host";
// request returns a PendingOrder, await it to get the host's Promise
const promise = await order({ type: "getHostPromise" });
// Then await the Promise to get the actual value
const result = await promise;
"Got: " + result;
"#,
);
let StepResult::Suspended { pending, .. } = result else {
panic!("Expected Suspended waiting for order");
};
assert_eq!(pending.len(), 1);
assert_eq!(
get_string_prop(pending[0].payload.value(), "type"),
Some("getHostPromise".into())
);
interp.fulfill_orders(vec![OrderResponse {
id: pending[0].id,
result: Ok(RuntimeValue::unguarded(host_promise.value().clone())),
}]);
let result2 = run_to_completion(&mut interp).unwrap();
let StepResult::Suspended { .. } = result2 else {
panic!("Expected Suspended waiting for Promise to be resolved");
};
let value = RuntimeValue::unguarded(JsValue::String("Hello from host!".into()));
api::resolve_promise(&mut interp, &host_promise, value).unwrap();
let result3 = run_to_completion(&mut interp).unwrap();
let StepResult::Complete(final_value) = result3 else {
panic!("Expected Complete after resolving Promise");
};
assert_eq!(
*final_value,
JsValue::String("Got: Hello from host!".into())
);
}
#[test]
fn test_host_create_and_reject_promise() {
let mut interp = create_test_interp();
let host_promise = api::create_promise(&mut interp);
let result = run_with_globals(
&mut interp,
r#"
import { order } from "tsrun:host";
try {
// Get the Promise from host and await it
const promise = await order({ type: "getHostPromise" });
const result = await promise;
"Success: " + result;
} catch (e) {
"Error: " + e;
}
"#,
);
let StepResult::Suspended { pending, .. } = result else {
panic!("Expected Suspended");
};
interp.fulfill_orders(vec![OrderResponse {
id: pending[0].id,
result: Ok(RuntimeValue::unguarded(host_promise.value().clone())),
}]);
let result2 = run_to_completion(&mut interp).unwrap();
let StepResult::Suspended { .. } = result2 else {
panic!("Expected Suspended waiting for Promise");
};
let reason = RuntimeValue::unguarded(JsValue::String("Something went wrong".into()));
api::reject_promise(&mut interp, &host_promise, reason).unwrap();
let result3 = run_to_completion(&mut interp).unwrap();
let StepResult::Complete(final_value) = result3 else {
panic!("Expected Complete after rejecting Promise");
};
assert_eq!(
*final_value,
JsValue::String("Error: Something went wrong".into())
);
}
#[test]
fn test_host_promise_immediate_resolve() {
let mut interp = create_test_interp();
let result = run_with_globals(
&mut interp,
r#"
import { order } from "tsrun:host";
// Get the Promise from host and await it
const promise = await order({ type: "quickResolve" });
const result = await promise;
"Result: " + result;
"#,
);
let StepResult::Suspended { pending, .. } = result else {
panic!("Expected Suspended");
};
let promise = api::create_promise(&mut interp);
let value = RuntimeValue::unguarded(JsValue::Number(42.0));
api::resolve_promise(&mut interp, &promise, value).unwrap();
interp.fulfill_orders(vec![OrderResponse {
id: pending[0].id,
result: Ok(RuntimeValue::unguarded(promise.value().clone())),
}]);
let result2 = run_to_completion(&mut interp).unwrap();
let StepResult::Complete(final_value) = result2 else {
panic!("Expected Complete");
};
assert_eq!(*final_value, JsValue::String("Result: 42".into()));
}
#[test]
fn test_concurrent_fetch_with_promise_all() {
let mut interp = create_test_interp();
let users_promise = api::create_promise(&mut interp);
let posts_promise = api::create_promise(&mut interp);
let result = run_with_globals(
&mut interp,
r#"
import { order } from "tsrun:host";
// Each request suspends immediately - host sees one at a time
const usersPromise = order({ type: "fetch", url: "/users" });
const postsPromise = order({ type: "fetch", url: "/posts" });
// await Promise.all waits for both host Promises
const [users, posts] = await Promise.all([usersPromise, postsPromise]);
`${users.count} users, ${posts.count} posts`;
"#,
);
let StepResult::Suspended { pending, .. } = result else {
panic!("Expected Suspended at first request");
};
assert_eq!(pending.len(), 1, "First order pending");
assert_eq!(
get_string_prop(pending[0].payload.value(), "url"),
Some("/users".into())
);
let users_order_id = pending[0].id;
interp.fulfill_orders(vec![OrderResponse {
id: users_order_id,
result: Ok(RuntimeValue::unguarded(users_promise.value().clone())),
}]);
let result = run_to_completion(&mut interp).unwrap();
let StepResult::Suspended { pending, .. } = result else {
panic!("Expected Suspended at second request, got {:?}", result);
};
assert_eq!(pending.len(), 1, "Second order pending");
assert_eq!(
get_string_prop(pending[0].payload.value(), "url"),
Some("/posts".into())
);
let posts_order_id = pending[0].id;
interp.fulfill_orders(vec![OrderResponse {
id: posts_order_id,
result: Ok(RuntimeValue::unguarded(posts_promise.value().clone())),
}]);
let result = run_to_completion(&mut interp).unwrap();
match &result {
StepResult::Suspended { pending, .. } if pending.is_empty() => {
}
StepResult::Done => {
}
other => {
panic!(
"Expected Done or Suspended while awaiting Promises, got {:?}",
other
);
}
}
let users_data = api::create_response_object(&mut interp, &json!({ "count": 5 })).unwrap();
let posts_data = api::create_response_object(&mut interp, &json!({ "count": 10 })).unwrap();
api::resolve_promise(&mut interp, &users_promise, users_data).unwrap();
api::resolve_promise(&mut interp, &posts_promise, posts_data).unwrap();
let result = run_to_completion(&mut interp).unwrap();
let StepResult::Complete(value) = result else {
panic!("Expected Complete after both resolved, got {:?}", result);
};
assert_eq!(*value, JsValue::String("5 users, 10 posts".into()));
}
#[test]
fn test_promise_race_first_wins() {
let mut interp = create_test_interp();
let fast_promise = api::create_promise(&mut interp);
let slow_promise = api::create_promise(&mut interp);
let result = run_with_globals(
&mut interp,
r#"
import { order } from "tsrun:host";
// Each request suspends, host returns a Promise
const fast = order({ type: "fetch", server: "fast" });
const slow = order({ type: "fetch", server: "slow" });
// Race: first to resolve wins
const winner = await Promise.race([fast, slow]);
winner.server;
"#,
);
let StepResult::Suspended { pending, .. } = result else {
panic!("Expected Suspended for first order");
};
assert_eq!(pending.len(), 1, "One order at a time");
let first_order_id = pending[0].id;
assert_eq!(
get_string_prop(pending[0].payload.value(), "server"),
Some("fast".into())
);
interp.fulfill_orders(vec![OrderResponse {
id: first_order_id,
result: Ok(RuntimeValue::unguarded(fast_promise.value().clone())),
}]);
let result = run_to_completion(&mut interp).unwrap();
let StepResult::Suspended { pending, .. } = result else {
panic!("Expected Suspended for second order");
};
assert_eq!(pending.len(), 1, "One order at a time");
let second_order_id = pending[0].id;
assert_eq!(
get_string_prop(pending[0].payload.value(), "server"),
Some("slow".into())
);
interp.fulfill_orders(vec![OrderResponse {
id: second_order_id,
result: Ok(RuntimeValue::unguarded(slow_promise.value().clone())),
}]);
let result = run_to_completion(&mut interp).unwrap();
let StepResult::Suspended { pending, .. } = result else {
panic!("Expected Suspended for Promise.race");
};
assert!(
pending.is_empty(),
"No orders pending, just awaiting Promises"
);
let fast_data = api::create_response_object(&mut interp, &json!({ "server": "fast" })).unwrap();
api::resolve_promise(&mut interp, &fast_promise, fast_data).unwrap();
let result = run_to_completion(&mut interp).unwrap();
let StepResult::Complete(value) = result else {
panic!("Expected Complete after fulfillment, got {:?}", result);
};
assert_eq!(*value, JsValue::String("fast".into()));
}
#[test]
fn test_promise_race_second_wins() {
let mut interp = create_test_interp();
let a_promise = api::create_promise(&mut interp);
let b_promise = api::create_promise(&mut interp);
let result = run_with_globals(
&mut interp,
r#"
import { order } from "tsrun:host";
// Each request suspends, host returns a Promise
const a = order({ type: "fetch", id: "a" });
const b = order({ type: "fetch", id: "b" });
const winner = await Promise.race([a, b]);
winner.winner;
"#,
);
let StepResult::Suspended { pending, .. } = result else {
panic!("Expected Suspended for first order");
};
assert_eq!(pending.len(), 1, "One order at a time");
let first_order_id = pending[0].id;
assert_eq!(
get_string_prop(pending[0].payload.value(), "id"),
Some("a".into())
);
interp.fulfill_orders(vec![OrderResponse {
id: first_order_id,
result: Ok(RuntimeValue::unguarded(a_promise.value().clone())),
}]);
let result = run_to_completion(&mut interp).unwrap();
let StepResult::Suspended { pending, .. } = result else {
panic!("Expected Suspended for second order");
};
assert_eq!(pending.len(), 1, "One order at a time");
let second_order_id = pending[0].id;
assert_eq!(
get_string_prop(pending[0].payload.value(), "id"),
Some("b".into())
);
interp.fulfill_orders(vec![OrderResponse {
id: second_order_id,
result: Ok(RuntimeValue::unguarded(b_promise.value().clone())),
}]);
let result = run_to_completion(&mut interp).unwrap();
let StepResult::Suspended { pending, .. } = result else {
panic!("Expected Suspended for Promise.race");
};
assert!(
pending.is_empty(),
"No orders pending, just awaiting Promises"
);
let b_data = api::create_response_object(&mut interp, &json!({ "winner": "B" })).unwrap();
api::resolve_promise(&mut interp, &b_promise, b_data).unwrap();
let result = run_to_completion(&mut interp).unwrap();
let StepResult::Complete(value) = result else {
panic!("Expected Complete, got {:?}", result);
};
assert_eq!(*value, JsValue::String("B".into()));
}
#[test]
fn test_concurrent_with_partial_failure() {
let mut interp = create_test_interp();
let ok_promise = api::create_promise(&mut interp);
let fail_promise = api::create_promise(&mut interp);
let result = run_with_globals(
&mut interp,
r#"
import { order } from "tsrun:host";
// Each request suspends, host returns a Promise
const ok = order({ type: "fetch", url: "/ok" });
const fail = order({ type: "fetch", url: "/fail" });
try {
const results = await Promise.all([ok, fail]);
"Success: " + JSON.stringify(results);
} catch (e) {
"Error: " + e;
}
"#,
);
let StepResult::Suspended { pending, .. } = result else {
panic!("Expected Suspended for first order");
};
assert_eq!(pending.len(), 1, "One order at a time");
let first_order_id = pending[0].id;
assert_eq!(
get_string_prop(pending[0].payload.value(), "url"),
Some("/ok".into())
);
interp.fulfill_orders(vec![OrderResponse {
id: first_order_id,
result: Ok(RuntimeValue::unguarded(ok_promise.value().clone())),
}]);
let result = run_to_completion(&mut interp).unwrap();
let StepResult::Suspended { pending, .. } = result else {
panic!("Expected Suspended for second order");
};
assert_eq!(pending.len(), 1, "One order at a time");
let second_order_id = pending[0].id;
assert_eq!(
get_string_prop(pending[0].payload.value(), "url"),
Some("/fail".into())
);
interp.fulfill_orders(vec![OrderResponse {
id: second_order_id,
result: Ok(RuntimeValue::unguarded(fail_promise.value().clone())),
}]);
let result = run_to_completion(&mut interp).unwrap();
let StepResult::Suspended { pending, .. } = result else {
panic!("Expected Suspended for Promise.all");
};
assert!(
pending.is_empty(),
"No orders pending, just awaiting Promises"
);
let ok_data = RuntimeValue::unguarded(JsValue::String("OK".into()));
api::resolve_promise(&mut interp, &ok_promise, ok_data).unwrap();
let error_value = RuntimeValue::unguarded(JsValue::String("Network error".into()));
api::reject_promise(&mut interp, &fail_promise, error_value).unwrap();
let result = run_to_completion(&mut interp).unwrap();
let StepResult::Complete(value) = result else {
panic!("Expected Complete with error, got {:?}", result);
};
assert_eq!(*value, JsValue::String("Error: Network error".into()));
}
#[test]
fn test_concurrent_three_way_race() {
let mut interp = create_test_interp();
let promise1 = api::create_promise(&mut interp);
let promise2 = api::create_promise(&mut interp);
let promise3 = api::create_promise(&mut interp);
let result = run_with_globals(
&mut interp,
r#"
import { order } from "tsrun:host";
// Get three Promises from host (each request suspends)
const p1 = order({ id: 1 });
const p2 = order({ id: 2 });
const p3 = order({ id: 3 });
// Race: first to resolve wins
const winner = await Promise.race([p1, p2, p3]);
"Winner: " + winner;
"#,
);
let StepResult::Suspended { pending, .. } = result else {
panic!("Expected Suspended for first order");
};
assert_eq!(pending.len(), 1, "One order at a time");
assert_eq!(get_number_prop(pending[0].payload.value(), "id"), Some(1.0));
interp.fulfill_orders(vec![OrderResponse {
id: pending[0].id,
result: Ok(RuntimeValue::unguarded(promise1.value().clone())),
}]);
let result = run_to_completion(&mut interp).unwrap();
let StepResult::Suspended { pending, .. } = result else {
panic!("Expected Suspended for second order");
};
assert_eq!(pending.len(), 1, "One order at a time");
assert_eq!(get_number_prop(pending[0].payload.value(), "id"), Some(2.0));
interp.fulfill_orders(vec![OrderResponse {
id: pending[0].id,
result: Ok(RuntimeValue::unguarded(promise2.value().clone())),
}]);
let result = run_to_completion(&mut interp).unwrap();
let StepResult::Suspended { pending, .. } = result else {
panic!("Expected Suspended for third order");
};
assert_eq!(pending.len(), 1, "One order at a time");
assert_eq!(get_number_prop(pending[0].payload.value(), "id"), Some(3.0));
interp.fulfill_orders(vec![OrderResponse {
id: pending[0].id,
result: Ok(RuntimeValue::unguarded(promise3.value().clone())),
}]);
let result = run_to_completion(&mut interp).unwrap();
let StepResult::Suspended { pending, .. } = result else {
panic!("Expected Suspended for Promise.race");
};
assert!(
pending.is_empty(),
"No orders pending, just awaiting Promises"
);
api::resolve_promise(
&mut interp,
&promise2,
RuntimeValue::unguarded(JsValue::Number(2.0)),
)
.unwrap();
let result = run_to_completion(&mut interp).unwrap();
let StepResult::Complete(value) = result else {
panic!("Expected Complete after resolving winner, got {:?}", result);
};
assert_eq!(*value, JsValue::String("Winner: 2".into()));
}
#[test]
fn test_concurrent_chained_operations() {
let mut interp = create_test_interp();
let user_promise = api::create_promise(&mut interp);
let profile_promise = api::create_promise(&mut interp);
let result = run_with_globals(
&mut interp,
r#"
import { order } from "tsrun:host";
// Each request suspends, host returns a Promise
const userPromise = order({ type: "getUser" });
const profilePromise = order({ type: "getProfile" });
// Chain transformations on each
const user = userPromise.then(u => ({ ...u, type: "user" }));
const profile = profilePromise.then(p => ({ ...p, type: "profile" }));
// Wait for both transformed results
const [u, p] = await Promise.all([user, profile]);
`${u.name} (${u.type}), ${p.bio} (${p.type})`;
"#,
);
let StepResult::Suspended { pending, .. } = result else {
panic!("Expected Suspended for first order");
};
assert_eq!(pending.len(), 1, "One order at a time");
let first_order_id = pending[0].id;
assert_eq!(
get_string_prop(pending[0].payload.value(), "type"),
Some("getUser".into())
);
interp.fulfill_orders(vec![OrderResponse {
id: first_order_id,
result: Ok(RuntimeValue::unguarded(user_promise.value().clone())),
}]);
let result = run_to_completion(&mut interp).unwrap();
let StepResult::Suspended { pending, .. } = result else {
panic!("Expected Suspended for second order");
};
assert_eq!(pending.len(), 1, "One order at a time");
let second_order_id = pending[0].id;
assert_eq!(
get_string_prop(pending[0].payload.value(), "type"),
Some("getProfile".into())
);
interp.fulfill_orders(vec![OrderResponse {
id: second_order_id,
result: Ok(RuntimeValue::unguarded(profile_promise.value().clone())),
}]);
let result = run_to_completion(&mut interp).unwrap();
let StepResult::Suspended { pending, .. } = result else {
panic!("Expected Suspended for Promise.all");
};
assert!(
pending.is_empty(),
"No orders pending, just awaiting Promises"
);
let user_data = api::create_response_object(&mut interp, &json!({ "name": "Alice" })).unwrap();
let profile_data =
api::create_response_object(&mut interp, &json!({ "bio": "Developer" })).unwrap();
api::resolve_promise(&mut interp, &user_promise, user_data).unwrap();
api::resolve_promise(&mut interp, &profile_promise, profile_data).unwrap();
let result = run_to_completion(&mut interp).unwrap();
let StepResult::Complete(value) = result else {
panic!("Expected Complete, got {:?}", result);
};
assert_eq!(
*value,
JsValue::String("Alice (user), Developer (profile)".into())
);
}
#[test]
fn test_promise_race_cancels_losing_order() {
let mut interp = create_test_interp();
let order1_id = OrderId(1);
let order2_id = OrderId(2);
let promise1 = api::create_order_promise(&mut interp, order1_id);
let promise2 = api::create_order_promise(&mut interp, order2_id);
let result = run_with_globals(
&mut interp,
r#"
import { order } from "tsrun:host";
const p1 = order({ id: 1 });
const p2 = order({ id: 2 });
const winner = await Promise.race([p1, p2]);
winner;
"#,
);
let StepResult::Suspended { pending, .. } = result else {
panic!("Expected Suspended for first order");
};
assert_eq!(pending.len(), 1, "One order at a time");
assert_eq!(get_number_prop(pending[0].payload.value(), "id"), Some(1.0));
interp.fulfill_orders(vec![OrderResponse {
id: pending[0].id,
result: Ok(RuntimeValue::unguarded(promise1.value().clone())),
}]);
let result = run_to_completion(&mut interp).unwrap();
let StepResult::Suspended { pending, .. } = result else {
panic!("Expected Suspended for second order");
};
assert_eq!(pending.len(), 1, "One order at a time");
assert_eq!(get_number_prop(pending[0].payload.value(), "id"), Some(2.0));
interp.fulfill_orders(vec![OrderResponse {
id: pending[0].id,
result: Ok(RuntimeValue::unguarded(promise2.value().clone())),
}]);
let result = run_to_completion(&mut interp).unwrap();
let StepResult::Suspended { pending, .. } = result else {
panic!("Expected Suspended for Promise.race");
};
assert!(
pending.is_empty(),
"No orders pending, just awaiting Promises"
);
api::resolve_promise(
&mut interp,
&promise1,
RuntimeValue::unguarded(JsValue::String("first".into())),
)
.unwrap();
let result = run_to_completion(&mut interp).unwrap();
match result {
StepResult::Complete(value) => {
assert_eq!(*value, JsValue::String("first".into()));
}
StepResult::Suspended { cancelled, .. } => {
assert!(
cancelled.contains(&order2_id),
"Expected order2_id ({:?}) in cancelled list: {:?}",
order2_id,
cancelled
);
let result = run_to_completion(&mut interp).unwrap();
if let StepResult::Complete(value) = result {
assert_eq!(*value, JsValue::String("first".into()));
}
}
_ => panic!("Unexpected result"),
}
}
#[test]
fn test_promise_race_second_wins_cancels_first() {
let mut interp = create_test_interp();
let order1_id = OrderId(1);
let order2_id = OrderId(2);
let promise1 = api::create_order_promise(&mut interp, order1_id);
let promise2 = api::create_order_promise(&mut interp, order2_id);
let result = run_with_globals(
&mut interp,
r#"
import { order } from "tsrun:host";
const p1 = order({ id: 1 });
const p2 = order({ id: 2 });
const winner = await Promise.race([p1, p2]);
winner;
"#,
);
let StepResult::Suspended { pending, .. } = result else {
panic!("Expected Suspended for first order");
};
assert_eq!(pending.len(), 1, "One order at a time");
assert_eq!(get_number_prop(pending[0].payload.value(), "id"), Some(1.0));
interp.fulfill_orders(vec![OrderResponse {
id: pending[0].id,
result: Ok(RuntimeValue::unguarded(promise1.value().clone())),
}]);
let result = run_to_completion(&mut interp).unwrap();
let StepResult::Suspended { pending, .. } = result else {
panic!("Expected Suspended for second order");
};
assert_eq!(pending.len(), 1, "One order at a time");
assert_eq!(get_number_prop(pending[0].payload.value(), "id"), Some(2.0));
interp.fulfill_orders(vec![OrderResponse {
id: pending[0].id,
result: Ok(RuntimeValue::unguarded(promise2.value().clone())),
}]);
let result = run_to_completion(&mut interp).unwrap();
let StepResult::Suspended { pending, .. } = result else {
panic!("Expected Suspended for Promise.race");
};
assert!(
pending.is_empty(),
"No orders pending, just awaiting Promises"
);
api::resolve_promise(
&mut interp,
&promise2,
RuntimeValue::unguarded(JsValue::String("second".into())),
)
.unwrap();
let result = run_to_completion(&mut interp).unwrap();
match result {
StepResult::Complete(value) => {
assert_eq!(*value, JsValue::String("second".into()));
}
StepResult::Suspended { cancelled, .. } => {
assert!(
cancelled.contains(&order1_id),
"Expected order1_id ({:?}) in cancelled list: {:?}",
order1_id,
cancelled
);
}
_ => panic!("Unexpected result"),
}
}
#[test]
fn test_promise_rejection_signals_cancelled_order() {
let mut interp = create_test_interp();
let order_id = OrderId(999);
let promise = api::create_order_promise(&mut interp, order_id);
let result = run_with_globals(
&mut interp,
r#"
import { order } from "tsrun:host";
try {
const p = order({ type: "will_fail" });
await p;
"resolved";
} catch (e) {
"caught: " + e;
}
"#,
);
let StepResult::Suspended { pending, .. } = result else {
panic!("Expected Suspended");
};
interp.fulfill_orders(vec![OrderResponse {
id: pending[0].id,
result: Ok(RuntimeValue::unguarded(promise.value().clone())),
}]);
let result = run_to_completion(&mut interp).unwrap();
let StepResult::Suspended { .. } = result else {
panic!("Expected Suspended");
};
api::reject_promise(
&mut interp,
&promise,
RuntimeValue::unguarded(JsValue::String("error".into())),
)
.unwrap();
let result = run_to_completion(&mut interp).unwrap();
match result {
StepResult::Complete(value) => {
assert_eq!(*value, JsValue::String("caught: error".into()));
}
StepResult::Suspended { cancelled, .. } => {
assert!(
cancelled.contains(&order_id),
"Expected order_id ({:?}) in cancelled list: {:?}",
order_id,
cancelled
);
let result = run_to_completion(&mut interp).unwrap();
if let StepResult::Complete(value) = result {
assert_eq!(*value, JsValue::String("caught: error".into()));
}
}
_ => panic!("Unexpected result"),
}
}
#[test]
fn test_three_way_race_cancels_two_losers() {
let mut interp = create_test_interp();
let order1_id = OrderId(1);
let order2_id = OrderId(2);
let order3_id = OrderId(3);
let promise1 = api::create_order_promise(&mut interp, order1_id);
let promise2 = api::create_order_promise(&mut interp, order2_id);
let promise3 = api::create_order_promise(&mut interp, order3_id);
let result = run_with_globals(
&mut interp,
r#"
import { order } from "tsrun:host";
const p1 = order({ id: 1 });
const p2 = order({ id: 2 });
const p3 = order({ id: 3 });
const winner = await Promise.race([p1, p2, p3]);
"Winner: " + winner;
"#,
);
let StepResult::Suspended { pending, .. } = result else {
panic!("Expected Suspended for first order");
};
assert_eq!(pending.len(), 1, "One order at a time");
assert_eq!(get_number_prop(pending[0].payload.value(), "id"), Some(1.0));
interp.fulfill_orders(vec![OrderResponse {
id: pending[0].id,
result: Ok(RuntimeValue::unguarded(promise1.value().clone())),
}]);
let result = run_to_completion(&mut interp).unwrap();
let StepResult::Suspended { pending, .. } = result else {
panic!("Expected Suspended for second order");
};
assert_eq!(pending.len(), 1, "One order at a time");
assert_eq!(get_number_prop(pending[0].payload.value(), "id"), Some(2.0));
interp.fulfill_orders(vec![OrderResponse {
id: pending[0].id,
result: Ok(RuntimeValue::unguarded(promise2.value().clone())),
}]);
let result = run_to_completion(&mut interp).unwrap();
let StepResult::Suspended { pending, .. } = result else {
panic!("Expected Suspended for third order");
};
assert_eq!(pending.len(), 1, "One order at a time");
assert_eq!(get_number_prop(pending[0].payload.value(), "id"), Some(3.0));
interp.fulfill_orders(vec![OrderResponse {
id: pending[0].id,
result: Ok(RuntimeValue::unguarded(promise3.value().clone())),
}]);
let result = run_to_completion(&mut interp).unwrap();
let StepResult::Suspended { pending, .. } = result else {
panic!("Expected Suspended for Promise.race");
};
assert!(
pending.is_empty(),
"No orders pending, just awaiting Promises"
);
api::resolve_promise(
&mut interp,
&promise2,
RuntimeValue::unguarded(JsValue::Number(2.0)),
)
.unwrap();
let result = run_to_completion(&mut interp).unwrap();
let cancelled = match &result {
StepResult::Suspended { cancelled, .. } => cancelled.clone(),
StepResult::Complete(_) => {
Vec::new()
}
_ => panic!("Unexpected result"),
};
assert!(
cancelled.contains(&order1_id) || cancelled.is_empty(),
"Expected order1_id in cancelled: {:?}",
cancelled
);
assert!(
cancelled.contains(&order3_id) || cancelled.is_empty(),
"Expected order3_id in cancelled: {:?}",
cancelled
);
assert!(
!cancelled.contains(&order2_id),
"Winner's order should not be cancelled: {:?}",
cancelled
);
}
#[test]
fn test_concurrent_mixed_order_types() {
let mut interp = create_test_interp();
let fetch_promise = api::create_promise(&mut interp);
let timeout_promise = api::create_promise(&mut interp);
let result = run_with_globals(
&mut interp,
r#"
import { order } from "tsrun:host";
// Issue two orders with different types
const fetchPromise = order({ type: "fetch", url: "/api/users" });
const timeoutPromise = order({ type: "timeout", ms: 100 });
// Wait for both concurrently
const [data, _] = await Promise.all([fetchPromise, timeoutPromise]);
data.name;
"#,
);
let StepResult::Suspended { pending, .. } = result else {
panic!("Expected Suspended for fetch order");
};
assert_eq!(pending.len(), 1);
let payload = pending[0].payload.value();
let order_type = get_string_prop(payload, "type");
assert_eq!(order_type, Some("fetch".into()));
interp.fulfill_orders(vec![OrderResponse {
id: pending[0].id,
result: Ok(RuntimeValue::unguarded(fetch_promise.value().clone())),
}]);
let result = run_to_completion(&mut interp).unwrap();
let StepResult::Suspended { pending, .. } = result else {
panic!("Expected Suspended for timeout order");
};
assert_eq!(pending.len(), 1);
let payload = pending[0].payload.value();
let order_type = get_string_prop(payload, "type");
assert_eq!(order_type, Some("timeout".into()));
let ms = get_number_prop(payload, "ms");
assert_eq!(ms, Some(100.0));
interp.fulfill_orders(vec![OrderResponse {
id: pending[0].id,
result: Ok(RuntimeValue::unguarded(timeout_promise.value().clone())),
}]);
let result = run_to_completion(&mut interp).unwrap();
let StepResult::Suspended { pending, .. } = result else {
panic!("Expected Suspended for Promise.all");
};
assert!(pending.is_empty());
api::resolve_promise(
&mut interp,
&timeout_promise,
RuntimeValue::unguarded(JsValue::Undefined),
)
.unwrap();
let fetch_data = api::create_response_object(&mut interp, &json!({ "name": "Alice" })).unwrap();
api::resolve_promise(&mut interp, &fetch_promise, fetch_data).unwrap();
let result = run_to_completion(&mut interp).unwrap();
let StepResult::Complete(value) = result else {
panic!("Expected Complete");
};
assert_eq!(*value, JsValue::String("Alice".into()));
}
#[test]
fn test_unknown_order_type_rejection() {
let mut interp = create_test_interp();
let result = run_with_globals(
&mut interp,
r#"
import { order } from "tsrun:host";
try {
const result = await order({ type: "unknown_type" });
"success: " + result;
} catch (e) {
"error: " + e;
}
"#,
);
let StepResult::Suspended { pending, .. } = result else {
panic!("Expected Suspended");
};
let order_type = get_string_prop(pending[0].payload.value(), "type");
assert_eq!(order_type, Some("unknown_type".into()));
interp.fulfill_orders(vec![OrderResponse {
id: pending[0].id,
result: Err(tsrun::JsError::type_error(
"Unknown order type: unknown_type",
)),
}]);
let result = run_to_completion(&mut interp).unwrap();
let StepResult::Complete(value) = result else {
panic!("Expected Complete with error");
};
let result_str = value.as_str().expect("Expected string result");
assert!(
result_str.contains("Unknown order type"),
"Expected error message, got: {}",
result_str
);
}