stdlib.test.ts 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427
  1. import { describe, expect, test } from "bun:test"
  2. import { Effect } 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. // RegExp/Map/Set -> {}.
  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(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])
  38. })
  39. test("invalid dates yield NaN times, guardable in-sandbox", async () => {
  40. expect(await value(`return Number.isNaN(new Date("garbage").getTime())`)).toBe(true)
  41. expect(await value(`return new Date("garbage").toJSON()`)).toBeNull()
  42. })
  43. test("toISOString on an invalid date is a catchable error", async () => {
  44. expect(await value(`try { new Date("garbage").toISOString(); return "no" } catch { return "caught" }`)).toBe("caught")
  45. })
  46. test("template interpolation renders the ISO form", async () => {
  47. expect(await value("return `at ${new Date(0)}`")).toBe("at 1970-01-01T00:00:00.000Z")
  48. })
  49. test("dates serialize to ISO strings at the boundary, direct and nested", async () => {
  50. expect(await value(`return new Date(0)`)).toBe("1970-01-01T00:00:00.000Z")
  51. expect(await value(`return { when: new Date(0), tags: [new Date(1000)] }`)).toEqual({
  52. when: "1970-01-01T00:00:00.000Z",
  53. tags: ["1970-01-01T00:00:01.000Z"],
  54. })
  55. expect(await value(`return JSON.stringify({ d: new Date(0) })`)).toBe('{"d":"1970-01-01T00:00:00.000Z"}')
  56. })
  57. test("coercions: Number is the time, String is ISO, Boolean is true", async () => {
  58. expect(await value(`return Number(new Date(5))`)).toBe(5)
  59. expect(await value(`return String(new Date(0))`)).toBe("1970-01-01T00:00:00.000Z")
  60. expect(await value(`return Boolean(new Date(0))`)).toBe(true)
  61. })
  62. test("sorting dates with a numeric comparator", async () => {
  63. expect(await value(`
  64. const dates = [new Date(3000), new Date(1000), new Date(2000)]
  65. return dates.sort((a, b) => a - b).map((d) => d.getTime())
  66. `)).toEqual([1000, 2000, 3000])
  67. })
  68. test("new Date(year, month, day) accepts component form", async () => {
  69. expect(await value(`const d = new Date(2024, 0, 2); return [d.getFullYear(), d.getMonth(), d.getDate()]`)).toEqual([2024, 0, 2])
  70. })
  71. test("typeof and unknown properties are forgiving", async () => {
  72. expect(await value(`return typeof new Date(0)`)).toBe("object")
  73. expect(await value(`return new Date(0).nope === undefined`)).toBe(true)
  74. })
  75. })
  76. describe("RegExp", () => {
  77. test("literal test", async () => {
  78. expect(await value(`return /ab+c/.test("xabbbc")`)).toBe(true)
  79. expect(await value(`return /ab+c/.test("nope")`)).toBe(false)
  80. })
  81. test("exec exposes captures and index", async () => {
  82. expect(await value(`const m = /a(b+)/.exec("xxabbc"); return { full: m[0], group: m[1], index: m.index }`)).toEqual({
  83. full: "abb",
  84. group: "bb",
  85. index: 2,
  86. })
  87. expect(await value(`return /a/.exec("zzz")`)).toBeNull()
  88. })
  89. test("named groups read through", async () => {
  90. expect(await value(`const m = /(?<word>[a-z]+)-(?<num>\\d+)/.exec("id ab-42"); return m.groups.word + m.groups.num`)).toBe("ab42")
  91. })
  92. test("global exec advances lastIndex across calls", async () => {
  93. expect(await value(`
  94. const r = /\\d+/g
  95. const first = r.exec("a1b22c")
  96. const second = r.exec("a1b22c")
  97. return [first[0], second[0]]
  98. `)).toEqual(["1", "22"])
  99. })
  100. test("string match: non-global carries index, global lists all matches", async () => {
  101. expect(await value(`const m = "a1b22".match(/\\d+/); return [m[0], m.index]`)).toEqual(["1", 1])
  102. expect(await value(`return "a1b22".match(/\\d+/g)`)).toEqual(["1", "22"])
  103. expect(await value(`return "abc".match(/\\d/)`)).toBeNull()
  104. })
  105. test("matchAll materializes match arrays with captures", async () => {
  106. expect(await value(`return "a1b22".matchAll(/(\\d+)/g).map((m) => m[1])`)).toEqual(["1", "22"])
  107. })
  108. test("replace and replaceAll with patterns and $1 substitution", async () => {
  109. expect(await value(`return "a1b2".replace(/\\d/, "#")`)).toBe("a#b2")
  110. expect(await value(`return "a1b2".replace(/\\d/g, "#")`)).toBe("a#b#")
  111. expect(await value(`return "a1b2".replaceAll(/\\d/g, "#")`)).toBe("a#b#")
  112. expect(await value(`return "hi bob".replace(/b(o)b/, "[$1]")`)).toBe("hi [o]")
  113. })
  114. test("replaceAll without the g flag is a catchable error", async () => {
  115. expect(await value(`try { "a".replaceAll(/a/, "b"); return "no" } catch { return "caught" }`)).toBe("caught")
  116. })
  117. test("split and search accept patterns", async () => {
  118. expect(await value(`return "a1b22c".split(/\\d+/)`)).toEqual(["a", "b", "c"])
  119. expect(await value(`return "ab42".search(/\\d/)`)).toBe(2)
  120. expect(await value(`return "ab".search(/\\d/)`)).toBe(-1)
  121. })
  122. test("new RegExp constructs from strings; invalid patterns are catchable", async () => {
  123. expect(await value(`return new RegExp("a+", "i").test("AAA")`)).toBe(true)
  124. expect(await value(`try { new RegExp("("); return "no" } catch { return "caught" }`)).toBe("caught")
  125. expect(await value(`return [/a/ instanceof RegExp, /a/.source]`)).toEqual([true, "a"])
  126. })
  127. test("invalid patterns fail with actionable messages", async () => {
  128. const fromString = await error(`return "abc".match("(")`)
  129. expect(fromString.message).toContain('String.match received the string "("')
  130. expect(fromString.message).toContain("escape them with a backslash")
  131. const fromConstructor = await error(`return new RegExp("(")`)
  132. expect(fromConstructor.message).toContain('new RegExp(...) received "("')
  133. expect(fromConstructor.message).toContain("escape them with a backslash")
  134. const fromFlags = await error(`return new RegExp("a", "xz")`)
  135. expect(fromFlags.message).toContain('invalid flags "xz"')
  136. expect(fromFlags.message).toContain("Valid flags are")
  137. })
  138. test("missing g-flag errors say how to fix the call", async () => {
  139. expect((await error(`return "aa".replaceAll(/a/, "b")`)).message).toContain("write /a/g, or use String.replace")
  140. expect((await error(`return "aa".matchAll(/a/)`)).message).toContain("write /a/g, or use String.match")
  141. })
  142. test("a non-pattern argument names the expected shapes", async () => {
  143. const err = await error(`return "abc".match(42)`)
  144. expect(err.message).toContain("expects a regular expression")
  145. expect(err.message).toContain("not number")
  146. })
  147. test("source and flags properties read through", async () => {
  148. expect(await value(`const r = /ab/gi; return { source: r.source, flags: r.flags, global: r.global }`)).toEqual({
  149. source: "ab",
  150. flags: "gi",
  151. global: true,
  152. })
  153. })
  154. test("regexes serialize to {} at the boundary, like JSON", async () => {
  155. expect(await value(`return /a/`)).toEqual({})
  156. expect(await value(`return JSON.stringify({ r: /a/g })`)).toBe('{"r":{}}')
  157. })
  158. test("template interpolation renders the literal form", async () => {
  159. expect(await value("return `${/ab/g}`")).toBe("/ab/g")
  160. })
  161. })
  162. describe("Map", () => {
  163. test("get/set/has/size with chaining", async () => {
  164. expect(await value(`
  165. const m = new Map()
  166. m.set("a", 1).set("b", 2)
  167. return { a: m.get("a"), b: m.get("b"), has: m.has("a"), miss: m.get("zz") === undefined, size: m.size }
  168. `)).toEqual({ a: 1, b: 2, has: true, miss: true, size: 5 - 3 })
  169. })
  170. test("object keys use identity", async () => {
  171. expect(await value(`
  172. const key = { id: 1 }
  173. const m = new Map()
  174. m.set(key, "hit")
  175. return [m.get(key), m.get({ id: 1 }) === undefined]
  176. `)).toEqual(["hit", true])
  177. })
  178. test("construction from entry pairs and another Map", async () => {
  179. expect(await value(`const m = new Map([["a", 1], ["b", 2]]); return m.get("b")`)).toBe(2)
  180. 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])
  181. expect((await error(`return new Map("nope")`)).message).toMatch(/\[key, value\] pairs/)
  182. expect((await error(`return new Map(["flat"])`)).message).toMatch(/\[key, value\] pairs/)
  183. })
  184. test("keys/values/entries return arrays", async () => {
  185. expect(await value(`
  186. const m = new Map([["a", 1], ["b", 2]])
  187. return { keys: m.keys(), values: m.values(), entries: m.entries() }
  188. `)).toEqual({ keys: ["a", "b"], values: [1, 2], entries: [["a", 1], ["b", 2]] })
  189. })
  190. test("Object.fromEntries(map) and Array.from(map)", async () => {
  191. expect(await value(`return Object.fromEntries(new Map([["a", 1], ["b", 2]]))`)).toEqual({ a: 1, b: 2 })
  192. expect(await value(`return Array.from(new Map([["a", 1]]))`)).toEqual([["a", 1]])
  193. })
  194. test("for...of iterates [key, value] pairs with destructuring", async () => {
  195. expect(await value(`
  196. const m = new Map([["a", 1], ["b", 2]])
  197. let total = 0
  198. let names = ""
  199. for (const [key, count] of m) { names += key; total += count }
  200. return names + total
  201. `)).toBe("ab3")
  202. })
  203. test("spread produces entry pairs", async () => {
  204. expect(await value(`return [...new Map([["a", 1]])]`)).toEqual([["a", 1]])
  205. })
  206. test("forEach passes (value, key)", async () => {
  207. expect(await value(`
  208. const m = new Map([["a", 1], ["b", 2]])
  209. const seen = []
  210. m.forEach((count, key) => seen.push(key + count))
  211. return seen
  212. `)).toEqual(["a1", "b2"])
  213. })
  214. test("delete and clear", async () => {
  215. expect(await value(`
  216. const m = new Map([["a", 1], ["b", 2]])
  217. const removed = m.delete("a")
  218. const missed = m.delete("zz")
  219. const sizeAfterDelete = m.size
  220. m.clear()
  221. return [removed, missed, sizeAfterDelete, m.size]
  222. `)).toEqual([true, false, 1, 0])
  223. })
  224. test("counting idiom: grouped tallies", async () => {
  225. expect(await value(`
  226. const words = ["a", "b", "a", "c", "a"]
  227. const counts = new Map()
  228. for (const word of words) counts.set(word, (counts.get(word) ?? 0) + 1)
  229. return Object.fromEntries(counts)
  230. `)).toEqual({ a: 3, b: 1, c: 1 })
  231. })
  232. test("maps serialize to {} at the boundary, like JSON", async () => {
  233. expect(await value(`return new Map([["a", 1]])`)).toEqual({})
  234. expect(await value(`return JSON.stringify(new Map([["a", 1]]))`)).toBe("{}")
  235. })
  236. test("console.log renders map contents for debugging", async () => {
  237. const result = await run(`console.log(new Map([["a", 1]])); return null`)
  238. expect(result.ok).toBe(true)
  239. expect(result.logs?.[0]).toBe(`Map(1) [["a",1]]`)
  240. })
  241. })
  242. describe("Set", () => {
  243. test("add/has/delete/size with chaining", async () => {
  244. expect(await value(`
  245. const s = new Set()
  246. s.add(1).add(2).add(1)
  247. const removed = s.delete(2)
  248. return [s.size, s.has(1), s.has(2), removed]
  249. `)).toEqual([1, true, false, true])
  250. })
  251. test("dedupe idiom: [...new Set(items)]", async () => {
  252. expect(await value(`return [...new Set([1, 2, 2, 3, 1])]`)).toEqual([1, 2, 3])
  253. })
  254. test("construction from strings and other Sets", async () => {
  255. expect(await value(`return [...new Set("aba")]`)).toEqual(["a", "b"])
  256. expect(await value(`return Array.from(new Set(new Set([1, 2])))`)).toEqual([1, 2])
  257. })
  258. test("SameValueZero: NaN is findable", async () => {
  259. expect(await value(`const s = new Set([NaN]); return s.has(NaN)`)).toBe(true)
  260. })
  261. test("for...of iterates values", async () => {
  262. expect(await value(`
  263. let total = 0
  264. for (const n of new Set([1, 2, 3])) total += n
  265. return total
  266. `)).toBe(6)
  267. })
  268. test("sets serialize to {} at the boundary, like JSON", async () => {
  269. expect(await value(`return { s: new Set([1]) }`)).toEqual({ s: {} })
  270. })
  271. })
  272. describe("stdlib integration", () => {
  273. test("typeof reports constructors as functions and never throws", async () => {
  274. expect(await value(`return typeof Map`)).toBe("function")
  275. expect(await value(`return typeof ((x) => x)`)).toBe("function")
  276. expect(await value(`return typeof Math`)).toBe("object")
  277. expect(await value(`return typeof tools`)).toBe("object")
  278. })
  279. test("negation works on any value", async () => {
  280. expect(await value(`return !new Map()`)).toBe(false)
  281. expect(await value(`const fn = () => 1; return !fn`)).toBe(false)
  282. })
  283. test("object spread of sandbox values is a no-op, like JS", async () => {
  284. expect(await value(`return { ...new Map([["a", 1]]), kept: true }`)).toEqual({ kept: true })
  285. })
  286. test("dates inside Map values survive in-sandbox reads", async () => {
  287. expect(await value(`
  288. const m = new Map([["start", new Date(1000)]])
  289. return m.get("start").getTime()
  290. `)).toBe(1000)
  291. })
  292. test("instanceof recognizes the stdlib value types", async () => {
  293. 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])
  294. expect(await value(`return [[1] instanceof Array, [1] instanceof Object, ({}) instanceof Object, 5 instanceof Object]`)).toEqual([true, true, true, false])
  295. expect(await value(`return [new Map() instanceof Set, "s" instanceof Date]`)).toEqual([false, false])
  296. expect(await value(`const p = Promise.resolve(1); const isPromise = p instanceof Promise; await p; return isPromise`)).toBe(true)
  297. })
  298. test("realistic pipeline: parse, extract with regex, dedupe, count by day", async () => {
  299. expect(await value(`
  300. 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"}]'
  301. const rows = JSON.parse(raw)
  302. const tags = new Set()
  303. const byDay = new Map()
  304. for (const row of rows) {
  305. for (const m of row.tag.matchAll(/[a-z]+/g)) tags.add(m[0])
  306. const day = new Date(row.at).toISOString().slice(0, 10)
  307. byDay.set(day, (byDay.get(day) ?? 0) + 1)
  308. }
  309. return { tags: [...tags].sort((a, b) => (a < b ? -1 : 1)), byDay: Object.fromEntries(byDay) }
  310. `)).toEqual({ tags: ["a", "b", "c"], byDay: { "2024-01-01": 2, "2024-01-02": 1 } })
  311. })
  312. })
  313. describe("sandbox values at intra-sandbox checkpoints", () => {
  314. test("Object.values/entries keep Dates usable", async () => {
  315. expect(await value(`return Object.values({ d: new Date(0) })[0].getTime()`)).toBe(0)
  316. expect(await value(`const [key, d] = Object.entries({ d: new Date(0) })[0]; return key + ":" + d.getTime()`)).toBe("d:0")
  317. })
  318. test("Object.assign keeps Maps usable", async () => {
  319. expect(await value(`const merged = Object.assign({}, { m: new Map([["a", 1]]) }); return merged.m.get("a")`)).toBe(1)
  320. })
  321. test("object and array spread keep sandbox values usable", async () => {
  322. expect(await value(`
  323. const src = { m: new Map([["a", 1]]) }
  324. const copy = { ...src }
  325. copy.m.set("b", 2)
  326. return [copy.m.get("a"), src.m.get("b")]
  327. `)).toEqual([1, 2])
  328. expect(await value(`const list = [new Date(1000)]; const copy = [...list]; return copy[0].getTime()`)).toBe(1000)
  329. })
  330. test("Array.from over arrays keeps nested sandbox values usable", async () => {
  331. expect(await value(`return Array.from([new Date(5)])[0].getTime()`)).toBe(5)
  332. })
  333. test("regexes stay callable through Object.values", async () => {
  334. expect(await value(`return Object.values({ r: /ab+/ })[0].test("abb")`)).toBe(true)
  335. })
  336. test("Object.* helpers see sandbox values as empty objects, never internals", async () => {
  337. expect(await value(`return Object.keys(new Map([["a", 1]]))`)).toEqual([])
  338. expect(await value(`return Object.values(new Date(0))`)).toEqual([])
  339. expect(await value(`return Object.entries(new Set([1]))`)).toEqual([])
  340. expect(await value(`return Object.assign({}, new Map([["a", 1]]))`)).toEqual({})
  341. expect(await value(`return Object.hasOwn(new Date(0), "time")`)).toBe(false)
  342. })
  343. test("the host boundary still serializes JSON forms: results, JSON.stringify, and tool arguments", async () => {
  344. expect(await value(`return { d: new Date(0), m: new Map([["a", 1]]) }`)).toEqual({ d: "1970-01-01T00:00:00.000Z", m: {} })
  345. expect(await value(`return JSON.stringify({ d: new Date(0) })`)).toBe('{"d":"1970-01-01T00:00:00.000Z"}')
  346. const observed: Array<unknown> = []
  347. const capture = Tool.make({
  348. description: "Capture the exact input the host receives",
  349. input: { type: "object" },
  350. run: (input) =>
  351. Effect.sync(() => {
  352. observed.push(input)
  353. return "ok"
  354. }),
  355. })
  356. const result = await Effect.runPromise(CodeMode.execute({
  357. tools: { host: { capture } },
  358. code: `return await tools.host.capture({ when: new Date(0), tags: new Map([["a", 1]]) })`,
  359. }))
  360. expect(result.ok).toBe(true)
  361. expect(observed).toStrictEqual([{ when: "1970-01-01T00:00:00.000Z", tags: {} }])
  362. })
  363. })