import { describe, expect, test } from "bun:test" import { Deferred, Effect, Schema } from "effect" import { CodeMode, Tool, toolError } from "../src/index.js" // Wave 5 acceptance suite: first-class promise values. Un-awaited tool calls start eagerly on // supervised fibers, `await` settles them, Promise.all/allSettled/race/resolve/reject are // ordinary functions over arbitrary arrays mixing promises and plain values, and // .then/.catch/.finally chain reactions onto any promise. type Trace = { starts: Array active: number maxActive: number completed: number interrupted: number } const makeTrace = (): Trace => ({ starts: [], active: 0, maxActive: 0, completed: 0, interrupted: 0 }) /** * Deterministic tool set: ordering and interruption are structural, never temporal. * * - `echo` settles immediately with its id. * - `gated` blocks until `open` releases the same id. Tool fibers start eagerly at the * call site, so several gated calls are provably live at once before any `open` runs. * - `pending` never settles; tests assert its interruption instead of racing a timer. * * Real clocks remain only in the wall-clock timeout tests (`timeoutMs`, `stubborn` * cleanup), where elapsed time is the behavior under test. */ const echoTool = (trace: Trace) => Tool.make({ description: "Echo an id immediately", input: Schema.Struct({ id: Schema.Number }), output: Schema.Number, run: ({ id }) => Effect.sync(() => { trace.starts.push(id) trace.completed += 1 return id }), }) const gatedTool = (trace: Trace, gate: (id: number) => Deferred.Deferred) => Tool.make({ description: "Echo an id once its gate opens", input: Schema.Struct({ id: Schema.Number }), output: Schema.Number, run: ({ id }) => Effect.gen(function* () { trace.starts.push(id) trace.active += 1 trace.maxActive = Math.max(trace.maxActive, trace.active) yield* Deferred.await(gate(id)) trace.active -= 1 trace.completed += 1 return id }).pipe( Effect.onInterrupt(() => Effect.sync(() => { trace.active -= 1 trace.interrupted += 1 }), ), ), }) const openTool = (gate: (id: number) => Deferred.Deferred) => Tool.make({ description: "Open the gate for an id", input: Schema.Struct({ id: Schema.Number }), output: Schema.Boolean, run: ({ id }) => Deferred.succeed(gate(id), undefined), }) const pendingTool = (trace: Trace) => Tool.make({ description: "Never settle", input: Schema.Struct({ id: Schema.Number }), output: Schema.Number, run: ({ id }) => Effect.gen(function* () { trace.starts.push(id) trace.active += 1 trace.maxActive = Math.max(trace.maxActive, trace.active) return yield* Effect.never }).pipe( Effect.onInterrupt(() => Effect.sync(() => { trace.active -= 1 trace.interrupted += 1 }), ), ), }) const failingTool = Tool.make({ description: "Always refuse", input: Schema.Struct({}), output: Schema.String, run: () => Effect.fail(toolError("Lookup refused")), }) const interruptedTool = Tool.make({ description: "Interrupt this call", input: Schema.Struct({}), output: Schema.String, run: () => Effect.interrupt, }) const completedTool = (trace: Trace) => Tool.make({ description: "Return the number of completed calls", input: Schema.Struct({}), output: Schema.Number, run: () => Effect.succeed(trace.completed), }) /** Never settles, and holds interruption cleanup for `cleanupMs` so completion cleanup can outlast a timeout. */ const stubbornTool = (trace: Trace) => Tool.make({ description: "Never settle; clean up slowly when interrupted", input: Schema.Struct({ cleanupMs: Schema.Number }), output: Schema.Number, run: ({ cleanupMs }) => Effect.never.pipe( Effect.onInterrupt(() => Effect.andThen( Effect.sleep(cleanupMs), Effect.sync(() => { trace.interrupted += 1 }), ), ), ), }) const run = ( code: string, options: { trace?: Trace; limits?: CodeMode.ExecutionLimits } = {}, ): Promise => { const trace = options.trace ?? makeTrace() const gates = new Map>() const gate = (id: number): Deferred.Deferred => { const existing = gates.get(id) if (existing) return existing const created = Deferred.makeUnsafe() gates.set(id, created) return created } return Effect.runPromise( CodeMode.execute({ tools: { host: { echo: echoTool(trace), gated: gatedTool(trace, gate), open: openTool(gate), pending: pendingTool(trace), fail: failingTool, interrupt: interruptedTool, completed: completedTool(trace), stubborn: stubbornTool(trace), }, }, code, ...(options.limits ? { limits: options.limits } : {}), }), ) } const value = async (code: string, options: { trace?: Trace; limits?: CodeMode.ExecutionLimits } = {}) => { const result = await run(code, options) if (!result.ok) throw new Error(`expected success, got ${result.error.kind}: ${result.error.message}`) return result.value } const error = async (code: string, options: { trace?: Trace; limits?: CodeMode.ExecutionLimits } = {}) => { const result = await run(code, options) if (result.ok) throw new Error(`expected failure, got value ${JSON.stringify(result.value)}`) return result.error } describe("first-class promise values", () => { test("async functions return promises with isolated concurrent invocations", async () => { expect( await value(` const load = async (id) => { const result = await tools.host.echo({ id }) return [id, result] } const first = load(1) const second = load(2) return [first instanceof Promise, second instanceof Promise, await Promise.all([first, second])] `), ).toEqual([ true, true, [ [1, 1], [2, 2], ], ]) }) test("async function errors reject instead of throwing at the call site", async () => { expect( await value(` const fail = async () => { throw new Error("boom") } const promise = fail() try { await promise return "no" } catch (error) { return error.message } `), ).toBe("boom") }) test("an un-awaited tool call starts eagerly, in call order, before any await", async () => { const trace = makeTrace() const result = await value( ` const a = tools.host.gated({ id: 1 }) const b = tools.host.gated({ id: 2 }) await tools.host.open({ id: 1 }) await tools.host.open({ id: 2 }) const rb = await b const ra = await a return [ra, rb] `, { trace }, ) expect(result).toEqual([1, 2]) expect(trace.starts).toEqual([1, 2]) // Both calls overlapped even though they were awaited sequentially. expect(trace.maxActive).toBeGreaterThan(1) }) test("awaiting the same promise twice settles once and never re-runs the call", async () => { const result = await run(` const p = tools.host.echo({ id: 7 }) const x = await p const y = await p return [x, y] `) expect(result.ok).toBe(true) if (!result.ok) return expect(result.value).toEqual([7, 7]) expect(result.toolCalls).toStrictEqual([{ name: "host.echo" }]) }) test("await of a non-promise value passes it through unchanged", async () => { expect(await value(`return await 42`)).toBe(42) expect(await value(`const x = await "s"; return x`)).toBe("s") expect(await value(`return await null`)).toBeNull() expect(await value(`return (await [1, 2]).length`)).toBe(2) }) test("returning an un-awaited tool call resolves it (async-function return semantics)", async () => { expect(await value(`return tools.host.echo({ id: 9 })`)).toBe(9) }) test("typeof a promise is 'object', and console.log renders it sensibly", async () => { const result = await run(` const p = Promise.resolve(1) console.log(p) return typeof p `) expect(result.ok).toBe(true) if (!result.ok) return expect(result.value).toBe("object") expect(result.logs).toStrictEqual(["[Promise (await it to get its value)]"]) }) test("an awaited failure is catchable exactly like a synchronous throw", async () => { const result = await run(` const p = tools.host.fail({}) try { await p return "no" } catch (e) { return e.message } `) expect(result.ok).toBe(true) if (!result.ok) return expect(result.value).toBe("Lookup refused") expect(result.warnings).toBeUndefined() }) test("a fire-and-forget call is interrupted when the program returns", async () => { const trace = makeTrace() const result = await run( ` tools.host.pending({ id: 1 }) return "done" `, { trace }, ) expect(result.ok).toBe(true) if (!result.ok) return expect(result.value).toBe("done") expect(result.warnings).toBeUndefined() expect(trace.completed).toBe(0) expect(trace.interrupted).toBe(1) }) test("a never-awaited failing call preserves the result and reports the rejection", async () => { const result = await run(` tools.host.fail({}) return "done" `) expect(result.ok).toBe(true) if (!result.ok) return expect(result.value).toBe("done") expect(result.warnings).toStrictEqual([ { kind: "ToolFailure", message: "Unhandled rejection from an un-awaited promise: Lookup refused" }, ]) expect(Schema.decodeUnknownSync(CodeMode.Result)(JSON.parse(JSON.stringify(result)))).toStrictEqual(result) }) test("a never-awaited failing async function is reported with a successful result", async () => { const result = await run(` const fail = async () => { throw new Error("boom") } fail() return "done" `) expect(result.ok).toBe(true) if (!result.ok) return expect(result.value).toBe("done") expect(result.warnings).toStrictEqual([ { kind: "ExecutionFailure", message: "Unhandled rejection from an un-awaited promise: Uncaught: boom" }, ]) }) test("output truncation bounds warning diagnostics with an in-band marker", async () => { const result = await run( ` for (let i = 0; i < 100; i += 1) Promise.reject(new Error("x".repeat(1_000))) return "done" `, { limits: { maxOutputBytes: 64 } }, ) expect(result.ok).toBe(true) if (!result.ok) return expect(result.truncated).toBe(true) expect(result.warnings).toStrictEqual([ { kind: "Truncated", message: "100 additional warnings omitted by the output limit." }, ]) }) test("a budget-consuming value does not starve warnings", async () => { const result = await run( ` Promise.reject(new Error("boom")) return "x".repeat(500) `, { limits: { maxOutputBytes: 128 } }, ) expect(result.ok).toBe(true) if (!result.ok) return expect(result.truncated).toBe(true) expect(typeof result.value).toBe("string") expect(result.warnings).toStrictEqual([ { kind: "ExecutionFailure", message: "Unhandled rejection from an un-awaited promise: Uncaught: boom" }, ]) }) test("an un-awaited async function's pending chain is interrupted at the return", async () => { const trace = makeTrace() const result = await run( ` const run = async () => { await tools.host.pending({ id: 1 }) tools.host.fail({}) } run() return "done" `, { trace }, ) expect(result.ok).toBe(true) if (!result.ok) return expect(result.value).toBe("done") expect(result.warnings).toBeUndefined() expect(trace.starts).toEqual([1]) expect(trace.completed).toBe(0) expect(trace.interrupted).toBe(1) }) test("reports every unhandled rejection in promise creation order", async () => { const result = await run(` Promise.reject(new Error("first")) tools.host.fail({}) Promise.reject(new Error("third")) return "done" `) expect(result.ok).toBe(true) if (!result.ok) return expect(result.warnings).toStrictEqual([ { kind: "ExecutionFailure", message: "Unhandled rejection from an un-awaited promise: Uncaught: first" }, { kind: "ToolFailure", message: "Unhandled rejection from an un-awaited promise: Lookup refused" }, { kind: "ExecutionFailure", message: "Unhandled rejection from an un-awaited promise: Uncaught: third" }, ]) }) test("orders an async function rejection before promises created inside its body", async () => { const result = await run(` const outer = async () => { Promise.reject(new Error("inner")) throw new Error("outer") } outer() return "done" `) expect(result.ok).toBe(true) if (!result.ok) return expect(result.warnings).toStrictEqual([ { kind: "ExecutionFailure", message: "Unhandled rejection from an un-awaited promise: Uncaught: outer" }, { kind: "ExecutionFailure", message: "Unhandled rejection from an un-awaited promise: Uncaught: inner" }, ]) }) test("un-awaited interruptions settle without becoming rejections", async () => { const result = await run(` tools.host.interrupt({}) Promise.all([tools.host.interrupt({})]) return "done" `) expect(result.ok).toBe(true) if (!result.ok) return expect(result.value).toBe("done") expect(result.warnings).toBeUndefined() }) test("a fatal program error cancels outstanding work without reporting unhandled rejections", async () => { const trace = makeTrace() const result = await run( ` tools.host.pending({ id: 1 }) throw new Error("boom") `, { trace }, ) expect(result.ok).toBe(false) if (result.ok) return expect(result.error.message).toBe("Uncaught: boom") expect("warnings" in result).toBe(false) expect(trace.completed).toBe(0) expect(trace.interrupted).toBe(1) }) test("async-function promises remain owned by the execution after the function returns", async () => { const trace = makeTrace() expect( await value( ` const launch = async () => { tools.host.pending({ id: 1 }) Promise.all([tools.host.pending({ id: 2 })]) return "returned" } return await launch() `, { trace }, ), ).toBe("returned") // Both calls outlive launch() itself - they belong to the execution, not the function - // and are interrupted only when the whole program returns. expect(trace.starts).toEqual([1, 2]) expect(trace.completed).toBe(0) expect(trace.interrupted).toBe(2) }) }) describe("promises at data boundaries", () => { test("returning an un-awaited promise inside data is a clear await-hinting diagnostic", async () => { const diagnostic = await error(`return { result: tools.host.echo({ id: 1 }) }`) expect(diagnostic.kind).toBe("InvalidDataValue") expect(diagnostic.message).toContain("un-awaited Promise") expect(diagnostic.message).toContain("await tools.ns.tool(...)") }) test("collection helpers do not let un-awaited promises cross the result boundary", async () => { const diagnostic = await error(`return Array.from([Promise.resolve(1)])`) expect(diagnostic.kind).toBe("InvalidDataValue") expect(diagnostic.message).toContain("un-awaited Promise") }) test("invalid returned data cancels pending work", async () => { const trace = makeTrace() const result = await run( ` const pending = tools.host.pending({ id: 1 }) return { pending } `, { trace, limits: { timeoutMs: 100 } }, ) expect(result.ok).toBe(false) if (result.ok) return expect(result.error.kind).toBe("InvalidDataValue") expect(trace.completed).toBe(0) expect(trace.interrupted).toBe(1) }) test("passing an un-awaited promise as a tool argument is a clear diagnostic", async () => { const diagnostic = await error(`return await tools.host.echo({ id: tools.host.echo({ id: 1 }) })`) expect(diagnostic.kind).toBe("InvalidDataValue") expect(diagnostic.message).toContain("un-awaited Promise") }) test("JSON.stringify of a promise is a diagnostic, not '{}'", async () => { const diagnostic = await error(`return JSON.stringify(Promise.resolve(1))`) expect(diagnostic.kind).toBe("InvalidDataValue") expect(diagnostic.message).toContain("un-awaited Promise") }) test("operators reject promise operands", async () => { const diagnostic = await error(`return Promise.resolve(1) + 1`) expect(diagnostic.kind).toBe("InvalidDataValue") }) }) describe("Promise.all over arbitrary arrays", () => { test("combinators return promises that can be assigned and awaited later", async () => { expect( await value(` const all = Promise.all([Promise.resolve(1)]) const settled = Promise.allSettled([Promise.reject("no")]) const race = Promise.race([Promise.resolve(2)]) const promises = [all instanceof Promise, settled instanceof Promise, race instanceof Promise] return [promises, await all, await settled, await race] `), ).toEqual([[true, true, true], [1], [{ status: "rejected", reason: "no" }], 2]) }) test("separately-created aggregate batches overlap before either is awaited", async () => { const trace = makeTrace() expect( await value( ` const first = Promise.all([tools.host.gated({ id: 1 })]) const second = Promise.all([tools.host.gated({ id: 2 })]) await tools.host.open({ id: 1 }) await tools.host.open({ id: 2 }) return [await first, await second] `, { trace }, ), ).toEqual([[1], [2]]) expect(trace.starts).toEqual([1, 2]) expect(trace.maxActive).toBeGreaterThan(1) }) test("an aggregate created before a try block rejects at its later await", async () => { expect( await value(` const aggregate = Promise.all([tools.host.fail({})]) try { await aggregate return "no" } catch (error) { return error.message } `), ).toBe("Lookup refused") }) test("awaiting an aggregate repeatedly does not rerun its members", async () => { const result = await run(` const aggregate = Promise.all([tools.host.echo({ id: 7 })]) return [await aggregate, await aggregate] `) expect(result.ok).toBe(true) if (!result.ok) return expect(result.value).toEqual([[7], [7]]) expect(result.toolCalls).toStrictEqual([{ name: "host.echo" }]) }) test("mixes promises and plain values, preserving order", async () => { expect( await value(` return await Promise.all([tools.host.echo({ id: 1 }), "plain", tools.host.echo({ id: 2 }), 42]) `), ).toEqual([1, "plain", 2, 42]) }) test("accepts arrays built beforehand, passed as identifiers, and spread elements", async () => { expect( await value(` const calls = [] calls.push(tools.host.echo({ id: 1 })) calls.push(7) const more = [tools.host.echo({ id: 2 })] const batch = [...calls, ...more, "x"] return await Promise.all(batch) `), ).toEqual([1, 7, 2, "x"]) }) test("runs items.map tool calls in parallel", async () => { const trace = makeTrace() const result = await value( ` const ids = [1, 2, 3, 4] const calls = ids.map((id) => tools.host.gated({ id })) for (const id of ids) await tools.host.open({ id }) return await Promise.all(calls) `, { trace }, ) expect(result).toEqual([1, 2, 3, 4]) // maxActive counts truly-overlapping live executions, so > 1 proves real // parallelism deterministically - no wall-clock assertion needed. expect(trace.maxActive).toBeGreaterThan(1) }) test("runs async map callbacks concurrently", async () => { const trace = makeTrace() const result = await value( ` const ids = [1, 2, 3, 4] const calls = ids.map(async (id) => await tools.host.gated({ id })) for (const id of ids) await tools.host.open({ id }) return await Promise.all(calls) `, { trace }, ) expect(result).toEqual([1, 2, 3, 4]) expect(trace.maxActive).toBeGreaterThan(1) }) test("does not cap live tool-call concurrency", async () => { const trace = makeTrace() const result = await value( ` const ids = [] for (let i = 0; i < 20; i += 1) ids.push(i) const calls = ids.map((id) => tools.host.gated({ id })) for (const id of ids) await tools.host.open({ id }) const results = await Promise.all(calls) return results.length `, { trace }, ) expect(result).toBe(20) expect(trace.maxActive).toBe(20) }) test("resolves the empty array", async () => { expect(await value(`return await Promise.all([])`)).toEqual([]) }) test("rejects with the first failure, catchable in-program", async () => { const result = await run(` try { await Promise.all([tools.host.echo({ id: 1 }), tools.host.fail({})]) return "no" } catch (e) { return e.message } `) expect(result.ok).toBe(true) if (!result.ok) return expect(result.value).toBe("Lookup refused") expect(result.warnings).toBeUndefined() }) test("rejects before an earlier slow promise fulfills", async () => { const trace = makeTrace() expect( await value( ` try { await Promise.all([ tools.host.pending({ id: 1 }), tools.host.fail({}), ]) return -1 } catch { return await tools.host.completed({}) } `, { trace }, ), ).toBe(0) // The surviving member is observed (Promise.all handled it), so completion interrupts // it instead of waiting for it. expect(trace.completed).toBe(0) expect(trace.interrupted).toBe(1) }) test("fail-fast does not cancel a sibling the program still holds and awaits", async () => { const trace = makeTrace() expect( await value( ` const slow = tools.host.gated({ id: 1 }) try { await Promise.all([slow, tools.host.fail({})]) return "no" } catch {} await tools.host.open({ id: 1 }) return await slow `, { trace }, ), ).toBe(1) expect(trace.completed).toBe(1) expect(trace.interrupted).toBe(0) }) test("a slower observed sibling is interrupted at completion after failing fast", async () => { const trace = makeTrace() expect( await value( ` const failLater = async () => { await tools.host.pending({ id: 1 }) throw new Error("later") } const aggregate = Promise.all([Promise.reject(new Error("first")), failLater()]) try { await aggregate return "no" } catch (error) { return error.message } `, { trace }, ), ).toBe("first") expect(trace.completed).toBe(0) expect(trace.interrupted).toBe(1) }) test("a non-collection argument is a clear error", async () => { const diagnostic = await error(`return await Promise.all(42)`) expect(diagnostic.message).toContain("Promise.all expects an array") }) test("exceeding maxToolCalls inside Promise.all is a ToolCallLimitExceeded diagnostic", async () => { const diagnostic = await error( `return await Promise.all([tools.host.echo({ id: 1 }), tools.host.echo({ id: 2 }), tools.host.echo({ id: 3 })])`, { limits: { maxToolCalls: 2 } }, ) expect(diagnostic.kind).toBe("ToolCallLimitExceeded") }) }) describe("Promise.allSettled", () => { test("reports fulfilled and rejected outcomes with catch-normalized reasons", async () => { expect( await value(` return await Promise.allSettled([ tools.host.echo({ id: 5 }), tools.host.fail({}), "plain", Promise.reject(new Error("boom")), ]) `), ).toEqual([ { status: "fulfilled", value: 5 }, { status: "rejected", reason: { name: "Error", message: "Lookup refused" } }, { status: "fulfilled", value: "plain" }, { status: "rejected", reason: { name: "Error", message: "boom" } }, ]) }) test("never rejects for program-level failures", async () => { const result = await run(` const settled = await Promise.allSettled([tools.host.fail({}), tools.host.fail({})]) return settled.filter((s) => s.status === "rejected").length `) expect(result.ok).toBe(true) if (!result.ok) return expect(result.value).toBe(2) expect(result.warnings).toBeUndefined() }) }) describe("Promise.race", () => { test("first settlement wins and a direct loser is interrupted at completion", async () => { const trace = makeTrace() const result = await value( ` const fast = tools.host.echo({ id: 1 }) const slow = tools.host.pending({ id: 2 }) return await Promise.race([fast, slow]) `, { trace }, ) expect(result).toBe(1) // The loser is observed (the race handled it), so the execution does not wait for it. expect(trace.completed).toBe(1) expect(trace.interrupted).toBe(1) }) test("a direct loser remains awaitable after the race settles", async () => { expect( await value(` const fast = tools.host.echo({ id: 1 }) const slow = tools.host.gated({ id: 2 }) const winner = await Promise.race([fast, slow]) await tools.host.open({ id: 2 }) return { winner, loser: await slow } `), ).toEqual({ winner: 1, loser: 2 }) }) test("a nested aggregate loser and its members are interrupted at completion", async () => { const trace = makeTrace() expect( await value( ` const nested = Promise.all([ tools.host.pending({ id: 1 }), tools.host.pending({ id: 2 }), ]) return await Promise.race(["immediate", nested]) `, { trace }, ), ).toBe("immediate") // The nested aggregate and its members are all observed, so nothing waits for them. expect(trace.completed).toBe(0) expect(trace.interrupted).toBe(2) }) test("a rejection can win the race", async () => { expect( await value(` try { await Promise.race([tools.host.fail({}), tools.host.pending({ id: 1 })]) return "no" } catch (e) { return e.message } `), ).toBe("Lookup refused") }) test("a plain value wins over pending promises", async () => { const trace = makeTrace() expect(await value(`return await Promise.race([tools.host.pending({ id: 1 }), "immediate"])`, { trace })).toBe( "immediate", ) expect(trace.completed).toBe(0) expect(trace.interrupted).toBe(1) }) test("a rejected race loser is observed by the aggregate", async () => { const result = await run(`return await Promise.race(["winner", tools.host.fail({})])`) expect(result.ok).toBe(true) if (!result.ok) return expect(result.value).toBe("winner") expect(result.warnings).toBeUndefined() }) test("an empty race is a clear error instead of hanging", async () => { const diagnostic = await error(`return await Promise.race([])`) expect(diagnostic.message).toContain("never settle") }) }) describe("Promise.resolve / Promise.reject", () => { test("resolve wraps plain values and passes promises through", async () => { expect(await value(`return await Promise.resolve(42)`)).toBe(42) expect(await value(`return await Promise.resolve(Promise.resolve("nested"))`)).toBe("nested") expect(await value(`return await Promise.resolve(tools.host.echo({ id: 3 }))`)).toBe(3) expect(await value(`const promise = Promise.resolve(1); return [promise].includes(Promise.resolve(promise))`)).toBe( true, ) }) test("reject produces a promise whose await throws the reason", async () => { expect( await value(` try { await Promise.reject("nope") return "no" } catch (e) { return e } `), ).toBe("nope") }) test("a rejection observed after settlement is handled", async () => { expect( await value(` const rejected = Promise.reject(new Error("handled")) await tools.host.echo({ id: 1 }) try { await rejected return "no" } catch (error) { return error.message } `), ).toBe("handled") }) test("an abandoned rejected promise is reported as unhandled", async () => { const result = await run(` Promise.reject(new Error("abandoned")) return "done" `) expect(result.ok).toBe(true) if (!result.ok) return expect(result.value).toBe("done") expect(result.warnings).toStrictEqual([ { kind: "ExecutionFailure", message: "Unhandled rejection from an un-awaited promise: Uncaught: abandoned" }, ]) }) }) describe("timeout interruption of forked calls", () => { test("the execution timeout interrupts in-flight forked fibers", async () => { const trace = makeTrace() const result = await run( ` const a = tools.host.pending({ id: 1 }) const b = tools.host.pending({ id: 2 }) return await a `, { trace, limits: { timeoutMs: 100 } }, ) expect(result.ok).toBe(false) if (result.ok) return expect(result.error.kind).toBe("TimeoutExceeded") // Both calls started; neither escaped the timeout - the awaited one AND the abandoned one. expect(trace.starts).toEqual([1, 2]) expect(trace.interrupted).toBe(2) expect(trace.completed).toBe(0) }) test("the timeout also interrupts calls inside Promise.all", async () => { const trace = makeTrace() const result = await run( `return await Promise.all([tools.host.pending({ id: 1 }), tools.host.pending({ id: 2 })])`, { trace, limits: { timeoutMs: 100 } }, ) expect(result.ok).toBe(false) if (result.ok) return expect(result.error.kind).toBe("TimeoutExceeded") expect(trace.interrupted).toBe(2) }) test("a non-settling race loser cannot hold the execution to the timeout", async () => { const trace = makeTrace() const result = await run(`return await Promise.race(["winner", tools.host.pending({ id: 1 })])`, { trace, limits: { timeoutMs: 100 }, }) // Completion interrupts the observed loser immediately; the race result survives. expect(result.ok).toBe(true) if (!result.ok) return expect(result.value).toBe("winner") expect(result.warnings).toBeUndefined() expect(trace.starts).toEqual([1]) expect(trace.completed).toBe(0) expect(trace.interrupted).toBe(1) }) test("a timeout during completion cleanup keeps the computed value and warns", async () => { const trace = makeTrace() const result = await run( ` tools.host.stubborn({ cleanupMs: 400 }) return "done" `, { trace, limits: { timeoutMs: 100 } }, ) expect(result.ok).toBe(true) if (!result.ok) return expect(result.value).toBe("done") expect(result.warnings).toStrictEqual([ { kind: "TimeoutExceeded", message: "The program returned, but background work was still running at the 100ms timeout and was interrupted. Await all started promises.", }, ]) expect(trace.interrupted).toBe(1) expect(trace.completed).toBe(0) }) test("a timeout during completion cleanup reports the timeout warning before settled rejections", async () => { const result = await run( ` tools.host.fail({}) tools.host.stubborn({ cleanupMs: 400 }) return "done" `, { limits: { timeoutMs: 100 } }, ) expect(result.ok).toBe(true) if (!result.ok) return expect(result.value).toBe("done") expect(result.warnings).toStrictEqual([ { kind: "TimeoutExceeded", message: "The program returned, but background work was still running at the 100ms timeout and was interrupted. Await all started promises.", }, { kind: "ToolFailure", message: "Unhandled rejection from an un-awaited promise: Lookup refused" }, ]) }) }) describe("promise chaining", () => { test("then transforms tool results and adopts returned promises across a chain", async () => { expect( await value(` return await tools.host .echo({ id: 2 }) .then((id) => tools.host.echo({ id: id + 1 })) .then((id) => id * 10) `), ).toBe(30) }) test("handlers are deferred and run in attach order", async () => { expect( await value(` const order = [] const promise = Promise.resolve(1) promise.then(() => order.push("h1")) promise.then(() => order.push("h2")) order.push("sync") await promise return order `), ).toEqual(["sync", "h1", "h2"]) }) test("catch recovers a tool failure and preserves fulfillment", async () => { expect( await value(` return [ await tools.host.fail({}).catch((error) => error.message), await tools.host.echo({ id: 4 }).catch(() => "unused"), ] `), ).toEqual(["Lookup refused", 4]) }) test("finally observes settlement without changing the value", async () => { expect( await value(` const events = [] const result = await tools.host.echo({ id: 5 }).finally(() => events.push("cleanup")) return [result, events] `), ).toEqual([5, ["cleanup"]]) }) test("a settled, un-awaited rejected chain tail warns exactly once", async () => { const result = await run(` Promise.reject(new Error("boom")).then((value) => value) await Promise.resolve() return "done" `) expect(result.ok).toBe(true) if (!result.ok) return expect(result.value).toBe("done") // The source rejection belongs to the chain (no warning); only the derived tail warns. expect(result.warnings).toStrictEqual([ { kind: "ExecutionFailure", message: "Unhandled rejection from an un-awaited promise: Uncaught: boom" }, ]) }) test("a catch handler silences the chain's rejection warning", async () => { const result = await run(` Promise.reject(new Error("boom")).catch(() => "handled") await Promise.resolve() return "done" `) expect(result.ok).toBe(true) if (!result.ok) return expect(result.warnings).toBeUndefined() }) test("unsupported callable handlers fail loudly with a wrap hint", async () => { const diagnostic = await error(`return await tools.host.echo({ id: 1 }).then(tools.host.completed)`) expect(diagnostic.message).toContain("Promise.prototype.then cannot use this callable as a handler") expect(diagnostic.message).toContain("wrap it in an arrow function") }) test("chaining methods are opaque references until called", async () => { expect(await value(`return typeof tools.host.echo({ id: 1 }).then`)).toBe("function") }) }) describe("combinator settlement timing", () => { test("a combinator settling one reaction turn after the program returns is interrupted silently", async () => { // The aggregate's one-turn settlement delay (V8 parity) means an immediately-returning // program abandons it while still pending: interrupted like any pending work, so no // rejection warning survives - the member itself was observed by the combinator. const result = await run(` Promise.all([Promise.reject(new Error("boom"))]) return "done" `) expect(result.ok).toBe(true) if (!result.ok) return expect(result.value).toBe("done") expect(result.warnings).toBeUndefined() }) test("a combinator settles one reaction turn after its members, as in V8", async () => { // Regression for the race winner flip: Promise.all's settlement burns a reaction turn, // so a plain resolved value entered in the same race wins, and a fail-fast aggregate // cannot beat it into rejection. expect( await value(` const pending = tools.host.pending({ id: 9 }) const winner = await Promise.race([Promise.all([Promise.resolve(1)]), Promise.resolve(2)]) try { const raced = await Promise.race([Promise.all([Promise.reject("x"), pending]), Promise.resolve("ok")]) return [winner, "fulfilled", raced] } catch (reason) { return [winner, "rejected", reason] } `), ).toEqual([2, "fulfilled", "ok"]) }) }) describe("unsupported promise surface", () => { test("other property reads on a promise hint at the missing await", async () => { const diagnostic = await error(`return tools.host.echo({ id: 1 }).value`) expect(diagnostic.kind).toBe("InvalidDataValue") expect(diagnostic.message).toContain("un-awaited Promise") expect(diagnostic.message).toContain("await it first") }) test("unknown Promise statics list what is available", async () => { const diagnostic = await error(`return await Promise.withResolvers()`) expect(diagnostic.message).toContain("Promise.withResolvers is not available") expect(diagnostic.message).toContain("Promise.any") }) }) describe("Promise.any", () => { test("first tool success wins; failing and losing calls are handled silently", async () => { const trace = makeTrace() const result = await run( ` const winner = await Promise.any([ tools.host.fail({}), tools.host.echo({ id: 1 }), tools.host.pending({ id: 2 }), ]) return winner `, { trace }, ) expect(result.ok).toBe(true) if (!result.ok) return expect(result.value).toBe(1) // The slow loser stays execution-owned and is interrupted at completion; the tool // failure was observed by the aggregate, so no rejection warning survives. expect(result.warnings).toBeUndefined() expect(trace.interrupted).toBe(1) }) test("all members failing rejects with catch-normalized reasons in input order", async () => { expect( await value(` try { await Promise.any([tools.host.fail({}), Promise.reject("plain")]) return "fulfilled" } catch (error) { return [error.name, error.errors.map((reason) => reason.message ?? reason)] } `), ).toEqual(["AggregateError", ["Lookup refused", "plain"]]) }) test("settles one reaction turn after its deciding member, as in V8", async () => { expect(await value(`return await Promise.race([Promise.any([Promise.resolve(1)]), Promise.resolve(2)])`)).toBe(2) }) test("a tie is decided by settlement order, not input order", async () => { // Handlers run in attach order, so `first` settles before `second` and wins // despite its later input position - as in real JS. expect( await value(` const first = Promise.resolve().then(() => "one") const second = Promise.resolve().then(() => "two") return await Promise.any([second, first]) `), ).toBe("one") }) test("an abandoned rejecting aggregate is interrupted silently at the return", async () => { const result = await run(` Promise.any([Promise.reject(new Error("boom"))]) return "done" `) expect(result.ok).toBe(true) if (!result.ok) return expect(result.value).toBe("done") expect(result.warnings).toBeUndefined() }) }) describe("promise construction", () => { test("a deferred gate coordinates tool results across async functions", async () => { expect( await value(` let openGate const gate = new Promise((resolve) => { openGate = resolve }) const worker = (async () => { const id = await gate return id * 2 })() openGate(await tools.host.echo({ id: 21 })) return await worker `), ).toBe(42) }) test("the .then(resolve) bridge settles a constructed promise", async () => { expect( await value(` const bridged = new Promise((resolve, reject) => { tools.host.echo({ id: 7 }).then(resolve, reject) }) return await bridged `), ).toBe(7) }) test("constructed promises participate in combinators", async () => { expect( await value(` let settle const manual = new Promise((resolve) => { settle = resolve }) const race = Promise.race([manual, tools.host.pending({ id: 3 })]) const all = Promise.all([manual, "plain"]) const any = Promise.any([manual, new Promise(() => {})]) settle("manual") return [await race, await all, await any] `), ).toEqual(["manual", ["manual", "plain"], "manual"]) }) test("resolving with a pending promise adopts its later settlement", async () => { expect( await value(` let innerResolve, innerReject const adopted = new Promise((resolve) => resolve(new Promise((resolve) => { innerResolve = resolve }))) const adoptedRejection = new Promise((resolve) => resolve(new Promise((_, reject) => { innerReject = reject }))) innerResolve("later") innerReject("bad") try { return [await adopted, await adoptedRejection] } catch (reason) { return [await adopted, reason] } `), ).toEqual(["later", "bad"]) }) test("an async executor's post-await resolve settles the promise", async () => { expect( await value(` const result = new Promise(async (resolve) => { const id = await tools.host.echo({ id: 5 }) resolve(id * 2) }) return await result `), ).toBe(10) }) test("a never-settled promise is abandoned silently at the return", async () => { const result = await run(` const forever = new Promise(() => {}) forever.then(() => {}) return "done" `) expect(result.ok).toBe(true) if (!result.ok) return expect(result.value).toBe("done") expect(result.warnings).toBeUndefined() }) test("an un-awaited constructed rejection is reported like any unhandled rejection", async () => { const result = await run(` new Promise((_, reject) => reject(new Error("dropped"))) await Promise.resolve() await Promise.resolve() return "done" `) expect(result.ok).toBe(true) if (!result.ok) return expect(result.value).toBe("done") expect(result.warnings).toHaveLength(1) expect(result.warnings?.[0].message).toContain("Unhandled rejection") expect(result.warnings?.[0].message).toContain("dropped") }) test("resolver functions cannot cross the data boundary", async () => { const diagnostic = await error(` let escaped new Promise((resolve) => { escaped = resolve }) return { escaped } `) expect(diagnostic.kind).toBe("InvalidDataValue") }) })