chore: generate
This commit is contained in:
parent
cb93114424
commit
83c638eaac
20 changed files with 2137 additions and 1207 deletions
|
|
@ -1,6 +1,13 @@
|
|||
import { describe, expect, test } from "bun:test"
|
||||
import { Cause, Effect, Schema } from "effect"
|
||||
import { CodeMode, ExecuteInputSchema, ExecuteResultSchema, Tool, toolError, type ExecutionLimits } from "../src/index.js"
|
||||
import {
|
||||
CodeMode,
|
||||
ExecuteInputSchema,
|
||||
ExecuteResultSchema,
|
||||
Tool,
|
||||
toolError,
|
||||
type ExecutionLimits,
|
||||
} from "../src/index.js"
|
||||
import type { Definition } from "../src/tool.js"
|
||||
|
||||
const run = (tool: Definition<never>) =>
|
||||
|
|
@ -75,11 +82,14 @@ describe("CodeMode host failure boundary", () => {
|
|||
output: Schema.Unknown,
|
||||
run: () =>
|
||||
Effect.succeed(
|
||||
new Proxy({}, {
|
||||
ownKeys: () => {
|
||||
throw new Error("host-output-secret")
|
||||
new Proxy(
|
||||
{},
|
||||
{
|
||||
ownKeys: () => {
|
||||
throw new Error("host-output-secret")
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
),
|
||||
}),
|
||||
)
|
||||
|
|
@ -162,9 +172,7 @@ describe("CodeMode tool-call observation", () => {
|
|||
)
|
||||
|
||||
expect(result.ok).toBe(true)
|
||||
expect(calls).toStrictEqual([
|
||||
{ index: 0, name: "context.lookup", input: { query: "deployment failure" } },
|
||||
])
|
||||
expect(calls).toStrictEqual([{ index: 0, name: "context.lookup", input: { query: "deployment failure" } }])
|
||||
})
|
||||
|
||||
test("observes settled calls with outcome and duration", async () => {
|
||||
|
|
@ -173,25 +181,26 @@ describe("CodeMode tool-call observation", () => {
|
|||
description: "Look up a value",
|
||||
input: Schema.Struct({ query: Schema.String }),
|
||||
output: Schema.String,
|
||||
run: ({ query }) =>
|
||||
query === "boom" ? Effect.fail(toolError("Lookup refused")) : Effect.succeed(query),
|
||||
run: ({ query }) => (query === "boom" ? Effect.fail(toolError("Lookup refused")) : Effect.succeed(query)),
|
||||
})
|
||||
|
||||
const runtime = CodeMode.make({
|
||||
tools: { context: { lookup } },
|
||||
onToolCallStart: (call) => Effect.sync(() => {
|
||||
events.push({ phase: "start", index: call.index, name: call.name })
|
||||
}),
|
||||
onToolCallEnd: (call) => Effect.sync(() => {
|
||||
expect(call.durationMs).toBeGreaterThanOrEqual(0)
|
||||
events.push({
|
||||
phase: "end",
|
||||
index: call.index,
|
||||
name: call.name,
|
||||
outcome: call.outcome,
|
||||
...(call.message === undefined ? {} : { message: call.message }),
|
||||
})
|
||||
}),
|
||||
onToolCallStart: (call) =>
|
||||
Effect.sync(() => {
|
||||
events.push({ phase: "start", index: call.index, name: call.name })
|
||||
}),
|
||||
onToolCallEnd: (call) =>
|
||||
Effect.sync(() => {
|
||||
expect(call.durationMs).toBeGreaterThanOrEqual(0)
|
||||
events.push({
|
||||
phase: "end",
|
||||
index: call.index,
|
||||
name: call.name,
|
||||
outcome: call.outcome,
|
||||
...(call.message === undefined ? {} : { message: call.message }),
|
||||
})
|
||||
}),
|
||||
})
|
||||
|
||||
const success = await Effect.runPromise(runtime.execute(`return await tools.context.lookup({ query: "ok" })`))
|
||||
|
|
@ -210,13 +219,15 @@ describe("CodeMode tool-call observation", () => {
|
|||
|
||||
describe("CodeMode console capture", () => {
|
||||
test("captures console output as bounded result logs", async () => {
|
||||
const result = await Effect.runPromise(CodeMode.execute({
|
||||
code: `
|
||||
const result = await Effect.runPromise(
|
||||
CodeMode.execute({
|
||||
code: `
|
||||
const returned = console.log("Thread info:", { name: "Demo", count: 2 })
|
||||
console.warn("careful")
|
||||
return returned
|
||||
`,
|
||||
}))
|
||||
}),
|
||||
)
|
||||
|
||||
expect(result).toStrictEqual({
|
||||
ok: true,
|
||||
|
|
@ -228,39 +239,45 @@ describe("CodeMode console capture", () => {
|
|||
})
|
||||
|
||||
test("keeps logs captured before failures", async () => {
|
||||
const result = await Effect.runPromise(CodeMode.execute({
|
||||
code: `
|
||||
const result = await Effect.runPromise(
|
||||
CodeMode.execute({
|
||||
code: `
|
||||
console.log("before failure")
|
||||
throw new Error("boom")
|
||||
`,
|
||||
}))
|
||||
}),
|
||||
)
|
||||
|
||||
expect(result.ok ? undefined : result.logs).toStrictEqual(["before failure"])
|
||||
expect(result.ok ? undefined : result.error.message).toBe("Uncaught: boom")
|
||||
})
|
||||
|
||||
test("prints NaN and Infinity literally instead of the JSON null", async () => {
|
||||
const result = await Effect.runPromise(CodeMode.execute({
|
||||
code: `
|
||||
const result = await Effect.runPromise(
|
||||
CodeMode.execute({
|
||||
code: `
|
||||
console.log(NaN)
|
||||
console.log(Infinity, -Infinity)
|
||||
console.log({ ratio: NaN, bounds: [Infinity] })
|
||||
return null
|
||||
`,
|
||||
}))
|
||||
}),
|
||||
)
|
||||
|
||||
expect(result.ok).toBe(true)
|
||||
expect(result.logs).toStrictEqual(["NaN", "Infinity -Infinity", '{"ratio":NaN,"bounds":[Infinity]}'])
|
||||
})
|
||||
|
||||
test("renders sandbox values nested inside logged containers", async () => {
|
||||
const result = await Effect.runPromise(CodeMode.execute({
|
||||
code: `
|
||||
const result = await Effect.runPromise(
|
||||
CodeMode.execute({
|
||||
code: `
|
||||
console.log({ m: new Map([["a", 1]]), when: new Date(0), r: /ab/g, s: new Set([1, 2]) })
|
||||
console.log([new Date(0)])
|
||||
return null
|
||||
`,
|
||||
}))
|
||||
}),
|
||||
)
|
||||
|
||||
expect(result.ok).toBe(true)
|
||||
expect(result.logs).toStrictEqual([
|
||||
|
|
@ -270,38 +287,40 @@ describe("CodeMode console capture", () => {
|
|||
})
|
||||
|
||||
test("console formatting is total: cycles and opaque references render as markers", async () => {
|
||||
const result = await Effect.runPromise(CodeMode.execute({
|
||||
code: `
|
||||
const result = await Effect.runPromise(
|
||||
CodeMode.execute({
|
||||
code: `
|
||||
const m = new Map()
|
||||
m.set("self", m)
|
||||
console.log({ box: m })
|
||||
console.log({ fn: (x) => x, ok: 1 })
|
||||
return null
|
||||
`,
|
||||
}))
|
||||
}),
|
||||
)
|
||||
|
||||
expect(result.ok).toBe(true)
|
||||
expect(result.logs).toStrictEqual([
|
||||
'{"box":Map(1) [["self",[Circular]]]}',
|
||||
'{"fn":[CodeMode reference],"ok":1}',
|
||||
])
|
||||
expect(result.logs).toStrictEqual(['{"box":Map(1) [["self",[Circular]]]}', '{"fn":[CodeMode reference],"ok":1}'])
|
||||
})
|
||||
|
||||
test("console.table renders sandbox value cells", async () => {
|
||||
const result = await Effect.runPromise(CodeMode.execute({
|
||||
code: `
|
||||
const result = await Effect.runPromise(
|
||||
CodeMode.execute({
|
||||
code: `
|
||||
console.table([{ when: new Date(0), n: NaN }])
|
||||
return null
|
||||
`,
|
||||
}))
|
||||
}),
|
||||
)
|
||||
|
||||
expect(result.ok).toBe(true)
|
||||
expect(result.logs).toStrictEqual(["(index)\twhen\tn\n0\t1970-01-01T00:00:00.000Z\tNaN"])
|
||||
})
|
||||
|
||||
test("captures console.dir and console.table output", async () => {
|
||||
const result = await Effect.runPromise(CodeMode.execute({
|
||||
code: `
|
||||
const result = await Effect.runPromise(
|
||||
CodeMode.execute({
|
||||
code: `
|
||||
console.dir({ nested: { ok: true } })
|
||||
console.table([
|
||||
{ name: "Kit", count: 1, hidden: "x" },
|
||||
|
|
@ -309,15 +328,13 @@ describe("CodeMode console capture", () => {
|
|||
], ["name", "count"])
|
||||
return "done"
|
||||
`,
|
||||
}))
|
||||
}),
|
||||
)
|
||||
|
||||
expect(result).toStrictEqual({
|
||||
ok: true,
|
||||
value: "done",
|
||||
logs: [
|
||||
'{"nested":{"ok":true}}',
|
||||
"(index)\tname\tcount\n0\tKit\t1\n1\tOlive\t2",
|
||||
],
|
||||
logs: ['{"nested":{"ok":true}}', "(index)\tname\tcount\n0\tKit\t1\n1\tOlive\t2"],
|
||||
toolCalls: [],
|
||||
})
|
||||
})
|
||||
|
|
@ -325,9 +342,11 @@ describe("CodeMode console capture", () => {
|
|||
|
||||
describe("CodeMode output budget", () => {
|
||||
test("absent maxOutputBytes means no truncation at all", async () => {
|
||||
const result = await Effect.runPromise(CodeMode.execute({
|
||||
code: `console.log("z".repeat(50_000)); return "x".repeat(100_000)`,
|
||||
}))
|
||||
const result = await Effect.runPromise(
|
||||
CodeMode.execute({
|
||||
code: `console.log("z".repeat(50_000)); return "x".repeat(100_000)`,
|
||||
}),
|
||||
)
|
||||
|
||||
expect(result.ok).toBe(true)
|
||||
if (!result.ok) return
|
||||
|
|
@ -338,29 +357,35 @@ describe("CodeMode output budget", () => {
|
|||
|
||||
test("truncates an oversized result value with a marker instead of failing", async () => {
|
||||
const limits: ExecutionLimits = { maxOutputBytes: 40 }
|
||||
const result = await Effect.runPromise(CodeMode.execute({
|
||||
code: `return { data: "${"x".repeat(200)}" }`,
|
||||
limits,
|
||||
}))
|
||||
const result = await Effect.runPromise(
|
||||
CodeMode.execute({
|
||||
code: `return { data: "${"x".repeat(200)}" }`,
|
||||
limits,
|
||||
}),
|
||||
)
|
||||
|
||||
expect(result.ok).toBe(true)
|
||||
if (!result.ok) return
|
||||
expect(result.truncated).toBe(true)
|
||||
expect(typeof result.value).toBe("string")
|
||||
expect(result.value).toMatch(/^\{"data":"x+ \[result truncated: \d+ bytes exceeds the 40-byte output limit; return a smaller value\]$/)
|
||||
expect(result.value).toMatch(
|
||||
/^\{"data":"x+ \[result truncated: \d+ bytes exceeds the 40-byte output limit; return a smaller value\]$/,
|
||||
)
|
||||
expect(Schema.decodeUnknownSync(ExecuteResultSchema)(JSON.parse(JSON.stringify(result)))).toStrictEqual(result)
|
||||
})
|
||||
|
||||
test("keeps leading logs within the remaining budget and marks the cut", async () => {
|
||||
const limits: ExecutionLimits = { maxOutputBytes: 40 }
|
||||
const result = await Effect.runPromise(CodeMode.execute({
|
||||
code: `
|
||||
const result = await Effect.runPromise(
|
||||
CodeMode.execute({
|
||||
code: `
|
||||
console.log("first line")
|
||||
console.log("${"y".repeat(200)}")
|
||||
return "ok"
|
||||
`,
|
||||
limits,
|
||||
}))
|
||||
limits,
|
||||
}),
|
||||
)
|
||||
|
||||
expect(result.ok).toBe(true)
|
||||
if (!result.ok) return
|
||||
|
|
@ -370,12 +395,14 @@ describe("CodeMode output budget", () => {
|
|||
})
|
||||
|
||||
test("does not mark results within the budget", async () => {
|
||||
const result = await Effect.runPromise(CodeMode.execute({
|
||||
code: `
|
||||
const result = await Effect.runPromise(
|
||||
CodeMode.execute({
|
||||
code: `
|
||||
console.log("fits")
|
||||
return { fits: true }
|
||||
`,
|
||||
}))
|
||||
}),
|
||||
)
|
||||
expect(result).toStrictEqual({
|
||||
ok: true,
|
||||
value: { fits: true },
|
||||
|
|
@ -395,18 +422,21 @@ describe("CodeMode schema flexibility", () => {
|
|||
properties: { id: { type: "string" }, count: { type: "number" } },
|
||||
required: ["id"],
|
||||
},
|
||||
run: (input) => Effect.sync(() => {
|
||||
observed.push(input)
|
||||
return { echoed: input }
|
||||
}),
|
||||
run: (input) =>
|
||||
Effect.sync(() => {
|
||||
observed.push(input)
|
||||
return { echoed: input }
|
||||
}),
|
||||
})
|
||||
const runtime = CodeMode.make({ tools: { adapter: { call } } })
|
||||
|
||||
expect(runtime.catalog()).toStrictEqual([{
|
||||
path: "adapter.call",
|
||||
description: "Call an adapter-described tool",
|
||||
signature: "tools.adapter.call(input: { id: string; count?: number }): Promise<unknown>",
|
||||
}])
|
||||
expect(runtime.catalog()).toStrictEqual([
|
||||
{
|
||||
path: "adapter.call",
|
||||
description: "Call an adapter-described tool",
|
||||
signature: "tools.adapter.call(input: { id: string; count?: number }): Promise<unknown>",
|
||||
},
|
||||
])
|
||||
|
||||
// JSON Schema is render-only: mistyped input passes through unvalidated.
|
||||
const result = await Effect.runPromise(runtime.execute(`return await tools.adapter.call({ id: 42 })`))
|
||||
|
|
@ -421,17 +451,25 @@ describe("CodeMode schema flexibility", () => {
|
|||
input: { type: "object", properties: { login: { type: "string" } }, required: ["login"] },
|
||||
output: {
|
||||
$ref: "#/$defs/User",
|
||||
$defs: { User: { type: "object", properties: { login: { type: "string" }, id: { type: "number" } }, required: ["login", "id"] } },
|
||||
$defs: {
|
||||
User: {
|
||||
type: "object",
|
||||
properties: { login: { type: "string" }, id: { type: "number" } },
|
||||
required: ["login", "id"],
|
||||
},
|
||||
},
|
||||
},
|
||||
run: () => Effect.succeed({ login: "kit", id: 7 }),
|
||||
})
|
||||
const runtime = CodeMode.make({ tools: { users: { lookup } } })
|
||||
|
||||
expect(runtime.catalog()).toStrictEqual([{
|
||||
path: "users.lookup",
|
||||
description: "Look up a user",
|
||||
signature: "tools.users.lookup(input: { login: string }): Promise<{ login: string; id: number }>",
|
||||
}])
|
||||
expect(runtime.catalog()).toStrictEqual([
|
||||
{
|
||||
path: "users.lookup",
|
||||
description: "Look up a user",
|
||||
signature: "tools.users.lookup(input: { login: string }): Promise<{ login: string; id: number }>",
|
||||
},
|
||||
])
|
||||
|
||||
const result = await Effect.runPromise(runtime.execute(`return await tools.users.lookup({ login: "kit" })`))
|
||||
expect(result.ok).toBe(true)
|
||||
|
|
@ -478,16 +516,20 @@ describe("CodeMode public contract", () => {
|
|||
expect(agentTool.input).toBe(ExecuteInputSchema)
|
||||
expect(agentTool.output).toBe(ExecuteResultSchema)
|
||||
expect(agentTool.description).toBe(runtime.instructions())
|
||||
expect(Schema.decodeUnknownSync(ExecuteResultSchema)(JSON.parse(JSON.stringify(projected)))).toStrictEqual(projected)
|
||||
expect(Schema.decodeUnknownSync(ExecuteResultSchema)(JSON.parse(JSON.stringify(projected)))).toStrictEqual(
|
||||
projected,
|
||||
)
|
||||
})
|
||||
|
||||
test("inlines a COMPLETE small catalog and keeps search registered but unadvertised", async () => {
|
||||
const runtime = CodeMode.make({ tools })
|
||||
expect(runtime.catalog()).toStrictEqual([{
|
||||
path: "orders.lookup",
|
||||
description: "Look up an order by ID",
|
||||
signature: "tools.orders.lookup(input: { id: string }): Promise<{ id: string; status: string }>",
|
||||
}])
|
||||
expect(runtime.catalog()).toStrictEqual([
|
||||
{
|
||||
path: "orders.lookup",
|
||||
description: "Look up an order by ID",
|
||||
signature: "tools.orders.lookup(input: { id: string }): Promise<{ id: string; status: string }>",
|
||||
},
|
||||
])
|
||||
expect(runtime.instructions()).toContain("Available tools (COMPLETE list")
|
||||
expect(runtime.instructions()).toContain("- orders (1 tool)")
|
||||
expect(runtime.instructions()).toContain(
|
||||
|
|
@ -502,11 +544,13 @@ describe("CodeMode public contract", () => {
|
|||
expect(result.ok).toBe(true)
|
||||
if (result.ok) {
|
||||
expect(result.value).toStrictEqual({
|
||||
items: [{
|
||||
path: "tools.orders.lookup",
|
||||
description: "Look up an order by ID",
|
||||
signature: "tools.orders.lookup(input: {\n id: string\n}): Promise<{\n id: string\n status: string\n}>",
|
||||
}],
|
||||
items: [
|
||||
{
|
||||
path: "tools.orders.lookup",
|
||||
description: "Look up an order by ID",
|
||||
signature: "tools.orders.lookup(input: {\n id: string\n}): Promise<{\n id: string\n status: string\n}>",
|
||||
},
|
||||
],
|
||||
total: 1,
|
||||
})
|
||||
}
|
||||
|
|
@ -521,31 +565,43 @@ describe("CodeMode public contract", () => {
|
|||
})
|
||||
const runtime = CodeMode.make({ tools: { context7: { "resolve-library-id": resolveLibrary } } })
|
||||
|
||||
expect(runtime.catalog()).toStrictEqual([{
|
||||
path: "context7.resolve-library-id",
|
||||
description: "Resolve a library ID",
|
||||
signature: 'tools.context7["resolve-library-id"](input: { libraryName: string }): Promise<string>',
|
||||
}])
|
||||
expect(runtime.instructions()).toContain('tools.context7["resolve-library-id"](input: { libraryName: string }): Promise<string>')
|
||||
expect(runtime.catalog()).toStrictEqual([
|
||||
{
|
||||
path: "context7.resolve-library-id",
|
||||
description: "Resolve a library ID",
|
||||
signature: 'tools.context7["resolve-library-id"](input: { libraryName: string }): Promise<string>',
|
||||
},
|
||||
])
|
||||
expect(runtime.instructions()).toContain(
|
||||
'tools.context7["resolve-library-id"](input: { libraryName: string }): Promise<string>',
|
||||
)
|
||||
|
||||
const search = await Effect.runPromise(runtime.execute(`return await tools.$codemode.search({ query: "resolve library id" })`))
|
||||
const search = await Effect.runPromise(
|
||||
runtime.execute(`return await tools.$codemode.search({ query: "resolve library id" })`),
|
||||
)
|
||||
expect(search.ok).toBe(true)
|
||||
if (search.ok) {
|
||||
expect(search.value).toStrictEqual({
|
||||
items: [{
|
||||
path: 'tools.context7["resolve-library-id"]',
|
||||
description: "Resolve a library ID",
|
||||
signature: 'tools.context7["resolve-library-id"](input: {\n libraryName: string\n}): Promise<string>',
|
||||
}],
|
||||
items: [
|
||||
{
|
||||
path: 'tools.context7["resolve-library-id"]',
|
||||
description: "Resolve a library ID",
|
||||
signature: 'tools.context7["resolve-library-id"](input: {\n libraryName: string\n}): Promise<string>',
|
||||
},
|
||||
],
|
||||
total: 1,
|
||||
})
|
||||
}
|
||||
|
||||
const call = await Effect.runPromise(runtime.execute(`return await tools.context7["resolve-library-id"]({ libraryName: "TypeScript" })`))
|
||||
const call = await Effect.runPromise(
|
||||
runtime.execute(`return await tools.context7["resolve-library-id"]({ libraryName: "TypeScript" })`),
|
||||
)
|
||||
expect(call.ok).toBe(true)
|
||||
if (call.ok) expect(call.value).toBe("/resolved/TypeScript")
|
||||
|
||||
const exact = await Effect.runPromise(runtime.execute(`return await tools.$codemode.search({ query: 'tools.context7["resolve-library-id"]' })`))
|
||||
const exact = await Effect.runPromise(
|
||||
runtime.execute(`return await tools.$codemode.search({ query: 'tools.context7["resolve-library-id"]' })`),
|
||||
)
|
||||
expect(exact.ok).toBe(true)
|
||||
if (exact.ok) expect((exact.value as { total: number }).total).toBe(1)
|
||||
})
|
||||
|
|
@ -561,7 +617,9 @@ describe("CodeMode public contract", () => {
|
|||
expect(instructions.indexOf("## Rules")).toBeLessThan(instructions.indexOf("## Syntax"))
|
||||
expect(instructions.indexOf("## Syntax")).toBeLessThan(instructions.indexOf("\n## Available tools (COMPLETE list"))
|
||||
// The workflow carries the result-shape guidance; Rules only add content beyond it.
|
||||
expect(instructions).toContain('`const data = typeof res === "string" ? JSON.parse(res) : res` — most tools return JSON as a string')
|
||||
expect(instructions).toContain(
|
||||
'`const data = typeof res === "string" ? JSON.parse(res) : res` — most tools return JSON as a string',
|
||||
)
|
||||
expect(instructions).toContain("Return only the fields you need")
|
||||
expect(instructions).toContain("raw payloads get truncated and waste context")
|
||||
expect(instructions).toContain("`const res = await tools.<namespace>.<tool>(input)`")
|
||||
|
|
@ -584,8 +642,12 @@ describe("CodeMode public contract", () => {
|
|||
expect(partial).toContain(
|
||||
'1. Find a tool (skip when it is already listed below): `const { items } = await tools.$codemode.search({ query: "<intent + key nouns>" })` — short phrases like "list issues" work best.',
|
||||
)
|
||||
expect(partial).toContain("Only tools listed here or returned by `tools.$codemode.search` are available inside `tools`")
|
||||
expect(partial).toContain('- Browse one namespace: `await tools.$codemode.search({ query: "", namespace: "<name>" })`.')
|
||||
expect(partial).toContain(
|
||||
"Only tools listed here or returned by `tools.$codemode.search` are available inside `tools`",
|
||||
)
|
||||
expect(partial).toContain(
|
||||
'- Browse one namespace: `await tools.$codemode.search({ query: "", namespace: "<name>" })`.',
|
||||
)
|
||||
expect(partial).not.toContain("total_count")
|
||||
expect(partial).not.toContain("tools.orders.lookup({")
|
||||
})
|
||||
|
|
@ -604,7 +666,9 @@ describe("CodeMode public contract", () => {
|
|||
expect(instructions).not.toContain("instanceof Error")
|
||||
expect(instructions).not.toContain("splice")
|
||||
// The data-boundary note survives.
|
||||
expect(instructions).toContain("Dates serialize to ISO strings at data boundaries; Map/Set/RegExp serialize to `{}`.")
|
||||
expect(instructions).toContain(
|
||||
"Dates serialize to ISO strings at data boundaries; Map/Set/RegExp serialize to `{}`.",
|
||||
)
|
||||
})
|
||||
|
||||
test("zero tools keep minimal sections and the no-tools notice", () => {
|
||||
|
|
@ -635,18 +699,22 @@ describe("CodeMode public contract", () => {
|
|||
tools: { thread: { uploadFile: upload, generateImage: generate }, orders: { lookup } },
|
||||
discovery: { maxInlineCatalogTokens: 0 },
|
||||
})
|
||||
expect(runtime.instructions()).toContain("Available tools (PARTIAL — 0 of 3 shown; find the rest with tools.$codemode.search)")
|
||||
expect(runtime.instructions()).toContain(
|
||||
"Available tools (PARTIAL — 0 of 3 shown; find the rest with tools.$codemode.search)",
|
||||
)
|
||||
expect(runtime.instructions()).toContain("- thread (2 tools, none shown)")
|
||||
expect(runtime.instructions()).toContain("- orders (1 tool, none shown)")
|
||||
expect(runtime.instructions()).toMatch(/\$codemode\.search/)
|
||||
expect(runtime.instructions()).not.toMatch(/tools\.thread\.uploadFile\(input/)
|
||||
|
||||
const result = await Effect.runPromise(runtime.execute(`
|
||||
const result = await Effect.runPromise(
|
||||
runtime.execute(`
|
||||
return await tools.$codemode.search({
|
||||
query: "send message attachment upload file to current Discord thread",
|
||||
limit: 2
|
||||
})
|
||||
`))
|
||||
`),
|
||||
)
|
||||
expect(result.ok).toBe(true)
|
||||
if (!result.ok) return
|
||||
expect(result.value).toStrictEqual({
|
||||
|
|
@ -666,19 +734,27 @@ describe("CodeMode public contract", () => {
|
|||
})
|
||||
expect(result.toolCalls).toStrictEqual([{ name: "$codemode.search" }])
|
||||
|
||||
const variants = await Effect.runPromise(runtime.execute(`
|
||||
const variants = await Effect.runPromise(
|
||||
runtime.execute(`
|
||||
return await Promise.all([
|
||||
tools.$codemode.search({ query: "file" }),
|
||||
tools.$codemode.search({ query: "image" })
|
||||
])
|
||||
`))
|
||||
`),
|
||||
)
|
||||
expect(variants.ok).toBe(true)
|
||||
if (variants.ok) {
|
||||
expect((variants.value as Array<{ items: Array<{ path: string }> }>)[0]?.items[0]?.path).toBe("tools.thread.uploadFile")
|
||||
expect((variants.value as Array<{ items: Array<{ path: string }> }>)[1]?.items[0]?.path).toBe("tools.thread.generateImage")
|
||||
expect((variants.value as Array<{ items: Array<{ path: string }> }>)[0]?.items[0]?.path).toBe(
|
||||
"tools.thread.uploadFile",
|
||||
)
|
||||
expect((variants.value as Array<{ items: Array<{ path: string }> }>)[1]?.items[0]?.path).toBe(
|
||||
"tools.thread.generateImage",
|
||||
)
|
||||
}
|
||||
|
||||
const removed = await Effect.runPromise(runtime.execute(`return await tools.$codemode.describe({ path: "thread.uploadFile" })`))
|
||||
const removed = await Effect.runPromise(
|
||||
runtime.execute(`return await tools.$codemode.describe({ path: "thread.uploadFile" })`),
|
||||
)
|
||||
expect(removed.ok).toBe(false)
|
||||
if (!removed.ok) expect(removed.error.kind).toBe("UnknownTool")
|
||||
})
|
||||
|
|
@ -706,15 +782,19 @@ describe("CodeMode public contract", () => {
|
|||
}
|
||||
|
||||
for (const query of ["many.tool13", "tools.many.tool13"]) {
|
||||
const exact = await Effect.runPromise(runtime.execute(`return await tools.$codemode.search({ query: ${JSON.stringify(query)} })`))
|
||||
const exact = await Effect.runPromise(
|
||||
runtime.execute(`return await tools.$codemode.search({ query: ${JSON.stringify(query)} })`),
|
||||
)
|
||||
expect(exact.ok).toBe(true)
|
||||
if (exact.ok) {
|
||||
expect(exact.value).toStrictEqual({
|
||||
items: [{
|
||||
path: "tools.many.tool13",
|
||||
description: "Numbered tool 13",
|
||||
signature: "tools.many.tool13(input: {\n id: string\n}): Promise<string>",
|
||||
}],
|
||||
items: [
|
||||
{
|
||||
path: "tools.many.tool13",
|
||||
description: "Numbered tool 13",
|
||||
signature: "tools.many.tool13(input: {\n id: string\n}): Promise<string>",
|
||||
},
|
||||
],
|
||||
total: 1,
|
||||
})
|
||||
}
|
||||
|
|
@ -737,20 +817,23 @@ describe("CodeMode public contract", () => {
|
|||
})
|
||||
|
||||
// Empty query + namespace browses just that namespace, alphabetical by path.
|
||||
const browse = await Effect.runPromise(runtime.execute(
|
||||
`return await tools.$codemode.search({ query: "", namespace: "github" })`,
|
||||
))
|
||||
const browse = await Effect.runPromise(
|
||||
runtime.execute(`return await tools.$codemode.search({ query: "", namespace: "github" })`),
|
||||
)
|
||||
expect(browse.ok).toBe(true)
|
||||
if (browse.ok) {
|
||||
const value = browse.value as { items: Array<{ path: string }>; total: number }
|
||||
expect(value.total).toBe(2)
|
||||
expect(value.items.map((item) => item.path)).toStrictEqual(["tools.github.create_issue", "tools.github.list_issues"])
|
||||
expect(value.items.map((item) => item.path)).toStrictEqual([
|
||||
"tools.github.create_issue",
|
||||
"tools.github.list_issues",
|
||||
])
|
||||
}
|
||||
|
||||
// A query + namespace ranks within that namespace only.
|
||||
const scoped = await Effect.runPromise(runtime.execute(
|
||||
`return await tools.$codemode.search({ query: "issues", namespace: "linear" })`,
|
||||
))
|
||||
const scoped = await Effect.runPromise(
|
||||
runtime.execute(`return await tools.$codemode.search({ query: "issues", namespace: "linear" })`),
|
||||
)
|
||||
expect(scoped.ok).toBe(true)
|
||||
if (scoped.ok) {
|
||||
const value = scoped.value as { items: Array<{ path: string }>; total: number }
|
||||
|
|
@ -758,9 +841,9 @@ describe("CodeMode public contract", () => {
|
|||
expect(value.items[0]?.path).toBe("tools.linear.list_issues")
|
||||
}
|
||||
|
||||
const invalid = await Effect.runPromise(runtime.execute(
|
||||
`return await tools.$codemode.search({ query: "issues", namespace: 7 })`,
|
||||
))
|
||||
const invalid = await Effect.runPromise(
|
||||
runtime.execute(`return await tools.$codemode.search({ query: "issues", namespace: 7 })`),
|
||||
)
|
||||
expect(invalid.ok).toBe(false)
|
||||
if (!invalid.ok) expect(invalid.error.kind).toBe("InvalidToolInput")
|
||||
})
|
||||
|
|
@ -785,9 +868,9 @@ describe("CodeMode public contract", () => {
|
|||
|
||||
// "attachment" appears in neither path nor description — only in the input schema's
|
||||
// property names, which the searchable text includes.
|
||||
const byParameter = await Effect.runPromise(runtime.execute(
|
||||
`return await tools.$codemode.search({ query: "attachment" })`,
|
||||
))
|
||||
const byParameter = await Effect.runPromise(
|
||||
runtime.execute(`return await tools.$codemode.search({ query: "attachment" })`),
|
||||
)
|
||||
expect(byParameter.ok).toBe(true)
|
||||
if (byParameter.ok) {
|
||||
const value = byParameter.value as { items: Array<{ path: string }>; total: number }
|
||||
|
|
@ -796,9 +879,9 @@ describe("CodeMode public contract", () => {
|
|||
}
|
||||
|
||||
// Substring matching: a partial word ("docum") still hits the description.
|
||||
const bySubstring = await Effect.runPromise(runtime.execute(
|
||||
`return await tools.$codemode.search({ query: "docum" })`,
|
||||
))
|
||||
const bySubstring = await Effect.runPromise(
|
||||
runtime.execute(`return await tools.$codemode.search({ query: "docum" })`),
|
||||
)
|
||||
expect(bySubstring.ok).toBe(true)
|
||||
if (bySubstring.ok) {
|
||||
const value = bySubstring.value as { items: Array<{ path: string }>; total: number }
|
||||
|
|
@ -825,9 +908,9 @@ describe("CodeMode public contract", () => {
|
|||
})
|
||||
|
||||
// "issues" still finds the singular-only tool (term OR singular(term) per field)...
|
||||
const plural = await Effect.runPromise(runtime.execute(
|
||||
`return await tools.$codemode.search({ query: "issues", namespace: "tracker" })`,
|
||||
))
|
||||
const plural = await Effect.runPromise(
|
||||
runtime.execute(`return await tools.$codemode.search({ query: "issues", namespace: "tracker" })`),
|
||||
)
|
||||
expect(plural.ok).toBe(true)
|
||||
if (plural.ok) {
|
||||
const value = plural.value as { items: Array<{ path: string }>; total: number }
|
||||
|
|
@ -836,14 +919,15 @@ describe("CodeMode public contract", () => {
|
|||
}
|
||||
|
||||
// ...while a true "issues" path match still outranks the singular-only description match.
|
||||
const ranked = await Effect.runPromise(runtime.execute(
|
||||
`return await tools.$codemode.search({ query: "issues" })`,
|
||||
))
|
||||
const ranked = await Effect.runPromise(runtime.execute(`return await tools.$codemode.search({ query: "issues" })`))
|
||||
expect(ranked.ok).toBe(true)
|
||||
if (ranked.ok) {
|
||||
const value = ranked.value as { items: Array<{ path: string }>; total: number }
|
||||
expect(value.total).toBe(2)
|
||||
expect(value.items.map((item) => item.path)).toStrictEqual(["tools.github.list_issues", "tools.tracker.fetch_all"])
|
||||
expect(value.items.map((item) => item.path)).toStrictEqual([
|
||||
"tools.github.list_issues",
|
||||
"tools.tracker.fetch_all",
|
||||
])
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -882,8 +966,12 @@ describe("CodeMode public contract", () => {
|
|||
run: () => Effect.succeed("ok"),
|
||||
})
|
||||
const expensive = Tool.make({
|
||||
description: "An expensive tool whose description alone consumes far more than the remaining inline catalog byte budget for this runtime",
|
||||
input: Schema.Struct({ someRatherLongParameterName: Schema.String, anotherEvenLongerParameterName: Schema.Number }),
|
||||
description:
|
||||
"An expensive tool whose description alone consumes far more than the remaining inline catalog byte budget for this runtime",
|
||||
input: Schema.Struct({
|
||||
someRatherLongParameterName: Schema.String,
|
||||
anotherEvenLongerParameterName: Schema.Number,
|
||||
}),
|
||||
output: Schema.String,
|
||||
run: () => Effect.succeed("ok"),
|
||||
})
|
||||
|
|
@ -896,7 +984,9 @@ describe("CodeMode public contract", () => {
|
|||
})
|
||||
|
||||
const instructions = runtime.instructions()
|
||||
expect(instructions).toContain("Available tools (PARTIAL — 2 of 3 shown; find the rest with tools.$codemode.search)")
|
||||
expect(instructions).toContain(
|
||||
"Available tools (PARTIAL — 2 of 3 shown; find the rest with tools.$codemode.search)",
|
||||
)
|
||||
expect(instructions).toContain("- alpha (2 tools, 1 shown)")
|
||||
expect(instructions).toContain(" - tools.alpha.cheap(input: { q: string }): Promise<string> // Cheap")
|
||||
expect(instructions).not.toContain("tools.alpha.expensive(")
|
||||
|
|
@ -912,10 +1002,11 @@ describe("CodeMode public contract", () => {
|
|||
description: "Double a number",
|
||||
input: Schema.Struct({ value: Schema.NumberFromString }),
|
||||
output: Schema.NumberFromString,
|
||||
run: ({ value }) => Effect.sync(() => {
|
||||
observed.push(value)
|
||||
return String(value * 2)
|
||||
}),
|
||||
run: ({ value }) =>
|
||||
Effect.sync(() => {
|
||||
observed.push(value)
|
||||
return String(value * 2)
|
||||
}),
|
||||
})
|
||||
const runtime = CodeMode.make({
|
||||
tools: { math: { double: transformed } },
|
||||
|
|
@ -934,9 +1025,11 @@ describe("CodeMode public contract", () => {
|
|||
})
|
||||
|
||||
test("returns JSON-safe data and normalizes undefined to null", async () => {
|
||||
const result = await Effect.runPromise(CodeMode.execute({
|
||||
code: `return { top: undefined, nested: [1, undefined] }`,
|
||||
}))
|
||||
const result = await Effect.runPromise(
|
||||
CodeMode.execute({
|
||||
code: `return { top: undefined, nested: [1, undefined] }`,
|
||||
}),
|
||||
)
|
||||
expect(result).toStrictEqual({
|
||||
ok: true,
|
||||
value: { top: null, nested: [1, null] },
|
||||
|
|
@ -947,18 +1040,20 @@ describe("CodeMode public contract", () => {
|
|||
|
||||
test("rejects invalid configuration and discovery limits", async () => {
|
||||
expect(() => CodeMode.execute({ code: "return 1", limits: { timeoutMs: 0 } })).toThrow(RangeError)
|
||||
expect(() => CodeMode.execute({ code: "return 1", limits: { timeoutMs: Number.POSITIVE_INFINITY } })).toThrow(RangeError)
|
||||
expect(() => CodeMode.execute({ code: "return 1", limits: { timeoutMs: Number.POSITIVE_INFINITY } })).toThrow(
|
||||
RangeError,
|
||||
)
|
||||
expect(() => CodeMode.execute({ code: "return 1", limits: { maxToolCalls: -1 } })).toThrow(RangeError)
|
||||
expect(() => CodeMode.execute({ code: "return 1", limits: { maxOutputBytes: -1 } })).toThrow(RangeError)
|
||||
|
||||
expect(() => CodeMode.make({ tools, discovery: { maxInlineCatalogTokens: -1 } })).toThrow(RangeError)
|
||||
|
||||
const result = await Effect.runPromise(CodeMode.make({
|
||||
tools,
|
||||
discovery: { maxInlineCatalogTokens: 0 },
|
||||
}).execute(
|
||||
`return await tools.$codemode.search({ query: "order", limit: 0.5 })`,
|
||||
))
|
||||
const result = await Effect.runPromise(
|
||||
CodeMode.make({
|
||||
tools,
|
||||
discovery: { maxInlineCatalogTokens: 0 },
|
||||
}).execute(`return await tools.$codemode.search({ query: "order", limit: 0.5 })`),
|
||||
)
|
||||
expect(result.ok).toBe(false)
|
||||
if (result.ok) return
|
||||
expect(result.error.kind).toBe("InvalidToolInput")
|
||||
|
|
@ -979,14 +1074,16 @@ describe("CodeMode public contract", () => {
|
|||
output: Schema.Number,
|
||||
run: () => Effect.succeed(1),
|
||||
})
|
||||
const result = await Effect.runPromise(CodeMode.execute({
|
||||
tools: { host: { count: counter } },
|
||||
code: `
|
||||
const result = await Effect.runPromise(
|
||||
CodeMode.execute({
|
||||
tools: { host: { count: counter } },
|
||||
code: `
|
||||
let total = 0
|
||||
for (let i = 0; i < 150; i += 1) total += await tools.host.count({})
|
||||
return total
|
||||
`,
|
||||
}))
|
||||
}),
|
||||
)
|
||||
expect(result).toMatchObject({ ok: true, value: 150 })
|
||||
if (result.ok) expect(result.toolCalls.length).toBe(150)
|
||||
})
|
||||
|
|
@ -1008,8 +1105,6 @@ describe("CodeMode public contract", () => {
|
|||
})
|
||||
|
||||
test("reserves the discovery namespace", () => {
|
||||
expect(() => CodeMode.make({ tools: { $codemode: { lookup } } })).toThrow(
|
||||
/reserved for CodeMode discovery tools/,
|
||||
)
|
||||
expect(() => CodeMode.make({ tools: { $codemode: { lookup } } })).toThrow(/reserved for CodeMode discovery tools/)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -36,10 +36,12 @@ const error = async (code: string) => {
|
|||
|
||||
describe("Object.keys over tool references", () => {
|
||||
test("enumerates top-level namespaces (the transcript program)", async () => {
|
||||
expect(await value(`
|
||||
expect(
|
||||
await value(`
|
||||
const namespaces = Object.keys(tools)
|
||||
return { namespaces, count: namespaces.length }
|
||||
`)).toEqual({ namespaces: ["github", "memory", "playwright"], count: 3 })
|
||||
`),
|
||||
).toEqual({ namespaces: ["github", "memory", "playwright"], count: 3 })
|
||||
})
|
||||
|
||||
test("enumerates tool names at a nested namespace", async () => {
|
||||
|
|
@ -92,7 +94,8 @@ describe("Object.keys over arrays", () => {
|
|||
|
||||
describe("for...in", () => {
|
||||
test("iterates own enumerable keys of a plain object with break/continue", async () => {
|
||||
expect(await value(`
|
||||
expect(
|
||||
await value(`
|
||||
const seen = []
|
||||
for (const key in { a: 1, b: 2, c: 3, d: 4 }) {
|
||||
if (key === "b") continue
|
||||
|
|
@ -100,41 +103,50 @@ describe("for...in", () => {
|
|||
seen.push(key)
|
||||
}
|
||||
return seen
|
||||
`)).toEqual(["a", "c"])
|
||||
`),
|
||||
).toEqual(["a", "c"])
|
||||
})
|
||||
|
||||
test("iterates index strings over arrays", async () => {
|
||||
expect(await value(`
|
||||
expect(
|
||||
await value(`
|
||||
const indexes = []
|
||||
for (const i in ["x", "y", "z"]) {
|
||||
if (i === "2") break
|
||||
indexes.push(i)
|
||||
}
|
||||
return indexes
|
||||
`)).toEqual(["0", "1"])
|
||||
`),
|
||||
).toEqual(["0", "1"])
|
||||
})
|
||||
|
||||
test("supports let declarations and bare identifiers", async () => {
|
||||
expect(await value(`
|
||||
expect(
|
||||
await value(`
|
||||
let last = ""
|
||||
for (let key in { a: 1, b: 2 }) last = key
|
||||
return last
|
||||
`)).toBe("b")
|
||||
expect(await value(`
|
||||
`),
|
||||
).toBe("b")
|
||||
expect(
|
||||
await value(`
|
||||
let key = "before"
|
||||
for (key in { only: 1 }) {}
|
||||
return key
|
||||
`)).toBe("only")
|
||||
`),
|
||||
).toBe("only")
|
||||
})
|
||||
|
||||
test("enumerates namespaces and tools from the host tool tree", async () => {
|
||||
expect(await value(`
|
||||
expect(
|
||||
await value(`
|
||||
const names = []
|
||||
for (const ns in tools) {
|
||||
for (const name in tools[ns]) names.push(ns + "." + name)
|
||||
}
|
||||
return names
|
||||
`)).toEqual(["github.list_issues", "github.get_issue", "memory.search", "playwright.navigate"])
|
||||
`),
|
||||
).toEqual(["github.list_issues", "github.get_issue", "memory.search", "playwright.navigate"])
|
||||
})
|
||||
|
||||
test("unsupported values fail with a hint at for...of and Object.keys", async () => {
|
||||
|
|
|
|||
|
|
@ -143,21 +143,40 @@ describe("H1: NaN/Infinity flow as intermediates and normalize to null at the bo
|
|||
|
||||
describe("Error values and instanceof", () => {
|
||||
test("new Error carries name/message and is instanceof Error", async () => {
|
||||
expect(await value(`const e = new Error("boom"); return [e instanceof Error, e.name, e.message]`)).toEqual([true, "Error", "boom"])
|
||||
expect(await value(`const e = new Error("boom"); return [e instanceof Error, e.name, e.message]`)).toEqual([
|
||||
true,
|
||||
"Error",
|
||||
"boom",
|
||||
])
|
||||
})
|
||||
|
||||
test("Error without new behaves like new Error", async () => {
|
||||
expect(await value(`const e = Error("plain"); return [e instanceof Error, e.name, e.message]`)).toEqual([true, "Error", "plain"])
|
||||
expect(await value(`const e = new Error(); return [e.name, e.message, e instanceof Error]`)).toEqual(["Error", "", true])
|
||||
expect(await value(`const e = Error("plain"); return [e instanceof Error, e.name, e.message]`)).toEqual([
|
||||
true,
|
||||
"Error",
|
||||
"plain",
|
||||
])
|
||||
expect(await value(`const e = new Error(); return [e.name, e.message, e instanceof Error]`)).toEqual([
|
||||
"Error",
|
||||
"",
|
||||
true,
|
||||
])
|
||||
})
|
||||
|
||||
test("specific error types are instanceof themselves and Error, not each other", async () => {
|
||||
expect(await value(`const e = new TypeError("t"); return [e instanceof TypeError, e instanceof Error, e instanceof RangeError]`)).toEqual([true, true, false])
|
||||
expect(
|
||||
await value(
|
||||
`const e = new TypeError("t"); return [e instanceof TypeError, e instanceof Error, e instanceof RangeError]`,
|
||||
),
|
||||
).toEqual([true, true, false])
|
||||
expect(await value(`return new Error("e") instanceof TypeError`)).toBe(false)
|
||||
})
|
||||
|
||||
test("thrown errors keep instanceof through try/catch", async () => {
|
||||
expect(await value(`try { throw new Error("x") } catch (e) { return [e instanceof Error, e.message] }`)).toEqual([true, "x"])
|
||||
expect(await value(`try { throw new Error("x") } catch (e) { return [e instanceof Error, e.message] }`)).toEqual([
|
||||
true,
|
||||
"x",
|
||||
])
|
||||
})
|
||||
|
||||
test("interpreter runtime failures are caught as Error values", async () => {
|
||||
|
|
@ -168,33 +187,48 @@ describe("Error values and instanceof", () => {
|
|||
test("caught failures carry the constructor name the real-JS failure would have", async () => {
|
||||
// JSON.parse throws SyntaxError: name and specific-instanceof both carry through, and the
|
||||
// message keeps the engine's position detail.
|
||||
expect(await value(`
|
||||
expect(
|
||||
await value(`
|
||||
try { JSON.parse("{oops") } catch (e) {
|
||||
return [e.name, e instanceof SyntaxError, e instanceof Error, e instanceof TypeError, e.message.includes("JSON")]
|
||||
}
|
||||
`)).toEqual(["SyntaxError", true, true, false, true])
|
||||
expect(await value(`try { undeclared() } catch (e) { return [e.name, e instanceof ReferenceError] }`))
|
||||
.toEqual(["ReferenceError", true])
|
||||
expect(await value(`try { const c = 1; c = 2 } catch (e) { return [e.name, e instanceof TypeError] }`))
|
||||
.toEqual(["TypeError", true])
|
||||
expect(await value(`try { "a".normalize("NOPE") } catch (e) { return [e.name, e instanceof RangeError] }`))
|
||||
.toEqual(["RangeError", true])
|
||||
expect(await value(`try { "a".match("(") } catch (e) { return [e.name, e instanceof SyntaxError] }`))
|
||||
.toEqual(["SyntaxError", true])
|
||||
expect(await value(`try { new RegExp("(") } catch (e) { return [e.name, e instanceof SyntaxError] }`))
|
||||
.toEqual(["SyntaxError", true])
|
||||
`),
|
||||
).toEqual(["SyntaxError", true, true, false, true])
|
||||
expect(await value(`try { undeclared() } catch (e) { return [e.name, e instanceof ReferenceError] }`)).toEqual([
|
||||
"ReferenceError",
|
||||
true,
|
||||
])
|
||||
expect(await value(`try { const c = 1; c = 2 } catch (e) { return [e.name, e instanceof TypeError] }`)).toEqual([
|
||||
"TypeError",
|
||||
true,
|
||||
])
|
||||
expect(await value(`try { "a".normalize("NOPE") } catch (e) { return [e.name, e instanceof RangeError] }`)).toEqual(
|
||||
["RangeError", true],
|
||||
)
|
||||
expect(await value(`try { "a".match("(") } catch (e) { return [e.name, e instanceof SyntaxError] }`)).toEqual([
|
||||
"SyntaxError",
|
||||
true,
|
||||
])
|
||||
expect(await value(`try { new RegExp("(") } catch (e) { return [e.name, e instanceof SyntaxError] }`)).toEqual([
|
||||
"SyntaxError",
|
||||
true,
|
||||
])
|
||||
})
|
||||
|
||||
test("diagnostics without a specific real-JS analogue are named plain Error", async () => {
|
||||
expect(await value(`try { JSON.parse(5) } catch (e) { return [e.name, e instanceof Error] }`))
|
||||
.toEqual(["Error", true])
|
||||
expect(await value(`try { JSON.parse(5) } catch (e) { return [e.name, e instanceof Error] }`)).toEqual([
|
||||
"Error",
|
||||
true,
|
||||
])
|
||||
})
|
||||
|
||||
test("Promise.allSettled rejection reasons are Error values", async () => {
|
||||
expect(await value(`
|
||||
expect(
|
||||
await value(`
|
||||
const settled = await Promise.allSettled([Promise.reject(new Error("b"))])
|
||||
return [settled[0].reason instanceof Error, settled[0].reason.message]
|
||||
`)).toEqual([true, "b"])
|
||||
`),
|
||||
).toEqual([true, "b"])
|
||||
})
|
||||
|
||||
test("non-error thrown values are not instanceof Error", async () => {
|
||||
|
|
@ -203,7 +237,11 @@ describe("Error values and instanceof", () => {
|
|||
})
|
||||
|
||||
test("plain data is never instanceof Error", async () => {
|
||||
expect(await value(`return [({}) instanceof Error, "s" instanceof Error, null instanceof Error]`)).toEqual([false, false, false])
|
||||
expect(await value(`return [({}) instanceof Error, "s" instanceof Error, null instanceof Error]`)).toEqual([
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
])
|
||||
})
|
||||
|
||||
test("error values still serialize as plain { name, message } data", async () => {
|
||||
|
|
@ -227,17 +265,29 @@ describe("Error values and instanceof", () => {
|
|||
|
||||
describe("array methods: splice, fill, copyWithin, keys/values/entries", () => {
|
||||
test("splice removes in place and returns the removed elements", async () => {
|
||||
expect(await value(`const a = [1,2,3,4]; const removed = a.splice(1, 2); return { removed, a }`)).toEqual({ removed: [2, 3], a: [1, 4] })
|
||||
expect(await value(`const a = [1,2,3,4]; const removed = a.splice(1, 2); return { removed, a }`)).toEqual({
|
||||
removed: [2, 3],
|
||||
a: [1, 4],
|
||||
})
|
||||
})
|
||||
|
||||
test("splice inserts new elements at the cut", async () => {
|
||||
expect(await value(`const a = ["a","d"]; a.splice(1, 0, "b", "c"); return a`)).toEqual(["a", "b", "c", "d"])
|
||||
expect(await value(`const a = [1,2,3]; const removed = a.splice(1, 1, "x"); return { removed, a }`)).toEqual({ removed: [2], a: [1, "x", 3] })
|
||||
expect(await value(`const a = [1,2,3]; const removed = a.splice(1, 1, "x"); return { removed, a }`)).toEqual({
|
||||
removed: [2],
|
||||
a: [1, "x", 3],
|
||||
})
|
||||
})
|
||||
|
||||
test("splice with one argument removes to the end; negative start counts back", async () => {
|
||||
expect(await value(`const a = [1,2,3]; const removed = a.splice(1); return { removed, a }`)).toEqual({ removed: [2, 3], a: [1] })
|
||||
expect(await value(`const a = [1,2,3]; const removed = a.splice(-1); return { removed, a }`)).toEqual({ removed: [3], a: [1, 2] })
|
||||
expect(await value(`const a = [1,2,3]; const removed = a.splice(1); return { removed, a }`)).toEqual({
|
||||
removed: [2, 3],
|
||||
a: [1],
|
||||
})
|
||||
expect(await value(`const a = [1,2,3]; const removed = a.splice(-1); return { removed, a }`)).toEqual({
|
||||
removed: [3],
|
||||
a: [1, 2],
|
||||
})
|
||||
})
|
||||
|
||||
test("splice rejects inserting a container into itself", async () => {
|
||||
|
|
@ -258,11 +308,13 @@ describe("array methods: splice, fill, copyWithin, keys/values/entries", () => {
|
|||
test("keys/values/entries return arrays usable with for...of and spread", async () => {
|
||||
expect(await value(`return [...["x","y","z"].keys()]`)).toEqual([0, 1, 2])
|
||||
expect(await value(`return ["x","y"].values()`)).toEqual(["x", "y"])
|
||||
expect(await value(`
|
||||
expect(
|
||||
await value(`
|
||||
const out = []
|
||||
for (const [index, item] of ["a","b"].entries()) out.push(index + ":" + item)
|
||||
return out
|
||||
`)).toEqual(["0:a", "1:b"])
|
||||
`),
|
||||
).toEqual(["0:a", "1:b"])
|
||||
expect(await value(`return [...[7].entries()]`)).toEqual([[0, 7]])
|
||||
})
|
||||
})
|
||||
|
|
@ -300,40 +352,33 @@ describe("compound assignment matches its binary operator", () => {
|
|||
}
|
||||
|
||||
test("sandbox Date += concatenates its string form, like d = d + 1", async () => {
|
||||
const result = await pair(
|
||||
`let d = new Date(1000); d += 1; return d`,
|
||||
`let d = new Date(1000); d = d + 1; return d`,
|
||||
)
|
||||
const result = await pair(`let d = new Date(1000); d += 1; return d`, `let d = new Date(1000); d = d + 1; return d`)
|
||||
expect(result).toBe("1970-01-01T00:00:01.000Z1")
|
||||
})
|
||||
|
||||
test("sandbox Date numeric compound ops use its time value", async () => {
|
||||
expect(await pair(
|
||||
`let d = new Date(1000); d -= 400; return d`,
|
||||
`let d = new Date(1000); d = d - 400; return d`,
|
||||
)).toBe(600)
|
||||
expect(await pair(
|
||||
`let d = new Date(1000); d /= 4; return d`,
|
||||
`let d = new Date(1000); d = d / 4; return d`,
|
||||
)).toBe(250)
|
||||
expect(
|
||||
await pair(`let d = new Date(1000); d -= 400; return d`, `let d = new Date(1000); d = d - 400; return d`),
|
||||
).toBe(600)
|
||||
expect(await pair(`let d = new Date(1000); d /= 4; return d`, `let d = new Date(1000); d = d / 4; return d`)).toBe(
|
||||
250,
|
||||
)
|
||||
})
|
||||
|
||||
test("string += object/array matches x = x + obj", async () => {
|
||||
expect(await pair(
|
||||
`let x = "a"; x += { b: 1 }; return x`,
|
||||
`let x = "a"; x = x + { b: 1 }; return x`,
|
||||
)).toBe("a[object Object]")
|
||||
expect(await pair(
|
||||
`let x = "a"; x += [1, 2]; return x`,
|
||||
`let x = "a"; x = x + [1, 2]; return x`,
|
||||
)).toBe("a1,2")
|
||||
expect(await pair(`let x = "a"; x += { b: 1 }; return x`, `let x = "a"; x = x + { b: 1 }; return x`)).toBe(
|
||||
"a[object Object]",
|
||||
)
|
||||
expect(await pair(`let x = "a"; x += [1, 2]; return x`, `let x = "a"; x = x + [1, 2]; return x`)).toBe("a1,2")
|
||||
})
|
||||
|
||||
test("compound assignment through a member target coerces the same way", async () => {
|
||||
expect(await pair(
|
||||
`const o = { s: "t" }; o.s += new Date(0); return o.s`,
|
||||
`const o = { s: "t" }; o.s = o.s + new Date(0); return o.s`,
|
||||
)).toBe("t1970-01-01T00:00:00.000Z")
|
||||
expect(
|
||||
await pair(
|
||||
`const o = { s: "t" }; o.s += new Date(0); return o.s`,
|
||||
`const o = { s: "t" }; o.s = o.s + new Date(0); return o.s`,
|
||||
),
|
||||
).toBe("t1970-01-01T00:00:00.000Z")
|
||||
})
|
||||
|
||||
test("numeric and string compound operators sweep identically to their expansions", async () => {
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ const sleepyTool = (trace: Trace) =>
|
|||
input: Schema.Struct({ id: Schema.Number, ms: Schema.optionalKey(Schema.Number) }),
|
||||
output: Schema.Number,
|
||||
run: ({ id, ms }) =>
|
||||
Effect.gen(function*() {
|
||||
Effect.gen(function* () {
|
||||
trace.starts.push(id)
|
||||
trace.active += 1
|
||||
trace.maxActive = Math.max(trace.maxActive, trace.active)
|
||||
|
|
@ -31,10 +31,14 @@ const sleepyTool = (trace: Trace) =>
|
|||
trace.active -= 1
|
||||
trace.completed += 1
|
||||
return id
|
||||
}).pipe(Effect.onInterrupt(() => Effect.sync(() => {
|
||||
trace.active -= 1
|
||||
trace.interrupted += 1
|
||||
}))),
|
||||
}).pipe(
|
||||
Effect.onInterrupt(() =>
|
||||
Effect.sync(() => {
|
||||
trace.active -= 1
|
||||
trace.interrupted += 1
|
||||
}),
|
||||
),
|
||||
),
|
||||
})
|
||||
|
||||
const failingTool = Tool.make({
|
||||
|
|
@ -46,11 +50,13 @@ const failingTool = Tool.make({
|
|||
|
||||
const run = (code: string, options: { trace?: Trace; limits?: ExecutionLimits } = {}): Promise<ExecuteResult> => {
|
||||
const trace = options.trace ?? makeTrace()
|
||||
return Effect.runPromise(CodeMode.execute({
|
||||
tools: { host: { sleepy: sleepyTool(trace), fail: failingTool } },
|
||||
code,
|
||||
...(options.limits ? { limits: options.limits } : {}),
|
||||
}))
|
||||
return Effect.runPromise(
|
||||
CodeMode.execute({
|
||||
tools: { host: { sleepy: sleepyTool(trace), fail: failingTool } },
|
||||
code,
|
||||
...(options.limits ? { limits: options.limits } : {}),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
const value = async (code: string, options: { trace?: Trace; limits?: ExecutionLimits } = {}) => {
|
||||
|
|
@ -121,7 +127,8 @@ describe("first-class promise values", () => {
|
|||
})
|
||||
|
||||
test("an awaited failure is catchable exactly like a synchronous throw", async () => {
|
||||
expect(await value(`
|
||||
expect(
|
||||
await value(`
|
||||
const p = tools.host.fail({})
|
||||
try {
|
||||
await p
|
||||
|
|
@ -129,7 +136,8 @@ describe("first-class promise values", () => {
|
|||
} catch (e) {
|
||||
return e.message
|
||||
}
|
||||
`)).toBe("Lookup refused")
|
||||
`),
|
||||
).toBe("Lookup refused")
|
||||
})
|
||||
|
||||
test("a fire-and-forget call completes before the execution ends", async () => {
|
||||
|
|
@ -186,20 +194,24 @@ describe("promises at data boundaries", () => {
|
|||
|
||||
describe("Promise.all over arbitrary arrays", () => {
|
||||
test("mixes promises and plain values, preserving order", async () => {
|
||||
expect(await value(`
|
||||
expect(
|
||||
await value(`
|
||||
return await Promise.all([tools.host.sleepy({ id: 1 }), "plain", tools.host.sleepy({ id: 2 }), 42])
|
||||
`)).toEqual([1, "plain", 2, 42])
|
||||
`),
|
||||
).toEqual([1, "plain", 2, 42])
|
||||
})
|
||||
|
||||
test("accepts arrays built beforehand, passed as identifiers, and spread elements", async () => {
|
||||
expect(await value(`
|
||||
expect(
|
||||
await value(`
|
||||
const calls = []
|
||||
calls.push(tools.host.sleepy({ id: 1 }))
|
||||
calls.push(7)
|
||||
const more = [tools.host.sleepy({ id: 2 })]
|
||||
const batch = [...calls, ...more, "x"]
|
||||
return await Promise.all(batch)
|
||||
`)).toEqual([1, 7, 2, "x"])
|
||||
`),
|
||||
).toEqual([1, 7, 2, "x"])
|
||||
})
|
||||
|
||||
test("runs items.map tool calls in parallel", async () => {
|
||||
|
|
@ -238,14 +250,16 @@ describe("Promise.all over arbitrary arrays", () => {
|
|||
})
|
||||
|
||||
test("rejects with the first failure, catchable in-program", async () => {
|
||||
expect(await value(`
|
||||
expect(
|
||||
await value(`
|
||||
try {
|
||||
await Promise.all([tools.host.sleepy({ id: 1 }), tools.host.fail({})])
|
||||
return "no"
|
||||
} catch (e) {
|
||||
return e.message
|
||||
}
|
||||
`)).toBe("Lookup refused")
|
||||
`),
|
||||
).toBe("Lookup refused")
|
||||
})
|
||||
|
||||
test("a non-collection argument is a clear error", async () => {
|
||||
|
|
@ -264,14 +278,16 @@ describe("Promise.all over arbitrary arrays", () => {
|
|||
|
||||
describe("Promise.allSettled", () => {
|
||||
test("reports fulfilled and rejected outcomes with catch-normalized reasons", async () => {
|
||||
expect(await value(`
|
||||
expect(
|
||||
await value(`
|
||||
return await Promise.allSettled([
|
||||
tools.host.sleepy({ id: 5 }),
|
||||
tools.host.fail({}),
|
||||
"plain",
|
||||
Promise.reject(new Error("boom")),
|
||||
])
|
||||
`)).toEqual([
|
||||
`),
|
||||
).toEqual([
|
||||
{ status: "fulfilled", value: 5 },
|
||||
{ status: "rejected", reason: { name: "Error", message: "Lookup refused" } },
|
||||
{ status: "fulfilled", value: "plain" },
|
||||
|
|
@ -306,7 +322,8 @@ describe("Promise.race", () => {
|
|||
})
|
||||
|
||||
test("awaiting an interrupted loser afterwards is a catchable program failure", async () => {
|
||||
expect(await value(`
|
||||
expect(
|
||||
await value(`
|
||||
const fast = tools.host.sleepy({ id: 1, ms: 10 })
|
||||
const slow = tools.host.sleepy({ id: 2, ms: 5000 })
|
||||
const winner = await Promise.race([fast, slow])
|
||||
|
|
@ -316,23 +333,31 @@ describe("Promise.race", () => {
|
|||
} catch (e) {
|
||||
return { winner, caught: e.message }
|
||||
}
|
||||
`)).toEqual({ winner: 1, caught: "This tool call was interrupted because another value settled a Promise.race first." })
|
||||
`),
|
||||
).toEqual({
|
||||
winner: 1,
|
||||
caught: "This tool call was interrupted because another value settled a Promise.race first.",
|
||||
})
|
||||
})
|
||||
|
||||
test("a rejection can win the race", async () => {
|
||||
expect(await value(`
|
||||
expect(
|
||||
await value(`
|
||||
try {
|
||||
await Promise.race([tools.host.fail({}), tools.host.sleepy({ id: 1, ms: 5000 })])
|
||||
return "no"
|
||||
} catch (e) {
|
||||
return e.message
|
||||
}
|
||||
`)).toBe("Lookup refused")
|
||||
`),
|
||||
).toBe("Lookup refused")
|
||||
})
|
||||
|
||||
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 })).toBe("immediate")
|
||||
expect(
|
||||
await value(`return await Promise.race([tools.host.sleepy({ id: 1, ms: 5000 }), "immediate"])`, { trace }),
|
||||
).toBe("immediate")
|
||||
expect(trace.interrupted).toBe(1)
|
||||
})
|
||||
|
||||
|
|
@ -350,14 +375,16 @@ describe("Promise.resolve / Promise.reject", () => {
|
|||
})
|
||||
|
||||
test("reject produces a promise whose await throws the reason", async () => {
|
||||
expect(await value(`
|
||||
expect(
|
||||
await value(`
|
||||
try {
|
||||
await Promise.reject("nope")
|
||||
return "no"
|
||||
} catch (e) {
|
||||
return e
|
||||
}
|
||||
`)).toBe("nope")
|
||||
`),
|
||||
).toBe("nope")
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -83,15 +83,9 @@ describe("pretty signature rendering", () => {
|
|||
true,
|
||||
)
|
||||
expect(pretty).toBe(
|
||||
[
|
||||
"{",
|
||||
" /** Search filter */",
|
||||
" filter?: {",
|
||||
" /** Issue state */",
|
||||
" state?: string",
|
||||
" }",
|
||||
"}",
|
||||
].join("\n"),
|
||||
["{", " /** Search filter */", " filter?: {", " /** Issue state */", " state?: string", " }", "}"].join(
|
||||
"\n",
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
|
|
@ -119,7 +113,14 @@ describe("pretty signature rendering", () => {
|
|||
expect(pretty).toContain(" /** @deprecated */\n legacy?: string")
|
||||
expect(pretty).toContain(" /** @format uri */\n homepage?: string")
|
||||
expect(pretty).toContain(
|
||||
[" /**", ' * @default ["a","b"]', " * @minItems 2", " * @maxItems 5", " */", " tags?: Array<string>"].join("\n"),
|
||||
[
|
||||
" /**",
|
||||
' * @default ["a","b"]',
|
||||
" * @minItems 2",
|
||||
" * @maxItems 5",
|
||||
" */",
|
||||
" tags?: Array<string>",
|
||||
].join("\n"),
|
||||
)
|
||||
})
|
||||
|
||||
|
|
@ -212,7 +213,11 @@ describe("non-identifier property names render as quoted keys", () => {
|
|||
const tool = Tool.make({
|
||||
description: "Adapter tool with awkward field names",
|
||||
input: rawSchema,
|
||||
output: { type: "object", properties: { "content-type": { type: "string" } }, required: ["content-type"] } as const,
|
||||
output: {
|
||||
type: "object",
|
||||
properties: { "content-type": { type: "string" } },
|
||||
required: ["content-type"],
|
||||
} as const,
|
||||
run: () => Effect.succeed({ "content-type": "text/plain" }),
|
||||
})
|
||||
expect(inputTypeScript(tool)).toContain('"foo-bar"?: string')
|
||||
|
|
@ -269,9 +274,9 @@ describe("pretty signatures in search results", () => {
|
|||
const runtime = CodeMode.make({ tools: { github: { list_issues: listIssues }, orders: { lookup: lookupOrder } } })
|
||||
|
||||
const search = async (query: string) => {
|
||||
const result = await Effect.runPromise(runtime.execute(
|
||||
`return await tools.$codemode.search({ query: ${JSON.stringify(query)} })`,
|
||||
))
|
||||
const result = await Effect.runPromise(
|
||||
runtime.execute(`return await tools.$codemode.search({ query: ${JSON.stringify(query)} })`),
|
||||
)
|
||||
expect(result.ok).toBe(true)
|
||||
if (!result.ok) throw new Error("search failed")
|
||||
return result.value as { items: Array<{ path: string; signature: string }>; total: number }
|
||||
|
|
|
|||
|
|
@ -40,7 +40,11 @@ describe("Date", () => {
|
|||
})
|
||||
|
||||
test("UTC getters read calendar components", async () => {
|
||||
expect(await value(`const d = new Date("2024-03-05T06:07:08.009Z"); return [d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate(), d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds(), d.getUTCMilliseconds()]`)).toEqual([2024, 2, 5, 6, 7, 8, 9])
|
||||
expect(
|
||||
await value(
|
||||
`const d = new Date("2024-03-05T06:07:08.009Z"); return [d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate(), d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds(), d.getUTCMilliseconds()]`,
|
||||
),
|
||||
).toEqual([2024, 2, 5, 6, 7, 8, 9])
|
||||
})
|
||||
|
||||
test("invalid dates yield NaN times, guardable in-sandbox", async () => {
|
||||
|
|
@ -49,7 +53,9 @@ describe("Date", () => {
|
|||
})
|
||||
|
||||
test("toISOString on an invalid date is a catchable error", async () => {
|
||||
expect(await value(`try { new Date("garbage").toISOString(); return "no" } catch { return "caught" }`)).toBe("caught")
|
||||
expect(await value(`try { new Date("garbage").toISOString(); return "no" } catch { return "caught" }`)).toBe(
|
||||
"caught",
|
||||
)
|
||||
})
|
||||
|
||||
test("template interpolation renders the ISO form", async () => {
|
||||
|
|
@ -72,14 +78,18 @@ describe("Date", () => {
|
|||
})
|
||||
|
||||
test("sorting dates with a numeric comparator", async () => {
|
||||
expect(await value(`
|
||||
expect(
|
||||
await value(`
|
||||
const dates = [new Date(3000), new Date(1000), new Date(2000)]
|
||||
return dates.sort((a, b) => a - b).map((d) => d.getTime())
|
||||
`)).toEqual([1000, 2000, 3000])
|
||||
`),
|
||||
).toEqual([1000, 2000, 3000])
|
||||
})
|
||||
|
||||
test("new Date(year, month, day) accepts component form", async () => {
|
||||
expect(await value(`const d = new Date(2024, 0, 2); return [d.getFullYear(), d.getMonth(), d.getDate()]`)).toEqual([2024, 0, 2])
|
||||
expect(await value(`const d = new Date(2024, 0, 2); return [d.getFullYear(), d.getMonth(), d.getDate()]`)).toEqual([
|
||||
2024, 0, 2,
|
||||
])
|
||||
})
|
||||
|
||||
test("typeof and unknown properties are forgiving", async () => {
|
||||
|
|
@ -95,25 +105,31 @@ describe("RegExp", () => {
|
|||
})
|
||||
|
||||
test("exec exposes captures and index", async () => {
|
||||
expect(await value(`const m = /a(b+)/.exec("xxabbc"); return { full: m[0], group: m[1], index: m.index }`)).toEqual({
|
||||
full: "abb",
|
||||
group: "bb",
|
||||
index: 2,
|
||||
})
|
||||
expect(await value(`const m = /a(b+)/.exec("xxabbc"); return { full: m[0], group: m[1], index: m.index }`)).toEqual(
|
||||
{
|
||||
full: "abb",
|
||||
group: "bb",
|
||||
index: 2,
|
||||
},
|
||||
)
|
||||
expect(await value(`return /a/.exec("zzz")`)).toBeNull()
|
||||
})
|
||||
|
||||
test("named groups read through", async () => {
|
||||
expect(await value(`const m = /(?<word>[a-z]+)-(?<num>\\d+)/.exec("id ab-42"); return m.groups.word + m.groups.num`)).toBe("ab42")
|
||||
expect(
|
||||
await value(`const m = /(?<word>[a-z]+)-(?<num>\\d+)/.exec("id ab-42"); return m.groups.word + m.groups.num`),
|
||||
).toBe("ab42")
|
||||
})
|
||||
|
||||
test("global exec advances lastIndex across calls", async () => {
|
||||
expect(await value(`
|
||||
expect(
|
||||
await value(`
|
||||
const r = /\\d+/g
|
||||
const first = r.exec("a1b22c")
|
||||
const second = r.exec("a1b22c")
|
||||
return [first[0], second[0]]
|
||||
`)).toEqual(["1", "22"])
|
||||
`),
|
||||
).toEqual(["1", "22"])
|
||||
})
|
||||
|
||||
test("string match: non-global carries index, global lists all matches", async () => {
|
||||
|
|
@ -194,34 +210,51 @@ describe("RegExp", () => {
|
|||
|
||||
describe("Map", () => {
|
||||
test("get/set/has/size with chaining", async () => {
|
||||
expect(await value(`
|
||||
expect(
|
||||
await value(`
|
||||
const m = new Map()
|
||||
m.set("a", 1).set("b", 2)
|
||||
return { a: m.get("a"), b: m.get("b"), has: m.has("a"), miss: m.get("zz") === undefined, size: m.size }
|
||||
`)).toEqual({ a: 1, b: 2, has: true, miss: true, size: 5 - 3 })
|
||||
`),
|
||||
).toEqual({ a: 1, b: 2, has: true, miss: true, size: 5 - 3 })
|
||||
})
|
||||
|
||||
test("object keys use identity", async () => {
|
||||
expect(await value(`
|
||||
expect(
|
||||
await value(`
|
||||
const key = { id: 1 }
|
||||
const m = new Map()
|
||||
m.set(key, "hit")
|
||||
return [m.get(key), m.get({ id: 1 }) === undefined]
|
||||
`)).toEqual(["hit", true])
|
||||
`),
|
||||
).toEqual(["hit", true])
|
||||
})
|
||||
|
||||
test("construction from entry pairs and another Map", async () => {
|
||||
expect(await value(`const m = new Map([["a", 1], ["b", 2]]); return m.get("b")`)).toBe(2)
|
||||
expect(await value(`const m = new Map([["a", 1]]); const n = new Map(m); n.set("b", 2); return [n.get("a"), n.get("b"), m.has("b")]`)).toEqual([1, 2, false])
|
||||
expect(
|
||||
await value(
|
||||
`const m = new Map([["a", 1]]); const n = new Map(m); n.set("b", 2); return [n.get("a"), n.get("b"), m.has("b")]`,
|
||||
),
|
||||
).toEqual([1, 2, false])
|
||||
expect((await error(`return new Map("nope")`)).message).toMatch(/\[key, value\] pairs/)
|
||||
expect((await error(`return new Map(["flat"])`)).message).toMatch(/\[key, value\] pairs/)
|
||||
})
|
||||
|
||||
test("keys/values/entries return arrays", async () => {
|
||||
expect(await value(`
|
||||
expect(
|
||||
await value(`
|
||||
const m = new Map([["a", 1], ["b", 2]])
|
||||
return { keys: m.keys(), values: m.values(), entries: m.entries() }
|
||||
`)).toEqual({ keys: ["a", "b"], values: [1, 2], entries: [["a", 1], ["b", 2]] })
|
||||
`),
|
||||
).toEqual({
|
||||
keys: ["a", "b"],
|
||||
values: [1, 2],
|
||||
entries: [
|
||||
["a", 1],
|
||||
["b", 2],
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
test("Object.fromEntries(map) and Array.from(map)", async () => {
|
||||
|
|
@ -230,13 +263,15 @@ describe("Map", () => {
|
|||
})
|
||||
|
||||
test("for...of iterates [key, value] pairs with destructuring", async () => {
|
||||
expect(await value(`
|
||||
expect(
|
||||
await value(`
|
||||
const m = new Map([["a", 1], ["b", 2]])
|
||||
let total = 0
|
||||
let names = ""
|
||||
for (const [key, count] of m) { names += key; total += count }
|
||||
return names + total
|
||||
`)).toBe("ab3")
|
||||
`),
|
||||
).toBe("ab3")
|
||||
})
|
||||
|
||||
test("spread produces entry pairs", async () => {
|
||||
|
|
@ -244,32 +279,38 @@ describe("Map", () => {
|
|||
})
|
||||
|
||||
test("forEach passes (value, key)", async () => {
|
||||
expect(await value(`
|
||||
expect(
|
||||
await value(`
|
||||
const m = new Map([["a", 1], ["b", 2]])
|
||||
const seen = []
|
||||
m.forEach((count, key) => seen.push(key + count))
|
||||
return seen
|
||||
`)).toEqual(["a1", "b2"])
|
||||
`),
|
||||
).toEqual(["a1", "b2"])
|
||||
})
|
||||
|
||||
test("delete and clear", async () => {
|
||||
expect(await value(`
|
||||
expect(
|
||||
await value(`
|
||||
const m = new Map([["a", 1], ["b", 2]])
|
||||
const removed = m.delete("a")
|
||||
const missed = m.delete("zz")
|
||||
const sizeAfterDelete = m.size
|
||||
m.clear()
|
||||
return [removed, missed, sizeAfterDelete, m.size]
|
||||
`)).toEqual([true, false, 1, 0])
|
||||
`),
|
||||
).toEqual([true, false, 1, 0])
|
||||
})
|
||||
|
||||
test("counting idiom: grouped tallies", async () => {
|
||||
expect(await value(`
|
||||
expect(
|
||||
await value(`
|
||||
const words = ["a", "b", "a", "c", "a"]
|
||||
const counts = new Map()
|
||||
for (const word of words) counts.set(word, (counts.get(word) ?? 0) + 1)
|
||||
return Object.fromEntries(counts)
|
||||
`)).toEqual({ a: 3, b: 1, c: 1 })
|
||||
`),
|
||||
).toEqual({ a: 3, b: 1, c: 1 })
|
||||
})
|
||||
|
||||
test("maps serialize to {} at the boundary, like JSON", async () => {
|
||||
|
|
@ -286,12 +327,14 @@ describe("Map", () => {
|
|||
|
||||
describe("Set", () => {
|
||||
test("add/has/delete/size with chaining", async () => {
|
||||
expect(await value(`
|
||||
expect(
|
||||
await value(`
|
||||
const s = new Set()
|
||||
s.add(1).add(2).add(1)
|
||||
const removed = s.delete(2)
|
||||
return [s.size, s.has(1), s.has(2), removed]
|
||||
`)).toEqual([1, true, false, true])
|
||||
`),
|
||||
).toEqual([1, true, false, true])
|
||||
})
|
||||
|
||||
test("dedupe idiom: [...new Set(items)]", async () => {
|
||||
|
|
@ -308,11 +351,13 @@ describe("Set", () => {
|
|||
})
|
||||
|
||||
test("for...of iterates values", async () => {
|
||||
expect(await value(`
|
||||
expect(
|
||||
await value(`
|
||||
let total = 0
|
||||
for (const n of new Set([1, 2, 3])) total += n
|
||||
return total
|
||||
`)).toBe(6)
|
||||
`),
|
||||
).toBe(6)
|
||||
})
|
||||
|
||||
test("sets serialize to {} at the boundary, like JSON", async () => {
|
||||
|
|
@ -338,21 +383,32 @@ describe("stdlib integration", () => {
|
|||
})
|
||||
|
||||
test("dates inside Map values survive in-sandbox reads", async () => {
|
||||
expect(await value(`
|
||||
expect(
|
||||
await value(`
|
||||
const m = new Map([["start", new Date(1000)]])
|
||||
return m.get("start").getTime()
|
||||
`)).toBe(1000)
|
||||
`),
|
||||
).toBe(1000)
|
||||
})
|
||||
|
||||
test("instanceof recognizes the stdlib value types", async () => {
|
||||
expect(await value(`return [new Date(0) instanceof Date, /a/ instanceof RegExp, new Map() instanceof Map, new Set() instanceof Set]`)).toEqual([true, true, true, true])
|
||||
expect(await value(`return [[1] instanceof Array, [1] instanceof Object, ({}) instanceof Object, 5 instanceof Object]`)).toEqual([true, true, true, false])
|
||||
expect(
|
||||
await value(
|
||||
`return [new Date(0) instanceof Date, /a/ instanceof RegExp, new Map() instanceof Map, new Set() instanceof Set]`,
|
||||
),
|
||||
).toEqual([true, true, true, true])
|
||||
expect(
|
||||
await value(`return [[1] instanceof Array, [1] instanceof Object, ({}) instanceof Object, 5 instanceof Object]`),
|
||||
).toEqual([true, true, true, false])
|
||||
expect(await value(`return [new Map() instanceof Set, "s" instanceof Date]`)).toEqual([false, false])
|
||||
expect(await value(`const p = Promise.resolve(1); const isPromise = p instanceof Promise; await p; return isPromise`)).toBe(true)
|
||||
expect(
|
||||
await value(`const p = Promise.resolve(1); const isPromise = p instanceof Promise; await p; return isPromise`),
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
test("realistic pipeline: parse, extract with regex, dedupe, count by day", async () => {
|
||||
expect(await value(`
|
||||
expect(
|
||||
await value(`
|
||||
const raw = '[{"at":"2024-01-01T05:00:00Z","tag":"a b"},{"at":"2024-01-01T09:00:00Z","tag":"b c"},{"at":"2024-01-02T01:00:00Z","tag":"a"}]'
|
||||
const rows = JSON.parse(raw)
|
||||
const tags = new Set()
|
||||
|
|
@ -363,27 +419,34 @@ describe("stdlib integration", () => {
|
|||
byDay.set(day, (byDay.get(day) ?? 0) + 1)
|
||||
}
|
||||
return { tags: [...tags].sort((a, b) => (a < b ? -1 : 1)), byDay: Object.fromEntries(byDay) }
|
||||
`)).toEqual({ tags: ["a", "b", "c"], byDay: { "2024-01-01": 2, "2024-01-02": 1 } })
|
||||
`),
|
||||
).toEqual({ tags: ["a", "b", "c"], byDay: { "2024-01-01": 2, "2024-01-02": 1 } })
|
||||
})
|
||||
})
|
||||
|
||||
describe("sandbox values at intra-sandbox checkpoints", () => {
|
||||
test("Object.values/entries keep Dates usable", async () => {
|
||||
expect(await value(`return Object.values({ d: new Date(0) })[0].getTime()`)).toBe(0)
|
||||
expect(await value(`const [key, d] = Object.entries({ d: new Date(0) })[0]; return key + ":" + d.getTime()`)).toBe("d:0")
|
||||
expect(await value(`const [key, d] = Object.entries({ d: new Date(0) })[0]; return key + ":" + d.getTime()`)).toBe(
|
||||
"d:0",
|
||||
)
|
||||
})
|
||||
|
||||
test("Object.assign keeps Maps usable", async () => {
|
||||
expect(await value(`const merged = Object.assign({}, { m: new Map([["a", 1]]) }); return merged.m.get("a")`)).toBe(1)
|
||||
expect(await value(`const merged = Object.assign({}, { m: new Map([["a", 1]]) }); return merged.m.get("a")`)).toBe(
|
||||
1,
|
||||
)
|
||||
})
|
||||
|
||||
test("object and array spread keep sandbox values usable", async () => {
|
||||
expect(await value(`
|
||||
expect(
|
||||
await value(`
|
||||
const src = { m: new Map([["a", 1]]) }
|
||||
const copy = { ...src }
|
||||
copy.m.set("b", 2)
|
||||
return [copy.m.get("a"), src.m.get("b")]
|
||||
`)).toEqual([1, 2])
|
||||
`),
|
||||
).toEqual([1, 2])
|
||||
expect(await value(`const list = [new Date(1000)]; const copy = [...list]; return copy[0].getTime()`)).toBe(1000)
|
||||
})
|
||||
|
||||
|
|
@ -404,7 +467,10 @@ describe("sandbox values at intra-sandbox checkpoints", () => {
|
|||
})
|
||||
|
||||
test("the host boundary still serializes JSON forms: results, JSON.stringify, and tool arguments", async () => {
|
||||
expect(await value(`return { d: new Date(0), m: new Map([["a", 1]]) }`)).toEqual({ d: "1970-01-01T00:00:00.000Z", m: {} })
|
||||
expect(await value(`return { d: new Date(0), m: new Map([["a", 1]]) }`)).toEqual({
|
||||
d: "1970-01-01T00:00:00.000Z",
|
||||
m: {},
|
||||
})
|
||||
expect(await value(`return JSON.stringify({ d: new Date(0) })`)).toBe('{"d":"1970-01-01T00:00:00.000Z"}')
|
||||
|
||||
const observed: Array<unknown> = []
|
||||
|
|
@ -417,10 +483,12 @@ describe("sandbox values at intra-sandbox checkpoints", () => {
|
|||
return "ok"
|
||||
}),
|
||||
})
|
||||
const result = await Effect.runPromise(CodeMode.execute({
|
||||
tools: { host: { capture } },
|
||||
code: `return await tools.host.capture({ when: new Date(0), tags: new Map([["a", 1]]) })`,
|
||||
}))
|
||||
const result = await Effect.runPromise(
|
||||
CodeMode.execute({
|
||||
tools: { host: { capture } },
|
||||
code: `return await tools.host.capture({ when: new Date(0), tags: new Map([["a", 1]]) })`,
|
||||
}),
|
||||
)
|
||||
expect(result.ok).toBe(true)
|
||||
expect(observed).toStrictEqual([{ when: "1970-01-01T00:00:00.000Z", tags: {} }])
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue