fix(codemode): return promises from combinators (#35782)
This commit is contained in:
parent
a6449cb45c
commit
39cceeb143
14 changed files with 1794 additions and 479 deletions
|
|
@ -506,6 +506,26 @@ describe("CodeMode public contract", () => {
|
|||
expect(Schema.decodeUnknownSync(CodeMode.Result)(JSON.parse(JSON.stringify(reusable)))).toStrictEqual(reusable)
|
||||
})
|
||||
|
||||
test("a reused execution Effect starts from a clean slate", async () => {
|
||||
const echo = Tool.make({
|
||||
description: "echo",
|
||||
input: Schema.Struct({}),
|
||||
output: Schema.Number,
|
||||
run: () => Effect.succeed(1),
|
||||
})
|
||||
const effect = CodeMode.execute({
|
||||
tools: { host: { echo } },
|
||||
code: `console.log("hi"); return await tools.host.echo({})`,
|
||||
limits: { maxToolCalls: 1 },
|
||||
})
|
||||
const first = await Effect.runPromise(effect)
|
||||
const second = await Effect.runPromise(effect)
|
||||
// Per-execution state (tool-call budget and audit list, logs, timeout bookkeeping) must
|
||||
// bind at run time, so the second run neither exhausts the budget nor leaks run 1's logs.
|
||||
expect(first).toStrictEqual(second)
|
||||
expect(second).toStrictEqual({ ok: true, value: 1, logs: ["hi"], toolCalls: [{ name: "host.echo" }] })
|
||||
})
|
||||
|
||||
test("inlines a COMPLETE small catalog and keeps search registered but unadvertised", async () => {
|
||||
const runtime = CodeMode.make({ tools })
|
||||
expect(runtime.catalog()).toStrictEqual([
|
||||
|
|
|
|||
906
packages/codemode/test/promise-test262.test.ts
Normal file
906
packages/codemode/test/promise-test262.test.ts
Normal file
|
|
@ -0,0 +1,906 @@
|
|||
/*
|
||||
* Portions adapted from Test262 at revision 250f204f23a9249ff204be2baec29600faae7b75.
|
||||
* Every test names its upstream source; test.failing cases are executable conformance
|
||||
* targets for intended Promise behavior that CodeMode does not implement yet.
|
||||
*
|
||||
* Copyright 2014 Cubane Canada, Inc. All rights reserved.
|
||||
* Copyright 2015 Microsoft Corporation. All rights reserved.
|
||||
* Copyright 2016 Microsoft, Inc. All rights reserved.
|
||||
* Copyright 2017 Caitlin Potter. All rights reserved.
|
||||
* Copyright (C) 2016-2020 the V8 project authors. All rights reserved.
|
||||
* Copyright (C) 2018-2020 Rick Waldron. All rights reserved.
|
||||
* Copyright (C) 2019 Leo Balter. All rights reserved.
|
||||
* Test262 portions are governed by the BSD license in LICENSE.test262.
|
||||
*/
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import { CodeMode } from "../src/index.js"
|
||||
|
||||
const execute = (code: string) =>
|
||||
Effect.runPromise(CodeMode.execute({ code, tools: {}, limits: { timeoutMs: 1_000 } }))
|
||||
|
||||
const value = async (code: string) => {
|
||||
const result = await execute(code)
|
||||
if (!result.ok) throw new Error(`expected success, got ${result.error.kind}: ${result.error.message}`)
|
||||
return result.value
|
||||
}
|
||||
|
||||
describe("Test262 Promise statics", () => {
|
||||
test("statics are callable and return promises", async () => {
|
||||
// Sources:
|
||||
// test/built-ins/Promise/all/S25.4.4.1_A1.1_T1.js
|
||||
// test/built-ins/Promise/allSettled/is-function.js
|
||||
// test/built-ins/Promise/allSettled/returns-promise.js
|
||||
// test/built-ins/Promise/race/S25.4.4.3_A1.1_T1.js
|
||||
// test/built-ins/Promise/resolve/S25.4.4.5_A1.1_T1.js
|
||||
// test/built-ins/Promise/reject/S25.4.4.4_A1.1_T1.js
|
||||
expect(
|
||||
await value(`
|
||||
const values = [
|
||||
Promise.all([]),
|
||||
Promise.allSettled([]),
|
||||
Promise.race([undefined]),
|
||||
Promise.resolve(),
|
||||
Promise.reject(),
|
||||
]
|
||||
const callable = [
|
||||
typeof Promise.all,
|
||||
typeof Promise.allSettled,
|
||||
typeof Promise.race,
|
||||
typeof Promise.resolve,
|
||||
typeof Promise.reject,
|
||||
]
|
||||
try { await values[4] } catch {}
|
||||
return [callable, values.map((item) => item instanceof Promise)]
|
||||
`),
|
||||
).toEqual([
|
||||
["function", "function", "function", "function", "function"],
|
||||
[true, true, true, true, true],
|
||||
])
|
||||
})
|
||||
|
||||
test("Promise.all returns fresh arrays for empty and settled inputs", async () => {
|
||||
// Sources:
|
||||
// test/built-ins/Promise/all/S25.4.4.1_A2.1_T1.js
|
||||
// test/built-ins/Promise/all/S25.4.4.1_A2.3_T1.js
|
||||
// test/built-ins/Promise/all/S25.4.4.1_A2.3_T2.js
|
||||
// test/built-ins/Promise/all/S25.4.4.1_A2.3_T3.js
|
||||
// test/built-ins/Promise/all/S25.4.4.1_A7.1_T1.js
|
||||
expect(
|
||||
await value(`
|
||||
const input = []
|
||||
const emptyPromise = Promise.all(input)
|
||||
const empty = await emptyPromise
|
||||
const onePromise = Promise.all([Promise.resolve(3)])
|
||||
const one = await onePromise
|
||||
return [
|
||||
emptyPromise instanceof Promise,
|
||||
empty instanceof Array,
|
||||
empty.length,
|
||||
empty !== input,
|
||||
onePromise instanceof Promise,
|
||||
one instanceof Array,
|
||||
one.length,
|
||||
one[0],
|
||||
]
|
||||
`),
|
||||
).toEqual([true, true, 0, true, true, true, 1, 3])
|
||||
})
|
||||
|
||||
test("Promise.all adopts values and preserves input order and identity", async () => {
|
||||
// Sources:
|
||||
// test/built-ins/Promise/all/resolve-non-thenable.js
|
||||
// test/built-ins/Promise/all/S25.4.4.1_A8.2_T1.js
|
||||
// test/built-ins/Promise/all/S25.4.4.1_A8.2_T2.js
|
||||
const result = await value(`
|
||||
const first = { id: 1 }
|
||||
const second = { id: 2 }
|
||||
const values = await Promise.all([Promise.resolve(3), first, Promise.resolve(second)])
|
||||
const observe = async (promise) => {
|
||||
try { await promise; return "fulfilled" } catch (reason) { return reason }
|
||||
}
|
||||
return [
|
||||
values.length,
|
||||
values[0],
|
||||
values[1] === first,
|
||||
values[2] === second,
|
||||
await observe(Promise.all([Promise.reject(1), Promise.resolve(2)])),
|
||||
await observe(Promise.all([Promise.resolve(1), Promise.reject(2)])),
|
||||
]
|
||||
`)
|
||||
expect(result).toEqual([3, 3, true, true, 1, 2])
|
||||
})
|
||||
|
||||
test("Promise.allSettled returns fresh arrays and ordered outcome records", async () => {
|
||||
// Sources:
|
||||
// test/built-ins/Promise/allSettled/resolves-empty-array.js
|
||||
// test/built-ins/Promise/allSettled/resolves-to-array.js
|
||||
// test/built-ins/Promise/allSettled/resolved-all-fulfilled.js
|
||||
// test/built-ins/Promise/allSettled/resolved-all-rejected.js
|
||||
// test/built-ins/Promise/allSettled/resolved-all-mixed.js
|
||||
// test/built-ins/Promise/allSettled/resolve-non-thenable.js
|
||||
expect(
|
||||
await value(`
|
||||
const input = []
|
||||
const empty = await Promise.allSettled(input)
|
||||
const reason = { id: 4 }
|
||||
const object = { id: 5 }
|
||||
const outcomes = await Promise.allSettled([
|
||||
Promise.resolve(1),
|
||||
Promise.reject(2),
|
||||
3,
|
||||
Promise.reject(reason),
|
||||
object,
|
||||
])
|
||||
return [
|
||||
empty instanceof Array,
|
||||
empty.length,
|
||||
empty !== input,
|
||||
outcomes,
|
||||
outcomes[4].value === object,
|
||||
outcomes.map((item) => Object.keys(item)),
|
||||
]
|
||||
`),
|
||||
).toEqual([
|
||||
true,
|
||||
0,
|
||||
true,
|
||||
[
|
||||
{ status: "fulfilled", value: 1 },
|
||||
{ status: "rejected", reason: 2 },
|
||||
{ status: "fulfilled", value: 3 },
|
||||
{ status: "rejected", reason: { id: 4 } },
|
||||
{ status: "fulfilled", value: { id: 5 } },
|
||||
],
|
||||
true,
|
||||
[
|
||||
["status", "value"],
|
||||
["status", "reason"],
|
||||
["status", "value"],
|
||||
["status", "reason"],
|
||||
["status", "value"],
|
||||
],
|
||||
])
|
||||
})
|
||||
|
||||
test("Promise.race preserves fulfillment, rejection, and iterable order", async () => {
|
||||
// Sources:
|
||||
// test/built-ins/Promise/race/S25.4.4.3_A6.2_T1.js
|
||||
// test/built-ins/Promise/race/S25.4.4.3_A7.1_T1.js
|
||||
// test/built-ins/Promise/race/S25.4.4.3_A7.2_T1.js
|
||||
// test/built-ins/Promise/race/S25.4.4.3_A7.3_T1.js
|
||||
// test/built-ins/Promise/race/S25.4.4.3_A7.3_T2.js
|
||||
expect(
|
||||
await value(`
|
||||
const observe = async (promise) => {
|
||||
try { return ["fulfilled", await promise] } catch (reason) { return ["rejected", reason] }
|
||||
}
|
||||
return await Promise.all([
|
||||
observe(Promise.race([23])),
|
||||
observe(Promise.race([Promise.reject(7)])),
|
||||
observe(Promise.race([Promise.resolve(1), Promise.resolve(2)])),
|
||||
observe(Promise.race([Promise.reject(3), Promise.resolve(4)])),
|
||||
])
|
||||
`),
|
||||
).toEqual([
|
||||
["fulfilled", 23],
|
||||
["rejected", 7],
|
||||
["fulfilled", 1],
|
||||
["rejected", 3],
|
||||
])
|
||||
})
|
||||
|
||||
test("combinators consume supported string iterables", async () => {
|
||||
// Sources:
|
||||
// test/built-ins/Promise/all/iter-arg-is-string-resolve.js
|
||||
// test/built-ins/Promise/allSettled/iter-arg-is-string-resolve.js
|
||||
// test/built-ins/Promise/race/iter-arg-is-string-resolve.js
|
||||
expect(
|
||||
await value(`
|
||||
return [
|
||||
await Promise.all("abc"),
|
||||
await Promise.allSettled("ab"),
|
||||
await Promise.race("abc"),
|
||||
]
|
||||
`),
|
||||
).toEqual([
|
||||
["a", "b", "c"],
|
||||
[
|
||||
{ status: "fulfilled", value: "a" },
|
||||
{ status: "fulfilled", value: "b" },
|
||||
],
|
||||
"a",
|
||||
])
|
||||
})
|
||||
|
||||
test("Promise.resolve adopts values and preserves sandbox-promise identity", async () => {
|
||||
// Sources:
|
||||
// test/built-ins/Promise/resolve/S25.4.4.5_A2.1_T1.js
|
||||
// test/built-ins/Promise/resolve/resolve-non-obj.js
|
||||
// test/built-ins/Promise/resolve/resolve-non-thenable.js
|
||||
expect(
|
||||
await value(`
|
||||
const object = { id: 1 }
|
||||
const promise = Promise.resolve(1)
|
||||
return [
|
||||
await Promise.resolve(23),
|
||||
await Promise.resolve(Promise.resolve(24)),
|
||||
(await Promise.resolve(object)) === object,
|
||||
[promise].includes(Promise.resolve(promise)),
|
||||
]
|
||||
`),
|
||||
).toEqual([23, 24, true, true])
|
||||
})
|
||||
|
||||
test("Promise.reject preserves primitive and object reasons", async () => {
|
||||
// Sources:
|
||||
// test/built-ins/Promise/reject/S25.4.4.4_A2.1_T1.js
|
||||
const result = await value(`
|
||||
const object = { reason: true }
|
||||
const reasons = [undefined, null, false, true, 0, "", 42, object]
|
||||
const observe = async (reason) => {
|
||||
try { await Promise.reject(reason); return false } catch (caught) { return caught === reason }
|
||||
}
|
||||
return await Promise.all(reasons.map(observe))
|
||||
`)
|
||||
expect(result).toEqual([true, true, true, true, true, true, true, true])
|
||||
})
|
||||
|
||||
test("Promise.all resolves duplicate members into every slot", async () => {
|
||||
// Sources:
|
||||
// test/built-ins/Promise/all/invoke-resolve-on-promises-every-iteration-of-promise.js
|
||||
// test/built-ins/Promise/all/invoke-resolve-on-values-every-iteration-of-promise.js
|
||||
// (adapted: CodeMode has no observable Promise.resolve hook, so per-iteration
|
||||
// handling of a repeated member is asserted through the resolved slots)
|
||||
expect(
|
||||
await value(`
|
||||
const settled = Promise.resolve(3)
|
||||
const computed = (async () => "computed")()
|
||||
return [
|
||||
await Promise.all([settled, settled, settled]),
|
||||
await Promise.all([computed, "plain", computed]),
|
||||
]
|
||||
`),
|
||||
).toEqual([
|
||||
[3, 3, 3],
|
||||
["computed", "plain", "computed"],
|
||||
])
|
||||
})
|
||||
|
||||
test("Promise.allSettled records duplicate members independently", async () => {
|
||||
// Source: test/built-ins/Promise/allSettled/invoke-resolve-on-promises-every-iteration-of-promise.js
|
||||
// (adapted: per-iteration handling of a repeated member is asserted through the
|
||||
// outcome records instead of a Promise.resolve hook)
|
||||
expect(
|
||||
await value(`
|
||||
const good = Promise.resolve(1)
|
||||
const bad = Promise.reject(2)
|
||||
return await Promise.allSettled([good, bad, good, bad])
|
||||
`),
|
||||
).toEqual([
|
||||
{ status: "fulfilled", value: 1 },
|
||||
{ status: "rejected", reason: 2 },
|
||||
{ status: "fulfilled", value: 1 },
|
||||
{ status: "rejected", reason: 2 },
|
||||
])
|
||||
})
|
||||
|
||||
test("combinators adopt members that settled before the call", async () => {
|
||||
// Sources:
|
||||
// test/built-ins/Promise/all/reject-immed.js
|
||||
// test/built-ins/Promise/allSettled/reject-immed.js
|
||||
// test/built-ins/Promise/race/reject-immed.js
|
||||
// (adapted: immediately-rejecting thenables become sandbox promises that settled,
|
||||
// and were even observed, before the combinator call)
|
||||
expect(
|
||||
await value(`
|
||||
const fulfilled = Promise.resolve("done")
|
||||
const rejected = Promise.reject("failed")
|
||||
try { await rejected } catch {}
|
||||
const observe = async (promise) => {
|
||||
try { return ["fulfilled", await promise] } catch (reason) { return ["rejected", reason] }
|
||||
}
|
||||
return [
|
||||
await observe(Promise.all([fulfilled, rejected])),
|
||||
await Promise.allSettled([rejected, fulfilled]),
|
||||
await observe(Promise.race([rejected, fulfilled])),
|
||||
]
|
||||
`),
|
||||
).toEqual([
|
||||
["rejected", "failed"],
|
||||
[
|
||||
{ status: "rejected", reason: "failed" },
|
||||
{ status: "fulfilled", value: "done" },
|
||||
],
|
||||
["rejected", "failed"],
|
||||
])
|
||||
})
|
||||
|
||||
test("combinator results follow input order, not settlement order", async () => {
|
||||
// Sources:
|
||||
// test/built-ins/Promise/all/resolve-non-thenable.js
|
||||
// test/built-ins/Promise/allSettled/resolved-all-mixed.js
|
||||
// (adapted: members are created, and therefore settle, in reverse of input order;
|
||||
// deferred settlement is not expressible without host-async work in this corpus)
|
||||
expect(
|
||||
await value(`
|
||||
const third = Promise.resolve("c")
|
||||
const failing = (async () => { throw "b" })()
|
||||
try { await failing } catch {}
|
||||
const second = (async () => "b")()
|
||||
const first = Promise.resolve("a")
|
||||
return [
|
||||
await Promise.all([first, second, third]),
|
||||
await Promise.allSettled([first, failing, third]),
|
||||
]
|
||||
`),
|
||||
).toEqual([
|
||||
["a", "b", "c"],
|
||||
[
|
||||
{ status: "fulfilled", value: "a" },
|
||||
{ status: "rejected", reason: "b" },
|
||||
{ status: "fulfilled", value: "c" },
|
||||
],
|
||||
])
|
||||
})
|
||||
|
||||
test("Promise.race ignores a rejected loser once the first contender wins", async () => {
|
||||
// Source: test/built-ins/Promise/race/reject-ignored-immed.js
|
||||
// (adapted: the losing rejection comes from an async function instead of a thenable;
|
||||
// the exact-equality check also asserts the loser leaves no unhandled-rejection warning)
|
||||
expect(
|
||||
await execute(`
|
||||
const loser = (async () => { throw "lost" })()
|
||||
return await Promise.race([Promise.resolve("won"), loser])
|
||||
`),
|
||||
).toEqual({ ok: true, value: "won", toolCalls: [] })
|
||||
})
|
||||
|
||||
test("Promise.race([]) returns a promise whose CodeMode failure is catchable", async () => {
|
||||
// Sources:
|
||||
// test/built-ins/Promise/race/S25.4.4.3_A2.1_T1.js
|
||||
// test/built-ins/Promise/race/S25.4.4.3_A5.1_T1.js
|
||||
// (adapted: upstream requires Promise.race([]) to never settle; CodeMode intentionally
|
||||
// rejects with a catchable diagnostic instead of hanging, so this asserts the sandbox
|
||||
// divergence rather than the spec never-settles behavior)
|
||||
expect(
|
||||
await value(`
|
||||
const empty = Promise.race([])
|
||||
try {
|
||||
await empty
|
||||
return "settled"
|
||||
} catch (error) {
|
||||
return [empty instanceof Promise, error instanceof Error]
|
||||
}
|
||||
`),
|
||||
).toEqual([true, true])
|
||||
})
|
||||
|
||||
test("Promise.resolve passes the same sandbox promise through nested chains", async () => {
|
||||
// Source: test/built-ins/Promise/resolve/S25.4.4.5_A2.2_T1.js
|
||||
// (adapted: no executor construction, and identity is observed with Array includes
|
||||
// because promises are not comparable data values in CodeMode)
|
||||
expect(
|
||||
await value(`
|
||||
const promise = Promise.resolve({ id: 1 })
|
||||
return [
|
||||
[promise].includes(Promise.resolve(promise)),
|
||||
[promise].includes(Promise.resolve(Promise.resolve(promise))),
|
||||
(await Promise.resolve(Promise.resolve(promise))).id,
|
||||
]
|
||||
`),
|
||||
).toEqual([true, true, 1])
|
||||
})
|
||||
|
||||
test("Promise.resolve of a rejected promise preserves identity and reason", async () => {
|
||||
// Source: test/built-ins/Promise/resolve/S25.4.4.5_A2.3_T1.js
|
||||
// (adapted: the source promise is already rejected instead of rejected later)
|
||||
expect(
|
||||
await value(`
|
||||
const rejected = Promise.reject("oops")
|
||||
const adopted = Promise.resolve(rejected)
|
||||
const identity = [rejected].includes(adopted)
|
||||
try {
|
||||
await adopted
|
||||
return "fulfilled"
|
||||
} catch (reason) {
|
||||
return [identity, reason]
|
||||
}
|
||||
`),
|
||||
).toEqual([true, "oops"])
|
||||
})
|
||||
|
||||
test("Promise.reject uses a promise reason without flattening it", async () => {
|
||||
// Sources:
|
||||
// test/built-ins/Promise/reject-via-fn-immed.js
|
||||
// test/built-ins/Promise/reject-via-fn-deferred.js
|
||||
// (adapted: the promise reason goes through Promise.reject instead of executor reject)
|
||||
expect(
|
||||
await value(`
|
||||
const observe = async (reason) => {
|
||||
try {
|
||||
await Promise.reject(reason)
|
||||
return "fulfilled"
|
||||
} catch (caught) {
|
||||
const identity = [reason].includes(caught)
|
||||
try { return [identity, caught instanceof Promise, await caught] }
|
||||
catch (inner) { return [identity, caught instanceof Promise, "rethrew " + inner] }
|
||||
}
|
||||
}
|
||||
return [await observe(Promise.resolve(1)), await observe(Promise.reject("inner"))]
|
||||
`),
|
||||
).toEqual([
|
||||
[true, true, 1],
|
||||
[true, true, "rethrew inner"],
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe("Test262 async functions and await", () => {
|
||||
test("declaration, expression, and arrow forms return promises", async () => {
|
||||
// Sources:
|
||||
// test/language/statements/async-function/declaration-returns-promise.js
|
||||
// test/language/expressions/async-function/expression-returns-promise.js
|
||||
// test/language/expressions/async-arrow-function/arrow-returns-promise.js
|
||||
expect(
|
||||
await value(`
|
||||
async function declaration() { return 1 }
|
||||
const expression = async function() { return 2 }
|
||||
const arrow = async () => 3
|
||||
const promises = [declaration(), expression(), arrow()]
|
||||
return [promises.map((item) => item instanceof Promise), await Promise.all(promises)]
|
||||
`),
|
||||
).toEqual([[true, true, true], [1, 2, 3]])
|
||||
})
|
||||
|
||||
test("async bodies adopt returns and reject throws before and after await", async () => {
|
||||
// Sources:
|
||||
// test/language/statements/async-function/evaluation-body.js
|
||||
// test/language/statements/async-function/evaluation-body-that-returns.js
|
||||
// test/language/statements/async-function/evaluation-body-that-returns-after-await.js
|
||||
// test/language/statements/async-function/evaluation-body-that-throws.js
|
||||
// test/language/statements/async-function/evaluation-body-that-throws-after-await.js
|
||||
expect(
|
||||
await value(`
|
||||
const order = []
|
||||
const plain = async () => { order.push("body"); return 42 }
|
||||
const afterAwait = async () => { await Promise.resolve(); return 43 }
|
||||
const throwsBefore = async () => { throw 1 }
|
||||
const throwsAfter = async () => { await Promise.resolve(); throw 2 }
|
||||
const observe = async (promise) => {
|
||||
try { return ["fulfilled", await promise] } catch (reason) { return ["rejected", reason] }
|
||||
}
|
||||
const first = plain()
|
||||
return [
|
||||
order,
|
||||
await observe(first),
|
||||
await observe(afterAwait()),
|
||||
await observe(throwsBefore()),
|
||||
await observe(throwsAfter()),
|
||||
]
|
||||
`),
|
||||
).toEqual([
|
||||
["body"],
|
||||
["fulfilled", 42],
|
||||
["fulfilled", 43],
|
||||
["rejected", 1],
|
||||
["rejected", 2],
|
||||
])
|
||||
})
|
||||
|
||||
test("default-parameter throws reject instead of escaping the call", async () => {
|
||||
// Source: test/language/statements/async-function/evaluation-default-that-throws.js
|
||||
expect(
|
||||
await value(`
|
||||
const fail = () => { throw new Error("default") }
|
||||
const run = async (value = fail()) => value
|
||||
let returned = false
|
||||
try {
|
||||
const promise = run()
|
||||
returned = promise instanceof Promise
|
||||
await promise
|
||||
return [returned, "fulfilled"]
|
||||
} catch (error) {
|
||||
return [returned, error.message]
|
||||
}
|
||||
`),
|
||||
).toEqual([true, "default"])
|
||||
})
|
||||
|
||||
test("async try/finally completion records override earlier completion", async () => {
|
||||
// Sources: the try-{return,throw,reject}-finally-{return,throw,reject}.js matrix under
|
||||
// test/language/statements/async-function, test/language/expressions/async-function,
|
||||
// and test/language/expressions/async-arrow-function.
|
||||
expect(
|
||||
await value(`
|
||||
const observe = async (promise) => {
|
||||
try { return ["fulfilled", await promise] } catch (reason) { return ["rejected", reason] }
|
||||
}
|
||||
const returnReturn = async () => { try { return "early" } finally { return await Promise.resolve("override") } }
|
||||
const returnThrow = async () => { try { return "early" } finally { throw "override" } }
|
||||
const returnReject = async () => { try { return "early" } finally { await Promise.reject("override") } }
|
||||
const throwReturn = async () => { try { throw "early" } finally { return await Promise.resolve("override") } }
|
||||
const throwThrow = async () => { try { throw "early" } finally { throw "override" } }
|
||||
const throwReject = async () => { try { throw "early" } finally { await Promise.reject("override") } }
|
||||
const rejectReturn = async () => { try { await Promise.reject("early") } finally { return await Promise.resolve("override") } }
|
||||
const rejectThrow = async () => { try { await Promise.reject("early") } finally { throw "override" } }
|
||||
const rejectReject = async () => { try { await Promise.reject("early") } finally { await Promise.reject("override") } }
|
||||
return await Promise.all([
|
||||
observe(returnReturn()), observe(returnThrow()), observe(returnReject()),
|
||||
observe(throwReturn()), observe(throwThrow()), observe(throwReject()),
|
||||
observe(rejectReturn()), observe(rejectThrow()), observe(rejectReject()),
|
||||
])
|
||||
`),
|
||||
).toEqual([
|
||||
["fulfilled", "override"],
|
||||
["rejected", "override"],
|
||||
["rejected", "override"],
|
||||
["fulfilled", "override"],
|
||||
["rejected", "override"],
|
||||
["rejected", "override"],
|
||||
["fulfilled", "override"],
|
||||
["rejected", "override"],
|
||||
["rejected", "override"],
|
||||
])
|
||||
})
|
||||
|
||||
test("await preserves an object whose then property is not callable", async () => {
|
||||
// Source: test/language/expressions/await/await-awaits-thenable-not-callable.js
|
||||
expect(
|
||||
await value(`
|
||||
const thenable = { then: 42 }
|
||||
return (await thenable) === thenable
|
||||
`),
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
test("await returns non-promise operands unchanged", async () => {
|
||||
// Source: test/language/expressions/await/await-non-promise.js
|
||||
// (adapted: only value pass-through is asserted here; the spec tick ordering around
|
||||
// await of non-promises is covered by the failing interleaving test below)
|
||||
expect(
|
||||
await value(`
|
||||
const object = { id: 1 }
|
||||
const array = [1, 2]
|
||||
return [
|
||||
await 1,
|
||||
await "text",
|
||||
await true,
|
||||
(await null) === null,
|
||||
(await undefined) === undefined,
|
||||
(await object) === object,
|
||||
(await array) === array,
|
||||
]
|
||||
`),
|
||||
).toEqual([1, "text", true, true, true, true, true])
|
||||
})
|
||||
})
|
||||
|
||||
describe("Test262 expected Promise conformance", () => {
|
||||
for (const name of ["all", "allSettled", "race"] as const) {
|
||||
test.failing(`Promise.${name} rejects invalid input with TypeError`, async () => {
|
||||
// Sources:
|
||||
// test/built-ins/Promise/all/S25.4.4.1_A3.1_T1.js
|
||||
// test/built-ins/Promise/all/S25.4.4.1_A3.1_T2.js
|
||||
// test/built-ins/Promise/allSettled/iter-arg-is-number-reject.js
|
||||
// test/built-ins/Promise/race/iter-arg-is-number-reject.js
|
||||
expect(
|
||||
await value(`
|
||||
try {
|
||||
const promise = Promise.${name}(42)
|
||||
const returned = promise instanceof Promise
|
||||
await promise
|
||||
return [returned, "fulfilled"]
|
||||
} catch (error) {
|
||||
return [true, error.name]
|
||||
}
|
||||
`),
|
||||
).toEqual([true, "TypeError"])
|
||||
})
|
||||
}
|
||||
|
||||
test.failing("Promise.all consumes sparse positions as undefined", async () => {
|
||||
// Source: test/built-ins/Array/from/from-array.js (array iterator hole behavior)
|
||||
expect(
|
||||
await value(`
|
||||
const input = []
|
||||
input[1] = 1
|
||||
const result = await Promise.all(input)
|
||||
return [result.length, result[0] === undefined, result[1]]
|
||||
`),
|
||||
).toEqual([2, true, 1])
|
||||
})
|
||||
|
||||
test.failing("Promise.allSettled consumes sparse positions as undefined", async () => {
|
||||
// Source: test/built-ins/Array/from/from-array.js (array iterator hole behavior)
|
||||
expect(
|
||||
await value(`
|
||||
const input = []
|
||||
input[1] = 1
|
||||
const result = await Promise.allSettled(input)
|
||||
return [result.length, result[0].status, result[0].value === undefined, result[1]]
|
||||
`),
|
||||
).toEqual([2, "fulfilled", true, { status: "fulfilled", value: 1 }])
|
||||
})
|
||||
|
||||
test.failing("Promise.race consumes a sparse first position as undefined", async () => {
|
||||
// Source: test/built-ins/Array/from/from-array.js (array iterator hole behavior)
|
||||
expect(
|
||||
await value(`
|
||||
const input = []
|
||||
input[1] = 1
|
||||
return (await Promise.race(input)) === undefined
|
||||
`),
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
test.failing("Promise.all settles after reactions attached to its inputs", async () => {
|
||||
// Sources:
|
||||
// test/built-ins/Promise/all/S25.4.4.1_A7.2_T1.js
|
||||
// test/built-ins/Promise/all/S25.4.4.1_A8.1_T1.js
|
||||
expect(
|
||||
await value(`
|
||||
const sequence = [1]
|
||||
const input = Promise.resolve(1)
|
||||
const aggregate = Promise.all([input])
|
||||
aggregate.then(() => sequence.push(4))
|
||||
input.then(() => sequence.push(3)).then(() => sequence.push(5))
|
||||
sequence.push(2)
|
||||
await aggregate
|
||||
await Promise.resolve()
|
||||
return sequence
|
||||
`),
|
||||
).toEqual([1, 2, 3, 4, 5])
|
||||
})
|
||||
|
||||
test.failing("Promise.allSettled settles after reactions attached to its inputs", async () => {
|
||||
// Sources:
|
||||
// test/built-ins/Promise/allSettled/resolved-sequence.js
|
||||
// test/built-ins/Promise/allSettled/resolved-sequence-extra-ticks.js
|
||||
// test/built-ins/Promise/allSettled/resolved-sequence-mixed.js
|
||||
// test/built-ins/Promise/allSettled/resolved-sequence-with-rejections.js
|
||||
expect(
|
||||
await value(`
|
||||
const sequence = [1]
|
||||
const input = Promise.resolve(1)
|
||||
const aggregate = Promise.allSettled([input])
|
||||
aggregate.then(() => sequence.push(4))
|
||||
input.then(() => sequence.push(3)).then(() => sequence.push(5))
|
||||
sequence.push(2)
|
||||
await aggregate
|
||||
await Promise.resolve()
|
||||
return sequence
|
||||
`),
|
||||
).toEqual([1, 2, 3, 4, 5])
|
||||
})
|
||||
|
||||
test.failing("Promise.race settles in a reaction after its winning input", async () => {
|
||||
// Sources:
|
||||
// test/built-ins/Promise/race/S25.4.4.3_A6.1_T1.js
|
||||
// test/built-ins/Promise/race/resolved-sequence-extra-ticks.js
|
||||
expect(
|
||||
await value(`
|
||||
const sequence = [1]
|
||||
const race = Promise.race([1])
|
||||
race.then(() => sequence.push(4))
|
||||
Promise.resolve().then(() => sequence.push(3)).then(() => sequence.push(5))
|
||||
sequence.push(2)
|
||||
await race
|
||||
await Promise.resolve()
|
||||
return sequence
|
||||
`),
|
||||
).toEqual([1, 2, 3, 4, 5])
|
||||
})
|
||||
|
||||
test.failing("then reactions route and propagate fulfillment and rejection", async () => {
|
||||
// Sources:
|
||||
// test/built-ins/Promise/prototype/then/prfm-fulfilled.js
|
||||
// test/built-ins/Promise/prototype/then/prfm-rejected.js
|
||||
// test/built-ins/Promise/prototype/then/rxn-handler-identity.js
|
||||
// test/built-ins/Promise/prototype/then/rxn-handler-thrower.js
|
||||
// test/built-ins/Promise/prototype/then/rxn-handler-fulfilled-return-normal.js
|
||||
// test/built-ins/Promise/prototype/then/rxn-handler-fulfilled-return-abrupt.js
|
||||
// test/built-ins/Promise/prototype/then/rxn-handler-rejected-return-normal.js
|
||||
// test/built-ins/Promise/prototype/then/rxn-handler-rejected-return-abrupt.js
|
||||
expect(
|
||||
await value(`
|
||||
const observe = async (promise) => {
|
||||
try { return ["fulfilled", await promise] } catch (reason) { return ["rejected", reason] }
|
||||
}
|
||||
return await Promise.all([
|
||||
observe(Promise.resolve(1).then((value) => value + 1)),
|
||||
observe(Promise.reject(2).then(undefined, (reason) => reason + 1)),
|
||||
observe(Promise.resolve(3).then(undefined)),
|
||||
observe(Promise.reject(4).then(undefined)),
|
||||
observe(Promise.resolve(5).then(() => { throw 6 })),
|
||||
observe(Promise.reject(7).then(undefined, () => { throw 8 })),
|
||||
])
|
||||
`),
|
||||
).toEqual([
|
||||
["fulfilled", 2],
|
||||
["fulfilled", 3],
|
||||
["fulfilled", 3],
|
||||
["rejected", 4],
|
||||
["rejected", 6],
|
||||
["rejected", 8],
|
||||
])
|
||||
})
|
||||
|
||||
test.failing("then reactions preserve breadth-first queue order", async () => {
|
||||
// Source: test/built-ins/Promise/prototype/then/S25.4.4_A1.1_T1.js
|
||||
expect(
|
||||
await value(`
|
||||
const sequence = [1]
|
||||
const promise = Promise.resolve()
|
||||
const first = promise.then(() => sequence.push(3)).then(() => sequence.push(5)).then(() => sequence.push(7))
|
||||
const second = promise.then(() => sequence.push(4)).then(() => sequence.push(6)).then(() => sequence.push(8))
|
||||
sequence.push(2)
|
||||
await Promise.all([first, second])
|
||||
return sequence
|
||||
`),
|
||||
).toEqual([1, 2, 3, 4, 5, 6, 7, 8])
|
||||
})
|
||||
|
||||
test.failing("then rejects direct self-resolution for fulfilled and rejected sources", async () => {
|
||||
// Sources:
|
||||
// test/built-ins/Promise/prototype/then/resolve-settled-fulfilled-self.js
|
||||
// test/built-ins/Promise/prototype/then/resolve-settled-rejected-self.js
|
||||
// test/built-ins/Promise/prototype/then/resolve-pending-fulfilled-self.js
|
||||
// test/built-ins/Promise/prototype/then/resolve-pending-rejected-self.js
|
||||
expect(
|
||||
await value(`
|
||||
const observe = async (promise) => {
|
||||
try { await promise; return "fulfilled" } catch (reason) { return reason.name }
|
||||
}
|
||||
let fulfilled
|
||||
let rejected
|
||||
fulfilled = Promise.resolve().then(() => fulfilled)
|
||||
rejected = Promise.reject().then(undefined, () => rejected)
|
||||
return await Promise.all([observe(fulfilled), observe(rejected)])
|
||||
`),
|
||||
).toEqual(["TypeError", "TypeError"])
|
||||
})
|
||||
|
||||
test.failing("catch delegates rejection handling and preserves fulfillment", async () => {
|
||||
// Sources:
|
||||
// test/built-ins/Promise/prototype/catch/S25.4.5.1_A2.1_T1.js
|
||||
// test/built-ins/Promise/prototype/catch/S25.4.5.1_A3.1_T1.js
|
||||
// test/built-ins/Promise/prototype/catch/S25.4.5.1_A3.1_T2.js
|
||||
expect(
|
||||
await value(`
|
||||
return [
|
||||
await Promise.resolve(1).catch(() => 2),
|
||||
await Promise.reject(3).catch((reason) => reason + 1),
|
||||
]
|
||||
`),
|
||||
).toEqual([1, 4])
|
||||
})
|
||||
|
||||
test.failing("finally preserves or replaces the original settlement", async () => {
|
||||
// Sources:
|
||||
// test/built-ins/Promise/prototype/finally/resolution-value-no-override.js
|
||||
// test/built-ins/Promise/prototype/finally/rejection-reason-no-fulfill.js
|
||||
// test/built-ins/Promise/prototype/finally/rejection-reason-override-with-throw.js
|
||||
expect(
|
||||
await value(`
|
||||
const observe = async (promise) => {
|
||||
try { return ["fulfilled", await promise] } catch (reason) { return ["rejected", reason] }
|
||||
}
|
||||
return await Promise.all([
|
||||
observe(Promise.resolve(1).finally(() => 2)),
|
||||
observe(Promise.reject(3).finally(() => 4)),
|
||||
observe(Promise.reject(5).finally(() => { throw 6 })),
|
||||
])
|
||||
`),
|
||||
).toEqual([
|
||||
["fulfilled", 1],
|
||||
["rejected", 3],
|
||||
["rejected", 6],
|
||||
])
|
||||
})
|
||||
|
||||
test.failing("await always resumes in a later reaction and interleaves async functions", async () => {
|
||||
// Sources:
|
||||
// test/language/expressions/await/async-await-interleaved.js
|
||||
// test/language/expressions/await/await-non-promise.js
|
||||
expect(
|
||||
await value(`
|
||||
const sequence = []
|
||||
const first = async () => { sequence.push("first:1"); await 0; sequence.push("first:2") }
|
||||
const second = async () => { sequence.push("second:1"); await 0; sequence.push("second:2") }
|
||||
await Promise.all([first(), second()])
|
||||
return sequence
|
||||
`),
|
||||
).toEqual(["first:1", "second:1", "first:2", "second:2"])
|
||||
})
|
||||
|
||||
test.failing("an async function rejects when it resolves with its own promise", async () => {
|
||||
// Adapted from the self-resolution requirement represented by:
|
||||
// test/built-ins/Promise/resolve-self.js
|
||||
// test/built-ins/Promise/resolve/S25.4.4.5_A4.1_T1.js
|
||||
expect(
|
||||
await value(`
|
||||
let promise
|
||||
const run = async () => {
|
||||
await Promise.resolve()
|
||||
return promise
|
||||
}
|
||||
promise = run()
|
||||
try {
|
||||
await promise
|
||||
return "fulfilled"
|
||||
} catch (error) {
|
||||
return error.name
|
||||
}
|
||||
`),
|
||||
).toBe("TypeError")
|
||||
})
|
||||
|
||||
test.failing("Promise.resolve recursively assimilates callable thenables", async () => {
|
||||
// Source: test/built-ins/Promise/resolve/resolve-thenable.js
|
||||
expect(
|
||||
await value(`
|
||||
const value = { id: 1 }
|
||||
const nested = { then: (resolve) => resolve(value) }
|
||||
const thenable = { then: (resolve) => resolve(nested) }
|
||||
return (await Promise.resolve(thenable)) === value
|
||||
`),
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
test.failing("Promise combinators assimilate callable thenable inputs", async () => {
|
||||
// Sources:
|
||||
// test/built-ins/Promise/all/reject-immed.js
|
||||
// test/built-ins/Promise/all/reject-ignored-immed.js
|
||||
// test/built-ins/Promise/allSettled/reject-ignored-immed.js
|
||||
// test/built-ins/Promise/race/resolve-thenable.js
|
||||
expect(
|
||||
await value(`
|
||||
const fulfills = { then: (resolve) => resolve(1) }
|
||||
const rejects = { then: (_, reject) => reject(2) }
|
||||
const resolvesFirst = { then: (resolve, reject) => { resolve(3); reject(4) } }
|
||||
const observe = async (promise) => {
|
||||
try { return ["fulfilled", await promise] } catch (reason) { return ["rejected", reason] }
|
||||
}
|
||||
return [
|
||||
await observe(Promise.all([fulfills, rejects])),
|
||||
await Promise.allSettled([fulfills, resolvesFirst]),
|
||||
await observe(Promise.race([rejects])),
|
||||
]
|
||||
`),
|
||||
).toEqual([
|
||||
["rejected", 2],
|
||||
[
|
||||
{ status: "fulfilled", value: 1 },
|
||||
{ status: "fulfilled", value: 3 },
|
||||
],
|
||||
["rejected", 2],
|
||||
])
|
||||
})
|
||||
|
||||
test.failing("await assimilates callable thenables", async () => {
|
||||
// Source: test/language/expressions/await/await-awaits-thenables.js
|
||||
expect(
|
||||
await value(`
|
||||
const thenable = { then: (resolve) => resolve(42) }
|
||||
return await thenable
|
||||
`),
|
||||
).toBe(42)
|
||||
})
|
||||
|
||||
test.failing("await rejects when a callable thenable throws", async () => {
|
||||
// Source: test/language/expressions/await/await-awaits-thenables-that-throw.js
|
||||
expect(
|
||||
await value(`
|
||||
const error = { id: 1 }
|
||||
const thenable = { then: () => { throw error } }
|
||||
try {
|
||||
await thenable
|
||||
return false
|
||||
} catch (caught) {
|
||||
return caught === error
|
||||
}
|
||||
`),
|
||||
).toBe(true)
|
||||
})
|
||||
})
|
||||
|
|
@ -48,6 +48,13 @@ const failingTool = Tool.make({
|
|||
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 sleepy calls",
|
||||
|
|
@ -56,6 +63,25 @@ const completedTool = (trace: Trace) =>
|
|||
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 } = {},
|
||||
|
|
@ -63,7 +89,15 @@ const run = (
|
|||
const trace = options.trace ?? makeTrace()
|
||||
return Effect.runPromise(
|
||||
CodeMode.execute({
|
||||
tools: { host: { sleepy: sleepyTool(trace), fail: failingTool, completed: completedTool(trace) } },
|
||||
tools: {
|
||||
host: {
|
||||
sleepy: sleepyTool(trace),
|
||||
fail: failingTool,
|
||||
interrupt: interruptedTool,
|
||||
completed: completedTool(trace),
|
||||
stubborn: stubbornTool(trace),
|
||||
},
|
||||
},
|
||||
code,
|
||||
...(options.limits ? { limits: options.limits } : {}),
|
||||
}),
|
||||
|
|
@ -174,8 +208,7 @@ describe("first-class promise values", () => {
|
|||
})
|
||||
|
||||
test("an awaited failure is catchable exactly like a synchronous throw", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
const result = await run(`
|
||||
const p = tools.host.fail({})
|
||||
try {
|
||||
await p
|
||||
|
|
@ -183,57 +216,195 @@ describe("first-class promise values", () => {
|
|||
} catch (e) {
|
||||
return e.message
|
||||
}
|
||||
`),
|
||||
).toBe("Lookup refused")
|
||||
`)
|
||||
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 completes before the execution ends", async () => {
|
||||
test("a fire-and-forget call is interrupted when the program returns", async () => {
|
||||
const trace = makeTrace()
|
||||
const result = await value(
|
||||
const result = await run(
|
||||
`
|
||||
tools.host.sleepy({ id: 1, ms: 30 })
|
||||
return "done"
|
||||
`,
|
||||
{ trace },
|
||||
)
|
||||
expect(result).toBe("done")
|
||||
expect(trace.completed).toBe(1)
|
||||
expect(trace.interrupted).toBe(0)
|
||||
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 surfaces as an unhandled-rejection diagnostic", async () => {
|
||||
const diagnostic = await error(`
|
||||
test("a never-awaited failing call preserves the result and reports the rejection", async () => {
|
||||
const result = await run(`
|
||||
tools.host.fail({})
|
||||
return "done"
|
||||
`)
|
||||
expect(diagnostic.kind).toBe("ToolFailure")
|
||||
expect(diagnostic.message).toContain("Unhandled rejection from an un-awaited promise")
|
||||
expect(diagnostic.message).toContain("Lookup refused")
|
||||
expect(diagnostic.suggestions?.join(" ")).toContain("Await promises")
|
||||
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 surfaces as an unhandled promise rejection", async () => {
|
||||
const diagnostic = await error(`
|
||||
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(diagnostic.kind).toBe("ExecutionFailure")
|
||||
expect(diagnostic.message).toContain("Unhandled rejection from an un-awaited promise")
|
||||
expect(diagnostic.message).toContain("boom")
|
||||
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("drains promises started by an async function after an await", async () => {
|
||||
const diagnostic = await error(`
|
||||
const run = async () => {
|
||||
await tools.host.sleepy({ id: 1 })
|
||||
tools.host.fail({})
|
||||
}
|
||||
run()
|
||||
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.sleepy({ id: 1, ms: 60000 })
|
||||
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(diagnostic.kind).toBe("ToolFailure")
|
||||
expect(diagnostic.message).toContain("Lookup refused")
|
||||
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.sleepy({ id: 1, ms: 1_000 })
|
||||
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.sleepy({ id: 1, ms: 60000 })
|
||||
Promise.all([tools.host.sleepy({ id: 2, ms: 60000 })])
|
||||
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)
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -251,6 +422,22 @@ describe("promises at data boundaries", () => {
|
|||
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.sleepy({ id: 1, ms: 60_000 })
|
||||
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.sleepy({ id: tools.host.sleepy({ id: 1 }) })`)
|
||||
expect(diagnostic.kind).toBe("InvalidDataValue")
|
||||
|
|
@ -270,6 +457,59 @@ describe("promises at data boundaries", () => {
|
|||
})
|
||||
|
||||
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.sleepy({ id: 1, ms: 40 })])
|
||||
const second = Promise.all([tools.host.sleepy({ id: 2, ms: 40 })])
|
||||
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.sleepy({ 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.sleepy" }])
|
||||
})
|
||||
|
||||
test("mixes promises and plain values, preserving order", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
|
|
@ -340,16 +580,18 @@ describe("Promise.all over arbitrary arrays", () => {
|
|||
})
|
||||
|
||||
test("rejects with the first failure, catchable in-program", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
const result = await run(`
|
||||
try {
|
||||
await Promise.all([tools.host.sleepy({ id: 1 }), tools.host.fail({})])
|
||||
return "no"
|
||||
} catch (e) {
|
||||
return e.message
|
||||
}
|
||||
`),
|
||||
).toBe("Lookup refused")
|
||||
`)
|
||||
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 () => {
|
||||
|
|
@ -370,10 +612,55 @@ describe("Promise.all over arbitrary arrays", () => {
|
|||
{ 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.sleepy({ id: 1, ms: 40 })
|
||||
try {
|
||||
await Promise.all([slow, tools.host.fail({})])
|
||||
return "no"
|
||||
} catch {}
|
||||
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.sleepy({ id: 1, ms: 40 })
|
||||
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")
|
||||
|
|
@ -413,50 +700,64 @@ describe("Promise.allSettled", () => {
|
|||
return settled.filter((s) => s.status === "rejected").length
|
||||
`)
|
||||
expect(result.ok).toBe(true)
|
||||
if (result.ok) expect(result.value).toBe(2)
|
||||
if (!result.ok) return
|
||||
expect(result.value).toBe(2)
|
||||
expect(result.warnings).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("Promise.race", () => {
|
||||
test("first settlement wins and losers are interrupted", async () => {
|
||||
test("first settlement wins and a direct loser is interrupted at completion", async () => {
|
||||
const trace = makeTrace()
|
||||
const result = await value(
|
||||
`
|
||||
const fast = tools.host.sleepy({ id: 1, ms: 10 })
|
||||
const slow = tools.host.sleepy({ id: 2, ms: 5000 })
|
||||
const slow = tools.host.sleepy({ id: 2, ms: 40 })
|
||||
return await Promise.race([fast, slow])
|
||||
`,
|
||||
{ trace },
|
||||
)
|
||||
expect(result).toBe(1)
|
||||
expect(trace.interrupted).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("awaiting an interrupted loser afterwards is a catchable program failure", async () => {
|
||||
test("a direct loser remains awaitable after the race settles", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
const fast = tools.host.sleepy({ id: 1, ms: 10 })
|
||||
const slow = tools.host.sleepy({ id: 2, ms: 5000 })
|
||||
const slow = tools.host.sleepy({ id: 2, ms: 40 })
|
||||
const winner = await Promise.race([fast, slow])
|
||||
try {
|
||||
await slow
|
||||
return "no"
|
||||
} catch (e) {
|
||||
return { winner, caught: e.message }
|
||||
}
|
||||
return { winner, loser: await slow }
|
||||
`),
|
||||
).toEqual({
|
||||
winner: 1,
|
||||
caught: "This tool call was interrupted because another value settled a Promise.race first.",
|
||||
})
|
||||
).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.sleepy({ id: 1, ms: 40 }),
|
||||
tools.host.sleepy({ id: 2, ms: 40 }),
|
||||
])
|
||||
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.sleepy({ id: 1, ms: 5000 })])
|
||||
await Promise.race([tools.host.fail({}), tools.host.sleepy({ id: 1, ms: 40 })])
|
||||
return "no"
|
||||
} catch (e) {
|
||||
return e.message
|
||||
|
|
@ -468,11 +769,20 @@ describe("Promise.race", () => {
|
|||
test("a plain value wins over pending promises", async () => {
|
||||
const trace = makeTrace()
|
||||
expect(
|
||||
await value(`return await Promise.race([tools.host.sleepy({ id: 1, ms: 5000 }), "immediate"])`, { trace }),
|
||||
await value(`return await Promise.race([tools.host.sleepy({ id: 1, ms: 40 }), "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")
|
||||
|
|
@ -484,6 +794,9 @@ describe("Promise.resolve / Promise.reject", () => {
|
|||
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.sleepy({ 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 () => {
|
||||
|
|
@ -498,6 +811,34 @@ describe("Promise.resolve / Promise.reject", () => {
|
|||
`),
|
||||
).toBe("nope")
|
||||
})
|
||||
|
||||
test("a rejection observed after settlement is handled", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
const rejected = Promise.reject(new Error("handled"))
|
||||
await tools.host.sleepy({ 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", () => {
|
||||
|
|
@ -531,6 +872,67 @@ describe("timeout interruption of forked calls", () => {
|
|||
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.sleepy({ id: 1, ms: 60000 })])`, {
|
||||
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("unsupported promise surface", () => {
|
||||
|
|
|
|||
|
|
@ -1,77 +0,0 @@
|
|||
# Test262 Array Coverage
|
||||
|
||||
The Array tests adapt Test262 at revision `250f204f23a9249ff204be2baec29600faae7b75`. They cover CodeMode's 35
|
||||
exposed instance methods and three static methods using actual arrays, accepted argument types, deterministic behavior,
|
||||
and CodeMode's materialized collection conventions. Each executable case names its exact upstream source path.
|
||||
`LICENSE.test262` contains the upstream BSD terms.
|
||||
|
||||
This is coverage of CodeMode's bounded Array surface, not a claim of ECMAScript or Test262 conformance. One upstream
|
||||
file may contain both adapted and inapplicable assertions, so a cited source means only that the represented assertions
|
||||
were adapted.
|
||||
|
||||
## Inventory
|
||||
|
||||
The 38 relevant upstream API directories contain 2,837 files. The executable suite adapts assertions from 83 distinct
|
||||
sources.
|
||||
|
||||
| API | Upstream files | Adapted sources |
|
||||
| ------------------------------- | -------------: | --------------: |
|
||||
| `Array.prototype.map` | 216 | 3 |
|
||||
| `Array.prototype.filter` | 242 | 3 |
|
||||
| `Array.prototype.find` | 23 | 4 |
|
||||
| `Array.prototype.findIndex` | 23 | 3 |
|
||||
| `Array.prototype.findLast` | 24 | 3 |
|
||||
| `Array.prototype.findLastIndex` | 24 | 3 |
|
||||
| `Array.prototype.some` | 219 | 2 |
|
||||
| `Array.prototype.every` | 218 | 2 |
|
||||
| `Array.prototype.includes` | 30 | 2 |
|
||||
| `Array.prototype.join` | 23 | 2 |
|
||||
| `Array.prototype.reduce` | 260 | 3 |
|
||||
| `Array.prototype.reduceRight` | 260 | 3 |
|
||||
| `Array.prototype.flatMap` | 24 | 2 |
|
||||
| `Array.prototype.forEach` | 190 | 2 |
|
||||
| `Array.prototype.sort` | 54 | 3 |
|
||||
| `Array.prototype.toSorted` | 21 | 4 |
|
||||
| `Array.prototype.slice` | 71 | 1 |
|
||||
| `Array.prototype.concat` | 69 | 3 |
|
||||
| `Array.prototype.indexOf` | 201 | 2 |
|
||||
| `Array.prototype.lastIndexOf` | 198 | 2 |
|
||||
| `Array.prototype.at` | 13 | 3 |
|
||||
| `Array.prototype.flat` | 19 | 2 |
|
||||
| `Array.prototype.reverse` | 18 | 1 |
|
||||
| `Array.prototype.toReversed` | 17 | 2 |
|
||||
| `Array.prototype.with` | 21 | 2 |
|
||||
| `Array.prototype.push` | 24 | 1 |
|
||||
| `Array.prototype.pop` | 23 | 1 |
|
||||
| `Array.prototype.shift` | 20 | 1 |
|
||||
| `Array.prototype.unshift` | 22 | 1 |
|
||||
| `Array.prototype.splice` | 81 | 3 |
|
||||
| `Array.prototype.fill` | 22 | 3 |
|
||||
| `Array.prototype.copyWithin` | 39 | 2 |
|
||||
| `Array.prototype.keys` | 12 | 1 |
|
||||
| `Array.prototype.values` | 12 | 1 |
|
||||
| `Array.prototype.entries` | 12 | 1 |
|
||||
| `Array.from` | 47 | 3 |
|
||||
| `Array.isArray` | 29 | 2 |
|
||||
| `Array.of` | 16 | 1 |
|
||||
|
||||
## Exclusions
|
||||
|
||||
Assertions are not adapted when they test behavior outside CodeMode's documented Array surface:
|
||||
|
||||
- Function metadata, property descriptors, constructibility, prototype mutation, species constructors, or cross-realm
|
||||
identity.
|
||||
- Generic receivers, detached methods, `.call`, `.apply`, boxed values, custom coercion objects, Symbols, BigInts,
|
||||
proxies, accessors, frozen arrays, typed arrays, or ArrayBuffers.
|
||||
- `Array.from` mappers, custom iterables, constructor substitution, and iterator-closing behavior.
|
||||
- Native iterator identity, `.next()`, completion records, or live iterator mutation. CodeMode deliberately materializes
|
||||
`keys`, `values`, and `entries` as arrays.
|
||||
- Sparse-array assertions that depend on literal elisions or inherited indexed properties. CodeMode's confined data
|
||||
model does not preserve those prototype and hole semantics at every boundary.
|
||||
- Argument coercions outside the accepted schema-like surface. Numeric positions must be numbers and `join` separators
|
||||
must be strings.
|
||||
- Exact native error brands where CodeMode exposes a safe runtime error instead.
|
||||
- Async/effectful callbacks, circular-data rejection, sandbox-value identity, diagnostics, and host-boundary behavior.
|
||||
Those remain covered by CodeMode-specific tests.
|
||||
|
||||
Handwritten tests remain where they specify CodeMode behavior rather than ordinary ECMAScript Array semantics.
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
# Test262 String Coverage
|
||||
|
||||
The String tests adapt Test262 at revision `250f204f23a9249ff204be2baec29600faae7b75`. They cover CodeMode's 32
|
||||
exposed instance methods and two static methods using primitive receivers, accepted argument types, and deterministic
|
||||
behavior. Each executable case names its exact upstream source path. `LICENSE.test262` contains the upstream BSD terms.
|
||||
|
||||
This is coverage of CodeMode's bounded String surface, not a claim of ECMAScript or Test262 conformance. One upstream
|
||||
file may contain both adapted and inapplicable assertions, so a cited source means only that the represented assertions
|
||||
were adapted.
|
||||
|
||||
## Inventory
|
||||
|
||||
The relevant upstream directories contain 1,048 files: 1,009 core built-in files, 29 Annex B files for exposed methods,
|
||||
and 10 Intl `localeCompare` files. The executable suite adapts assertions from 298 distinct sources.
|
||||
|
||||
| API | Upstream files | Adapted sources |
|
||||
| --- | ---: | ---: |
|
||||
| `String.fromCharCode` | 17 | 6 |
|
||||
| `String.fromCodePoint` | 11 | 4 |
|
||||
| `String.prototype.at` | 11 | 5 |
|
||||
| `String.prototype.charAt` | 30 | 9 |
|
||||
| `String.prototype.charCodeAt` | 25 | 4 |
|
||||
| `String.prototype.codePointAt` | 16 | 6 |
|
||||
| `String.prototype.concat` | 22 | 1 |
|
||||
| `String.prototype.endsWith` | 27 | 13 |
|
||||
| `String.prototype.includes` | 27 | 12 |
|
||||
| `String.prototype.indexOf` | 47 | 8 |
|
||||
| `String.prototype.lastIndexOf` | 25 | 1 |
|
||||
| `String.prototype.localeCompare` | 23 | 1 |
|
||||
| `String.prototype.match` | 52 | 9 |
|
||||
| `String.prototype.matchAll` | 26 | 1 |
|
||||
| `String.prototype.normalize` | 14 | 3 |
|
||||
| `String.prototype.padEnd` | 13 | 4 |
|
||||
| `String.prototype.padStart` | 13 | 4 |
|
||||
| `String.prototype.repeat` | 16 | 4 |
|
||||
| `String.prototype.replace` | 56 | 16 |
|
||||
| `String.prototype.replaceAll` | 46 | 12 |
|
||||
| `String.prototype.search` | 44 | 10 |
|
||||
| `String.prototype.slice` | 38 | 11 |
|
||||
| `String.prototype.split` | 121 | 50 |
|
||||
| `String.prototype.startsWith` | 21 | 7 |
|
||||
| `String.prototype.substr` | 15 | 6 |
|
||||
| `String.prototype.substring` | 46 | 12 |
|
||||
| `String.prototype.toLowerCase` | 30 | 5 |
|
||||
| `String.prototype.toString` | 7 | 1 |
|
||||
| `String.prototype.toUpperCase` | 26 | 3 |
|
||||
| `String.prototype.trim` | 129 | 66 |
|
||||
| `String.prototype.trimEnd` | 23 | 2 |
|
||||
| `String.prototype.trimLeft` | 4 | 0 |
|
||||
| `String.prototype.trimRight` | 4 | 0 |
|
||||
| `String.prototype.trimStart` | 23 | 2 |
|
||||
|
||||
## Exclusions
|
||||
|
||||
Assertions are not adapted when they test behavior outside CodeMode's documented String surface:
|
||||
|
||||
- Function metadata, property descriptors, constructibility, prototype mutation, or cross-realm identity.
|
||||
- The `trimLeft`/`trimRight` Test262 files assert prototype function identity, which CodeMode does not expose. Their
|
||||
supported call behavior remains covered by CodeMode-specific tests.
|
||||
- Boxed strings, generic receivers, custom coercion objects, Symbols, BigInts, or argument types CodeMode rejects.
|
||||
- Symbol-based RegExp dispatch, custom matchers, species constructors, or iterator protocol details. CodeMode materializes
|
||||
`matchAll` results instead of exposing iterators.
|
||||
- Locale selection and options. CodeMode deliberately uses the host default locale and ignores those arguments.
|
||||
- Test262 harness behavior or setup syntax unavailable in the confined interpreter.
|
||||
- Function-replacer behavior that is covered by CodeMode-specific tests for sequential callbacks, async tool calls,
|
||||
result coercion, diagnostics, and sandbox boundaries.
|
||||
- Assertions requiring an exact native error type when CodeMode deliberately exposes only its safe runtime error.
|
||||
|
||||
Handwritten tests remain where they specify CodeMode behavior rather than ordinary ECMAScript String semantics.
|
||||
Loading…
Add table
Add a link
Reference in a new issue