stdlib.test.ts 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715
  1. import { describe, expect, test } from "bun:test"
  2. import { Effect, Schema } from "effect"
  3. import { CodeMode, Tool } from "../src/index.js"
  4. // Standard-library value types: Date, RegExp, Map, Set. Programs use them as ordinary JS;
  5. // intra-sandbox checkpoints (Object.* helpers, spread, coercion inputs) preserve the live
  6. // values, while at the host boundary (final result, tool arguments, JSON.stringify) they
  7. // serialize exactly as JSON.stringify would: Date -> ISO string (invalid -> null),
  8. // URL -> href, and RegExp/Map/Set/URLSearchParams -> {}.
  9. const run = (code: string) => Effect.runPromise(CodeMode.execute({ code, tools: {} }))
  10. const value = async (code: string) => {
  11. const result = await run(code)
  12. if (!result.ok) throw new Error(`expected success, got ${result.error.kind}: ${result.error.message}`)
  13. return result.value
  14. }
  15. const error = async (code: string) => {
  16. const result = await run(code)
  17. if (result.ok) throw new Error(`expected failure, got value ${JSON.stringify(result.value)}`)
  18. return result.error
  19. }
  20. describe("Date", () => {
  21. test("Date.now() returns a number", async () => {
  22. expect(await value(`return typeof Date.now()`)).toBe("number")
  23. })
  24. test("epoch construction and ISO rendering", async () => {
  25. expect(await value(`return new Date(0).toISOString()`)).toBe("1970-01-01T00:00:00.000Z")
  26. })
  27. test("string parsing round-trips", async () => {
  28. expect(await value(`return new Date("2024-01-02T03:04:05.000Z").getTime()`)).toBe(1704164645000)
  29. expect(await value(`return Date.parse("2024-01-02T03:04:05.000Z")`)).toBe(1704164645000)
  30. })
  31. test("date arithmetic and comparison use the time value", async () => {
  32. expect(await value(`const a = new Date(1000); const b = new Date(3000); return b - a`)).toBe(2000)
  33. expect(await value(`const a = new Date(1000); const b = new Date(3000); return a < b`)).toBe(true)
  34. expect(await value(`return +new Date(42)`)).toBe(42)
  35. })
  36. test("UTC getters read calendar components", async () => {
  37. expect(
  38. await value(
  39. `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()]`,
  40. ),
  41. ).toEqual([2024, 2, 5, 6, 7, 8, 9])
  42. })
  43. test("invalid dates yield NaN times, guardable in-sandbox", async () => {
  44. expect(await value(`return Number.isNaN(new Date("garbage").getTime())`)).toBe(true)
  45. expect(await value(`return new Date("garbage").toJSON()`)).toBeNull()
  46. })
  47. test("toISOString on an invalid date is a catchable error", async () => {
  48. expect(await value(`try { new Date("garbage").toISOString(); return "no" } catch { return "caught" }`)).toBe(
  49. "caught",
  50. )
  51. })
  52. test("template interpolation renders the ISO form", async () => {
  53. expect(await value("return `at ${new Date(0)}`")).toBe("at 1970-01-01T00:00:00.000Z")
  54. })
  55. test("dates serialize to ISO strings at the boundary, direct and nested", async () => {
  56. expect(await value(`return new Date(0)`)).toBe("1970-01-01T00:00:00.000Z")
  57. expect(await value(`return { when: new Date(0), tags: [new Date(1000)] }`)).toEqual({
  58. when: "1970-01-01T00:00:00.000Z",
  59. tags: ["1970-01-01T00:00:01.000Z"],
  60. })
  61. expect(await value(`return JSON.stringify({ d: new Date(0) })`)).toBe('{"d":"1970-01-01T00:00:00.000Z"}')
  62. })
  63. test("coercions: Number is the time, String is ISO, Boolean is true", async () => {
  64. expect(await value(`return Number(new Date(5))`)).toBe(5)
  65. expect(await value(`return String(new Date(0))`)).toBe("1970-01-01T00:00:00.000Z")
  66. expect(await value(`return Boolean(new Date(0))`)).toBe(true)
  67. })
  68. test("sorting dates with a numeric comparator", async () => {
  69. expect(
  70. await value(`
  71. const dates = [new Date(3000), new Date(1000), new Date(2000)]
  72. return dates.sort((a, b) => a - b).map((d) => d.getTime())
  73. `),
  74. ).toEqual([1000, 2000, 3000])
  75. })
  76. test("new Date(year, month, day) accepts component form", async () => {
  77. expect(await value(`const d = new Date(2024, 0, 2); return [d.getFullYear(), d.getMonth(), d.getDate()]`)).toEqual([
  78. 2024, 0, 2,
  79. ])
  80. })
  81. test("typeof and unknown properties are forgiving", async () => {
  82. expect(await value(`return typeof new Date(0)`)).toBe("object")
  83. expect(await value(`return new Date(0).nope === undefined`)).toBe(true)
  84. })
  85. })
  86. describe("RegExp", () => {
  87. test("literal test", async () => {
  88. expect(await value(`return /ab+c/.test("xabbbc")`)).toBe(true)
  89. expect(await value(`return /ab+c/.test("nope")`)).toBe(false)
  90. })
  91. test("exec exposes captures and index", async () => {
  92. expect(await value(`const m = /a(b+)/.exec("xxabbc"); return { full: m[0], group: m[1], index: m.index }`)).toEqual(
  93. {
  94. full: "abb",
  95. group: "bb",
  96. index: 2,
  97. },
  98. )
  99. expect(await value(`return /a/.exec("zzz")`)).toBeNull()
  100. })
  101. test("named groups read through", async () => {
  102. expect(
  103. await value(`const m = /(?<word>[a-z]+)-(?<num>\\d+)/.exec("id ab-42"); return m.groups.word + m.groups.num`),
  104. ).toBe("ab42")
  105. })
  106. test("global exec advances lastIndex across calls", async () => {
  107. expect(
  108. await value(`
  109. const r = /\\d+/g
  110. const first = r.exec("a1b22c")
  111. const second = r.exec("a1b22c")
  112. return [first[0], second[0]]
  113. `),
  114. ).toEqual(["1", "22"])
  115. })
  116. test("string match: non-global carries index, global lists all matches", async () => {
  117. expect(await value(`const m = "a1b22".match(/\\d+/); return [m[0], m.index]`)).toEqual(["1", 1])
  118. expect(await value(`return "a1b22".match(/\\d+/g)`)).toEqual(["1", "22"])
  119. expect(await value(`return "abc".match(/\\d/)`)).toBeNull()
  120. })
  121. test("matchAll materializes match arrays with captures", async () => {
  122. expect(await value(`return "a1b22".matchAll(/(\\d+)/g).map((m) => m[1])`)).toEqual(["1", "22"])
  123. })
  124. test("replace and replaceAll with patterns and $1 substitution", async () => {
  125. expect(await value(`return "a1b2".replace(/\\d/, "#")`)).toBe("a#b2")
  126. expect(await value(`return "a1b2".replace(/\\d/g, "#")`)).toBe("a#b#")
  127. expect(await value(`return "a1b2".replaceAll(/\\d/g, "#")`)).toBe("a#b#")
  128. expect(await value(`return "hi bob".replace(/b(o)b/, "[$1]")`)).toBe("hi [o]")
  129. })
  130. test("function replacers receive captures, offsets, input, and named groups", async () => {
  131. expect(
  132. await value(`
  133. const seen = []
  134. const output = "a1b22".replace(/(\\d)(\\d)?/g, (match, first, second, offset, input) => {
  135. seen.push([match, first, second === undefined, offset, input])
  136. return Number(match) * 2
  137. })
  138. return { output, seen }
  139. `),
  140. ).toEqual({
  141. output: "a2b44",
  142. seen: [
  143. ["1", "1", true, 1, "a1b22"],
  144. ["22", "2", false, 3, "a1b22"],
  145. ],
  146. })
  147. expect(
  148. await value(`
  149. return "red-blue".replace(
  150. /(?<left>[a-z]+)-(?<right>[a-z]+)/,
  151. (match, left, right, offset, input, groups) => groups.right + ":" + groups.left,
  152. )
  153. `),
  154. ).toBe("blue:red")
  155. })
  156. test("function replacers support string searches, zero-length matches, and result coercion", async () => {
  157. expect(await value(`return "banana".replace("na", (match, offset, input) => "[" + offset + "]")`)).toBe("ba[2]na")
  158. expect(await value(`return "ab".replaceAll("", (match, offset) => offset)`)).toBe("0a1b2")
  159. expect(await value(`return "😀".replaceAll(/(?:)/gu, (match, offset) => "[" + offset + "]")`)).toBe("[0]😀[2]")
  160. expect(
  161. await value(`return "123".replace(/\\d/g, (match) => match === "1" ? 7 : match === "2" ? null : { n: 3 })`),
  162. ).toBe("7null[object Object]")
  163. })
  164. test("function replacers can await effectful tool calls", async () => {
  165. const decorate = Tool.make({
  166. description: "Decorate a string",
  167. input: Schema.String,
  168. output: Schema.String,
  169. run: (input) => Effect.succeed(`[${input}]`),
  170. })
  171. const result = await Effect.runPromise(
  172. CodeMode.execute({
  173. tools: { host: { decorate } },
  174. code: `return "a1b22".replace(/\\d+/g, async (match) => await tools.host.decorate(match))`,
  175. }),
  176. )
  177. expect(result.ok && result.value).toBe("a[1]b[22]")
  178. const missingAwait = await Effect.runPromise(
  179. CodeMode.execute({
  180. tools: { host: { decorate } },
  181. code: `return "a1".replace(/\\d/, (match) => tools.host.decorate(match))`,
  182. }),
  183. )
  184. expect(!missingAwait.ok && missingAwait.error.kind).toBe("InvalidDataValue")
  185. expect(!missingAwait.ok && missingAwait.error.message).toContain("un-awaited Promise")
  186. })
  187. test("replaceAll without the g flag is a catchable error", async () => {
  188. expect(await value(`try { "a".replaceAll(/a/, "b"); return "no" } catch { return "caught" }`)).toBe("caught")
  189. })
  190. test("split and search accept patterns", async () => {
  191. expect(await value(`return "a1b22c".split(/\\d+/)`)).toEqual(["a", "b", "c"])
  192. expect(await value(`return "ab42".search(/\\d/)`)).toBe(2)
  193. expect(await value(`return "ab".search(/\\d/)`)).toBe(-1)
  194. })
  195. test("new RegExp constructs from strings; invalid patterns are catchable", async () => {
  196. expect(await value(`return new RegExp("a+", "i").test("AAA")`)).toBe(true)
  197. expect(await value(`try { new RegExp("("); return "no" } catch { return "caught" }`)).toBe("caught")
  198. expect(await value(`return [/a/ instanceof RegExp, /a/.source]`)).toEqual([true, "a"])
  199. })
  200. test("invalid patterns fail with actionable messages", async () => {
  201. const fromString = await error(`return "abc".match("(")`)
  202. expect(fromString.message).toContain('String.match received the string "("')
  203. expect(fromString.message).toContain("escape them with a backslash")
  204. const fromConstructor = await error(`return new RegExp("(")`)
  205. expect(fromConstructor.message).toContain('new RegExp(...) received "("')
  206. expect(fromConstructor.message).toContain("escape them with a backslash")
  207. const fromFlags = await error(`return new RegExp("a", "xz")`)
  208. expect(fromFlags.message).toContain('invalid flags "xz"')
  209. expect(fromFlags.message).toContain("Valid flags are")
  210. })
  211. test("missing g-flag errors say how to fix the call", async () => {
  212. expect((await error(`return "aa".replaceAll(/a/, "b")`)).message).toContain("write /a/g, or use String.replace")
  213. expect((await error(`return "aa".matchAll(/a/)`)).message).toContain("write /a/g, or use String.match")
  214. })
  215. test("a non-pattern argument names the expected shapes", async () => {
  216. const err = await error(`return "abc".match(42)`)
  217. expect(err.message).toContain("expects a regular expression")
  218. expect(err.message).toContain("not number")
  219. })
  220. test("source and flags properties read through", async () => {
  221. expect(await value(`const r = /ab/gi; return { source: r.source, flags: r.flags, global: r.global }`)).toEqual({
  222. source: "ab",
  223. flags: "gi",
  224. global: true,
  225. })
  226. })
  227. test("regexes serialize to {} at the boundary, like JSON", async () => {
  228. expect(await value(`return /a/`)).toEqual({})
  229. expect(await value(`return JSON.stringify({ r: /a/g })`)).toBe('{"r":{}}')
  230. })
  231. test("template interpolation renders the literal form", async () => {
  232. expect(await value("return `${/ab/g}`")).toBe("/ab/g")
  233. })
  234. })
  235. describe("URL and URI helpers", () => {
  236. test("encodes and decodes complete URIs and URI components", async () => {
  237. expect(
  238. await value(`
  239. return [
  240. encodeURI("https://example.test/a b?q=a/b"),
  241. encodeURIComponent("a b/c?"),
  242. decodeURI("https://example.test/a%20b?q=a/b"),
  243. decodeURIComponent("a%20b%2Fc%3F"),
  244. ["a b", "c/d"].map(encodeURIComponent),
  245. ]
  246. `),
  247. ).toEqual([
  248. "https://example.test/a%20b?q=a/b",
  249. "a%20b%2Fc%3F",
  250. "https://example.test/a b?q=a/b",
  251. "a b/c?",
  252. ["a%20b", "c%2Fd"],
  253. ])
  254. expect(
  255. await value(`try { decodeURIComponent("%zz"); return false } catch (error) { return error instanceof URIError }`),
  256. ).toBe(true)
  257. })
  258. test("resolves and mutates URLs with linked search parameters", async () => {
  259. expect(
  260. await value(`
  261. const url = new URL("../users?id=old#top", "https://user:pass@example.com:8443/api/v1/")
  262. url.pathname = "/items/a b"
  263. url.searchParams.set("id", "a b")
  264. url.searchParams.append("tag", "x/y")
  265. url.hash = "part 1"
  266. return {
  267. href: url.href,
  268. origin: url.origin,
  269. host: url.host,
  270. pathname: url.pathname,
  271. search: url.search,
  272. id: url.searchParams.get("id"),
  273. string: String(url),
  274. json: url.toJSON(),
  275. instances: [
  276. url instanceof URL,
  277. url.searchParams instanceof URLSearchParams,
  278. url.searchParams === url.searchParams,
  279. ],
  280. }
  281. `),
  282. ).toEqual({
  283. href: "https://user:pass@example.com:8443/items/a%20b?id=a+b&tag=x%2Fy#part%201",
  284. origin: "https://example.com:8443",
  285. host: "example.com:8443",
  286. pathname: "/items/a%20b",
  287. search: "?id=a+b&tag=x%2Fy",
  288. id: "a b",
  289. string: "https://user:pass@example.com:8443/items/a%20b?id=a+b&tag=x%2Fy#part%201",
  290. json: "https://user:pass@example.com:8443/items/a%20b?id=a+b&tag=x%2Fy#part%201",
  291. instances: [true, true, true],
  292. })
  293. })
  294. test("URLSearchParams supports records, pairs, mutation, callbacks, and materialization", async () => {
  295. expect(
  296. await value(`
  297. const params = new URLSearchParams([["tag", "b"], ["tag", "a"], ["q", "a b"]])
  298. const seen = []
  299. params.forEach((value, key) => seen.push(key + "=" + value))
  300. params.delete("tag", "b")
  301. params.append("tag", "c")
  302. params.sort()
  303. return {
  304. text: params.toString(),
  305. size: params.size,
  306. tags: params.getAll("tag"),
  307. has: params.has("tag", "c"),
  308. entries: Array.from(params),
  309. object: Object.fromEntries(params),
  310. record: new URLSearchParams({ page: 2, filter: "open" }).toString(),
  311. seen,
  312. }
  313. `),
  314. ).toEqual({
  315. text: "q=a+b&tag=a&tag=c",
  316. size: 3,
  317. tags: ["a", "c"],
  318. has: true,
  319. entries: [
  320. ["q", "a b"],
  321. ["tag", "a"],
  322. ["tag", "c"],
  323. ],
  324. object: { q: "a b", tag: "c" },
  325. record: "page=2&filter=open",
  326. seen: ["tag=b", "tag=a", "q=a b"],
  327. })
  328. })
  329. test("URL parsing failures are catchable and values use native JSON forms", async () => {
  330. expect(
  331. await value(`
  332. const parsed = URL.parse("/users", "https://example.test/api/")
  333. let invalidIsTypeError = false
  334. try { new URL("not relative without a base") } catch (error) { invalidIsTypeError = error instanceof TypeError }
  335. return {
  336. canParse: URL.canParse("/users", "https://example.test/api/"),
  337. cannotParse: URL.canParse("not relative without a base"),
  338. parsed: parsed.href,
  339. invalidIsTypeError,
  340. boundary: [new URL("https://example.test/a"), new URLSearchParams("q=one")],
  341. json: JSON.stringify({ url: new URL("https://example.test/a"), params: new URLSearchParams("q=one") }),
  342. }
  343. `),
  344. ).toEqual({
  345. canParse: true,
  346. cannotParse: false,
  347. parsed: "https://example.test/users",
  348. invalidIsTypeError: true,
  349. boundary: ["https://example.test/a", {}],
  350. json: '{"url":"https://example.test/a","params":{}}',
  351. })
  352. })
  353. test("distinguishes omitted URL arguments from explicit undefined", async () => {
  354. expect(
  355. await value(`
  356. function throwsTypeError(run) {
  357. try { run(); return false } catch (error) { return error instanceof TypeError }
  358. }
  359. const params = new URLSearchParams()
  360. const required = [
  361. () => params.append(),
  362. () => params.delete(),
  363. () => params.get(),
  364. () => params.getAll(),
  365. () => params.has(),
  366. () => params.set(),
  367. () => params.forEach(),
  368. ].map(throwsTypeError)
  369. params.append(undefined, undefined)
  370. return {
  371. construct: throwsTypeError(() => new URL()),
  372. canParse: throwsTypeError(() => URL.canParse()),
  373. parse: throwsTypeError(() => URL.parse()),
  374. explicitUndefined: new URL(undefined, "https://example.test/base/").href,
  375. params: params.toString(),
  376. required,
  377. }
  378. `),
  379. ).toEqual({
  380. construct: true,
  381. canParse: true,
  382. parse: true,
  383. explicitUndefined: "https://example.test/base/undefined",
  384. params: "undefined=undefined",
  385. required: [true, true, true, true, true, true, true],
  386. })
  387. })
  388. })
  389. describe("Map", () => {
  390. test("get/set/has/size with chaining", async () => {
  391. expect(
  392. await value(`
  393. const m = new Map()
  394. m.set("a", 1).set("b", 2)
  395. return { a: m.get("a"), b: m.get("b"), has: m.has("a"), miss: m.get("zz") === undefined, size: m.size }
  396. `),
  397. ).toEqual({ a: 1, b: 2, has: true, miss: true, size: 5 - 3 })
  398. })
  399. test("object keys use identity", async () => {
  400. expect(
  401. await value(`
  402. const key = { id: 1 }
  403. const m = new Map()
  404. m.set(key, "hit")
  405. return [m.get(key), m.get({ id: 1 }) === undefined]
  406. `),
  407. ).toEqual(["hit", true])
  408. })
  409. test("construction from entry pairs and another Map", async () => {
  410. expect(await value(`const m = new Map([["a", 1], ["b", 2]]); return m.get("b")`)).toBe(2)
  411. expect(
  412. await value(
  413. `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")]`,
  414. ),
  415. ).toEqual([1, 2, false])
  416. expect((await error(`return new Map("nope")`)).message).toMatch(/\[key, value\] pairs/)
  417. expect((await error(`return new Map(["flat"])`)).message).toMatch(/\[key, value\] pairs/)
  418. })
  419. test("keys/values/entries return arrays", async () => {
  420. expect(
  421. await value(`
  422. const m = new Map([["a", 1], ["b", 2]])
  423. return { keys: m.keys(), values: m.values(), entries: m.entries() }
  424. `),
  425. ).toEqual({
  426. keys: ["a", "b"],
  427. values: [1, 2],
  428. entries: [
  429. ["a", 1],
  430. ["b", 2],
  431. ],
  432. })
  433. })
  434. test("Object.fromEntries(map) and Array.from(map)", async () => {
  435. expect(await value(`return Object.fromEntries(new Map([["a", 1], ["b", 2]]))`)).toEqual({ a: 1, b: 2 })
  436. expect(await value(`return Array.from(new Map([["a", 1]]))`)).toEqual([["a", 1]])
  437. })
  438. test("for...of iterates [key, value] pairs with destructuring", async () => {
  439. expect(
  440. await value(`
  441. const m = new Map([["a", 1], ["b", 2]])
  442. let total = 0
  443. let names = ""
  444. for (const [key, count] of m) { names += key; total += count }
  445. return names + total
  446. `),
  447. ).toBe("ab3")
  448. })
  449. test("spread produces entry pairs", async () => {
  450. expect(await value(`return [...new Map([["a", 1]])]`)).toEqual([["a", 1]])
  451. })
  452. test("forEach passes (value, key)", async () => {
  453. expect(
  454. await value(`
  455. const m = new Map([["a", 1], ["b", 2]])
  456. const seen = []
  457. m.forEach((count, key) => seen.push(key + count))
  458. return seen
  459. `),
  460. ).toEqual(["a1", "b2"])
  461. })
  462. test("delete and clear", async () => {
  463. expect(
  464. await value(`
  465. const m = new Map([["a", 1], ["b", 2]])
  466. const removed = m.delete("a")
  467. const missed = m.delete("zz")
  468. const sizeAfterDelete = m.size
  469. m.clear()
  470. return [removed, missed, sizeAfterDelete, m.size]
  471. `),
  472. ).toEqual([true, false, 1, 0])
  473. })
  474. test("counting idiom: grouped tallies", async () => {
  475. expect(
  476. await value(`
  477. const words = ["a", "b", "a", "c", "a"]
  478. const counts = new Map()
  479. for (const word of words) counts.set(word, (counts.get(word) ?? 0) + 1)
  480. return Object.fromEntries(counts)
  481. `),
  482. ).toEqual({ a: 3, b: 1, c: 1 })
  483. })
  484. test("maps serialize to {} at the boundary, like JSON", async () => {
  485. expect(await value(`return new Map([["a", 1]])`)).toEqual({})
  486. expect(await value(`return JSON.stringify(new Map([["a", 1]]))`)).toBe("{}")
  487. })
  488. test("console.log renders map contents for debugging", async () => {
  489. const result = await run(`console.log(new Map([["a", 1]])); return null`)
  490. expect(result.ok).toBe(true)
  491. expect(result.logs?.[0]).toBe(`Map(1) [["a",1]]`)
  492. })
  493. })
  494. describe("Set", () => {
  495. test("add/has/delete/size with chaining", async () => {
  496. expect(
  497. await value(`
  498. const s = new Set()
  499. s.add(1).add(2).add(1)
  500. const removed = s.delete(2)
  501. return [s.size, s.has(1), s.has(2), removed]
  502. `),
  503. ).toEqual([1, true, false, true])
  504. })
  505. test("dedupe idiom: [...new Set(items)]", async () => {
  506. expect(await value(`return [...new Set([1, 2, 2, 3, 1])]`)).toEqual([1, 2, 3])
  507. })
  508. test("construction from strings and other Sets", async () => {
  509. expect(await value(`return [...new Set("aba")]`)).toEqual(["a", "b"])
  510. expect(await value(`return Array.from(new Set(new Set([1, 2])))`)).toEqual([1, 2])
  511. })
  512. test("SameValueZero: NaN is findable", async () => {
  513. expect(await value(`const s = new Set([NaN]); return s.has(NaN)`)).toBe(true)
  514. })
  515. test("for...of iterates values", async () => {
  516. expect(
  517. await value(`
  518. let total = 0
  519. for (const n of new Set([1, 2, 3])) total += n
  520. return total
  521. `),
  522. ).toBe(6)
  523. })
  524. test("sets serialize to {} at the boundary, like JSON", async () => {
  525. expect(await value(`return { s: new Set([1]) }`)).toEqual({ s: {} })
  526. })
  527. })
  528. describe("stdlib integration", () => {
  529. test("typeof reports constructors as functions and never throws", async () => {
  530. expect(await value(`return typeof Map`)).toBe("function")
  531. expect(await value(`return typeof ((x) => x)`)).toBe("function")
  532. expect(await value(`return typeof Math`)).toBe("object")
  533. expect(await value(`return typeof tools`)).toBe("object")
  534. })
  535. test("negation works on any value", async () => {
  536. expect(await value(`return !new Map()`)).toBe(false)
  537. expect(await value(`const fn = () => 1; return !fn`)).toBe(false)
  538. })
  539. test("object spread of sandbox values is a no-op, like JS", async () => {
  540. expect(await value(`return { ...new Map([["a", 1]]), kept: true }`)).toEqual({ kept: true })
  541. })
  542. test("dates inside Map values survive in-sandbox reads", async () => {
  543. expect(
  544. await value(`
  545. const m = new Map([["start", new Date(1000)]])
  546. return m.get("start").getTime()
  547. `),
  548. ).toBe(1000)
  549. })
  550. test("instanceof recognizes the stdlib value types", async () => {
  551. expect(
  552. await value(
  553. `return [new Date(0) instanceof Date, /a/ instanceof RegExp, new Map() instanceof Map, new Set() instanceof Set]`,
  554. ),
  555. ).toEqual([true, true, true, true])
  556. expect(
  557. await value(`return [[1] instanceof Array, [1] instanceof Object, ({}) instanceof Object, 5 instanceof Object]`),
  558. ).toEqual([true, true, true, false])
  559. expect(await value(`return [new Map() instanceof Set, "s" instanceof Date]`)).toEqual([false, false])
  560. expect(
  561. await value(`const p = Promise.resolve(1); const isPromise = p instanceof Promise; await p; return isPromise`),
  562. ).toBe(true)
  563. })
  564. test("realistic pipeline: parse, extract with regex, dedupe, count by day", async () => {
  565. expect(
  566. await value(`
  567. 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"}]'
  568. const rows = JSON.parse(raw)
  569. const tags = new Set()
  570. const byDay = new Map()
  571. for (const row of rows) {
  572. for (const m of row.tag.matchAll(/[a-z]+/g)) tags.add(m[0])
  573. const day = new Date(row.at).toISOString().slice(0, 10)
  574. byDay.set(day, (byDay.get(day) ?? 0) + 1)
  575. }
  576. return { tags: [...tags].sort((a, b) => (a < b ? -1 : 1)), byDay: Object.fromEntries(byDay) }
  577. `),
  578. ).toEqual({ tags: ["a", "b", "c"], byDay: { "2024-01-01": 2, "2024-01-02": 1 } })
  579. })
  580. })
  581. describe("sandbox values at intra-sandbox checkpoints", () => {
  582. test("Object.values/entries keep Dates usable", async () => {
  583. expect(await value(`return Object.values({ d: new Date(0) })[0].getTime()`)).toBe(0)
  584. expect(await value(`const [key, d] = Object.entries({ d: new Date(0) })[0]; return key + ":" + d.getTime()`)).toBe(
  585. "d:0",
  586. )
  587. })
  588. test("Object.assign keeps Maps usable", async () => {
  589. expect(await value(`const merged = Object.assign({}, { m: new Map([["a", 1]]) }); return merged.m.get("a")`)).toBe(
  590. 1,
  591. )
  592. })
  593. test("object and array spread keep sandbox values usable", async () => {
  594. expect(
  595. await value(`
  596. const src = { m: new Map([["a", 1]]) }
  597. const copy = { ...src }
  598. copy.m.set("b", 2)
  599. return [copy.m.get("a"), src.m.get("b")]
  600. `),
  601. ).toEqual([1, 2])
  602. expect(await value(`const list = [new Date(1000)]; const copy = [...list]; return copy[0].getTime()`)).toBe(1000)
  603. })
  604. test("Array.from over arrays keeps nested sandbox values usable", async () => {
  605. expect(await value(`return Array.from([new Date(5)])[0].getTime()`)).toBe(5)
  606. })
  607. test("regexes stay callable through Object.values", async () => {
  608. expect(await value(`return Object.values({ r: /ab+/ })[0].test("abb")`)).toBe(true)
  609. })
  610. test("Object.* helpers see sandbox values as empty objects, never internals", async () => {
  611. expect(await value(`return Object.keys(new Map([["a", 1]]))`)).toEqual([])
  612. expect(await value(`return Object.values(new Date(0))`)).toEqual([])
  613. expect(await value(`return Object.entries(new Set([1]))`)).toEqual([])
  614. expect(await value(`return Object.assign({}, new Map([["a", 1]]))`)).toEqual({})
  615. expect(await value(`return Object.hasOwn(new Date(0), "time")`)).toBe(false)
  616. })
  617. test("the host boundary still serializes JSON forms: results, JSON.stringify, and tool arguments", async () => {
  618. expect(await value(`return { d: new Date(0), m: new Map([["a", 1]]) }`)).toEqual({
  619. d: "1970-01-01T00:00:00.000Z",
  620. m: {},
  621. })
  622. expect(await value(`return JSON.stringify({ d: new Date(0) })`)).toBe('{"d":"1970-01-01T00:00:00.000Z"}')
  623. const observed: Array<unknown> = []
  624. const capture = Tool.make({
  625. description: "Capture the exact input the host receives",
  626. input: { type: "object" },
  627. run: (input) =>
  628. Effect.sync(() => {
  629. observed.push(input)
  630. return "ok"
  631. }),
  632. })
  633. const result = await Effect.runPromise(
  634. CodeMode.execute({
  635. tools: { host: { capture } },
  636. code: `return await tools.host.capture({ when: new Date(0), tags: new Map([["a", 1]]) })`,
  637. }),
  638. )
  639. expect(result.ok).toBe(true)
  640. expect(observed).toStrictEqual([{ when: "1970-01-01T00:00:00.000Z", tags: {} }])
  641. })
  642. })