test(codemode): make promise tests deterministic (#36780)
This commit is contained in:
parent
6963f2f6da
commit
c15e3487b2
1 changed files with 144 additions and 69 deletions
|
|
@ -1,5 +1,5 @@
|
||||||
import { describe, expect, test } from "bun:test"
|
import { describe, expect, test } from "bun:test"
|
||||||
import { Effect, Schema } from "effect"
|
import { Deferred, Effect, Schema } from "effect"
|
||||||
import { CodeMode, Tool, toolError } from "../src/index.js"
|
import { CodeMode, Tool, toolError } from "../src/index.js"
|
||||||
|
|
||||||
// Wave 5 acceptance suite: first-class promise values. Un-awaited tool calls start eagerly on
|
// Wave 5 acceptance suite: first-class promise values. Un-awaited tool calls start eagerly on
|
||||||
|
|
@ -17,18 +17,41 @@ type Trace = {
|
||||||
|
|
||||||
const makeTrace = (): Trace => ({ starts: [], active: 0, maxActive: 0, completed: 0, interrupted: 0 })
|
const makeTrace = (): Trace => ({ starts: [], active: 0, maxActive: 0, completed: 0, interrupted: 0 })
|
||||||
|
|
||||||
/** Echoes `id` after `ms` milliseconds, recording start order, live concurrency, and interruption. */
|
/**
|
||||||
const sleepyTool = (trace: Trace) =>
|
* Deterministic tool set: ordering and interruption are structural, never temporal.
|
||||||
|
*
|
||||||
|
* - `echo` settles immediately with its id.
|
||||||
|
* - `gated` blocks until `open` releases the same id. Tool fibers start eagerly at the
|
||||||
|
* call site, so several gated calls are provably live at once before any `open` runs.
|
||||||
|
* - `pending` never settles; tests assert its interruption instead of racing a timer.
|
||||||
|
*
|
||||||
|
* Real clocks remain only in the wall-clock timeout tests (`timeoutMs`, `stubborn`
|
||||||
|
* cleanup), where elapsed time is the behavior under test.
|
||||||
|
*/
|
||||||
|
const echoTool = (trace: Trace) =>
|
||||||
Tool.make({
|
Tool.make({
|
||||||
description: "Echo an id after a delay",
|
description: "Echo an id immediately",
|
||||||
input: Schema.Struct({ id: Schema.Number, ms: Schema.optionalKey(Schema.Number) }),
|
input: Schema.Struct({ id: Schema.Number }),
|
||||||
output: Schema.Number,
|
output: Schema.Number,
|
||||||
run: ({ id, ms }) =>
|
run: ({ id }) =>
|
||||||
|
Effect.sync(() => {
|
||||||
|
trace.starts.push(id)
|
||||||
|
trace.completed += 1
|
||||||
|
return id
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
const gatedTool = (trace: Trace, gate: (id: number) => Deferred.Deferred<void>) =>
|
||||||
|
Tool.make({
|
||||||
|
description: "Echo an id once its gate opens",
|
||||||
|
input: Schema.Struct({ id: Schema.Number }),
|
||||||
|
output: Schema.Number,
|
||||||
|
run: ({ id }) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
trace.starts.push(id)
|
trace.starts.push(id)
|
||||||
trace.active += 1
|
trace.active += 1
|
||||||
trace.maxActive = Math.max(trace.maxActive, trace.active)
|
trace.maxActive = Math.max(trace.maxActive, trace.active)
|
||||||
yield* Effect.sleep(ms ?? 20)
|
yield* Deferred.await(gate(id))
|
||||||
trace.active -= 1
|
trace.active -= 1
|
||||||
trace.completed += 1
|
trace.completed += 1
|
||||||
return id
|
return id
|
||||||
|
|
@ -42,6 +65,35 @@ const sleepyTool = (trace: Trace) =>
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const openTool = (gate: (id: number) => Deferred.Deferred<void>) =>
|
||||||
|
Tool.make({
|
||||||
|
description: "Open the gate for an id",
|
||||||
|
input: Schema.Struct({ id: Schema.Number }),
|
||||||
|
output: Schema.Boolean,
|
||||||
|
run: ({ id }) => Deferred.succeed(gate(id), undefined),
|
||||||
|
})
|
||||||
|
|
||||||
|
const pendingTool = (trace: Trace) =>
|
||||||
|
Tool.make({
|
||||||
|
description: "Never settle",
|
||||||
|
input: Schema.Struct({ id: Schema.Number }),
|
||||||
|
output: Schema.Number,
|
||||||
|
run: ({ id }) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
trace.starts.push(id)
|
||||||
|
trace.active += 1
|
||||||
|
trace.maxActive = Math.max(trace.maxActive, trace.active)
|
||||||
|
return yield* Effect.never
|
||||||
|
}).pipe(
|
||||||
|
Effect.onInterrupt(() =>
|
||||||
|
Effect.sync(() => {
|
||||||
|
trace.active -= 1
|
||||||
|
trace.interrupted += 1
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
})
|
||||||
|
|
||||||
const failingTool = Tool.make({
|
const failingTool = Tool.make({
|
||||||
description: "Always refuse",
|
description: "Always refuse",
|
||||||
input: Schema.Struct({}),
|
input: Schema.Struct({}),
|
||||||
|
|
@ -58,7 +110,7 @@ const interruptedTool = Tool.make({
|
||||||
|
|
||||||
const completedTool = (trace: Trace) =>
|
const completedTool = (trace: Trace) =>
|
||||||
Tool.make({
|
Tool.make({
|
||||||
description: "Return the number of completed sleepy calls",
|
description: "Return the number of completed calls",
|
||||||
input: Schema.Struct({}),
|
input: Schema.Struct({}),
|
||||||
output: Schema.Number,
|
output: Schema.Number,
|
||||||
run: () => Effect.succeed(trace.completed),
|
run: () => Effect.succeed(trace.completed),
|
||||||
|
|
@ -88,11 +140,22 @@ const run = (
|
||||||
options: { trace?: Trace; limits?: CodeMode.ExecutionLimits } = {},
|
options: { trace?: Trace; limits?: CodeMode.ExecutionLimits } = {},
|
||||||
): Promise<CodeMode.Result> => {
|
): Promise<CodeMode.Result> => {
|
||||||
const trace = options.trace ?? makeTrace()
|
const trace = options.trace ?? makeTrace()
|
||||||
|
const gates = new Map<number, Deferred.Deferred<void>>()
|
||||||
|
const gate = (id: number): Deferred.Deferred<void> => {
|
||||||
|
const existing = gates.get(id)
|
||||||
|
if (existing) return existing
|
||||||
|
const created = Deferred.makeUnsafe<void>()
|
||||||
|
gates.set(id, created)
|
||||||
|
return created
|
||||||
|
}
|
||||||
return Effect.runPromise(
|
return Effect.runPromise(
|
||||||
CodeMode.execute({
|
CodeMode.execute({
|
||||||
tools: {
|
tools: {
|
||||||
host: {
|
host: {
|
||||||
sleepy: sleepyTool(trace),
|
echo: echoTool(trace),
|
||||||
|
gated: gatedTool(trace, gate),
|
||||||
|
open: openTool(gate),
|
||||||
|
pending: pendingTool(trace),
|
||||||
fail: failingTool,
|
fail: failingTool,
|
||||||
interrupt: interruptedTool,
|
interrupt: interruptedTool,
|
||||||
completed: completedTool(trace),
|
completed: completedTool(trace),
|
||||||
|
|
@ -122,7 +185,7 @@ describe("first-class promise values", () => {
|
||||||
expect(
|
expect(
|
||||||
await value(`
|
await value(`
|
||||||
const load = async (id) => {
|
const load = async (id) => {
|
||||||
const result = await tools.host.sleepy({ id, ms: 20 })
|
const result = await tools.host.echo({ id })
|
||||||
return [id, result]
|
return [id, result]
|
||||||
}
|
}
|
||||||
const first = load(1)
|
const first = load(1)
|
||||||
|
|
@ -158,8 +221,10 @@ describe("first-class promise values", () => {
|
||||||
const trace = makeTrace()
|
const trace = makeTrace()
|
||||||
const result = await value(
|
const result = await value(
|
||||||
`
|
`
|
||||||
const a = tools.host.sleepy({ id: 1, ms: 40 })
|
const a = tools.host.gated({ id: 1 })
|
||||||
const b = tools.host.sleepy({ id: 2, ms: 40 })
|
const b = tools.host.gated({ id: 2 })
|
||||||
|
await tools.host.open({ id: 1 })
|
||||||
|
await tools.host.open({ id: 2 })
|
||||||
const rb = await b
|
const rb = await b
|
||||||
const ra = await a
|
const ra = await a
|
||||||
return [ra, rb]
|
return [ra, rb]
|
||||||
|
|
@ -174,7 +239,7 @@ describe("first-class promise values", () => {
|
||||||
|
|
||||||
test("awaiting the same promise twice settles once and never re-runs the call", async () => {
|
test("awaiting the same promise twice settles once and never re-runs the call", async () => {
|
||||||
const result = await run(`
|
const result = await run(`
|
||||||
const p = tools.host.sleepy({ id: 7 })
|
const p = tools.host.echo({ id: 7 })
|
||||||
const x = await p
|
const x = await p
|
||||||
const y = await p
|
const y = await p
|
||||||
return [x, y]
|
return [x, y]
|
||||||
|
|
@ -182,7 +247,7 @@ describe("first-class promise values", () => {
|
||||||
expect(result.ok).toBe(true)
|
expect(result.ok).toBe(true)
|
||||||
if (!result.ok) return
|
if (!result.ok) return
|
||||||
expect(result.value).toEqual([7, 7])
|
expect(result.value).toEqual([7, 7])
|
||||||
expect(result.toolCalls).toStrictEqual([{ name: "host.sleepy" }])
|
expect(result.toolCalls).toStrictEqual([{ name: "host.echo" }])
|
||||||
})
|
})
|
||||||
|
|
||||||
test("await of a non-promise value passes it through unchanged", async () => {
|
test("await of a non-promise value passes it through unchanged", async () => {
|
||||||
|
|
@ -193,7 +258,7 @@ describe("first-class promise values", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
test("returning an un-awaited tool call resolves it (async-function return semantics)", async () => {
|
test("returning an un-awaited tool call resolves it (async-function return semantics)", async () => {
|
||||||
expect(await value(`return tools.host.sleepy({ id: 9 })`)).toBe(9)
|
expect(await value(`return tools.host.echo({ id: 9 })`)).toBe(9)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("typeof a promise is 'object', and console.log renders it sensibly", async () => {
|
test("typeof a promise is 'object', and console.log renders it sensibly", async () => {
|
||||||
|
|
@ -228,7 +293,7 @@ describe("first-class promise values", () => {
|
||||||
const trace = makeTrace()
|
const trace = makeTrace()
|
||||||
const result = await run(
|
const result = await run(
|
||||||
`
|
`
|
||||||
tools.host.sleepy({ id: 1, ms: 30 })
|
tools.host.pending({ id: 1 })
|
||||||
return "done"
|
return "done"
|
||||||
`,
|
`,
|
||||||
{ trace },
|
{ trace },
|
||||||
|
|
@ -307,7 +372,7 @@ describe("first-class promise values", () => {
|
||||||
const result = await run(
|
const result = await run(
|
||||||
`
|
`
|
||||||
const run = async () => {
|
const run = async () => {
|
||||||
await tools.host.sleepy({ id: 1, ms: 60000 })
|
await tools.host.pending({ id: 1 })
|
||||||
tools.host.fail({})
|
tools.host.fail({})
|
||||||
}
|
}
|
||||||
run()
|
run()
|
||||||
|
|
@ -373,7 +438,7 @@ describe("first-class promise values", () => {
|
||||||
const trace = makeTrace()
|
const trace = makeTrace()
|
||||||
const result = await run(
|
const result = await run(
|
||||||
`
|
`
|
||||||
tools.host.sleepy({ id: 1, ms: 1_000 })
|
tools.host.pending({ id: 1 })
|
||||||
throw new Error("boom")
|
throw new Error("boom")
|
||||||
`,
|
`,
|
||||||
{ trace },
|
{ trace },
|
||||||
|
|
@ -392,8 +457,8 @@ describe("first-class promise values", () => {
|
||||||
await value(
|
await value(
|
||||||
`
|
`
|
||||||
const launch = async () => {
|
const launch = async () => {
|
||||||
tools.host.sleepy({ id: 1, ms: 60000 })
|
tools.host.pending({ id: 1 })
|
||||||
Promise.all([tools.host.sleepy({ id: 2, ms: 60000 })])
|
Promise.all([tools.host.pending({ id: 2 })])
|
||||||
return "returned"
|
return "returned"
|
||||||
}
|
}
|
||||||
return await launch()
|
return await launch()
|
||||||
|
|
@ -411,7 +476,7 @@ describe("first-class promise values", () => {
|
||||||
|
|
||||||
describe("promises at data boundaries", () => {
|
describe("promises at data boundaries", () => {
|
||||||
test("returning an un-awaited promise inside data is a clear await-hinting diagnostic", async () => {
|
test("returning an un-awaited promise inside data is a clear await-hinting diagnostic", async () => {
|
||||||
const diagnostic = await error(`return { result: tools.host.sleepy({ id: 1 }) }`)
|
const diagnostic = await error(`return { result: tools.host.echo({ id: 1 }) }`)
|
||||||
expect(diagnostic.kind).toBe("InvalidDataValue")
|
expect(diagnostic.kind).toBe("InvalidDataValue")
|
||||||
expect(diagnostic.message).toContain("un-awaited Promise")
|
expect(diagnostic.message).toContain("un-awaited Promise")
|
||||||
expect(diagnostic.message).toContain("await tools.ns.tool(...)")
|
expect(diagnostic.message).toContain("await tools.ns.tool(...)")
|
||||||
|
|
@ -427,7 +492,7 @@ describe("promises at data boundaries", () => {
|
||||||
const trace = makeTrace()
|
const trace = makeTrace()
|
||||||
const result = await run(
|
const result = await run(
|
||||||
`
|
`
|
||||||
const pending = tools.host.sleepy({ id: 1, ms: 60_000 })
|
const pending = tools.host.pending({ id: 1 })
|
||||||
return { pending }
|
return { pending }
|
||||||
`,
|
`,
|
||||||
{ trace, limits: { timeoutMs: 100 } },
|
{ trace, limits: { timeoutMs: 100 } },
|
||||||
|
|
@ -440,7 +505,7 @@ describe("promises at data boundaries", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
test("passing an un-awaited promise as a tool argument is a clear diagnostic", async () => {
|
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 }) })`)
|
const diagnostic = await error(`return await tools.host.echo({ id: tools.host.echo({ id: 1 }) })`)
|
||||||
expect(diagnostic.kind).toBe("InvalidDataValue")
|
expect(diagnostic.kind).toBe("InvalidDataValue")
|
||||||
expect(diagnostic.message).toContain("un-awaited Promise")
|
expect(diagnostic.message).toContain("un-awaited Promise")
|
||||||
})
|
})
|
||||||
|
|
@ -475,8 +540,10 @@ describe("Promise.all over arbitrary arrays", () => {
|
||||||
expect(
|
expect(
|
||||||
await value(
|
await value(
|
||||||
`
|
`
|
||||||
const first = Promise.all([tools.host.sleepy({ id: 1, ms: 40 })])
|
const first = Promise.all([tools.host.gated({ id: 1 })])
|
||||||
const second = Promise.all([tools.host.sleepy({ id: 2, ms: 40 })])
|
const second = Promise.all([tools.host.gated({ id: 2 })])
|
||||||
|
await tools.host.open({ id: 1 })
|
||||||
|
await tools.host.open({ id: 2 })
|
||||||
return [await first, await second]
|
return [await first, await second]
|
||||||
`,
|
`,
|
||||||
{ trace },
|
{ trace },
|
||||||
|
|
@ -502,19 +569,19 @@ describe("Promise.all over arbitrary arrays", () => {
|
||||||
|
|
||||||
test("awaiting an aggregate repeatedly does not rerun its members", async () => {
|
test("awaiting an aggregate repeatedly does not rerun its members", async () => {
|
||||||
const result = await run(`
|
const result = await run(`
|
||||||
const aggregate = Promise.all([tools.host.sleepy({ id: 7 })])
|
const aggregate = Promise.all([tools.host.echo({ id: 7 })])
|
||||||
return [await aggregate, await aggregate]
|
return [await aggregate, await aggregate]
|
||||||
`)
|
`)
|
||||||
expect(result.ok).toBe(true)
|
expect(result.ok).toBe(true)
|
||||||
if (!result.ok) return
|
if (!result.ok) return
|
||||||
expect(result.value).toEqual([[7], [7]])
|
expect(result.value).toEqual([[7], [7]])
|
||||||
expect(result.toolCalls).toStrictEqual([{ name: "host.sleepy" }])
|
expect(result.toolCalls).toStrictEqual([{ name: "host.echo" }])
|
||||||
})
|
})
|
||||||
|
|
||||||
test("mixes promises and plain values, preserving order", async () => {
|
test("mixes promises and plain values, preserving order", async () => {
|
||||||
expect(
|
expect(
|
||||||
await value(`
|
await value(`
|
||||||
return await Promise.all([tools.host.sleepy({ id: 1 }), "plain", tools.host.sleepy({ id: 2 }), 42])
|
return await Promise.all([tools.host.echo({ id: 1 }), "plain", tools.host.echo({ id: 2 }), 42])
|
||||||
`),
|
`),
|
||||||
).toEqual([1, "plain", 2, 42])
|
).toEqual([1, "plain", 2, 42])
|
||||||
})
|
})
|
||||||
|
|
@ -523,9 +590,9 @@ describe("Promise.all over arbitrary arrays", () => {
|
||||||
expect(
|
expect(
|
||||||
await value(`
|
await value(`
|
||||||
const calls = []
|
const calls = []
|
||||||
calls.push(tools.host.sleepy({ id: 1 }))
|
calls.push(tools.host.echo({ id: 1 }))
|
||||||
calls.push(7)
|
calls.push(7)
|
||||||
const more = [tools.host.sleepy({ id: 2 })]
|
const more = [tools.host.echo({ id: 2 })]
|
||||||
const batch = [...calls, ...more, "x"]
|
const batch = [...calls, ...more, "x"]
|
||||||
return await Promise.all(batch)
|
return await Promise.all(batch)
|
||||||
`),
|
`),
|
||||||
|
|
@ -537,7 +604,9 @@ describe("Promise.all over arbitrary arrays", () => {
|
||||||
const result = await value(
|
const result = await value(
|
||||||
`
|
`
|
||||||
const ids = [1, 2, 3, 4]
|
const ids = [1, 2, 3, 4]
|
||||||
return await Promise.all(ids.map((id) => tools.host.sleepy({ id, ms: 40 })))
|
const calls = ids.map((id) => tools.host.gated({ id }))
|
||||||
|
for (const id of ids) await tools.host.open({ id })
|
||||||
|
return await Promise.all(calls)
|
||||||
`,
|
`,
|
||||||
{ trace },
|
{ trace },
|
||||||
)
|
)
|
||||||
|
|
@ -552,7 +621,9 @@ describe("Promise.all over arbitrary arrays", () => {
|
||||||
const result = await value(
|
const result = await value(
|
||||||
`
|
`
|
||||||
const ids = [1, 2, 3, 4]
|
const ids = [1, 2, 3, 4]
|
||||||
return await Promise.all(ids.map(async (id) => await tools.host.sleepy({ id, ms: 40 })))
|
const calls = ids.map(async (id) => await tools.host.gated({ id }))
|
||||||
|
for (const id of ids) await tools.host.open({ id })
|
||||||
|
return await Promise.all(calls)
|
||||||
`,
|
`,
|
||||||
{ trace },
|
{ trace },
|
||||||
)
|
)
|
||||||
|
|
@ -566,7 +637,9 @@ describe("Promise.all over arbitrary arrays", () => {
|
||||||
`
|
`
|
||||||
const ids = []
|
const ids = []
|
||||||
for (let i = 0; i < 20; i += 1) ids.push(i)
|
for (let i = 0; i < 20; i += 1) ids.push(i)
|
||||||
const results = await Promise.all(ids.map((id) => tools.host.sleepy({ id, ms: 10 })))
|
const calls = ids.map((id) => tools.host.gated({ id }))
|
||||||
|
for (const id of ids) await tools.host.open({ id })
|
||||||
|
const results = await Promise.all(calls)
|
||||||
return results.length
|
return results.length
|
||||||
`,
|
`,
|
||||||
{ trace },
|
{ trace },
|
||||||
|
|
@ -582,7 +655,7 @@ describe("Promise.all over arbitrary arrays", () => {
|
||||||
test("rejects with the first failure, catchable in-program", async () => {
|
test("rejects with the first failure, catchable in-program", async () => {
|
||||||
const result = await run(`
|
const result = await run(`
|
||||||
try {
|
try {
|
||||||
await Promise.all([tools.host.sleepy({ id: 1 }), tools.host.fail({})])
|
await Promise.all([tools.host.echo({ id: 1 }), tools.host.fail({})])
|
||||||
return "no"
|
return "no"
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return e.message
|
return e.message
|
||||||
|
|
@ -601,7 +674,7 @@ describe("Promise.all over arbitrary arrays", () => {
|
||||||
`
|
`
|
||||||
try {
|
try {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
tools.host.sleepy({ id: 1, ms: 100 }),
|
tools.host.pending({ id: 1 }),
|
||||||
tools.host.fail({}),
|
tools.host.fail({}),
|
||||||
])
|
])
|
||||||
return -1
|
return -1
|
||||||
|
|
@ -623,11 +696,12 @@ describe("Promise.all over arbitrary arrays", () => {
|
||||||
expect(
|
expect(
|
||||||
await value(
|
await value(
|
||||||
`
|
`
|
||||||
const slow = tools.host.sleepy({ id: 1, ms: 40 })
|
const slow = tools.host.gated({ id: 1 })
|
||||||
try {
|
try {
|
||||||
await Promise.all([slow, tools.host.fail({})])
|
await Promise.all([slow, tools.host.fail({})])
|
||||||
return "no"
|
return "no"
|
||||||
} catch {}
|
} catch {}
|
||||||
|
await tools.host.open({ id: 1 })
|
||||||
return await slow
|
return await slow
|
||||||
`,
|
`,
|
||||||
{ trace },
|
{ trace },
|
||||||
|
|
@ -643,7 +717,7 @@ describe("Promise.all over arbitrary arrays", () => {
|
||||||
await value(
|
await value(
|
||||||
`
|
`
|
||||||
const failLater = async () => {
|
const failLater = async () => {
|
||||||
await tools.host.sleepy({ id: 1, ms: 40 })
|
await tools.host.pending({ id: 1 })
|
||||||
throw new Error("later")
|
throw new Error("later")
|
||||||
}
|
}
|
||||||
const aggregate = Promise.all([Promise.reject(new Error("first")), failLater()])
|
const aggregate = Promise.all([Promise.reject(new Error("first")), failLater()])
|
||||||
|
|
@ -668,7 +742,7 @@ describe("Promise.all over arbitrary arrays", () => {
|
||||||
|
|
||||||
test("exceeding maxToolCalls inside Promise.all is a ToolCallLimitExceeded diagnostic", async () => {
|
test("exceeding maxToolCalls inside Promise.all is a ToolCallLimitExceeded diagnostic", async () => {
|
||||||
const diagnostic = await error(
|
const diagnostic = await error(
|
||||||
`return await Promise.all([tools.host.sleepy({ id: 1 }), tools.host.sleepy({ id: 2 }), tools.host.sleepy({ id: 3 })])`,
|
`return await Promise.all([tools.host.echo({ id: 1 }), tools.host.echo({ id: 2 }), tools.host.echo({ id: 3 })])`,
|
||||||
{ limits: { maxToolCalls: 2 } },
|
{ limits: { maxToolCalls: 2 } },
|
||||||
)
|
)
|
||||||
expect(diagnostic.kind).toBe("ToolCallLimitExceeded")
|
expect(diagnostic.kind).toBe("ToolCallLimitExceeded")
|
||||||
|
|
@ -680,7 +754,7 @@ describe("Promise.allSettled", () => {
|
||||||
expect(
|
expect(
|
||||||
await value(`
|
await value(`
|
||||||
return await Promise.allSettled([
|
return await Promise.allSettled([
|
||||||
tools.host.sleepy({ id: 5 }),
|
tools.host.echo({ id: 5 }),
|
||||||
tools.host.fail({}),
|
tools.host.fail({}),
|
||||||
"plain",
|
"plain",
|
||||||
Promise.reject(new Error("boom")),
|
Promise.reject(new Error("boom")),
|
||||||
|
|
@ -711,8 +785,8 @@ describe("Promise.race", () => {
|
||||||
const trace = makeTrace()
|
const trace = makeTrace()
|
||||||
const result = await value(
|
const result = await value(
|
||||||
`
|
`
|
||||||
const fast = tools.host.sleepy({ id: 1, ms: 10 })
|
const fast = tools.host.echo({ id: 1 })
|
||||||
const slow = tools.host.sleepy({ id: 2, ms: 40 })
|
const slow = tools.host.pending({ id: 2 })
|
||||||
return await Promise.race([fast, slow])
|
return await Promise.race([fast, slow])
|
||||||
`,
|
`,
|
||||||
{ trace },
|
{ trace },
|
||||||
|
|
@ -726,9 +800,10 @@ describe("Promise.race", () => {
|
||||||
test("a direct loser remains awaitable after the race settles", async () => {
|
test("a direct loser remains awaitable after the race settles", async () => {
|
||||||
expect(
|
expect(
|
||||||
await value(`
|
await value(`
|
||||||
const fast = tools.host.sleepy({ id: 1, ms: 10 })
|
const fast = tools.host.echo({ id: 1 })
|
||||||
const slow = tools.host.sleepy({ id: 2, ms: 40 })
|
const slow = tools.host.gated({ id: 2 })
|
||||||
const winner = await Promise.race([fast, slow])
|
const winner = await Promise.race([fast, slow])
|
||||||
|
await tools.host.open({ id: 2 })
|
||||||
return { winner, loser: await slow }
|
return { winner, loser: await slow }
|
||||||
`),
|
`),
|
||||||
).toEqual({ winner: 1, loser: 2 })
|
).toEqual({ winner: 1, loser: 2 })
|
||||||
|
|
@ -740,8 +815,8 @@ describe("Promise.race", () => {
|
||||||
await value(
|
await value(
|
||||||
`
|
`
|
||||||
const nested = Promise.all([
|
const nested = Promise.all([
|
||||||
tools.host.sleepy({ id: 1, ms: 40 }),
|
tools.host.pending({ id: 1 }),
|
||||||
tools.host.sleepy({ id: 2, ms: 40 }),
|
tools.host.pending({ id: 2 }),
|
||||||
])
|
])
|
||||||
return await Promise.race(["immediate", nested])
|
return await Promise.race(["immediate", nested])
|
||||||
`,
|
`,
|
||||||
|
|
@ -757,7 +832,7 @@ describe("Promise.race", () => {
|
||||||
expect(
|
expect(
|
||||||
await value(`
|
await value(`
|
||||||
try {
|
try {
|
||||||
await Promise.race([tools.host.fail({}), tools.host.sleepy({ id: 1, ms: 40 })])
|
await Promise.race([tools.host.fail({}), tools.host.pending({ id: 1 })])
|
||||||
return "no"
|
return "no"
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return e.message
|
return e.message
|
||||||
|
|
@ -768,9 +843,9 @@ describe("Promise.race", () => {
|
||||||
|
|
||||||
test("a plain value wins over pending promises", async () => {
|
test("a plain value wins over pending promises", async () => {
|
||||||
const trace = makeTrace()
|
const trace = makeTrace()
|
||||||
expect(
|
expect(await value(`return await Promise.race([tools.host.pending({ id: 1 }), "immediate"])`, { trace })).toBe(
|
||||||
await value(`return await Promise.race([tools.host.sleepy({ id: 1, ms: 40 }), "immediate"])`, { trace }),
|
"immediate",
|
||||||
).toBe("immediate")
|
)
|
||||||
expect(trace.completed).toBe(0)
|
expect(trace.completed).toBe(0)
|
||||||
expect(trace.interrupted).toBe(1)
|
expect(trace.interrupted).toBe(1)
|
||||||
})
|
})
|
||||||
|
|
@ -793,7 +868,7 @@ describe("Promise.resolve / Promise.reject", () => {
|
||||||
test("resolve wraps plain values and passes promises through", async () => {
|
test("resolve wraps plain values and passes promises through", async () => {
|
||||||
expect(await value(`return await Promise.resolve(42)`)).toBe(42)
|
expect(await value(`return await Promise.resolve(42)`)).toBe(42)
|
||||||
expect(await value(`return await Promise.resolve(Promise.resolve("nested"))`)).toBe("nested")
|
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(`return await Promise.resolve(tools.host.echo({ id: 3 }))`)).toBe(3)
|
||||||
expect(await value(`const promise = Promise.resolve(1); return [promise].includes(Promise.resolve(promise))`)).toBe(
|
expect(await value(`const promise = Promise.resolve(1); return [promise].includes(Promise.resolve(promise))`)).toBe(
|
||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
|
|
@ -816,7 +891,7 @@ describe("Promise.resolve / Promise.reject", () => {
|
||||||
expect(
|
expect(
|
||||||
await value(`
|
await value(`
|
||||||
const rejected = Promise.reject(new Error("handled"))
|
const rejected = Promise.reject(new Error("handled"))
|
||||||
await tools.host.sleepy({ id: 1 })
|
await tools.host.echo({ id: 1 })
|
||||||
try {
|
try {
|
||||||
await rejected
|
await rejected
|
||||||
return "no"
|
return "no"
|
||||||
|
|
@ -846,8 +921,8 @@ describe("timeout interruption of forked calls", () => {
|
||||||
const trace = makeTrace()
|
const trace = makeTrace()
|
||||||
const result = await run(
|
const result = await run(
|
||||||
`
|
`
|
||||||
const a = tools.host.sleepy({ id: 1, ms: 60000 })
|
const a = tools.host.pending({ id: 1 })
|
||||||
const b = tools.host.sleepy({ id: 2, ms: 60000 })
|
const b = tools.host.pending({ id: 2 })
|
||||||
return await a
|
return await a
|
||||||
`,
|
`,
|
||||||
{ trace, limits: { timeoutMs: 100 } },
|
{ trace, limits: { timeoutMs: 100 } },
|
||||||
|
|
@ -864,7 +939,7 @@ describe("timeout interruption of forked calls", () => {
|
||||||
test("the timeout also interrupts calls inside Promise.all", async () => {
|
test("the timeout also interrupts calls inside Promise.all", async () => {
|
||||||
const trace = makeTrace()
|
const trace = makeTrace()
|
||||||
const result = await run(
|
const result = await run(
|
||||||
`return await Promise.all([tools.host.sleepy({ id: 1, ms: 60000 }), tools.host.sleepy({ id: 2, ms: 60000 })])`,
|
`return await Promise.all([tools.host.pending({ id: 1 }), tools.host.pending({ id: 2 })])`,
|
||||||
{ trace, limits: { timeoutMs: 100 } },
|
{ trace, limits: { timeoutMs: 100 } },
|
||||||
)
|
)
|
||||||
expect(result.ok).toBe(false)
|
expect(result.ok).toBe(false)
|
||||||
|
|
@ -875,7 +950,7 @@ describe("timeout interruption of forked calls", () => {
|
||||||
|
|
||||||
test("a non-settling race loser cannot hold the execution to the timeout", async () => {
|
test("a non-settling race loser cannot hold the execution to the timeout", async () => {
|
||||||
const trace = makeTrace()
|
const trace = makeTrace()
|
||||||
const result = await run(`return await Promise.race(["winner", tools.host.sleepy({ id: 1, ms: 60000 })])`, {
|
const result = await run(`return await Promise.race(["winner", tools.host.pending({ id: 1 })])`, {
|
||||||
trace,
|
trace,
|
||||||
limits: { timeoutMs: 100 },
|
limits: { timeoutMs: 100 },
|
||||||
})
|
})
|
||||||
|
|
@ -940,8 +1015,8 @@ describe("promise chaining", () => {
|
||||||
expect(
|
expect(
|
||||||
await value(`
|
await value(`
|
||||||
return await tools.host
|
return await tools.host
|
||||||
.sleepy({ id: 2 })
|
.echo({ id: 2 })
|
||||||
.then((id) => tools.host.sleepy({ id: id + 1 }))
|
.then((id) => tools.host.echo({ id: id + 1 }))
|
||||||
.then((id) => id * 10)
|
.then((id) => id * 10)
|
||||||
`),
|
`),
|
||||||
).toBe(30)
|
).toBe(30)
|
||||||
|
|
@ -966,7 +1041,7 @@ describe("promise chaining", () => {
|
||||||
await value(`
|
await value(`
|
||||||
return [
|
return [
|
||||||
await tools.host.fail({}).catch((error) => error.message),
|
await tools.host.fail({}).catch((error) => error.message),
|
||||||
await tools.host.sleepy({ id: 4 }).catch(() => "unused"),
|
await tools.host.echo({ id: 4 }).catch(() => "unused"),
|
||||||
]
|
]
|
||||||
`),
|
`),
|
||||||
).toEqual(["Lookup refused", 4])
|
).toEqual(["Lookup refused", 4])
|
||||||
|
|
@ -976,7 +1051,7 @@ describe("promise chaining", () => {
|
||||||
expect(
|
expect(
|
||||||
await value(`
|
await value(`
|
||||||
const events = []
|
const events = []
|
||||||
const result = await tools.host.sleepy({ id: 5 }).finally(() => events.push("cleanup"))
|
const result = await tools.host.echo({ id: 5 }).finally(() => events.push("cleanup"))
|
||||||
return [result, events]
|
return [result, events]
|
||||||
`),
|
`),
|
||||||
).toEqual([5, ["cleanup"]])
|
).toEqual([5, ["cleanup"]])
|
||||||
|
|
@ -1009,12 +1084,12 @@ describe("promise chaining", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
test("non-plain-function handlers fail loudly instead of being ignored", async () => {
|
test("non-plain-function handlers fail loudly instead of being ignored", async () => {
|
||||||
const diagnostic = await error(`return await tools.host.sleepy({ id: 1 }).then(tools.host.completed)`)
|
const diagnostic = await error(`return await tools.host.echo({ id: 1 }).then(tools.host.completed)`)
|
||||||
expect(diagnostic.message).toContain("Promise.prototype.then handlers must be plain functions")
|
expect(diagnostic.message).toContain("Promise.prototype.then handlers must be plain functions")
|
||||||
})
|
})
|
||||||
|
|
||||||
test("chaining methods are opaque references until called", async () => {
|
test("chaining methods are opaque references until called", async () => {
|
||||||
expect(await value(`return typeof tools.host.sleepy({ id: 1 }).then`)).toBe("function")
|
expect(await value(`return typeof tools.host.echo({ id: 1 }).then`)).toBe("function")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -1039,7 +1114,7 @@ describe("combinator settlement timing", () => {
|
||||||
// cannot beat it into rejection.
|
// cannot beat it into rejection.
|
||||||
expect(
|
expect(
|
||||||
await value(`
|
await value(`
|
||||||
const pending = tools.host.sleepy({ id: 9, ms: 60000 })
|
const pending = tools.host.pending({ id: 9 })
|
||||||
const winner = await Promise.race([Promise.all([Promise.resolve(1)]), Promise.resolve(2)])
|
const winner = await Promise.race([Promise.all([Promise.resolve(1)]), Promise.resolve(2)])
|
||||||
try {
|
try {
|
||||||
const raced = await Promise.race([Promise.all([Promise.reject("x"), pending]), Promise.resolve("ok")])
|
const raced = await Promise.race([Promise.all([Promise.reject("x"), pending]), Promise.resolve("ok")])
|
||||||
|
|
@ -1054,7 +1129,7 @@ describe("combinator settlement timing", () => {
|
||||||
|
|
||||||
describe("unsupported promise surface", () => {
|
describe("unsupported promise surface", () => {
|
||||||
test("other property reads on a promise hint at the missing await", async () => {
|
test("other property reads on a promise hint at the missing await", async () => {
|
||||||
const diagnostic = await error(`return tools.host.sleepy({ id: 1 }).value`)
|
const diagnostic = await error(`return tools.host.echo({ id: 1 }).value`)
|
||||||
expect(diagnostic.kind).toBe("InvalidDataValue")
|
expect(diagnostic.kind).toBe("InvalidDataValue")
|
||||||
expect(diagnostic.message).toContain("un-awaited Promise")
|
expect(diagnostic.message).toContain("un-awaited Promise")
|
||||||
expect(diagnostic.message).toContain("await it first")
|
expect(diagnostic.message).toContain("await it first")
|
||||||
|
|
@ -1074,8 +1149,8 @@ describe("Promise.any", () => {
|
||||||
`
|
`
|
||||||
const winner = await Promise.any([
|
const winner = await Promise.any([
|
||||||
tools.host.fail({}),
|
tools.host.fail({}),
|
||||||
tools.host.sleepy({ id: 1, ms: 5 }),
|
tools.host.echo({ id: 1 }),
|
||||||
tools.host.sleepy({ id: 2, ms: 60000 }),
|
tools.host.pending({ id: 2 }),
|
||||||
])
|
])
|
||||||
return winner
|
return winner
|
||||||
`,
|
`,
|
||||||
|
|
@ -1141,7 +1216,7 @@ describe("promise construction", () => {
|
||||||
const id = await gate
|
const id = await gate
|
||||||
return id * 2
|
return id * 2
|
||||||
})()
|
})()
|
||||||
openGate(await tools.host.sleepy({ id: 21, ms: 5 }))
|
openGate(await tools.host.echo({ id: 21 }))
|
||||||
return await worker
|
return await worker
|
||||||
`),
|
`),
|
||||||
).toBe(42)
|
).toBe(42)
|
||||||
|
|
@ -1151,7 +1226,7 @@ describe("promise construction", () => {
|
||||||
expect(
|
expect(
|
||||||
await value(`
|
await value(`
|
||||||
const bridged = new Promise((resolve, reject) => {
|
const bridged = new Promise((resolve, reject) => {
|
||||||
tools.host.sleepy({ id: 7, ms: 5 }).then(resolve, reject)
|
tools.host.echo({ id: 7 }).then(resolve, reject)
|
||||||
})
|
})
|
||||||
return await bridged
|
return await bridged
|
||||||
`),
|
`),
|
||||||
|
|
@ -1163,7 +1238,7 @@ describe("promise construction", () => {
|
||||||
await value(`
|
await value(`
|
||||||
let settle
|
let settle
|
||||||
const manual = new Promise((resolve) => { settle = resolve })
|
const manual = new Promise((resolve) => { settle = resolve })
|
||||||
const race = Promise.race([manual, tools.host.sleepy({ id: 3, ms: 60000 })])
|
const race = Promise.race([manual, tools.host.pending({ id: 3 })])
|
||||||
const all = Promise.all([manual, "plain"])
|
const all = Promise.all([manual, "plain"])
|
||||||
const any = Promise.any([manual, new Promise(() => {})])
|
const any = Promise.any([manual, new Promise(() => {})])
|
||||||
settle("manual")
|
settle("manual")
|
||||||
|
|
@ -1193,7 +1268,7 @@ describe("promise construction", () => {
|
||||||
expect(
|
expect(
|
||||||
await value(`
|
await value(`
|
||||||
const result = new Promise(async (resolve) => {
|
const result = new Promise(async (resolve) => {
|
||||||
const id = await tools.host.sleepy({ id: 5, ms: 5 })
|
const id = await tools.host.echo({ id: 5 })
|
||||||
resolve(id * 2)
|
resolve(id * 2)
|
||||||
})
|
})
|
||||||
return await result
|
return await result
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue