promise.test.ts 43 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315
  1. import { describe, expect, test } from "bun:test"
  2. import { Deferred, Effect, Schema } from "effect"
  3. import { CodeMode, Tool, toolError } from "../src/index.js"
  4. // Wave 5 acceptance suite: first-class promise values. Un-awaited tool calls start eagerly on
  5. // supervised fibers, `await` settles them, Promise.all/allSettled/race/resolve/reject are
  6. // ordinary functions over arbitrary arrays mixing promises and plain values, and
  7. // .then/.catch/.finally chain reactions onto any promise.
  8. type Trace = {
  9. starts: Array<number>
  10. active: number
  11. maxActive: number
  12. completed: number
  13. interrupted: number
  14. }
  15. const makeTrace = (): Trace => ({ starts: [], active: 0, maxActive: 0, completed: 0, interrupted: 0 })
  16. /**
  17. * Deterministic tool set: ordering and interruption are structural, never temporal.
  18. *
  19. * - `echo` settles immediately with its id.
  20. * - `gated` blocks until `open` releases the same id. Tool fibers start eagerly at the
  21. * call site, so several gated calls are provably live at once before any `open` runs.
  22. * - `pending` never settles; tests assert its interruption instead of racing a timer.
  23. *
  24. * Real clocks remain only in the wall-clock timeout tests (`timeoutMs`, `stubborn`
  25. * cleanup), where elapsed time is the behavior under test.
  26. */
  27. const echoTool = (trace: Trace) =>
  28. Tool.make({
  29. description: "Echo an id immediately",
  30. input: Schema.Struct({ id: Schema.Number }),
  31. output: Schema.Number,
  32. execute: ({ id }) =>
  33. Effect.sync(() => {
  34. trace.starts.push(id)
  35. trace.completed += 1
  36. return id
  37. }),
  38. })
  39. const gatedTool = (trace: Trace, gate: (id: number) => Deferred.Deferred<void>) =>
  40. Tool.make({
  41. description: "Echo an id once its gate opens",
  42. input: Schema.Struct({ id: Schema.Number }),
  43. output: Schema.Number,
  44. execute: ({ id }) =>
  45. Effect.gen(function* () {
  46. trace.starts.push(id)
  47. trace.active += 1
  48. trace.maxActive = Math.max(trace.maxActive, trace.active)
  49. yield* Deferred.await(gate(id))
  50. trace.active -= 1
  51. trace.completed += 1
  52. return id
  53. }).pipe(
  54. Effect.onInterrupt(() =>
  55. Effect.sync(() => {
  56. trace.active -= 1
  57. trace.interrupted += 1
  58. }),
  59. ),
  60. ),
  61. })
  62. const openTool = (gate: (id: number) => Deferred.Deferred<void>) =>
  63. Tool.make({
  64. description: "Open the gate for an id",
  65. input: Schema.Struct({ id: Schema.Number }),
  66. output: Schema.Boolean,
  67. execute: ({ id }) => Deferred.succeed(gate(id), undefined),
  68. })
  69. const pendingTool = (trace: Trace) =>
  70. Tool.make({
  71. description: "Never settle",
  72. input: Schema.Struct({ id: Schema.Number }),
  73. output: Schema.Number,
  74. execute: ({ id }) =>
  75. Effect.gen(function* () {
  76. trace.starts.push(id)
  77. trace.active += 1
  78. trace.maxActive = Math.max(trace.maxActive, trace.active)
  79. return yield* Effect.never
  80. }).pipe(
  81. Effect.onInterrupt(() =>
  82. Effect.sync(() => {
  83. trace.active -= 1
  84. trace.interrupted += 1
  85. }),
  86. ),
  87. ),
  88. })
  89. const failingTool = Tool.make({
  90. description: "Always refuse",
  91. input: Schema.Struct({}),
  92. output: Schema.String,
  93. execute: () => Effect.fail(toolError("Lookup refused")),
  94. })
  95. const interruptedTool = Tool.make({
  96. description: "Interrupt this call",
  97. input: Schema.Struct({}),
  98. output: Schema.String,
  99. execute: () => Effect.interrupt,
  100. })
  101. const completedTool = (trace: Trace) =>
  102. Tool.make({
  103. description: "Return the number of completed calls",
  104. input: Schema.Struct({}),
  105. output: Schema.Number,
  106. execute: () => Effect.succeed(trace.completed),
  107. })
  108. /** Never settles, and holds interruption cleanup for `cleanupMs` so completion cleanup can outlast a timeout. */
  109. const stubbornTool = (trace: Trace) =>
  110. Tool.make({
  111. description: "Never settle; clean up slowly when interrupted",
  112. input: Schema.Struct({ cleanupMs: Schema.Number }),
  113. output: Schema.Number,
  114. execute: ({ cleanupMs }) =>
  115. Effect.never.pipe(
  116. Effect.onInterrupt(() =>
  117. Effect.andThen(
  118. Effect.sleep(cleanupMs),
  119. Effect.sync(() => {
  120. trace.interrupted += 1
  121. }),
  122. ),
  123. ),
  124. ),
  125. })
  126. const run = (
  127. code: string,
  128. options: { trace?: Trace; limits?: CodeMode.ExecutionLimits } = {},
  129. ): Promise<CodeMode.Result> => {
  130. const trace = options.trace ?? makeTrace()
  131. const gates = new Map<number, Deferred.Deferred<void>>()
  132. const gate = (id: number): Deferred.Deferred<void> => {
  133. const existing = gates.get(id)
  134. if (existing) return existing
  135. const created = Deferred.makeUnsafe<void>()
  136. gates.set(id, created)
  137. return created
  138. }
  139. return Effect.runPromise(
  140. CodeMode.execute({
  141. tools: {
  142. host: {
  143. echo: echoTool(trace),
  144. gated: gatedTool(trace, gate),
  145. open: openTool(gate),
  146. pending: pendingTool(trace),
  147. fail: failingTool,
  148. interrupt: interruptedTool,
  149. completed: completedTool(trace),
  150. stubborn: stubbornTool(trace),
  151. },
  152. },
  153. code,
  154. ...(options.limits ? { limits: options.limits } : {}),
  155. }),
  156. )
  157. }
  158. const value = async (code: string, options: { trace?: Trace; limits?: CodeMode.ExecutionLimits } = {}) => {
  159. const result = await run(code, options)
  160. if (!result.ok) throw new Error(`expected success, got ${result.error.kind}: ${result.error.message}`)
  161. return result.value
  162. }
  163. const error = async (code: string, options: { trace?: Trace; limits?: CodeMode.ExecutionLimits } = {}) => {
  164. const result = await run(code, options)
  165. if (result.ok) throw new Error(`expected failure, got value ${JSON.stringify(result.value)}`)
  166. return result.error
  167. }
  168. describe("first-class promise values", () => {
  169. test("async functions return promises with isolated concurrent invocations", async () => {
  170. expect(
  171. await value(`
  172. const load = async (id) => {
  173. const result = await tools.host.echo({ id })
  174. return [id, result]
  175. }
  176. const first = load(1)
  177. const second = load(2)
  178. return [first instanceof Promise, second instanceof Promise, await Promise.all([first, second])]
  179. `),
  180. ).toEqual([
  181. true,
  182. true,
  183. [
  184. [1, 1],
  185. [2, 2],
  186. ],
  187. ])
  188. })
  189. test("async function errors reject instead of throwing at the call site", async () => {
  190. expect(
  191. await value(`
  192. const fail = async () => { throw new Error("boom") }
  193. const promise = fail()
  194. try {
  195. await promise
  196. return "no"
  197. } catch (error) {
  198. return error.message
  199. }
  200. `),
  201. ).toBe("boom")
  202. })
  203. test("an un-awaited tool call starts eagerly, in call order, before any await", async () => {
  204. const trace = makeTrace()
  205. const result = await value(
  206. `
  207. const a = tools.host.gated({ id: 1 })
  208. const b = tools.host.gated({ id: 2 })
  209. await tools.host.open({ id: 1 })
  210. await tools.host.open({ id: 2 })
  211. const rb = await b
  212. const ra = await a
  213. return [ra, rb]
  214. `,
  215. { trace },
  216. )
  217. expect(result).toEqual([1, 2])
  218. expect(trace.starts).toEqual([1, 2])
  219. // Both calls overlapped even though they were awaited sequentially.
  220. expect(trace.maxActive).toBeGreaterThan(1)
  221. })
  222. test("awaiting the same promise twice settles once and never re-runs the call", async () => {
  223. const result = await run(`
  224. const p = tools.host.echo({ id: 7 })
  225. const x = await p
  226. const y = await p
  227. return [x, y]
  228. `)
  229. expect(result.ok).toBe(true)
  230. if (!result.ok) return
  231. expect(result.value).toEqual([7, 7])
  232. expect(result.toolCalls).toStrictEqual([{ name: "host.echo" }])
  233. })
  234. test("await of a non-promise value passes it through unchanged", async () => {
  235. expect(await value(`return await 42`)).toBe(42)
  236. expect(await value(`const x = await "s"; return x`)).toBe("s")
  237. expect(await value(`return await null`)).toBeNull()
  238. expect(await value(`return (await [1, 2]).length`)).toBe(2)
  239. })
  240. test("returning an un-awaited tool call resolves it (async-function return semantics)", async () => {
  241. expect(await value(`return tools.host.echo({ id: 9 })`)).toBe(9)
  242. })
  243. test("typeof a promise is 'object', and console.log renders it sensibly", async () => {
  244. const result = await run(`
  245. const p = Promise.resolve(1)
  246. console.log(p)
  247. return typeof p
  248. `)
  249. expect(result.ok).toBe(true)
  250. if (!result.ok) return
  251. expect(result.value).toBe("object")
  252. expect(result.logs).toStrictEqual(["[Promise (await it to get its value)]"])
  253. })
  254. test("an awaited failure is catchable exactly like a synchronous throw", async () => {
  255. const result = await run(`
  256. const p = tools.host.fail({})
  257. try {
  258. await p
  259. return "no"
  260. } catch (e) {
  261. return e.message
  262. }
  263. `)
  264. expect(result.ok).toBe(true)
  265. if (!result.ok) return
  266. expect(result.value).toBe("Lookup refused")
  267. expect(result.warnings).toBeUndefined()
  268. })
  269. test("a fire-and-forget call is interrupted when the program returns", async () => {
  270. const trace = makeTrace()
  271. const result = await run(
  272. `
  273. tools.host.pending({ id: 1 })
  274. return "done"
  275. `,
  276. { trace },
  277. )
  278. expect(result.ok).toBe(true)
  279. if (!result.ok) return
  280. expect(result.value).toBe("done")
  281. expect(result.warnings).toBeUndefined()
  282. expect(trace.completed).toBe(0)
  283. expect(trace.interrupted).toBe(1)
  284. })
  285. test("a never-awaited failing call preserves the result and reports the rejection", async () => {
  286. const result = await run(`
  287. tools.host.fail({})
  288. return "done"
  289. `)
  290. expect(result.ok).toBe(true)
  291. if (!result.ok) return
  292. expect(result.value).toBe("done")
  293. expect(result.warnings).toStrictEqual([
  294. { kind: "ToolFailure", message: "Unhandled rejection from an un-awaited promise: Lookup refused" },
  295. ])
  296. expect(Schema.decodeUnknownSync(CodeMode.Result)(JSON.parse(JSON.stringify(result)))).toStrictEqual(result)
  297. })
  298. test("a never-awaited failing async function is reported with a successful result", async () => {
  299. const result = await run(`
  300. const fail = async () => { throw new Error("boom") }
  301. fail()
  302. return "done"
  303. `)
  304. expect(result.ok).toBe(true)
  305. if (!result.ok) return
  306. expect(result.value).toBe("done")
  307. expect(result.warnings).toStrictEqual([
  308. { kind: "ExecutionFailure", message: "Unhandled rejection from an un-awaited promise: Uncaught: boom" },
  309. ])
  310. })
  311. test("output truncation bounds warning diagnostics with an in-band marker", async () => {
  312. const result = await run(
  313. `
  314. for (let i = 0; i < 100; i += 1) Promise.reject(new Error("x".repeat(1_000)))
  315. return "done"
  316. `,
  317. { limits: { maxOutputBytes: 64 } },
  318. )
  319. expect(result.ok).toBe(true)
  320. if (!result.ok) return
  321. expect(result.truncated).toBe(true)
  322. expect(result.warnings).toStrictEqual([
  323. { kind: "Truncated", message: "100 additional warnings omitted by the output limit." },
  324. ])
  325. })
  326. test("a budget-consuming value does not starve warnings", async () => {
  327. const result = await run(
  328. `
  329. Promise.reject(new Error("boom"))
  330. return "x".repeat(500)
  331. `,
  332. { limits: { maxOutputBytes: 128 } },
  333. )
  334. expect(result.ok).toBe(true)
  335. if (!result.ok) return
  336. expect(result.truncated).toBe(true)
  337. expect(typeof result.value).toBe("string")
  338. expect(result.warnings).toStrictEqual([
  339. { kind: "ExecutionFailure", message: "Unhandled rejection from an un-awaited promise: Uncaught: boom" },
  340. ])
  341. })
  342. test("an un-awaited async function's pending chain is interrupted at the return", async () => {
  343. const trace = makeTrace()
  344. const result = await run(
  345. `
  346. const run = async () => {
  347. await tools.host.pending({ id: 1 })
  348. tools.host.fail({})
  349. }
  350. run()
  351. return "done"
  352. `,
  353. { trace },
  354. )
  355. expect(result.ok).toBe(true)
  356. if (!result.ok) return
  357. expect(result.value).toBe("done")
  358. expect(result.warnings).toBeUndefined()
  359. expect(trace.starts).toEqual([1])
  360. expect(trace.completed).toBe(0)
  361. expect(trace.interrupted).toBe(1)
  362. })
  363. test("reports every unhandled rejection in promise creation order", async () => {
  364. const result = await run(`
  365. Promise.reject(new Error("first"))
  366. tools.host.fail({})
  367. Promise.reject(new Error("third"))
  368. return "done"
  369. `)
  370. expect(result.ok).toBe(true)
  371. if (!result.ok) return
  372. expect(result.warnings).toStrictEqual([
  373. { kind: "ExecutionFailure", message: "Unhandled rejection from an un-awaited promise: Uncaught: first" },
  374. { kind: "ToolFailure", message: "Unhandled rejection from an un-awaited promise: Lookup refused" },
  375. { kind: "ExecutionFailure", message: "Unhandled rejection from an un-awaited promise: Uncaught: third" },
  376. ])
  377. })
  378. test("orders an async function rejection before promises created inside its body", async () => {
  379. const result = await run(`
  380. const outer = async () => {
  381. Promise.reject(new Error("inner"))
  382. throw new Error("outer")
  383. }
  384. outer()
  385. return "done"
  386. `)
  387. expect(result.ok).toBe(true)
  388. if (!result.ok) return
  389. expect(result.warnings).toStrictEqual([
  390. { kind: "ExecutionFailure", message: "Unhandled rejection from an un-awaited promise: Uncaught: outer" },
  391. { kind: "ExecutionFailure", message: "Unhandled rejection from an un-awaited promise: Uncaught: inner" },
  392. ])
  393. })
  394. test("un-awaited interruptions settle without becoming rejections", async () => {
  395. const result = await run(`
  396. tools.host.interrupt({})
  397. Promise.all([tools.host.interrupt({})])
  398. return "done"
  399. `)
  400. expect(result.ok).toBe(true)
  401. if (!result.ok) return
  402. expect(result.value).toBe("done")
  403. expect(result.warnings).toBeUndefined()
  404. })
  405. test("a fatal program error cancels outstanding work without reporting unhandled rejections", async () => {
  406. const trace = makeTrace()
  407. const result = await run(
  408. `
  409. tools.host.pending({ id: 1 })
  410. throw new Error("boom")
  411. `,
  412. { trace },
  413. )
  414. expect(result.ok).toBe(false)
  415. if (result.ok) return
  416. expect(result.error.message).toBe("Uncaught: boom")
  417. expect("warnings" in result).toBe(false)
  418. expect(trace.completed).toBe(0)
  419. expect(trace.interrupted).toBe(1)
  420. })
  421. test("async-function promises remain owned by the execution after the function returns", async () => {
  422. const trace = makeTrace()
  423. expect(
  424. await value(
  425. `
  426. const launch = async () => {
  427. tools.host.pending({ id: 1 })
  428. Promise.all([tools.host.pending({ id: 2 })])
  429. return "returned"
  430. }
  431. return await launch()
  432. `,
  433. { trace },
  434. ),
  435. ).toBe("returned")
  436. // Both calls outlive launch() itself - they belong to the execution, not the function -
  437. // and are interrupted only when the whole program returns.
  438. expect(trace.starts).toEqual([1, 2])
  439. expect(trace.completed).toBe(0)
  440. expect(trace.interrupted).toBe(2)
  441. })
  442. })
  443. describe("promises at data boundaries", () => {
  444. test("returning an un-awaited promise inside data is a clear await-hinting diagnostic", async () => {
  445. const diagnostic = await error(`return { result: tools.host.echo({ id: 1 }) }`)
  446. expect(diagnostic.kind).toBe("InvalidDataValue")
  447. expect(diagnostic.message).toContain("un-awaited Promise")
  448. expect(diagnostic.message).toContain("await tools.ns.tool(...)")
  449. })
  450. test("collection helpers do not let un-awaited promises cross the result boundary", async () => {
  451. const diagnostic = await error(`return Array.from([Promise.resolve(1)])`)
  452. expect(diagnostic.kind).toBe("InvalidDataValue")
  453. expect(diagnostic.message).toContain("un-awaited Promise")
  454. })
  455. test("invalid returned data cancels pending work", async () => {
  456. const trace = makeTrace()
  457. const result = await run(
  458. `
  459. const pending = tools.host.pending({ id: 1 })
  460. return { pending }
  461. `,
  462. { trace, limits: { timeoutMs: 100 } },
  463. )
  464. expect(result.ok).toBe(false)
  465. if (result.ok) return
  466. expect(result.error.kind).toBe("InvalidDataValue")
  467. expect(trace.completed).toBe(0)
  468. expect(trace.interrupted).toBe(1)
  469. })
  470. test("passing an un-awaited promise as a tool argument is a clear diagnostic", async () => {
  471. const diagnostic = await error(`return await tools.host.echo({ id: tools.host.echo({ id: 1 }) })`)
  472. expect(diagnostic.kind).toBe("InvalidDataValue")
  473. expect(diagnostic.message).toContain("un-awaited Promise")
  474. })
  475. test("JSON.stringify of a promise is a diagnostic, not '{}'", async () => {
  476. const diagnostic = await error(`return JSON.stringify(Promise.resolve(1))`)
  477. expect(diagnostic.kind).toBe("InvalidDataValue")
  478. expect(diagnostic.message).toContain("un-awaited Promise")
  479. })
  480. test("operators reject promise operands", async () => {
  481. const diagnostic = await error(`return Promise.resolve(1) + 1`)
  482. expect(diagnostic.kind).toBe("InvalidDataValue")
  483. })
  484. })
  485. describe("Promise.all over arbitrary arrays", () => {
  486. test("combinators return promises that can be assigned and awaited later", async () => {
  487. expect(
  488. await value(`
  489. const all = Promise.all([Promise.resolve(1)])
  490. const settled = Promise.allSettled([Promise.reject("no")])
  491. const race = Promise.race([Promise.resolve(2)])
  492. const promises = [all instanceof Promise, settled instanceof Promise, race instanceof Promise]
  493. return [promises, await all, await settled, await race]
  494. `),
  495. ).toEqual([[true, true, true], [1], [{ status: "rejected", reason: "no" }], 2])
  496. })
  497. test("separately-created aggregate batches overlap before either is awaited", async () => {
  498. const trace = makeTrace()
  499. expect(
  500. await value(
  501. `
  502. const first = Promise.all([tools.host.gated({ id: 1 })])
  503. const second = Promise.all([tools.host.gated({ id: 2 })])
  504. await tools.host.open({ id: 1 })
  505. await tools.host.open({ id: 2 })
  506. return [await first, await second]
  507. `,
  508. { trace },
  509. ),
  510. ).toEqual([[1], [2]])
  511. expect(trace.starts).toEqual([1, 2])
  512. expect(trace.maxActive).toBeGreaterThan(1)
  513. })
  514. test("an aggregate created before a try block rejects at its later await", async () => {
  515. expect(
  516. await value(`
  517. const aggregate = Promise.all([tools.host.fail({})])
  518. try {
  519. await aggregate
  520. return "no"
  521. } catch (error) {
  522. return error.message
  523. }
  524. `),
  525. ).toBe("Lookup refused")
  526. })
  527. test("awaiting an aggregate repeatedly does not rerun its members", async () => {
  528. const result = await run(`
  529. const aggregate = Promise.all([tools.host.echo({ id: 7 })])
  530. return [await aggregate, await aggregate]
  531. `)
  532. expect(result.ok).toBe(true)
  533. if (!result.ok) return
  534. expect(result.value).toEqual([[7], [7]])
  535. expect(result.toolCalls).toStrictEqual([{ name: "host.echo" }])
  536. })
  537. test("mixes promises and plain values, preserving order", async () => {
  538. expect(
  539. await value(`
  540. return await Promise.all([tools.host.echo({ id: 1 }), "plain", tools.host.echo({ id: 2 }), 42])
  541. `),
  542. ).toEqual([1, "plain", 2, 42])
  543. })
  544. test("accepts arrays built beforehand, passed as identifiers, and spread elements", async () => {
  545. expect(
  546. await value(`
  547. const calls = []
  548. calls.push(tools.host.echo({ id: 1 }))
  549. calls.push(7)
  550. const more = [tools.host.echo({ id: 2 })]
  551. const batch = [...calls, ...more, "x"]
  552. return await Promise.all(batch)
  553. `),
  554. ).toEqual([1, 7, 2, "x"])
  555. })
  556. test("runs items.map tool calls in parallel", async () => {
  557. const trace = makeTrace()
  558. const result = await value(
  559. `
  560. const ids = [1, 2, 3, 4]
  561. const calls = ids.map((id) => tools.host.gated({ id }))
  562. for (const id of ids) await tools.host.open({ id })
  563. return await Promise.all(calls)
  564. `,
  565. { trace },
  566. )
  567. expect(result).toEqual([1, 2, 3, 4])
  568. // maxActive counts truly-overlapping live executions, so > 1 proves real
  569. // parallelism deterministically - no wall-clock assertion needed.
  570. expect(trace.maxActive).toBeGreaterThan(1)
  571. })
  572. test("runs async map callbacks concurrently", async () => {
  573. const trace = makeTrace()
  574. const result = await value(
  575. `
  576. const ids = [1, 2, 3, 4]
  577. const calls = ids.map(async (id) => await tools.host.gated({ id }))
  578. for (const id of ids) await tools.host.open({ id })
  579. return await Promise.all(calls)
  580. `,
  581. { trace },
  582. )
  583. expect(result).toEqual([1, 2, 3, 4])
  584. expect(trace.maxActive).toBeGreaterThan(1)
  585. })
  586. test("does not cap live tool-call concurrency", async () => {
  587. const trace = makeTrace()
  588. const result = await value(
  589. `
  590. const ids = []
  591. for (let i = 0; i < 20; i += 1) ids.push(i)
  592. const calls = ids.map((id) => tools.host.gated({ id }))
  593. for (const id of ids) await tools.host.open({ id })
  594. const results = await Promise.all(calls)
  595. return results.length
  596. `,
  597. { trace },
  598. )
  599. expect(result).toBe(20)
  600. expect(trace.maxActive).toBe(20)
  601. })
  602. test("resolves the empty array", async () => {
  603. expect(await value(`return await Promise.all([])`)).toEqual([])
  604. })
  605. test("rejects with the first failure, catchable in-program", async () => {
  606. const result = await run(`
  607. try {
  608. await Promise.all([tools.host.echo({ id: 1 }), tools.host.fail({})])
  609. return "no"
  610. } catch (e) {
  611. return e.message
  612. }
  613. `)
  614. expect(result.ok).toBe(true)
  615. if (!result.ok) return
  616. expect(result.value).toBe("Lookup refused")
  617. expect(result.warnings).toBeUndefined()
  618. })
  619. test("rejects before an earlier slow promise fulfills", async () => {
  620. const trace = makeTrace()
  621. expect(
  622. await value(
  623. `
  624. try {
  625. await Promise.all([
  626. tools.host.pending({ id: 1 }),
  627. tools.host.fail({}),
  628. ])
  629. return -1
  630. } catch {
  631. return await tools.host.completed({})
  632. }
  633. `,
  634. { trace },
  635. ),
  636. ).toBe(0)
  637. // The surviving member is observed (Promise.all handled it), so completion interrupts
  638. // it instead of waiting for it.
  639. expect(trace.completed).toBe(0)
  640. expect(trace.interrupted).toBe(1)
  641. })
  642. test("fail-fast does not cancel a sibling the program still holds and awaits", async () => {
  643. const trace = makeTrace()
  644. expect(
  645. await value(
  646. `
  647. const slow = tools.host.gated({ id: 1 })
  648. try {
  649. await Promise.all([slow, tools.host.fail({})])
  650. return "no"
  651. } catch {}
  652. await tools.host.open({ id: 1 })
  653. return await slow
  654. `,
  655. { trace },
  656. ),
  657. ).toBe(1)
  658. expect(trace.completed).toBe(1)
  659. expect(trace.interrupted).toBe(0)
  660. })
  661. test("a slower observed sibling is interrupted at completion after failing fast", async () => {
  662. const trace = makeTrace()
  663. expect(
  664. await value(
  665. `
  666. const failLater = async () => {
  667. await tools.host.pending({ id: 1 })
  668. throw new Error("later")
  669. }
  670. const aggregate = Promise.all([Promise.reject(new Error("first")), failLater()])
  671. try {
  672. await aggregate
  673. return "no"
  674. } catch (error) {
  675. return error.message
  676. }
  677. `,
  678. { trace },
  679. ),
  680. ).toBe("first")
  681. expect(trace.completed).toBe(0)
  682. expect(trace.interrupted).toBe(1)
  683. })
  684. test("a non-collection argument is a clear error", async () => {
  685. const diagnostic = await error(`return await Promise.all(42)`)
  686. expect(diagnostic.message).toContain("Promise.all expects an array")
  687. })
  688. test("exceeding maxToolCalls inside Promise.all is a ToolCallLimitExceeded diagnostic", async () => {
  689. const diagnostic = await error(
  690. `return await Promise.all([tools.host.echo({ id: 1 }), tools.host.echo({ id: 2 }), tools.host.echo({ id: 3 })])`,
  691. { limits: { maxToolCalls: 2 } },
  692. )
  693. expect(diagnostic.kind).toBe("ToolCallLimitExceeded")
  694. })
  695. })
  696. describe("Promise.allSettled", () => {
  697. test("reports fulfilled and rejected outcomes with catch-normalized reasons", async () => {
  698. expect(
  699. await value(`
  700. return await Promise.allSettled([
  701. tools.host.echo({ id: 5 }),
  702. tools.host.fail({}),
  703. "plain",
  704. Promise.reject(new Error("boom")),
  705. ])
  706. `),
  707. ).toEqual([
  708. { status: "fulfilled", value: 5 },
  709. { status: "rejected", reason: { name: "Error", message: "Lookup refused" } },
  710. { status: "fulfilled", value: "plain" },
  711. { status: "rejected", reason: { name: "Error", message: "boom" } },
  712. ])
  713. })
  714. test("never rejects for program-level failures", async () => {
  715. const result = await run(`
  716. const settled = await Promise.allSettled([tools.host.fail({}), tools.host.fail({})])
  717. return settled.filter((s) => s.status === "rejected").length
  718. `)
  719. expect(result.ok).toBe(true)
  720. if (!result.ok) return
  721. expect(result.value).toBe(2)
  722. expect(result.warnings).toBeUndefined()
  723. })
  724. })
  725. describe("Promise.race", () => {
  726. test("first settlement wins and a direct loser is interrupted at completion", async () => {
  727. const trace = makeTrace()
  728. const result = await value(
  729. `
  730. const fast = tools.host.echo({ id: 1 })
  731. const slow = tools.host.pending({ id: 2 })
  732. return await Promise.race([fast, slow])
  733. `,
  734. { trace },
  735. )
  736. expect(result).toBe(1)
  737. // The loser is observed (the race handled it), so the execution does not wait for it.
  738. expect(trace.completed).toBe(1)
  739. expect(trace.interrupted).toBe(1)
  740. })
  741. test("a direct loser remains awaitable after the race settles", async () => {
  742. expect(
  743. await value(`
  744. const fast = tools.host.echo({ id: 1 })
  745. const slow = tools.host.gated({ id: 2 })
  746. const winner = await Promise.race([fast, slow])
  747. await tools.host.open({ id: 2 })
  748. return { winner, loser: await slow }
  749. `),
  750. ).toEqual({ winner: 1, loser: 2 })
  751. })
  752. test("a nested aggregate loser and its members are interrupted at completion", async () => {
  753. const trace = makeTrace()
  754. expect(
  755. await value(
  756. `
  757. const nested = Promise.all([
  758. tools.host.pending({ id: 1 }),
  759. tools.host.pending({ id: 2 }),
  760. ])
  761. return await Promise.race(["immediate", nested])
  762. `,
  763. { trace },
  764. ),
  765. ).toBe("immediate")
  766. // The nested aggregate and its members are all observed, so nothing waits for them.
  767. expect(trace.completed).toBe(0)
  768. expect(trace.interrupted).toBe(2)
  769. })
  770. test("a rejection can win the race", async () => {
  771. expect(
  772. await value(`
  773. try {
  774. await Promise.race([tools.host.fail({}), tools.host.pending({ id: 1 })])
  775. return "no"
  776. } catch (e) {
  777. return e.message
  778. }
  779. `),
  780. ).toBe("Lookup refused")
  781. })
  782. test("a plain value wins over pending promises", async () => {
  783. const trace = makeTrace()
  784. expect(await value(`return await Promise.race([tools.host.pending({ id: 1 }), "immediate"])`, { trace })).toBe(
  785. "immediate",
  786. )
  787. expect(trace.completed).toBe(0)
  788. expect(trace.interrupted).toBe(1)
  789. })
  790. test("a rejected race loser is observed by the aggregate", async () => {
  791. const result = await run(`return await Promise.race(["winner", tools.host.fail({})])`)
  792. expect(result.ok).toBe(true)
  793. if (!result.ok) return
  794. expect(result.value).toBe("winner")
  795. expect(result.warnings).toBeUndefined()
  796. })
  797. test("an empty race is a clear error instead of hanging", async () => {
  798. const diagnostic = await error(`return await Promise.race([])`)
  799. expect(diagnostic.message).toContain("never settle")
  800. })
  801. })
  802. describe("Promise.resolve / Promise.reject", () => {
  803. test("resolve wraps plain values and passes promises through", async () => {
  804. expect(await value(`return await Promise.resolve(42)`)).toBe(42)
  805. expect(await value(`return await Promise.resolve(Promise.resolve("nested"))`)).toBe("nested")
  806. expect(await value(`return await Promise.resolve(tools.host.echo({ id: 3 }))`)).toBe(3)
  807. expect(await value(`const promise = Promise.resolve(1); return [promise].includes(Promise.resolve(promise))`)).toBe(
  808. true,
  809. )
  810. })
  811. test("reject produces a promise whose await throws the reason", async () => {
  812. expect(
  813. await value(`
  814. try {
  815. await Promise.reject("nope")
  816. return "no"
  817. } catch (e) {
  818. return e
  819. }
  820. `),
  821. ).toBe("nope")
  822. })
  823. test("a rejection observed after settlement is handled", async () => {
  824. expect(
  825. await value(`
  826. const rejected = Promise.reject(new Error("handled"))
  827. await tools.host.echo({ id: 1 })
  828. try {
  829. await rejected
  830. return "no"
  831. } catch (error) {
  832. return error.message
  833. }
  834. `),
  835. ).toBe("handled")
  836. })
  837. test("an abandoned rejected promise is reported as unhandled", async () => {
  838. const result = await run(`
  839. Promise.reject(new Error("abandoned"))
  840. return "done"
  841. `)
  842. expect(result.ok).toBe(true)
  843. if (!result.ok) return
  844. expect(result.value).toBe("done")
  845. expect(result.warnings).toStrictEqual([
  846. { kind: "ExecutionFailure", message: "Unhandled rejection from an un-awaited promise: Uncaught: abandoned" },
  847. ])
  848. })
  849. })
  850. describe("timeout interruption of forked calls", () => {
  851. test("the execution timeout interrupts in-flight forked fibers", async () => {
  852. const trace = makeTrace()
  853. const result = await run(
  854. `
  855. const a = tools.host.pending({ id: 1 })
  856. const b = tools.host.pending({ id: 2 })
  857. return await a
  858. `,
  859. { trace, limits: { timeoutMs: 100 } },
  860. )
  861. expect(result.ok).toBe(false)
  862. if (result.ok) return
  863. expect(result.error.kind).toBe("TimeoutExceeded")
  864. // Both calls started; neither escaped the timeout - the awaited one AND the abandoned one.
  865. expect(trace.starts).toEqual([1, 2])
  866. expect(trace.interrupted).toBe(2)
  867. expect(trace.completed).toBe(0)
  868. })
  869. test("the timeout also interrupts calls inside Promise.all", async () => {
  870. const trace = makeTrace()
  871. const result = await run(
  872. `return await Promise.all([tools.host.pending({ id: 1 }), tools.host.pending({ id: 2 })])`,
  873. { trace, limits: { timeoutMs: 100 } },
  874. )
  875. expect(result.ok).toBe(false)
  876. if (result.ok) return
  877. expect(result.error.kind).toBe("TimeoutExceeded")
  878. expect(trace.interrupted).toBe(2)
  879. })
  880. test("a non-settling race loser cannot hold the execution to the timeout", async () => {
  881. const trace = makeTrace()
  882. const result = await run(`return await Promise.race(["winner", tools.host.pending({ id: 1 })])`, {
  883. trace,
  884. limits: { timeoutMs: 100 },
  885. })
  886. // Completion interrupts the observed loser immediately; the race result survives.
  887. expect(result.ok).toBe(true)
  888. if (!result.ok) return
  889. expect(result.value).toBe("winner")
  890. expect(result.warnings).toBeUndefined()
  891. expect(trace.starts).toEqual([1])
  892. expect(trace.completed).toBe(0)
  893. expect(trace.interrupted).toBe(1)
  894. })
  895. test("a timeout during completion cleanup keeps the computed value and warns", async () => {
  896. const trace = makeTrace()
  897. const result = await run(
  898. `
  899. tools.host.stubborn({ cleanupMs: 400 })
  900. return "done"
  901. `,
  902. { trace, limits: { timeoutMs: 100 } },
  903. )
  904. expect(result.ok).toBe(true)
  905. if (!result.ok) return
  906. expect(result.value).toBe("done")
  907. expect(result.warnings).toStrictEqual([
  908. {
  909. kind: "TimeoutExceeded",
  910. message:
  911. "The program returned, but background work was still running at the 100ms timeout and was interrupted. Await all started promises.",
  912. },
  913. ])
  914. expect(trace.interrupted).toBe(1)
  915. expect(trace.completed).toBe(0)
  916. })
  917. test("a timeout during completion cleanup reports the timeout warning before settled rejections", async () => {
  918. const result = await run(
  919. `
  920. tools.host.fail({})
  921. tools.host.stubborn({ cleanupMs: 400 })
  922. return "done"
  923. `,
  924. { limits: { timeoutMs: 100 } },
  925. )
  926. expect(result.ok).toBe(true)
  927. if (!result.ok) return
  928. expect(result.value).toBe("done")
  929. expect(result.warnings).toStrictEqual([
  930. {
  931. kind: "TimeoutExceeded",
  932. message:
  933. "The program returned, but background work was still running at the 100ms timeout and was interrupted. Await all started promises.",
  934. },
  935. { kind: "ToolFailure", message: "Unhandled rejection from an un-awaited promise: Lookup refused" },
  936. ])
  937. })
  938. })
  939. describe("promise chaining", () => {
  940. test("then transforms tool results and adopts returned promises across a chain", async () => {
  941. expect(
  942. await value(`
  943. return await tools.host
  944. .echo({ id: 2 })
  945. .then((id) => tools.host.echo({ id: id + 1 }))
  946. .then((id) => id * 10)
  947. `),
  948. ).toBe(30)
  949. })
  950. test("handlers are deferred and run in attach order", async () => {
  951. expect(
  952. await value(`
  953. const order = []
  954. const promise = Promise.resolve(1)
  955. promise.then(() => order.push("h1"))
  956. promise.then(() => order.push("h2"))
  957. order.push("sync")
  958. await promise
  959. return order
  960. `),
  961. ).toEqual(["sync", "h1", "h2"])
  962. })
  963. test("catch recovers a tool failure and preserves fulfillment", async () => {
  964. expect(
  965. await value(`
  966. return [
  967. await tools.host.fail({}).catch((error) => error.message),
  968. await tools.host.echo({ id: 4 }).catch(() => "unused"),
  969. ]
  970. `),
  971. ).toEqual(["Lookup refused", 4])
  972. })
  973. test("finally observes settlement without changing the value", async () => {
  974. expect(
  975. await value(`
  976. const events = []
  977. const result = await tools.host.echo({ id: 5 }).finally(() => events.push("cleanup"))
  978. return [result, events]
  979. `),
  980. ).toEqual([5, ["cleanup"]])
  981. })
  982. test("a settled, un-awaited rejected chain tail warns exactly once", async () => {
  983. const result = await run(`
  984. Promise.reject(new Error("boom")).then((value) => value)
  985. await Promise.resolve()
  986. return "done"
  987. `)
  988. expect(result.ok).toBe(true)
  989. if (!result.ok) return
  990. expect(result.value).toBe("done")
  991. // The source rejection belongs to the chain (no warning); only the derived tail warns.
  992. expect(result.warnings).toStrictEqual([
  993. { kind: "ExecutionFailure", message: "Unhandled rejection from an un-awaited promise: Uncaught: boom" },
  994. ])
  995. })
  996. test("a catch handler silences the chain's rejection warning", async () => {
  997. const result = await run(`
  998. Promise.reject(new Error("boom")).catch(() => "handled")
  999. await Promise.resolve()
  1000. return "done"
  1001. `)
  1002. expect(result.ok).toBe(true)
  1003. if (!result.ok) return
  1004. expect(result.warnings).toBeUndefined()
  1005. })
  1006. test("unsupported callable handlers fail loudly with a wrap hint", async () => {
  1007. const diagnostic = await error(`return await tools.host.echo({ id: 1 }).then(tools.host.completed)`)
  1008. expect(diagnostic.message).toContain("Promise.prototype.then cannot use this callable as a handler")
  1009. expect(diagnostic.message).toContain("wrap it in an arrow function")
  1010. })
  1011. test("chaining methods are opaque references until called", async () => {
  1012. expect(await value(`return typeof tools.host.echo({ id: 1 }).then`)).toBe("function")
  1013. })
  1014. })
  1015. describe("combinator settlement timing", () => {
  1016. test("a combinator settling one reaction turn after the program returns is interrupted silently", async () => {
  1017. // The aggregate's one-turn settlement delay (V8 parity) means an immediately-returning
  1018. // program abandons it while still pending: interrupted like any pending work, so no
  1019. // rejection warning survives - the member itself was observed by the combinator.
  1020. const result = await run(`
  1021. Promise.all([Promise.reject(new Error("boom"))])
  1022. return "done"
  1023. `)
  1024. expect(result.ok).toBe(true)
  1025. if (!result.ok) return
  1026. expect(result.value).toBe("done")
  1027. expect(result.warnings).toBeUndefined()
  1028. })
  1029. test("a combinator settles one reaction turn after its members, as in V8", async () => {
  1030. // Regression for the race winner flip: Promise.all's settlement burns a reaction turn,
  1031. // so a plain resolved value entered in the same race wins, and a fail-fast aggregate
  1032. // cannot beat it into rejection.
  1033. expect(
  1034. await value(`
  1035. const pending = tools.host.pending({ id: 9 })
  1036. const winner = await Promise.race([Promise.all([Promise.resolve(1)]), Promise.resolve(2)])
  1037. try {
  1038. const raced = await Promise.race([Promise.all([Promise.reject("x"), pending]), Promise.resolve("ok")])
  1039. return [winner, "fulfilled", raced]
  1040. } catch (reason) {
  1041. return [winner, "rejected", reason]
  1042. }
  1043. `),
  1044. ).toEqual([2, "fulfilled", "ok"])
  1045. })
  1046. })
  1047. describe("unsupported promise surface", () => {
  1048. test("other property reads on a promise hint at the missing await", async () => {
  1049. const diagnostic = await error(`return tools.host.echo({ id: 1 }).value`)
  1050. expect(diagnostic.kind).toBe("InvalidDataValue")
  1051. expect(diagnostic.message).toContain("un-awaited Promise")
  1052. expect(diagnostic.message).toContain("await it first")
  1053. })
  1054. test("unknown Promise statics list what is available", async () => {
  1055. const diagnostic = await error(`return await Promise.withResolvers()`)
  1056. expect(diagnostic.message).toContain("Promise.withResolvers is not available")
  1057. expect(diagnostic.message).toContain("Promise.any")
  1058. })
  1059. })
  1060. describe("Promise.any", () => {
  1061. test("first tool success wins; failing and losing calls are handled silently", async () => {
  1062. const trace = makeTrace()
  1063. const result = await run(
  1064. `
  1065. const winner = await Promise.any([
  1066. tools.host.fail({}),
  1067. tools.host.echo({ id: 1 }),
  1068. tools.host.pending({ id: 2 }),
  1069. ])
  1070. return winner
  1071. `,
  1072. { trace },
  1073. )
  1074. expect(result.ok).toBe(true)
  1075. if (!result.ok) return
  1076. expect(result.value).toBe(1)
  1077. // The slow loser stays execution-owned and is interrupted at completion; the tool
  1078. // failure was observed by the aggregate, so no rejection warning survives.
  1079. expect(result.warnings).toBeUndefined()
  1080. expect(trace.interrupted).toBe(1)
  1081. })
  1082. test("all members failing rejects with catch-normalized reasons in input order", async () => {
  1083. expect(
  1084. await value(`
  1085. try {
  1086. await Promise.any([tools.host.fail({}), Promise.reject("plain")])
  1087. return "fulfilled"
  1088. } catch (error) {
  1089. return [error.name, error.errors.map((reason) => reason.message ?? reason)]
  1090. }
  1091. `),
  1092. ).toEqual(["AggregateError", ["Lookup refused", "plain"]])
  1093. })
  1094. test("settles one reaction turn after its deciding member, as in V8", async () => {
  1095. expect(await value(`return await Promise.race([Promise.any([Promise.resolve(1)]), Promise.resolve(2)])`)).toBe(2)
  1096. })
  1097. test("a tie is decided by settlement order, not input order", async () => {
  1098. // Handlers run in attach order, so `first` settles before `second` and wins
  1099. // despite its later input position - as in real JS.
  1100. expect(
  1101. await value(`
  1102. const first = Promise.resolve().then(() => "one")
  1103. const second = Promise.resolve().then(() => "two")
  1104. return await Promise.any([second, first])
  1105. `),
  1106. ).toBe("one")
  1107. })
  1108. test("an abandoned rejecting aggregate is interrupted silently at the return", async () => {
  1109. const result = await run(`
  1110. Promise.any([Promise.reject(new Error("boom"))])
  1111. return "done"
  1112. `)
  1113. expect(result.ok).toBe(true)
  1114. if (!result.ok) return
  1115. expect(result.value).toBe("done")
  1116. expect(result.warnings).toBeUndefined()
  1117. })
  1118. })
  1119. describe("promise construction", () => {
  1120. test("a deferred gate coordinates tool results across async functions", async () => {
  1121. expect(
  1122. await value(`
  1123. let openGate
  1124. const gate = new Promise((resolve) => { openGate = resolve })
  1125. const worker = (async () => {
  1126. const id = await gate
  1127. return id * 2
  1128. })()
  1129. openGate(await tools.host.echo({ id: 21 }))
  1130. return await worker
  1131. `),
  1132. ).toBe(42)
  1133. })
  1134. test("the .then(resolve) bridge settles a constructed promise", async () => {
  1135. expect(
  1136. await value(`
  1137. const bridged = new Promise((resolve, reject) => {
  1138. tools.host.echo({ id: 7 }).then(resolve, reject)
  1139. })
  1140. return await bridged
  1141. `),
  1142. ).toBe(7)
  1143. })
  1144. test("constructed promises participate in combinators", async () => {
  1145. expect(
  1146. await value(`
  1147. let settle
  1148. const manual = new Promise((resolve) => { settle = resolve })
  1149. const race = Promise.race([manual, tools.host.pending({ id: 3 })])
  1150. const all = Promise.all([manual, "plain"])
  1151. const any = Promise.any([manual, new Promise(() => {})])
  1152. settle("manual")
  1153. return [await race, await all, await any]
  1154. `),
  1155. ).toEqual(["manual", ["manual", "plain"], "manual"])
  1156. })
  1157. test("resolving with a pending promise adopts its later settlement", async () => {
  1158. expect(
  1159. await value(`
  1160. let innerResolve, innerReject
  1161. const adopted = new Promise((resolve) => resolve(new Promise((resolve) => { innerResolve = resolve })))
  1162. const adoptedRejection = new Promise((resolve) => resolve(new Promise((_, reject) => { innerReject = reject })))
  1163. innerResolve("later")
  1164. innerReject("bad")
  1165. try {
  1166. return [await adopted, await adoptedRejection]
  1167. } catch (reason) {
  1168. return [await adopted, reason]
  1169. }
  1170. `),
  1171. ).toEqual(["later", "bad"])
  1172. })
  1173. test("an async executor's post-await resolve settles the promise", async () => {
  1174. expect(
  1175. await value(`
  1176. const result = new Promise(async (resolve) => {
  1177. const id = await tools.host.echo({ id: 5 })
  1178. resolve(id * 2)
  1179. })
  1180. return await result
  1181. `),
  1182. ).toBe(10)
  1183. })
  1184. test("a never-settled promise is abandoned silently at the return", async () => {
  1185. const result = await run(`
  1186. const forever = new Promise(() => {})
  1187. forever.then(() => {})
  1188. return "done"
  1189. `)
  1190. expect(result.ok).toBe(true)
  1191. if (!result.ok) return
  1192. expect(result.value).toBe("done")
  1193. expect(result.warnings).toBeUndefined()
  1194. })
  1195. test("an un-awaited constructed rejection is reported like any unhandled rejection", async () => {
  1196. const result = await run(`
  1197. new Promise((_, reject) => reject(new Error("dropped")))
  1198. await Promise.resolve()
  1199. await Promise.resolve()
  1200. return "done"
  1201. `)
  1202. expect(result.ok).toBe(true)
  1203. if (!result.ok) return
  1204. expect(result.value).toBe("done")
  1205. expect(result.warnings).toHaveLength(1)
  1206. expect(result.warnings?.[0].message).toContain("Unhandled rejection")
  1207. expect(result.warnings?.[0].message).toContain("dropped")
  1208. })
  1209. test("resolver functions cannot cross the data boundary", async () => {
  1210. const diagnostic = await error(`
  1211. let escaped
  1212. new Promise((resolve) => { escaped = resolve })
  1213. return { escaped }
  1214. `)
  1215. expect(diagnostic.kind).toBe("InvalidDataValue")
  1216. })
  1217. })