stdlib.test.ts 37 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046
  1. /*
  2. * Portions adapted from Test262 at revision 250f204f23a9249ff204be2baec29600faae7b75:
  3. * - test/built-ins/Date/value-to-primitive-result-non-string-prim.js
  4. * - test/built-ins/Date/value-to-primitive-result-string.js
  5. * - test/built-ins/Date/prototype/toUTCString/format.js
  6. * - test/built-ins/Date/prototype/toUTCString/invalid-date.js
  7. * - test/built-ins/RegExp/prototype/exec/S15.10.6.2_A4_T8.js
  8. *
  9. * CodeMode does not support Symbol.toPrimitive, so the Date-constructor cases exercise the same primitive-result
  10. * handling through supported own valueOf and toString functions.
  11. *
  12. * Copyright (C) 2016 the V8 project authors. All rights reserved.
  13. * Copyright (C) 2017 the V8 project authors. All rights reserved.
  14. * Copyright 2009 the Sputnik authors. All rights reserved.
  15. * Test262 portions are governed by the BSD license in LICENSE.test262.
  16. */
  17. import { describe, expect, test } from "bun:test"
  18. import { Effect, Schema } from "effect"
  19. import { CodeMode, Tool } from "../src/index.js"
  20. // Standard-library value types: Date, RegExp, Map, Set. Programs use them as ordinary JS;
  21. // intra-CodeMode checkpoints (Object.* helpers, spread, coercion inputs) preserve the live
  22. // values, while at the host boundary (final result, tool arguments, JSON.stringify) they
  23. // serialize exactly as JSON.stringify would: Date -> ISO string (invalid -> null),
  24. // URL -> href, and RegExp/Map/Set/URLSearchParams -> {}.
  25. const run = (code: string) => Effect.runPromise(CodeMode.execute({ code, tools: {} }))
  26. const value = async (code: string) => {
  27. const result = await run(code)
  28. if (!result.ok) throw new Error(`expected success, got ${result.error.kind}: ${result.error.message}`)
  29. return result.value
  30. }
  31. const error = async (code: string) => {
  32. const result = await run(code)
  33. if (result.ok) throw new Error(`expected failure, got value ${JSON.stringify(result.value)}`)
  34. return result.error
  35. }
  36. describe("Number and Math", () => {
  37. test("Math.random returns a number in [0, 1)", async () => {
  38. expect(await value(`const n = Math.random(); return typeof n === "number" && n >= 0 && n < 1`)).toBe(true)
  39. })
  40. test("Number exposes native non-finite constants", async () => {
  41. expect(
  42. await value(
  43. `return [Number.isNaN(Number.NaN), Number.POSITIVE_INFINITY === Infinity, Number.NEGATIVE_INFINITY === -Infinity]`,
  44. ),
  45. ).toEqual([true, true, true])
  46. })
  47. test("Number valueOf returns its primitive receiver", async () => {
  48. expect(await value(`return (42).valueOf()`)).toBe(42)
  49. })
  50. test("Number valueOf does not enable boxed numbers", async () => {
  51. expect((await error(`return new Number(42)`)).kind).toBe("UnsupportedSyntax")
  52. })
  53. })
  54. describe("Date", () => {
  55. test("Date.now() returns a number", async () => {
  56. expect(await value(`return typeof Date.now()`)).toBe("number")
  57. })
  58. test("epoch construction and ISO rendering", async () => {
  59. expect(await value(`return new Date(0).toISOString()`)).toBe("1970-01-01T00:00:00.000Z")
  60. })
  61. test("string parsing round-trips", async () => {
  62. expect(await value(`return new Date("2024-01-02T03:04:05.000Z").getTime()`)).toBe(1704164645000)
  63. expect(await value(`return Date.parse("2024-01-02T03:04:05.000Z")`)).toBe(1704164645000)
  64. })
  65. test("one-argument construction coerces supported values like JavaScript", async () => {
  66. expect(
  67. await value(`return [new Date(true).getTime(), new Date(false).getTime(), new Date(null).getTime()]`),
  68. ).toEqual([1, 0, 0])
  69. expect(await value(`return Number.isNaN(new Date(undefined).getTime())`)).toBe(true)
  70. expect(await value(`return Number.isNaN(new Date([]).getTime())`)).toBe(true)
  71. expect(await value(`return new Date(["1970-01-01T00:00:00.000Z"]).getTime()`)).toBe(0)
  72. expect(await value(`return Number.isNaN(new Date({}).getTime())`)).toBe(true)
  73. })
  74. test("one-argument construction uses valueOf then toString for objects", async () => {
  75. expect(
  76. await value(`
  77. const calls = []
  78. const number = { valueOf: () => 8 }
  79. const text = {
  80. valueOf: () => { calls.push("valueOf"); return {} },
  81. toString: () => { calls.push("toString"); return "2016-06-05T18:40:00.000Z" },
  82. }
  83. return [new Date(number).getTime(), new Date(text).getTime(), calls]
  84. `),
  85. ).toEqual([8, 1465152000000, ["valueOf", "toString"]])
  86. expect(
  87. await value(`
  88. const values = [
  89. { valueOf: () => undefined },
  90. { valueOf: () => true },
  91. { valueOf: () => false },
  92. { valueOf: () => null },
  93. ]
  94. return values.map((item) => new Date(item).getTime())
  95. `),
  96. ).toEqual([null, 1, 0, 0])
  97. expect(
  98. await value(`
  99. try {
  100. new Date({ valueOf: () => ({}), toString: () => ({}) })
  101. } catch (error) {
  102. return error.name
  103. }
  104. `),
  105. ).toBe("TypeError")
  106. })
  107. test("date arithmetic and comparison use the time value", async () => {
  108. expect(await value(`const a = new Date(1000); const b = new Date(3000); return b - a`)).toBe(2000)
  109. expect(await value(`const a = new Date(1000); const b = new Date(3000); return a < b`)).toBe(true)
  110. expect(await value(`return +new Date(42)`)).toBe(42)
  111. })
  112. test("UTC getters read calendar components", async () => {
  113. expect(
  114. await value(
  115. `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()]`,
  116. ),
  117. ).toEqual([2024, 2, 5, 6, 7, 8, 9])
  118. })
  119. test("invalid dates yield NaN times, guardable in-CodeMode", async () => {
  120. expect(await value(`return Number.isNaN(new Date("garbage").getTime())`)).toBe(true)
  121. expect(await value(`return new Date("garbage").toJSON()`)).toBeNull()
  122. })
  123. test("toISOString on an invalid date throws RangeError", async () => {
  124. expect(await value(`try { new Date("garbage").toISOString() } catch (error) { return error.name }`)).toBe(
  125. "RangeError",
  126. )
  127. })
  128. test("template interpolation renders the ISO form", async () => {
  129. expect(await value("return `at ${new Date(0)}`")).toBe("at 1970-01-01T00:00:00.000Z")
  130. })
  131. test("dates serialize to ISO strings at the boundary, direct and nested", async () => {
  132. expect(await value(`return new Date(0)`)).toBe("1970-01-01T00:00:00.000Z")
  133. expect(await value(`return { when: new Date(0), tags: [new Date(1000)] }`)).toEqual({
  134. when: "1970-01-01T00:00:00.000Z",
  135. tags: ["1970-01-01T00:00:01.000Z"],
  136. })
  137. expect(await value(`return JSON.stringify({ d: new Date(0) })`)).toBe('{"d":"1970-01-01T00:00:00.000Z"}')
  138. })
  139. test("coercions: Number is the time, String is ISO, Boolean is true", async () => {
  140. expect(await value(`return Number(new Date(5))`)).toBe(5)
  141. expect(await value(`return String(new Date(0))`)).toBe("1970-01-01T00:00:00.000Z")
  142. expect(await value(`return Boolean(new Date(0))`)).toBe(true)
  143. })
  144. test("sorting dates with a numeric comparator", async () => {
  145. expect(
  146. await value(`
  147. const dates = [new Date(3000), new Date(1000), new Date(2000)]
  148. return dates.sort((a, b) => a - b).map((d) => d.getTime())
  149. `),
  150. ).toEqual([1000, 2000, 3000])
  151. })
  152. test("new Date(year, month, day) accepts component form", async () => {
  153. expect(await value(`const d = new Date(2024, 0, 2); return [d.getFullYear(), d.getMonth(), d.getDate()]`)).toEqual([
  154. 2024, 0, 2,
  155. ])
  156. })
  157. test("typeof and unknown properties are forgiving", async () => {
  158. expect(await value(`return typeof new Date(0)`)).toBe("object")
  159. expect(await value(`return new Date(0).nope === undefined`)).toBe(true)
  160. })
  161. test("toUTCString and toGMTString use the native UTC format", async () => {
  162. expect(
  163. await value(`
  164. const date = new Date(0)
  165. return [
  166. date.toUTCString(),
  167. date.toGMTString(),
  168. new Date(NaN).toUTCString(),
  169. new Date("0020-01-01T00:00:00Z").toUTCString(),
  170. ]
  171. `),
  172. ).toEqual([
  173. "Thu, 01 Jan 1970 00:00:00 GMT",
  174. "Thu, 01 Jan 1970 00:00:00 GMT",
  175. "Invalid Date",
  176. "Wed, 01 Jan 0020 00:00:00 GMT",
  177. ])
  178. })
  179. })
  180. describe("RegExp", () => {
  181. test("literal test", async () => {
  182. expect(await value(`return /ab+c/.test("xabbbc")`)).toBe(true)
  183. expect(await value(`return /ab+c/.test("nope")`)).toBe(false)
  184. })
  185. test("exec exposes captures and index", async () => {
  186. expect(await value(`const m = /a(b+)/.exec("xxabbc"); return { full: m[0], group: m[1], index: m.index }`)).toEqual(
  187. {
  188. full: "abb",
  189. group: "bb",
  190. index: 2,
  191. },
  192. )
  193. expect(await value(`return /a/.exec("zzz")`)).toBeNull()
  194. })
  195. test("named groups read through", async () => {
  196. expect(
  197. await value(`const m = /(?<word>[a-z]+)-(?<num>\\d+)/.exec("id ab-42"); return m.groups.word + m.groups.num`),
  198. ).toBe("ab42")
  199. })
  200. test("global exec advances lastIndex across calls", async () => {
  201. expect(
  202. await value(`
  203. const r = /\\d+/g
  204. const first = r.exec("a1b22c")
  205. const second = r.exec("a1b22c")
  206. return [first[0], second[0]]
  207. `),
  208. ).toEqual(["1", "22"])
  209. })
  210. test("lastIndex is writable and exec coerces its stored value", async () => {
  211. expect(
  212. await value(`
  213. const pattern = /(?:ab|cd)\\d?/g
  214. pattern.lastIndex = "12"
  215. const stored = [pattern.lastIndex, typeof pattern.lastIndex]
  216. const match = pattern.exec("aacd2233ab12nm444ab42")
  217. pattern.lastIndex = 0
  218. return [stored, match[0], match.index, pattern.lastIndex, delete pattern.lastIndex]
  219. `),
  220. ).toEqual([["12", "string"], "ab4", 17, 0, false])
  221. })
  222. test("exec coerces CodeMode data objects assigned to lastIndex", async () => {
  223. expect(
  224. await value(`
  225. const pattern = /a/g
  226. pattern.lastIndex = {}
  227. const stored = pattern.lastIndex
  228. const match = pattern.exec("ba")
  229. pattern.lastIndex = 10
  230. const missed = pattern.exec("a")
  231. return [stored, match.index, pattern.lastIndex, missed]
  232. `),
  233. ).toEqual([{}, 1, 0, null])
  234. })
  235. test("non-global exec and test coerce and preserve lastIndex", async () => {
  236. expect(
  237. await value(`
  238. const execPattern = /a/
  239. const execIndex = {}
  240. execPattern.lastIndex = execIndex
  241. const match = execPattern.exec("ba")
  242. const testPattern = /a/
  243. const testIndex = {}
  244. testPattern.lastIndex = testIndex
  245. const matched = testPattern.test("ba")
  246. return [match.index, execPattern.lastIndex === execIndex, matched, testPattern.lastIndex === testIndex]
  247. `),
  248. ).toEqual([1, true, true, true])
  249. })
  250. test("an unmatched string pattern returns null", async () => {
  251. expect(await value(`return "abc".match(/\\d/)`)).toBeNull()
  252. })
  253. test("matchAll materializes match arrays with captures", async () => {
  254. expect(await value(`return "a1b22".matchAll(/(\\d+)/g).map((m) => m[1])`)).toEqual(["1", "22"])
  255. })
  256. test("function replacers receive captures, offsets, input, and named groups", async () => {
  257. expect(
  258. await value(`
  259. const seen = []
  260. const output = "a1b22".replace(/(\\d)(\\d)?/g, (match, first, second, offset, input) => {
  261. seen.push([match, first, second === undefined, offset, input])
  262. return Number(match) * 2
  263. })
  264. return { output, seen }
  265. `),
  266. ).toEqual({
  267. output: "a2b44",
  268. seen: [
  269. ["1", "1", true, 1, "a1b22"],
  270. ["22", "2", false, 3, "a1b22"],
  271. ],
  272. })
  273. expect(
  274. await value(`
  275. return "red-blue".replace(
  276. /(?<left>[a-z]+)-(?<right>[a-z]+)/,
  277. (match, left, right, offset, input, groups) => groups.right + ":" + groups.left,
  278. )
  279. `),
  280. ).toBe("blue:red")
  281. })
  282. test("function replacers support string searches, zero-length matches, and result coercion", async () => {
  283. expect(await value(`return "banana".replace("na", (match, offset, input) => "[" + offset + "]")`)).toBe("ba[2]na")
  284. expect(await value(`return "ab".replaceAll("", (match, offset) => offset)`)).toBe("0a1b2")
  285. expect(await value(`return "😀".replaceAll(/(?:)/gu, (match, offset) => "[" + offset + "]")`)).toBe("[0]😀[2]")
  286. expect(
  287. await value(`return "123".replace(/\\d/g, (match) => match === "1" ? 7 : match === "2" ? null : { n: 3 })`),
  288. ).toBe("7null[object Object]")
  289. })
  290. test("promise-returning string replacers are coerced synchronously", async () => {
  291. const decorate = Tool.make({
  292. description: "Decorate a string",
  293. input: Schema.String,
  294. output: Schema.String,
  295. execute: (input) => Effect.succeed(`[${input}]`),
  296. })
  297. const result = await Effect.runPromise(
  298. CodeMode.execute({
  299. tools: { host: { decorate } },
  300. code: `return "a1b22".replace(/\\d+/g, async (match) => await tools.host.decorate(match))`,
  301. }),
  302. )
  303. expect(result.ok && result.value).toBe("a[object Promise]b[object Promise]")
  304. const missingAwait = await Effect.runPromise(
  305. CodeMode.execute({
  306. tools: { host: { decorate } },
  307. code: `return "a1".replace(/\\d/, (match) => tools.host.decorate(match))`,
  308. }),
  309. )
  310. expect(missingAwait.ok && missingAwait.value).toBe("a[object Promise]")
  311. })
  312. test("replaceAll without the g flag is a catchable error", async () => {
  313. expect(await value(`try { "a".replaceAll(/a/, "b"); return "no" } catch { return "caught" }`)).toBe("caught")
  314. })
  315. test("new RegExp constructs from strings; invalid patterns are catchable", async () => {
  316. expect(await value(`return new RegExp("a+", "i").test("AAA")`)).toBe(true)
  317. expect(await value(`try { new RegExp("("); return "no" } catch { return "caught" }`)).toBe("caught")
  318. expect(await value(`return [/a/ instanceof RegExp, /a/.source]`)).toEqual([true, "a"])
  319. })
  320. test("invalid patterns fail with actionable messages", async () => {
  321. const fromString = await error(`return "abc".match("(")`)
  322. expect(fromString.message).toContain('String.match received the string "("')
  323. expect(fromString.message).toContain("escape them with a backslash")
  324. const fromConstructor = await error(`return new RegExp("(")`)
  325. expect(fromConstructor.message).toContain('new RegExp(...) received "("')
  326. expect(fromConstructor.message).toContain("escape them with a backslash")
  327. const fromFlags = await error(`return new RegExp("a", "xz")`)
  328. expect(fromFlags.message).toContain('invalid flags "xz"')
  329. expect(fromFlags.message).toContain("Valid flags are")
  330. })
  331. test("missing g-flag errors say how to fix the call", async () => {
  332. expect((await error(`return "aa".replaceAll(/a/, "b")`)).message).toContain("write /a/g, or use String.replace")
  333. expect((await error(`return "aa".matchAll(/a/)`)).message).toContain("write /a/g, or use String.match")
  334. })
  335. test("a non-pattern argument names the expected shapes", async () => {
  336. const err = await error(`return "abc".match(42)`)
  337. expect(err.message).toContain("expects a regular expression")
  338. expect(err.message).toContain("not number")
  339. })
  340. test("source and flags properties read through", async () => {
  341. expect(await value(`const r = /ab/gi; return { source: r.source, flags: r.flags, global: r.global }`)).toEqual({
  342. source: "ab",
  343. flags: "gi",
  344. global: true,
  345. })
  346. })
  347. test("regexes serialize to {} at the boundary, like JSON", async () => {
  348. expect(await value(`return /a/`)).toEqual({})
  349. expect(await value(`return JSON.stringify({ r: /a/g })`)).toBe('{"r":{}}')
  350. })
  351. test("template interpolation renders the literal form", async () => {
  352. expect(await value("return `${/ab/g}`")).toBe("/ab/g")
  353. })
  354. })
  355. describe("URL and URI helpers", () => {
  356. test("encodes and decodes complete URIs and URI components", async () => {
  357. expect(
  358. await value(`
  359. return [
  360. encodeURI("https://example.test/a b?q=a/b"),
  361. encodeURIComponent("a b/c?"),
  362. decodeURI("https://example.test/a%20b?q=a/b"),
  363. decodeURIComponent("a%20b%2Fc%3F"),
  364. ["a b", "c/d"].map(encodeURIComponent),
  365. ]
  366. `),
  367. ).toEqual([
  368. "https://example.test/a%20b?q=a/b",
  369. "a%20b%2Fc%3F",
  370. "https://example.test/a b?q=a/b",
  371. "a b/c?",
  372. ["a%20b", "c%2Fd"],
  373. ])
  374. expect(
  375. await value(`try { decodeURIComponent("%zz"); return false } catch (error) { return error instanceof URIError }`),
  376. ).toBe(true)
  377. })
  378. test("resolves and mutates URLs with linked search parameters", async () => {
  379. expect(
  380. await value(`
  381. const url = new URL("../users?id=old#top", "https://user:pass@example.com:8443/api/v1/")
  382. url.pathname = "/items/a b"
  383. url.searchParams.set("id", "a b")
  384. url.searchParams.append("tag", "x/y")
  385. url.hash = "part 1"
  386. return {
  387. href: url.href,
  388. origin: url.origin,
  389. host: url.host,
  390. pathname: url.pathname,
  391. search: url.search,
  392. id: url.searchParams.get("id"),
  393. string: String(url),
  394. json: url.toJSON(),
  395. instances: [
  396. url instanceof URL,
  397. url.searchParams instanceof URLSearchParams,
  398. url.searchParams === url.searchParams,
  399. ],
  400. }
  401. `),
  402. ).toEqual({
  403. href: "https://user:pass@example.com:8443/items/a%20b?id=a+b&tag=x%2Fy#part%201",
  404. origin: "https://example.com:8443",
  405. host: "example.com:8443",
  406. pathname: "/items/a%20b",
  407. search: "?id=a+b&tag=x%2Fy",
  408. id: "a b",
  409. string: "https://user:pass@example.com:8443/items/a%20b?id=a+b&tag=x%2Fy#part%201",
  410. json: "https://user:pass@example.com:8443/items/a%20b?id=a+b&tag=x%2Fy#part%201",
  411. instances: [true, true, true],
  412. })
  413. })
  414. test("URLSearchParams supports records, pairs, mutation, callbacks, and materialization", async () => {
  415. expect(
  416. await value(`
  417. const params = new URLSearchParams([["tag", "b"], ["tag", "a"], ["q", "a b"]])
  418. const seen = []
  419. params.forEach((value, key) => seen.push(key + "=" + value))
  420. params.delete("tag", "b")
  421. params.append("tag", "c")
  422. params.sort()
  423. return {
  424. text: params.toString(),
  425. size: params.size,
  426. tags: params.getAll("tag"),
  427. has: params.has("tag", "c"),
  428. entries: Array.from(params),
  429. object: Object.fromEntries(params),
  430. record: new URLSearchParams({ page: 2, filter: "open" }).toString(),
  431. seen,
  432. }
  433. `),
  434. ).toEqual({
  435. text: "q=a+b&tag=a&tag=c",
  436. size: 3,
  437. tags: ["a", "c"],
  438. has: true,
  439. entries: [
  440. ["q", "a b"],
  441. ["tag", "a"],
  442. ["tag", "c"],
  443. ],
  444. object: { q: "a b", tag: "c" },
  445. record: "page=2&filter=open",
  446. seen: ["tag=b", "tag=a", "q=a b"],
  447. })
  448. })
  449. test("URL parsing failures are catchable and values use native JSON forms", async () => {
  450. expect(
  451. await value(`
  452. const parsed = URL.parse("/users", "https://example.test/api/")
  453. let invalidIsTypeError = false
  454. try { new URL("not relative without a base") } catch (error) { invalidIsTypeError = error instanceof TypeError }
  455. return {
  456. canParse: URL.canParse("/users", "https://example.test/api/"),
  457. cannotParse: URL.canParse("not relative without a base"),
  458. parsed: parsed.href,
  459. invalidIsTypeError,
  460. boundary: [new URL("https://example.test/a"), new URLSearchParams("q=one")],
  461. json: JSON.stringify({ url: new URL("https://example.test/a"), params: new URLSearchParams("q=one") }),
  462. }
  463. `),
  464. ).toEqual({
  465. canParse: true,
  466. cannotParse: false,
  467. parsed: "https://example.test/users",
  468. invalidIsTypeError: true,
  469. boundary: ["https://example.test/a", {}],
  470. json: '{"url":"https://example.test/a","params":{}}',
  471. })
  472. })
  473. test("distinguishes omitted URL arguments from explicit undefined", async () => {
  474. expect(
  475. await value(`
  476. function throwsTypeError(run) {
  477. try { run(); return false } catch (error) { return error instanceof TypeError }
  478. }
  479. const params = new URLSearchParams()
  480. const required = [
  481. () => params.append(),
  482. () => params.delete(),
  483. () => params.get(),
  484. () => params.getAll(),
  485. () => params.has(),
  486. () => params.set(),
  487. () => params.forEach(),
  488. ].map(throwsTypeError)
  489. params.append(undefined, undefined)
  490. return {
  491. construct: throwsTypeError(() => new URL()),
  492. canParse: throwsTypeError(() => URL.canParse()),
  493. parse: throwsTypeError(() => URL.parse()),
  494. explicitUndefined: new URL(undefined, "https://example.test/base/").href,
  495. params: params.toString(),
  496. required,
  497. }
  498. `),
  499. ).toEqual({
  500. construct: true,
  501. canParse: true,
  502. parse: true,
  503. explicitUndefined: "https://example.test/base/undefined",
  504. params: "undefined=undefined",
  505. required: [true, true, true, true, true, true, true],
  506. })
  507. })
  508. })
  509. describe("Map", () => {
  510. test("get/set/has/size with chaining", async () => {
  511. expect(
  512. await value(`
  513. const m = new Map()
  514. m.set("a", 1).set("b", 2)
  515. return { a: m.get("a"), b: m.get("b"), has: m.has("a"), miss: m.get("zz") === undefined, size: m.size }
  516. `),
  517. ).toEqual({ a: 1, b: 2, has: true, miss: true, size: 5 - 3 })
  518. })
  519. test("object keys use identity", async () => {
  520. expect(
  521. await value(`
  522. const key = { id: 1 }
  523. const m = new Map()
  524. m.set(key, "hit")
  525. return [m.get(key), m.get({ id: 1 }) === undefined]
  526. `),
  527. ).toEqual(["hit", true])
  528. })
  529. test("construction from entry pairs and another Map", async () => {
  530. expect(await value(`const m = new Map([["a", 1], ["b", 2]]); return m.get("b")`)).toBe(2)
  531. expect(
  532. await value(
  533. `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")]`,
  534. ),
  535. ).toEqual([1, 2, false])
  536. expect((await error(`return new Map("nope")`)).message).toMatch(/\[key, value\] pairs/)
  537. expect((await error(`return new Map(["flat"])`)).message).toMatch(/\[key, value\] pairs/)
  538. })
  539. test("keys/values/entries return arrays", async () => {
  540. expect(
  541. await value(`
  542. const m = new Map([["a", 1], ["b", 2]])
  543. return { keys: m.keys(), values: m.values(), entries: m.entries() }
  544. `),
  545. ).toEqual({
  546. keys: ["a", "b"],
  547. values: [1, 2],
  548. entries: [
  549. ["a", 1],
  550. ["b", 2],
  551. ],
  552. })
  553. })
  554. test("Object.fromEntries(map) and Array.from(map)", async () => {
  555. expect(await value(`return Object.fromEntries(new Map([["a", 1], ["b", 2]]))`)).toEqual({ a: 1, b: 2 })
  556. expect(await value(`return Array.from(new Map([["a", 1]]))`)).toEqual([["a", 1]])
  557. })
  558. test("for...of iterates [key, value] pairs with destructuring", async () => {
  559. expect(
  560. await value(`
  561. const m = new Map([["a", 1], ["b", 2]])
  562. let total = 0
  563. let names = ""
  564. for (const [key, count] of m) { names += key; total += count }
  565. return names + total
  566. `),
  567. ).toBe("ab3")
  568. })
  569. test("spread produces entry pairs", async () => {
  570. expect(await value(`return [...new Map([["a", 1]])]`)).toEqual([["a", 1]])
  571. })
  572. test("forEach passes (value, key)", async () => {
  573. expect(
  574. await value(`
  575. const m = new Map([["a", 1], ["b", 2]])
  576. const seen = []
  577. m.forEach((count, key) => seen.push(key + count))
  578. return seen
  579. `),
  580. ).toEqual(["a1", "b2"])
  581. })
  582. test("delete and clear", async () => {
  583. expect(
  584. await value(`
  585. const m = new Map([["a", 1], ["b", 2]])
  586. const removed = m.delete("a")
  587. const missed = m.delete("zz")
  588. const sizeAfterDelete = m.size
  589. m.clear()
  590. return [removed, missed, sizeAfterDelete, m.size]
  591. `),
  592. ).toEqual([true, false, 1, 0])
  593. })
  594. test("counting idiom: grouped tallies", async () => {
  595. expect(
  596. await value(`
  597. const words = ["a", "b", "a", "c", "a"]
  598. const counts = new Map()
  599. for (const word of words) counts.set(word, (counts.get(word) ?? 0) + 1)
  600. return Object.fromEntries(counts)
  601. `),
  602. ).toEqual({ a: 3, b: 1, c: 1 })
  603. })
  604. test("maps serialize to {} at the boundary, like JSON", async () => {
  605. expect(await value(`return new Map([["a", 1]])`)).toEqual({})
  606. expect(await value(`return JSON.stringify(new Map([["a", 1]]))`)).toBe("{}")
  607. })
  608. test("console.log renders map contents for debugging", async () => {
  609. const result = await run(`console.log(new Map([["a", 1]])); return null`)
  610. expect(result.ok).toBe(true)
  611. expect(result.logs?.[0]).toBe(`Map(1) [["a",1]]`)
  612. })
  613. })
  614. describe("Set", () => {
  615. test("add/has/delete/size with chaining", async () => {
  616. expect(
  617. await value(`
  618. const s = new Set()
  619. s.add(1).add(2).add(1)
  620. const removed = s.delete(2)
  621. return [s.size, s.has(1), s.has(2), removed]
  622. `),
  623. ).toEqual([1, true, false, true])
  624. })
  625. test("dedupe idiom: [...new Set(items)]", async () => {
  626. expect(await value(`return [...new Set([1, 2, 2, 3, 1])]`)).toEqual([1, 2, 3])
  627. })
  628. test("construction from strings and other Sets", async () => {
  629. expect(await value(`return [...new Set("aba")]`)).toEqual(["a", "b"])
  630. expect(await value(`return Array.from(new Set(new Set([1, 2])))`)).toEqual([1, 2])
  631. })
  632. test("SameValueZero: NaN is findable", async () => {
  633. expect(await value(`const s = new Set([NaN]); return s.has(NaN)`)).toBe(true)
  634. })
  635. test("for...of iterates values", async () => {
  636. expect(
  637. await value(`
  638. let total = 0
  639. for (const n of new Set([1, 2, 3])) total += n
  640. return total
  641. `),
  642. ).toBe(6)
  643. })
  644. test("sets serialize to {} at the boundary, like JSON", async () => {
  645. expect(await value(`return { s: new Set([1]) }`)).toEqual({ s: {} })
  646. })
  647. })
  648. describe("stdlib integration", () => {
  649. test("Object.is uses SameValue semantics", async () => {
  650. expect(
  651. await value(`
  652. const object = {}
  653. return [
  654. Object.is(NaN, NaN),
  655. Object.is(0, -0),
  656. Object.is(object, object),
  657. Object.is({}, {}),
  658. ]
  659. `),
  660. ).toEqual([true, false, true, false])
  661. })
  662. test("Object.is rejects opaque runtime references", async () => {
  663. expect((await error(`return Object.is(Math.max, Math.max)`)).kind).toBe("InvalidDataValue")
  664. })
  665. test("Object values and entries accept arrays", async () => {
  666. expect(await value(`return [Object.values(["a", "b"]), Object.entries(["a", "b"])]`)).toEqual([
  667. ["a", "b"],
  668. [
  669. ["0", "a"],
  670. ["1", "b"],
  671. ],
  672. ])
  673. expect(await value(`const match = /a/.exec("ba"); return [Object.values(match), Object.entries(match)]`)).toEqual([
  674. ["a", 1],
  675. [
  676. ["0", "a"],
  677. ["index", 1],
  678. ],
  679. ])
  680. expect(await value(`return Object.keys(Object.values({ match: /a/.exec("ba") })[0])`)).toEqual(["0", "index"])
  681. })
  682. test("Object.fromEntries accepts every supported entry collection", async () => {
  683. expect(
  684. await value(`
  685. return [
  686. Object.fromEntries([["a", 1]]),
  687. Object.fromEntries(new Map([["b", 2]])),
  688. Object.fromEntries(new Set([["c", 3]])),
  689. Object.fromEntries(new URLSearchParams("d=4")),
  690. Object.fromEntries([{ 0: "e", 1: 5 }]),
  691. Object.fromEntries(new Set([[{}, 6], [new Date(0), 7], [null, 8], [undefined, 9]])),
  692. ]
  693. `),
  694. ).toEqual([
  695. { a: 1 },
  696. { b: 2 },
  697. { c: 3 },
  698. { d: "4" },
  699. { e: 5 },
  700. { "[object Object]": 6, "1970-01-01T00:00:00.000Z": 7, null: 8, undefined: 9 },
  701. ])
  702. expect(await value(`try { Object.fromEntries(new Set([Math.max])); return false } catch { return true }`)).toBe(
  703. true,
  704. )
  705. expect(
  706. await value(`try { Object.fromEntries(new Map([["fn", Math.max]])); return false } catch { return true }`),
  707. ).toBe(true)
  708. })
  709. test("deterministic Math methods match the host runtime", async () => {
  710. const result = await value(`
  711. return [
  712. 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),
  713. Math.cos(0.5), Math.cosh(0.5), Math.sin(0.5), Math.sinh(0.5), Math.tan(0.5), Math.tanh(0.5),
  714. Math.log1p(0.5), Math.expm1(0.5), Math.f16round(1.337), Math.fround(1.337), Math.clz32(1), Math.imul(2, 3),
  715. ]
  716. `)
  717. expect(result).toEqual([
  718. Math.acos(0.5),
  719. Math.acosh(2),
  720. Math.asin(0.5),
  721. Math.asinh(2),
  722. Math.atan(1),
  723. Math.atan2(1, 2),
  724. Math.atanh(0.5),
  725. Math.cos(0.5),
  726. Math.cosh(0.5),
  727. Math.sin(0.5),
  728. Math.sinh(0.5),
  729. Math.tan(0.5),
  730. Math.tanh(0.5),
  731. Math.log1p(0.5),
  732. Math.expm1(0.5),
  733. Math.f16round(1.337),
  734. Math.fround(1.337),
  735. Math.clz32(1),
  736. Math.imul(2, 3),
  737. ])
  738. })
  739. test("Object.assign mutates and returns its target", async () => {
  740. expect(
  741. await value(`
  742. const target = { a: 1 }
  743. const result = Object.assign(target, { b: 2 })
  744. return { target, result, same: target === result }
  745. `),
  746. ).toEqual({ target: { a: 1, b: 2 }, result: { a: 1, b: 2 }, same: true })
  747. expect(await value(`try { Object.assign(null, { a: 1 }); return false } catch { return true }`)).toBe(true)
  748. })
  749. test("assignment resolves and reads its left side before evaluating the right side", async () => {
  750. expect(await value(`let x = 1; x += (x = 5); return x`)).toBe(6)
  751. expect(await value(`let i = 0; const values = [9]; values[i++] = i; return [values, i]`)).toEqual([[1], 1])
  752. expect(await value(`let i = 0; const values = [10, 20]; values[i++] += i; return [values, i]`)).toEqual([
  753. [11, 20],
  754. 1,
  755. ])
  756. })
  757. test("typeof reports constructors as functions and never throws", async () => {
  758. expect(await value(`return typeof Map`)).toBe("function")
  759. expect(await value(`return typeof ((x) => x)`)).toBe("function")
  760. expect(await value(`return typeof Math`)).toBe("object")
  761. expect(await value(`return typeof tools`)).toBe("object")
  762. })
  763. test("negation works on any value", async () => {
  764. expect(await value(`return !new Map()`)).toBe(false)
  765. expect(await value(`const fn = () => 1; return !fn`)).toBe(false)
  766. })
  767. test("object spread of CodeMode values is a no-op, like JS", async () => {
  768. expect(await value(`return { ...new Map([["a", 1]]), kept: true }`)).toEqual({ kept: true })
  769. })
  770. test("dates inside Map values survive in-CodeMode reads", async () => {
  771. expect(
  772. await value(`
  773. const m = new Map([["start", new Date(1000)]])
  774. return m.get("start").getTime()
  775. `),
  776. ).toBe(1000)
  777. })
  778. test("instanceof recognizes the stdlib value types", async () => {
  779. expect(
  780. await value(
  781. `return [new Date(0) instanceof Date, /a/ instanceof RegExp, new Map() instanceof Map, new Set() instanceof Set]`,
  782. ),
  783. ).toEqual([true, true, true, true])
  784. expect(
  785. await value(`return [[1] instanceof Array, [1] instanceof Object, ({}) instanceof Object, 5 instanceof Object]`),
  786. ).toEqual([true, true, true, false])
  787. expect(await value(`return [new Map() instanceof Set, "s" instanceof Date]`)).toEqual([false, false])
  788. expect(
  789. await value(`const p = Promise.resolve(1); const isPromise = p instanceof Promise; await p; return isPromise`),
  790. ).toBe(true)
  791. })
  792. test("realistic pipeline: parse, extract with regex, dedupe, count by day", async () => {
  793. expect(
  794. await value(`
  795. 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"}]'
  796. const rows = JSON.parse(raw)
  797. const tags = new Set()
  798. const byDay = new Map()
  799. for (const row of rows) {
  800. for (const m of row.tag.matchAll(/[a-z]+/g)) tags.add(m[0])
  801. const day = new Date(row.at).toISOString().slice(0, 10)
  802. byDay.set(day, (byDay.get(day) ?? 0) + 1)
  803. }
  804. return { tags: [...tags].sort((a, b) => (a < b ? -1 : 1)), byDay: Object.fromEntries(byDay) }
  805. `),
  806. ).toEqual({ tags: ["a", "b", "c"], byDay: { "2024-01-01": 2, "2024-01-02": 1 } })
  807. })
  808. })
  809. describe("CodeMode values at intra-CodeMode checkpoints", () => {
  810. test("Object.values/entries keep Dates usable", async () => {
  811. expect(await value(`return Object.values({ d: new Date(0) })[0].getTime()`)).toBe(0)
  812. expect(await value(`const [key, d] = Object.entries({ d: new Date(0) })[0]; return key + ":" + d.getTime()`)).toBe(
  813. "d:0",
  814. )
  815. })
  816. test("Object.values/entries preserve nested object identity", async () => {
  817. expect(
  818. await value(`
  819. const child = { selected: false }
  820. const rows = { a: child }
  821. Object.values(rows)[0].selected = true
  822. return child.selected
  823. `),
  824. ).toBe(true)
  825. expect(
  826. await value(`
  827. const child = { selected: false }
  828. const rows = { a: child }
  829. Object.entries(rows)[0][1].selected = true
  830. return child.selected
  831. `),
  832. ).toBe(true)
  833. })
  834. test("Object enumeration preserves promises and callable references", async () => {
  835. expect(
  836. await value(`
  837. const pending = Promise.resolve(1)
  838. const source = { pending }
  839. return [Object.keys(source), Object.hasOwn(source, "pending"), await Object.values(source)[0], await Object.entries(source)[0][1]]
  840. `),
  841. ).toEqual([["pending"], true, 1, 1])
  842. expect(await value(`return Object.values({ max: Math.max })[0](1, 2)`)).toBe(2)
  843. })
  844. test("Object enumeration rejects invalid receivers and gives promises an await hint", async () => {
  845. const diagnostic = await error(`return Object.keys(Promise.resolve({ a: 1 }))`)
  846. expect(diagnostic.kind).toBe("InvalidDataValue")
  847. expect(diagnostic.message).toContain("await")
  848. expect((await error(`return Object.keys(Math)`)).kind).toBe("InvalidDataValue")
  849. })
  850. test("Object.assign keeps Maps usable", async () => {
  851. expect(await value(`const merged = Object.assign({}, { m: new Map([["a", 1]]) }); return merged.m.get("a")`)).toBe(
  852. 1,
  853. )
  854. })
  855. test("object and array spread keep CodeMode values usable", async () => {
  856. expect(
  857. await value(`
  858. const src = { m: new Map([["a", 1]]) }
  859. const copy = { ...src }
  860. copy.m.set("b", 2)
  861. return [copy.m.get("a"), src.m.get("b")]
  862. `),
  863. ).toEqual([1, 2])
  864. expect(await value(`const list = [new Date(1000)]; const copy = [...list]; return copy[0].getTime()`)).toBe(1000)
  865. })
  866. test("Array.from over arrays keeps nested CodeMode values usable", async () => {
  867. expect(await value(`return Array.from([new Date(5)])[0].getTime()`)).toBe(5)
  868. })
  869. test("Array.from and Array.of preserve nested object identity", async () => {
  870. expect(
  871. await value(`
  872. const child = { selected: false }
  873. Array.from([child])[0].selected = true
  874. return child.selected
  875. `),
  876. ).toBe(true)
  877. expect(
  878. await value(`
  879. const child = { selected: false }
  880. Array.of(child)[0].selected = true
  881. return child.selected
  882. `),
  883. ).toBe(true)
  884. })
  885. test("Array.from and Array.of preserve promises and callable references", async () => {
  886. expect(
  887. await value(`
  888. const pending = Promise.resolve(1)
  889. return [await Array.from([pending])[0], await Array.of(pending)[0]]
  890. `),
  891. ).toEqual([1, 1])
  892. expect(await value(`return [Array.from([Math.max])[0](1, 2), Array.of(Math.max)[0](3, 4)]`)).toEqual([2, 4])
  893. })
  894. test("Array.from preserves identity across supported collection shapes", async () => {
  895. expect(
  896. await value(`
  897. const child = { selected: false }
  898. const fromArrayLike = Array.from({ 0: child, length: 1 })
  899. const fromMap = Array.from(new Map([["child", child]]))
  900. const fromSet = Array.from(new Set([child]))
  901. fromArrayLike[0].selected = true
  902. return [fromMap[0][1] === child, fromSet[0] === child, child.selected]
  903. `),
  904. ).toEqual([true, true, true])
  905. })
  906. test("Array.from rejects invalid receivers and gives promises an await hint", async () => {
  907. const diagnostic = await error(`return Array.from(Promise.resolve([1]))`)
  908. expect(diagnostic.kind).toBe("InvalidDataValue")
  909. expect(diagnostic.message).toContain("await")
  910. expect((await error(`return Array.from(() => 1)`)).kind).toBe("InvalidDataValue")
  911. })
  912. test("regexes stay callable through Object.values", async () => {
  913. expect(await value(`return Object.values({ r: /ab+/ })[0].test("abb")`)).toBe(true)
  914. })
  915. test("Object.* helpers see CodeMode values as empty objects, never internals", async () => {
  916. expect(await value(`return Object.keys(new Map([["a", 1]]))`)).toEqual([])
  917. expect(await value(`return Object.values(new Date(0))`)).toEqual([])
  918. expect(await value(`return Object.entries(new Set([1]))`)).toEqual([])
  919. expect(await value(`return Object.assign({}, new Map([["a", 1]]))`)).toEqual({})
  920. expect(await value(`return Object.hasOwn(new Date(0), "time")`)).toBe(false)
  921. })
  922. test("the host boundary still serializes JSON forms: results, JSON.stringify, and tool arguments", async () => {
  923. expect(await value(`return { d: new Date(0), m: new Map([["a", 1]]) }`)).toEqual({
  924. d: "1970-01-01T00:00:00.000Z",
  925. m: {},
  926. })
  927. expect(await value(`return JSON.stringify({ d: new Date(0) })`)).toBe('{"d":"1970-01-01T00:00:00.000Z"}')
  928. const observed: Array<unknown> = []
  929. const capture = Tool.make({
  930. description: "Capture the exact input the host receives",
  931. input: { type: "object" },
  932. execute: (input) =>
  933. Effect.sync(() => {
  934. observed.push(input)
  935. return "ok"
  936. }),
  937. })
  938. const result = await Effect.runPromise(
  939. CodeMode.execute({
  940. tools: { host: { capture } },
  941. code: `return await tools.host.capture({ when: new Date(0), tags: new Map([["a", 1]]) })`,
  942. }),
  943. )
  944. expect(result.ok).toBe(true)
  945. expect(observed).toStrictEqual([{ when: "1970-01-01T00:00:00.000Z", tags: {} }])
  946. })
  947. })