| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921 |
- import { describe, expect, test } from "bun:test"
- import { Effect, Schema } from "effect"
- import { CodeMode, Tool } from "../src/index.js"
- // Standard-library value types: Date, RegExp, Map, Set. Programs use them as ordinary JS;
- // intra-CodeMode checkpoints (Object.* helpers, spread, coercion inputs) preserve the live
- // values, while at the host boundary (final result, tool arguments, JSON.stringify) they
- // serialize exactly as JSON.stringify would: Date -> ISO string (invalid -> null),
- // URL -> href, and RegExp/Map/Set/URLSearchParams -> {}.
- const run = (code: string) => Effect.runPromise(CodeMode.execute({ code, tools: {} }))
- const value = async (code: string) => {
- const result = await run(code)
- if (!result.ok) throw new Error(`expected success, got ${result.error.kind}: ${result.error.message}`)
- return result.value
- }
- const error = async (code: string) => {
- const result = await run(code)
- if (result.ok) throw new Error(`expected failure, got value ${JSON.stringify(result.value)}`)
- return result.error
- }
- describe("Number and Math", () => {
- test("Math.random returns a number in [0, 1)", async () => {
- expect(await value(`const n = Math.random(); return typeof n === "number" && n >= 0 && n < 1`)).toBe(true)
- })
- test("Number exposes native non-finite constants", async () => {
- expect(
- await value(
- `return [Number.isNaN(Number.NaN), Number.POSITIVE_INFINITY === Infinity, Number.NEGATIVE_INFINITY === -Infinity]`,
- ),
- ).toEqual([true, true, true])
- })
- test("Number valueOf returns its primitive receiver", async () => {
- expect(await value(`return (42).valueOf()`)).toBe(42)
- })
- test("Number valueOf does not enable boxed numbers", async () => {
- expect((await error(`return new Number(42)`)).kind).toBe("UnsupportedSyntax")
- })
- })
- describe("Date", () => {
- test("Date.now() returns a number", async () => {
- expect(await value(`return typeof Date.now()`)).toBe("number")
- })
- test("epoch construction and ISO rendering", async () => {
- expect(await value(`return new Date(0).toISOString()`)).toBe("1970-01-01T00:00:00.000Z")
- })
- test("string parsing round-trips", async () => {
- expect(await value(`return new Date("2024-01-02T03:04:05.000Z").getTime()`)).toBe(1704164645000)
- expect(await value(`return Date.parse("2024-01-02T03:04:05.000Z")`)).toBe(1704164645000)
- })
- test("date arithmetic and comparison use the time value", async () => {
- expect(await value(`const a = new Date(1000); const b = new Date(3000); return b - a`)).toBe(2000)
- expect(await value(`const a = new Date(1000); const b = new Date(3000); return a < b`)).toBe(true)
- expect(await value(`return +new Date(42)`)).toBe(42)
- })
- 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])
- })
- test("invalid dates yield NaN times, guardable in-CodeMode", async () => {
- expect(await value(`return Number.isNaN(new Date("garbage").getTime())`)).toBe(true)
- expect(await value(`return new Date("garbage").toJSON()`)).toBeNull()
- })
- 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",
- )
- })
- test("template interpolation renders the ISO form", async () => {
- expect(await value("return `at ${new Date(0)}`")).toBe("at 1970-01-01T00:00:00.000Z")
- })
- test("dates serialize to ISO strings at the boundary, direct and nested", async () => {
- expect(await value(`return new Date(0)`)).toBe("1970-01-01T00:00:00.000Z")
- expect(await value(`return { when: new Date(0), tags: [new Date(1000)] }`)).toEqual({
- when: "1970-01-01T00:00:00.000Z",
- tags: ["1970-01-01T00:00:01.000Z"],
- })
- expect(await value(`return JSON.stringify({ d: new Date(0) })`)).toBe('{"d":"1970-01-01T00:00:00.000Z"}')
- })
- test("coercions: Number is the time, String is ISO, Boolean is true", async () => {
- expect(await value(`return Number(new Date(5))`)).toBe(5)
- expect(await value(`return String(new Date(0))`)).toBe("1970-01-01T00:00:00.000Z")
- expect(await value(`return Boolean(new Date(0))`)).toBe(true)
- })
- test("sorting dates with a numeric comparator", async () => {
- 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])
- })
- 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,
- ])
- })
- test("typeof and unknown properties are forgiving", async () => {
- expect(await value(`return typeof new Date(0)`)).toBe("object")
- expect(await value(`return new Date(0).nope === undefined`)).toBe(true)
- })
- })
- describe("RegExp", () => {
- test("literal test", async () => {
- expect(await value(`return /ab+c/.test("xabbbc")`)).toBe(true)
- expect(await value(`return /ab+c/.test("nope")`)).toBe(false)
- })
- 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(`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")
- })
- test("global exec advances lastIndex across calls", async () => {
- 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"])
- })
- test("an unmatched string pattern returns null", async () => {
- expect(await value(`return "abc".match(/\\d/)`)).toBeNull()
- })
- test("matchAll materializes match arrays with captures", async () => {
- expect(await value(`return "a1b22".matchAll(/(\\d+)/g).map((m) => m[1])`)).toEqual(["1", "22"])
- })
- test("function replacers receive captures, offsets, input, and named groups", async () => {
- expect(
- await value(`
- const seen = []
- const output = "a1b22".replace(/(\\d)(\\d)?/g, (match, first, second, offset, input) => {
- seen.push([match, first, second === undefined, offset, input])
- return Number(match) * 2
- })
- return { output, seen }
- `),
- ).toEqual({
- output: "a2b44",
- seen: [
- ["1", "1", true, 1, "a1b22"],
- ["22", "2", false, 3, "a1b22"],
- ],
- })
- expect(
- await value(`
- return "red-blue".replace(
- /(?<left>[a-z]+)-(?<right>[a-z]+)/,
- (match, left, right, offset, input, groups) => groups.right + ":" + groups.left,
- )
- `),
- ).toBe("blue:red")
- })
- test("function replacers support string searches, zero-length matches, and result coercion", async () => {
- expect(await value(`return "banana".replace("na", (match, offset, input) => "[" + offset + "]")`)).toBe("ba[2]na")
- expect(await value(`return "ab".replaceAll("", (match, offset) => offset)`)).toBe("0a1b2")
- expect(await value(`return "😀".replaceAll(/(?:)/gu, (match, offset) => "[" + offset + "]")`)).toBe("[0]😀[2]")
- expect(
- await value(`return "123".replace(/\\d/g, (match) => match === "1" ? 7 : match === "2" ? null : { n: 3 })`),
- ).toBe("7null[object Object]")
- })
- test("function replacers can await effectful tool calls", async () => {
- const decorate = Tool.make({
- description: "Decorate a string",
- input: Schema.String,
- output: Schema.String,
- run: (input) => Effect.succeed(`[${input}]`),
- })
- const result = await Effect.runPromise(
- CodeMode.execute({
- tools: { host: { decorate } },
- code: `return "a1b22".replace(/\\d+/g, async (match) => await tools.host.decorate(match))`,
- }),
- )
- expect(result.ok && result.value).toBe("a[1]b[22]")
- const missingAwait = await Effect.runPromise(
- CodeMode.execute({
- tools: { host: { decorate } },
- code: `return "a1".replace(/\\d/, (match) => tools.host.decorate(match))`,
- }),
- )
- expect(!missingAwait.ok && missingAwait.error.kind).toBe("InvalidDataValue")
- expect(!missingAwait.ok && missingAwait.error.message).toContain("un-awaited Promise")
- })
- test("replaceAll without the g flag is a catchable error", async () => {
- expect(await value(`try { "a".replaceAll(/a/, "b"); return "no" } catch { return "caught" }`)).toBe("caught")
- })
- test("new RegExp constructs from strings; invalid patterns are catchable", async () => {
- expect(await value(`return new RegExp("a+", "i").test("AAA")`)).toBe(true)
- expect(await value(`try { new RegExp("("); return "no" } catch { return "caught" }`)).toBe("caught")
- expect(await value(`return [/a/ instanceof RegExp, /a/.source]`)).toEqual([true, "a"])
- })
- test("invalid patterns fail with actionable messages", async () => {
- const fromString = await error(`return "abc".match("(")`)
- expect(fromString.message).toContain('String.match received the string "("')
- expect(fromString.message).toContain("escape them with a backslash")
- const fromConstructor = await error(`return new RegExp("(")`)
- expect(fromConstructor.message).toContain('new RegExp(...) received "("')
- expect(fromConstructor.message).toContain("escape them with a backslash")
- const fromFlags = await error(`return new RegExp("a", "xz")`)
- expect(fromFlags.message).toContain('invalid flags "xz"')
- expect(fromFlags.message).toContain("Valid flags are")
- })
- test("missing g-flag errors say how to fix the call", async () => {
- expect((await error(`return "aa".replaceAll(/a/, "b")`)).message).toContain("write /a/g, or use String.replace")
- expect((await error(`return "aa".matchAll(/a/)`)).message).toContain("write /a/g, or use String.match")
- })
- test("a non-pattern argument names the expected shapes", async () => {
- const err = await error(`return "abc".match(42)`)
- expect(err.message).toContain("expects a regular expression")
- expect(err.message).toContain("not number")
- })
- test("source and flags properties read through", async () => {
- expect(await value(`const r = /ab/gi; return { source: r.source, flags: r.flags, global: r.global }`)).toEqual({
- source: "ab",
- flags: "gi",
- global: true,
- })
- })
- test("regexes serialize to {} at the boundary, like JSON", async () => {
- expect(await value(`return /a/`)).toEqual({})
- expect(await value(`return JSON.stringify({ r: /a/g })`)).toBe('{"r":{}}')
- })
- test("template interpolation renders the literal form", async () => {
- expect(await value("return `${/ab/g}`")).toBe("/ab/g")
- })
- })
- describe("URL and URI helpers", () => {
- test("encodes and decodes complete URIs and URI components", async () => {
- expect(
- await value(`
- return [
- encodeURI("https://example.test/a b?q=a/b"),
- encodeURIComponent("a b/c?"),
- decodeURI("https://example.test/a%20b?q=a/b"),
- decodeURIComponent("a%20b%2Fc%3F"),
- ["a b", "c/d"].map(encodeURIComponent),
- ]
- `),
- ).toEqual([
- "https://example.test/a%20b?q=a/b",
- "a%20b%2Fc%3F",
- "https://example.test/a b?q=a/b",
- "a b/c?",
- ["a%20b", "c%2Fd"],
- ])
- expect(
- await value(`try { decodeURIComponent("%zz"); return false } catch (error) { return error instanceof URIError }`),
- ).toBe(true)
- })
- test("resolves and mutates URLs with linked search parameters", async () => {
- expect(
- await value(`
- const url = new URL("../users?id=old#top", "https://user:pass@example.com:8443/api/v1/")
- url.pathname = "/items/a b"
- url.searchParams.set("id", "a b")
- url.searchParams.append("tag", "x/y")
- url.hash = "part 1"
- return {
- href: url.href,
- origin: url.origin,
- host: url.host,
- pathname: url.pathname,
- search: url.search,
- id: url.searchParams.get("id"),
- string: String(url),
- json: url.toJSON(),
- instances: [
- url instanceof URL,
- url.searchParams instanceof URLSearchParams,
- url.searchParams === url.searchParams,
- ],
- }
- `),
- ).toEqual({
- href: "https://user:pass@example.com:8443/items/a%20b?id=a+b&tag=x%2Fy#part%201",
- origin: "https://example.com:8443",
- host: "example.com:8443",
- pathname: "/items/a%20b",
- search: "?id=a+b&tag=x%2Fy",
- id: "a b",
- string: "https://user:pass@example.com:8443/items/a%20b?id=a+b&tag=x%2Fy#part%201",
- json: "https://user:pass@example.com:8443/items/a%20b?id=a+b&tag=x%2Fy#part%201",
- instances: [true, true, true],
- })
- })
- test("URLSearchParams supports records, pairs, mutation, callbacks, and materialization", async () => {
- expect(
- await value(`
- const params = new URLSearchParams([["tag", "b"], ["tag", "a"], ["q", "a b"]])
- const seen = []
- params.forEach((value, key) => seen.push(key + "=" + value))
- params.delete("tag", "b")
- params.append("tag", "c")
- params.sort()
- return {
- text: params.toString(),
- size: params.size,
- tags: params.getAll("tag"),
- has: params.has("tag", "c"),
- entries: Array.from(params),
- object: Object.fromEntries(params),
- record: new URLSearchParams({ page: 2, filter: "open" }).toString(),
- seen,
- }
- `),
- ).toEqual({
- text: "q=a+b&tag=a&tag=c",
- size: 3,
- tags: ["a", "c"],
- has: true,
- entries: [
- ["q", "a b"],
- ["tag", "a"],
- ["tag", "c"],
- ],
- object: { q: "a b", tag: "c" },
- record: "page=2&filter=open",
- seen: ["tag=b", "tag=a", "q=a b"],
- })
- })
- test("URL parsing failures are catchable and values use native JSON forms", async () => {
- expect(
- await value(`
- const parsed = URL.parse("/users", "https://example.test/api/")
- let invalidIsTypeError = false
- try { new URL("not relative without a base") } catch (error) { invalidIsTypeError = error instanceof TypeError }
- return {
- canParse: URL.canParse("/users", "https://example.test/api/"),
- cannotParse: URL.canParse("not relative without a base"),
- parsed: parsed.href,
- invalidIsTypeError,
- boundary: [new URL("https://example.test/a"), new URLSearchParams("q=one")],
- json: JSON.stringify({ url: new URL("https://example.test/a"), params: new URLSearchParams("q=one") }),
- }
- `),
- ).toEqual({
- canParse: true,
- cannotParse: false,
- parsed: "https://example.test/users",
- invalidIsTypeError: true,
- boundary: ["https://example.test/a", {}],
- json: '{"url":"https://example.test/a","params":{}}',
- })
- })
- test("distinguishes omitted URL arguments from explicit undefined", async () => {
- expect(
- await value(`
- function throwsTypeError(run) {
- try { run(); return false } catch (error) { return error instanceof TypeError }
- }
- const params = new URLSearchParams()
- const required = [
- () => params.append(),
- () => params.delete(),
- () => params.get(),
- () => params.getAll(),
- () => params.has(),
- () => params.set(),
- () => params.forEach(),
- ].map(throwsTypeError)
- params.append(undefined, undefined)
- return {
- construct: throwsTypeError(() => new URL()),
- canParse: throwsTypeError(() => URL.canParse()),
- parse: throwsTypeError(() => URL.parse()),
- explicitUndefined: new URL(undefined, "https://example.test/base/").href,
- params: params.toString(),
- required,
- }
- `),
- ).toEqual({
- construct: true,
- canParse: true,
- parse: true,
- explicitUndefined: "https://example.test/base/undefined",
- params: "undefined=undefined",
- required: [true, true, true, true, true, true, true],
- })
- })
- })
- describe("Map", () => {
- test("get/set/has/size with chaining", async () => {
- 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 })
- })
- test("object keys use identity", async () => {
- 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])
- })
- 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 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(`
- 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],
- ],
- })
- })
- test("Object.fromEntries(map) and Array.from(map)", async () => {
- expect(await value(`return Object.fromEntries(new Map([["a", 1], ["b", 2]]))`)).toEqual({ a: 1, b: 2 })
- expect(await value(`return Array.from(new Map([["a", 1]]))`)).toEqual([["a", 1]])
- })
- test("for...of iterates [key, value] pairs with destructuring", async () => {
- 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")
- })
- test("spread produces entry pairs", async () => {
- expect(await value(`return [...new Map([["a", 1]])]`)).toEqual([["a", 1]])
- })
- test("forEach passes (value, key)", async () => {
- 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"])
- })
- test("delete and clear", async () => {
- 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])
- })
- test("counting idiom: grouped tallies", async () => {
- 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 })
- })
- test("maps serialize to {} at the boundary, like JSON", async () => {
- expect(await value(`return new Map([["a", 1]])`)).toEqual({})
- expect(await value(`return JSON.stringify(new Map([["a", 1]]))`)).toBe("{}")
- })
- test("console.log renders map contents for debugging", async () => {
- const result = await run(`console.log(new Map([["a", 1]])); return null`)
- expect(result.ok).toBe(true)
- expect(result.logs?.[0]).toBe(`Map(1) [["a",1]]`)
- })
- })
- describe("Set", () => {
- test("add/has/delete/size with chaining", async () => {
- 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])
- })
- test("dedupe idiom: [...new Set(items)]", async () => {
- expect(await value(`return [...new Set([1, 2, 2, 3, 1])]`)).toEqual([1, 2, 3])
- })
- test("construction from strings and other Sets", async () => {
- expect(await value(`return [...new Set("aba")]`)).toEqual(["a", "b"])
- expect(await value(`return Array.from(new Set(new Set([1, 2])))`)).toEqual([1, 2])
- })
- test("SameValueZero: NaN is findable", async () => {
- expect(await value(`const s = new Set([NaN]); return s.has(NaN)`)).toBe(true)
- })
- test("for...of iterates values", async () => {
- expect(
- await value(`
- let total = 0
- for (const n of new Set([1, 2, 3])) total += n
- return total
- `),
- ).toBe(6)
- })
- test("sets serialize to {} at the boundary, like JSON", async () => {
- expect(await value(`return { s: new Set([1]) }`)).toEqual({ s: {} })
- })
- })
- describe("stdlib integration", () => {
- test("Object.is uses SameValue semantics", async () => {
- expect(
- await value(`
- const object = {}
- return [
- Object.is(NaN, NaN),
- Object.is(0, -0),
- Object.is(object, object),
- Object.is({}, {}),
- ]
- `),
- ).toEqual([true, false, true, false])
- })
- test("Object.is rejects opaque runtime references", async () => {
- expect((await error(`return Object.is(Math.max, Math.max)`)).kind).toBe("InvalidDataValue")
- })
- test("Object values and entries accept arrays", async () => {
- expect(await value(`return [Object.values(["a", "b"]), Object.entries(["a", "b"])]`)).toEqual([
- ["a", "b"],
- [
- ["0", "a"],
- ["1", "b"],
- ],
- ])
- expect(await value(`const match = /a/.exec("ba"); return [Object.values(match), Object.entries(match)]`)).toEqual([
- ["a", 1],
- [
- ["0", "a"],
- ["index", 1],
- ],
- ])
- expect(await value(`return Object.keys(Object.values({ match: /a/.exec("ba") })[0])`)).toEqual(["0", "index"])
- })
- test("Object.fromEntries accepts every supported entry collection", async () => {
- expect(
- await value(`
- return [
- Object.fromEntries([["a", 1]]),
- Object.fromEntries(new Map([["b", 2]])),
- Object.fromEntries(new Set([["c", 3]])),
- Object.fromEntries(new URLSearchParams("d=4")),
- Object.fromEntries([{ 0: "e", 1: 5 }]),
- Object.fromEntries(new Set([[{}, 6], [new Date(0), 7], [null, 8], [undefined, 9]])),
- ]
- `),
- ).toEqual([
- { a: 1 },
- { b: 2 },
- { c: 3 },
- { d: "4" },
- { e: 5 },
- { "[object Object]": 6, "1970-01-01T00:00:00.000Z": 7, null: 8, undefined: 9 },
- ])
- expect(await value(`try { Object.fromEntries(new Set([Math.max])); return false } catch { return true }`)).toBe(
- true,
- )
- expect(
- await value(`try { Object.fromEntries(new Map([["fn", Math.max]])); return false } catch { return true }`),
- ).toBe(true)
- })
- test("deterministic Math methods match the host runtime", async () => {
- const result = await value(`
- return [
- Math.acos(0.5), Math.acosh(2), Math.asin(0.5), Math.asinh(2), Math.atan(1), Math.atan2(1, 2), Math.atanh(0.5),
- Math.cos(0.5), Math.cosh(0.5), Math.sin(0.5), Math.sinh(0.5), Math.tan(0.5), Math.tanh(0.5),
- Math.log1p(0.5), Math.expm1(0.5), Math.f16round(1.337), Math.fround(1.337), Math.clz32(1), Math.imul(2, 3),
- ]
- `)
- expect(result).toEqual([
- Math.acos(0.5),
- Math.acosh(2),
- Math.asin(0.5),
- Math.asinh(2),
- Math.atan(1),
- Math.atan2(1, 2),
- Math.atanh(0.5),
- Math.cos(0.5),
- Math.cosh(0.5),
- Math.sin(0.5),
- Math.sinh(0.5),
- Math.tan(0.5),
- Math.tanh(0.5),
- Math.log1p(0.5),
- Math.expm1(0.5),
- Math.f16round(1.337),
- Math.fround(1.337),
- Math.clz32(1),
- Math.imul(2, 3),
- ])
- })
- test("Object.assign mutates and returns its target", async () => {
- expect(
- await value(`
- const target = { a: 1 }
- const result = Object.assign(target, { b: 2 })
- return { target, result, same: target === result }
- `),
- ).toEqual({ target: { a: 1, b: 2 }, result: { a: 1, b: 2 }, same: true })
- expect(await value(`try { Object.assign(null, { a: 1 }); return false } catch { return true }`)).toBe(true)
- })
- test("assignment resolves and reads its left side before evaluating the right side", async () => {
- expect(await value(`let x = 1; x += (x = 5); return x`)).toBe(6)
- expect(await value(`let i = 0; const values = [9]; values[i++] = i; return [values, i]`)).toEqual([[1], 1])
- expect(await value(`let i = 0; const values = [10, 20]; values[i++] += i; return [values, i]`)).toEqual([
- [11, 20],
- 1,
- ])
- })
- test("typeof reports constructors as functions and never throws", async () => {
- expect(await value(`return typeof Map`)).toBe("function")
- expect(await value(`return typeof ((x) => x)`)).toBe("function")
- expect(await value(`return typeof Math`)).toBe("object")
- expect(await value(`return typeof tools`)).toBe("object")
- })
- test("negation works on any value", async () => {
- expect(await value(`return !new Map()`)).toBe(false)
- expect(await value(`const fn = () => 1; return !fn`)).toBe(false)
- })
- test("object spread of CodeMode values is a no-op, like JS", async () => {
- expect(await value(`return { ...new Map([["a", 1]]), kept: true }`)).toEqual({ kept: true })
- })
- test("dates inside Map values survive in-CodeMode reads", async () => {
- expect(
- await value(`
- const m = new Map([["start", new Date(1000)]])
- return m.get("start").getTime()
- `),
- ).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 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)
- })
- test("realistic pipeline: parse, extract with regex, dedupe, count by day", async () => {
- 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()
- const byDay = new Map()
- for (const row of rows) {
- for (const m of row.tag.matchAll(/[a-z]+/g)) tags.add(m[0])
- const day = new Date(row.at).toISOString().slice(0, 10)
- 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 } })
- })
- })
- describe("CodeMode values at intra-CodeMode 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",
- )
- })
- test("Object.values/entries preserve nested object identity", async () => {
- expect(
- await value(`
- const child = { selected: false }
- const rows = { a: child }
- Object.values(rows)[0].selected = true
- return child.selected
- `),
- ).toBe(true)
- expect(
- await value(`
- const child = { selected: false }
- const rows = { a: child }
- Object.entries(rows)[0][1].selected = true
- return child.selected
- `),
- ).toBe(true)
- })
- test("Object enumeration preserves promises and callable references", async () => {
- expect(
- await value(`
- const pending = Promise.resolve(1)
- const source = { pending }
- return [Object.keys(source), Object.hasOwn(source, "pending"), await Object.values(source)[0], await Object.entries(source)[0][1]]
- `),
- ).toEqual([["pending"], true, 1, 1])
- expect(await value(`return Object.values({ max: Math.max })[0](1, 2)`)).toBe(2)
- })
- test("Object enumeration rejects invalid receivers and gives promises an await hint", async () => {
- const diagnostic = await error(`return Object.keys(Promise.resolve({ a: 1 }))`)
- expect(diagnostic.kind).toBe("InvalidDataValue")
- expect(diagnostic.message).toContain("await")
- expect((await error(`return Object.keys(Math)`)).kind).toBe("InvalidDataValue")
- })
- 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,
- )
- })
- test("object and array spread keep CodeMode values usable", async () => {
- 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])
- expect(await value(`const list = [new Date(1000)]; const copy = [...list]; return copy[0].getTime()`)).toBe(1000)
- })
- test("Array.from over arrays keeps nested CodeMode values usable", async () => {
- expect(await value(`return Array.from([new Date(5)])[0].getTime()`)).toBe(5)
- })
- test("Array.from and Array.of preserve nested object identity", async () => {
- expect(
- await value(`
- const child = { selected: false }
- Array.from([child])[0].selected = true
- return child.selected
- `),
- ).toBe(true)
- expect(
- await value(`
- const child = { selected: false }
- Array.of(child)[0].selected = true
- return child.selected
- `),
- ).toBe(true)
- })
- test("Array.from and Array.of preserve promises and callable references", async () => {
- expect(
- await value(`
- const pending = Promise.resolve(1)
- return [await Array.from([pending])[0], await Array.of(pending)[0]]
- `),
- ).toEqual([1, 1])
- expect(await value(`return [Array.from([Math.max])[0](1, 2), Array.of(Math.max)[0](3, 4)]`)).toEqual([2, 4])
- })
- test("Array.from preserves identity across supported collection shapes", async () => {
- expect(
- await value(`
- const child = { selected: false }
- const fromArrayLike = Array.from({ 0: child, length: 1 })
- const fromMap = Array.from(new Map([["child", child]]))
- const fromSet = Array.from(new Set([child]))
- fromArrayLike[0].selected = true
- return [fromMap[0][1] === child, fromSet[0] === child, child.selected]
- `),
- ).toEqual([true, true, true])
- })
- test("Array.from rejects invalid receivers and gives promises an await hint", async () => {
- const diagnostic = await error(`return Array.from(Promise.resolve([1]))`)
- expect(diagnostic.kind).toBe("InvalidDataValue")
- expect(diagnostic.message).toContain("await")
- expect((await error(`return Array.from(() => 1)`)).kind).toBe("InvalidDataValue")
- })
- test("regexes stay callable through Object.values", async () => {
- expect(await value(`return Object.values({ r: /ab+/ })[0].test("abb")`)).toBe(true)
- })
- test("Object.* helpers see CodeMode values as empty objects, never internals", async () => {
- expect(await value(`return Object.keys(new Map([["a", 1]]))`)).toEqual([])
- expect(await value(`return Object.values(new Date(0))`)).toEqual([])
- expect(await value(`return Object.entries(new Set([1]))`)).toEqual([])
- expect(await value(`return Object.assign({}, new Map([["a", 1]]))`)).toEqual({})
- expect(await value(`return Object.hasOwn(new Date(0), "time")`)).toBe(false)
- })
- 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 JSON.stringify({ d: new Date(0) })`)).toBe('{"d":"1970-01-01T00:00:00.000Z"}')
- const observed: Array<unknown> = []
- const capture = Tool.make({
- description: "Capture the exact input the host receives",
- input: { type: "object" },
- run: (input) =>
- Effect.sync(() => {
- observed.push(input)
- 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]]) })`,
- }),
- )
- expect(result.ok).toBe(true)
- expect(observed).toStrictEqual([{ when: "1970-01-01T00:00:00.000Z", tags: {} }])
- })
- })
|