| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046 |
- /*
- * Portions adapted from Test262 at revision 250f204f23a9249ff204be2baec29600faae7b75:
- * - test/built-ins/Date/value-to-primitive-result-non-string-prim.js
- * - test/built-ins/Date/value-to-primitive-result-string.js
- * - test/built-ins/Date/prototype/toUTCString/format.js
- * - test/built-ins/Date/prototype/toUTCString/invalid-date.js
- * - test/built-ins/RegExp/prototype/exec/S15.10.6.2_A4_T8.js
- *
- * CodeMode does not support Symbol.toPrimitive, so the Date-constructor cases exercise the same primitive-result
- * handling through supported own valueOf and toString functions.
- *
- * Copyright (C) 2016 the V8 project authors. All rights reserved.
- * Copyright (C) 2017 the V8 project authors. All rights reserved.
- * Copyright 2009 the Sputnik authors. All rights reserved.
- * Test262 portions are governed by the BSD license in LICENSE.test262.
- */
- 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("one-argument construction coerces supported values like JavaScript", async () => {
- expect(
- await value(`return [new Date(true).getTime(), new Date(false).getTime(), new Date(null).getTime()]`),
- ).toEqual([1, 0, 0])
- expect(await value(`return Number.isNaN(new Date(undefined).getTime())`)).toBe(true)
- expect(await value(`return Number.isNaN(new Date([]).getTime())`)).toBe(true)
- expect(await value(`return new Date(["1970-01-01T00:00:00.000Z"]).getTime()`)).toBe(0)
- expect(await value(`return Number.isNaN(new Date({}).getTime())`)).toBe(true)
- })
- test("one-argument construction uses valueOf then toString for objects", async () => {
- expect(
- await value(`
- const calls = []
- const number = { valueOf: () => 8 }
- const text = {
- valueOf: () => { calls.push("valueOf"); return {} },
- toString: () => { calls.push("toString"); return "2016-06-05T18:40:00.000Z" },
- }
- return [new Date(number).getTime(), new Date(text).getTime(), calls]
- `),
- ).toEqual([8, 1465152000000, ["valueOf", "toString"]])
- expect(
- await value(`
- const values = [
- { valueOf: () => undefined },
- { valueOf: () => true },
- { valueOf: () => false },
- { valueOf: () => null },
- ]
- return values.map((item) => new Date(item).getTime())
- `),
- ).toEqual([null, 1, 0, 0])
- expect(
- await value(`
- try {
- new Date({ valueOf: () => ({}), toString: () => ({}) })
- } catch (error) {
- return error.name
- }
- `),
- ).toBe("TypeError")
- })
- 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 throws RangeError", async () => {
- expect(await value(`try { new Date("garbage").toISOString() } catch (error) { return error.name }`)).toBe(
- "RangeError",
- )
- })
- 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)
- })
- test("toUTCString and toGMTString use the native UTC format", async () => {
- expect(
- await value(`
- const date = new Date(0)
- return [
- date.toUTCString(),
- date.toGMTString(),
- new Date(NaN).toUTCString(),
- new Date("0020-01-01T00:00:00Z").toUTCString(),
- ]
- `),
- ).toEqual([
- "Thu, 01 Jan 1970 00:00:00 GMT",
- "Thu, 01 Jan 1970 00:00:00 GMT",
- "Invalid Date",
- "Wed, 01 Jan 0020 00:00:00 GMT",
- ])
- })
- })
- 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("lastIndex is writable and exec coerces its stored value", async () => {
- expect(
- await value(`
- const pattern = /(?:ab|cd)\\d?/g
- pattern.lastIndex = "12"
- const stored = [pattern.lastIndex, typeof pattern.lastIndex]
- const match = pattern.exec("aacd2233ab12nm444ab42")
- pattern.lastIndex = 0
- return [stored, match[0], match.index, pattern.lastIndex, delete pattern.lastIndex]
- `),
- ).toEqual([["12", "string"], "ab4", 17, 0, false])
- })
- test("exec coerces CodeMode data objects assigned to lastIndex", async () => {
- expect(
- await value(`
- const pattern = /a/g
- pattern.lastIndex = {}
- const stored = pattern.lastIndex
- const match = pattern.exec("ba")
- pattern.lastIndex = 10
- const missed = pattern.exec("a")
- return [stored, match.index, pattern.lastIndex, missed]
- `),
- ).toEqual([{}, 1, 0, null])
- })
- test("non-global exec and test coerce and preserve lastIndex", async () => {
- expect(
- await value(`
- const execPattern = /a/
- const execIndex = {}
- execPattern.lastIndex = execIndex
- const match = execPattern.exec("ba")
- const testPattern = /a/
- const testIndex = {}
- testPattern.lastIndex = testIndex
- const matched = testPattern.test("ba")
- return [match.index, execPattern.lastIndex === execIndex, matched, testPattern.lastIndex === testIndex]
- `),
- ).toEqual([1, true, true, true])
- })
- 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("promise-returning string replacers are coerced synchronously", async () => {
- const decorate = Tool.make({
- description: "Decorate a string",
- input: Schema.String,
- output: Schema.String,
- execute: (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[object Promise]b[object Promise]")
- const missingAwait = await Effect.runPromise(
- CodeMode.execute({
- tools: { host: { decorate } },
- code: `return "a1".replace(/\\d/, (match) => tools.host.decorate(match))`,
- }),
- )
- expect(missingAwait.ok && missingAwait.value).toBe("a[object 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" },
- execute: (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: {} }])
- })
- })
|