@neurocode-ai/codemode 1.18.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +23 -0
- package/README.md +369 -0
- package/codemode.md +176 -0
- package/package.json +25 -0
- package/src/codemode.ts +159 -0
- package/src/index.ts +4 -0
- package/src/interpreter/model.ts +201 -0
- package/src/interpreter/runtime.ts +3465 -0
- package/src/openapi/TODO.md +19 -0
- package/src/openapi/index.ts +130 -0
- package/src/openapi/runtime.ts +326 -0
- package/src/openapi/spec.ts +511 -0
- package/src/openapi/types.ts +112 -0
- package/src/stdlib/collections.ts +51 -0
- package/src/stdlib/console.ts +4 -0
- package/src/stdlib/date.ts +94 -0
- package/src/stdlib/json.ts +42 -0
- package/src/stdlib/math.ts +65 -0
- package/src/stdlib/number.ts +66 -0
- package/src/stdlib/object.ts +77 -0
- package/src/stdlib/promise.ts +6 -0
- package/src/stdlib/regexp.ts +74 -0
- package/src/stdlib/string.ts +52 -0
- package/src/stdlib/url.ts +90 -0
- package/src/stdlib/value.ts +86 -0
- package/src/tool-error.ts +11 -0
- package/src/tool-runtime.ts +806 -0
- package/src/tool-schema.ts +301 -0
- package/src/tool.ts +96 -0
- package/src/values.ts +49 -0
- package/sst-env.d.ts +10 -0
- package/test/codemode.test.ts +1163 -0
- package/test/enumeration.test.ts +159 -0
- package/test/fixtures/openapi-happy-path.json +230 -0
- package/test/fixtures/opencode-v2-openapi.json +23730 -0
- package/test/openapi.test.ts +964 -0
- package/test/parity.test.ts +425 -0
- package/test/promise.test.ts +456 -0
- package/test/signature.test.ts +449 -0
- package/test/stdlib.test.ts +715 -0
- package/tsconfig.json +7 -0
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test"
|
|
2
|
+
import { Effect } from "effect"
|
|
3
|
+
import { CodeMode } from "../src/index.js"
|
|
4
|
+
import { ToolRuntime } from "../src/tool-runtime.js"
|
|
5
|
+
|
|
6
|
+
// Runs a CodeMode program with no host tools and returns the CodeMode.Result. These tests pin the
|
|
7
|
+
// JS-parity behaviors for the "99% of ordinary defensive JavaScript just works" goal: cases where
|
|
8
|
+
// a strict interpreter would throw but idiomatic JS yields undefined / succeeds.
|
|
9
|
+
//
|
|
10
|
+
// Note on the result boundary: this package normalizes a bare `undefined` result to `null` when
|
|
11
|
+
// it crosses out of the sandbox (results are JSON data), so tests asserting an in-sandbox
|
|
12
|
+
// `undefined` read check `=== undefined` inside the program and `null` at the boundary.
|
|
13
|
+
const run = (code: string) => Effect.runPromise(CodeMode.execute({ code, tools: {} }))
|
|
14
|
+
const value = async (code: string) => {
|
|
15
|
+
const result = await run(code)
|
|
16
|
+
if (!result.ok) throw new Error(`expected success, got ${result.error.kind}: ${result.error.message}`)
|
|
17
|
+
return result.value
|
|
18
|
+
}
|
|
19
|
+
const error = async (code: string) => {
|
|
20
|
+
const result = await run(code)
|
|
21
|
+
if (result.ok) throw new Error(`expected failure, got value ${JSON.stringify(result.value)}`)
|
|
22
|
+
return result.error
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe("H2: string property access reads as undefined (not a throw)", () => {
|
|
26
|
+
test("unknown property on a string is undefined", async () => {
|
|
27
|
+
expect(await value(`const s = "hi"; return s.login === undefined`)).toBe(true)
|
|
28
|
+
expect(await value(`const s = "hi"; return s.login`)).toBeNull()
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test("optional chaining + fallback on a string does not throw", async () => {
|
|
32
|
+
expect(await value(`const s = "hi"; return s?.login ?? "fallback"`)).toBe("fallback")
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
test("the real MCP pattern: result is a JSON string, defensive read falls through", async () => {
|
|
36
|
+
// me.result is a string; me.result?.login is undefined, so we fall back to the raw string.
|
|
37
|
+
expect(await value(`const me = { result: '{"login":"x"}' }; return me.result?.login ?? me.result`)).toBe(
|
|
38
|
+
'{"login":"x"}',
|
|
39
|
+
)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
test("unknown property on a number is undefined", async () => {
|
|
43
|
+
expect(await value(`return (5).foo ?? "n"`)).toBe("n")
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test("supported string methods still work", async () => {
|
|
47
|
+
expect(await value(`return "AB".toLowerCase()`)).toBe("ab")
|
|
48
|
+
expect(await value(`return "hello".length`)).toBe(5)
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
describe("H3: array property access reads as undefined (not a throw)", () => {
|
|
53
|
+
test("unknown property on an array is undefined", async () => {
|
|
54
|
+
expect(await value(`return [1,2,3].foo === undefined`)).toBe(true)
|
|
55
|
+
expect(await value(`return [1,2,3].foo`)).toBeNull()
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test("optional chaining on an array does not throw", async () => {
|
|
59
|
+
expect(await value(`return [1,2,3]?.foo ?? "fb"`)).toBe("fb")
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test("unknown property reads stay undefined for methods CodeMode does not implement", async () => {
|
|
63
|
+
expect(await value(`return [1,2,3].toSpliced === undefined`)).toBe(true)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
test("supported array methods and indexing still work", async () => {
|
|
67
|
+
expect(await value(`return [1,2,3].map(x => x + 1)`)).toEqual([2, 3, 4])
|
|
68
|
+
expect(await value(`return [1,2,3][9] === undefined`)).toBe(true)
|
|
69
|
+
expect(await value(`return [1,2,3][9]`)).toBeNull()
|
|
70
|
+
})
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
describe("H6: object spread of null/undefined is a no-op", () => {
|
|
74
|
+
test("spreading null is a no-op", async () => {
|
|
75
|
+
expect(await value(`const o = null; return { ...o, a: 1 }`)).toEqual({ a: 1 })
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test("spreading an absent argument merges cleanly", async () => {
|
|
79
|
+
expect(await value(`function f(opts){ return { ...opts, a: 1 } } return f(undefined)`)).toEqual({ a: 1 })
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
test("spreading a real object still works", async () => {
|
|
83
|
+
expect(await value(`const o = { a: 1 }; return { ...o, b: 2 }`)).toEqual({ a: 1, b: 2 })
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
test("spreading an array into an object still errors", async () => {
|
|
87
|
+
const err = await error(`return { ...[1,2], a: 1 }`)
|
|
88
|
+
expect(err.kind).toBe("InvalidDataValue")
|
|
89
|
+
})
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
describe("H4: typeof on an undeclared identifier is 'undefined'", () => {
|
|
93
|
+
test("feature-detection guard does not throw", async () => {
|
|
94
|
+
expect(await value(`return typeof foo === "undefined" ? "safe" : "no"`)).toBe("safe")
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
test("typeof of a declared binding is unaffected", async () => {
|
|
98
|
+
expect(await value(`const x = 5; return typeof x`)).toBe("number")
|
|
99
|
+
expect(await value(`const s = "a"; return typeof s`)).toBe("string")
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
test("referencing an undeclared identifier outside typeof still throws", async () => {
|
|
103
|
+
const err = await error(`return foo + 1`)
|
|
104
|
+
expect(err.message).toContain("foo")
|
|
105
|
+
})
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
describe("H1: NaN/Infinity flow as intermediates and normalize to null at the boundary", () => {
|
|
109
|
+
test("guards run instead of the program crashing on a transient NaN", async () => {
|
|
110
|
+
expect(await value(`return parseInt("abc") || 0`)).toBe(0)
|
|
111
|
+
expect(await value(`const x = Number("abc"); return Number.isNaN(x) ? 0 : x`)).toBe(0)
|
|
112
|
+
expect(await value(`const o = {}; o.count = (o.count || 0) + 1; return o.count`)).toBe(1)
|
|
113
|
+
// average of an empty list, guarded - the classic divide-by-zero that used to throw pre-guard
|
|
114
|
+
expect(await value(`const a = []; return a.length ? a.reduce((s,x)=>s+x,0)/a.length : 0`)).toBe(0)
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
test("a non-finite value becomes null when it leaves the sandbox", async () => {
|
|
118
|
+
expect(await value(`return 5/0`)).toBeNull()
|
|
119
|
+
expect(await value(`return 0/0`)).toBeNull()
|
|
120
|
+
expect(await value(`return Math.max()`)).toBeNull()
|
|
121
|
+
// nested, too - normalization walks the returned structure
|
|
122
|
+
expect(await value(`return { a: Number("x"), b: 2, c: [1/0] }`)).toEqual({ a: null, b: 2, c: [null] })
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
test("NaN and Infinity are usable identifiers and inspectable in-sandbox", async () => {
|
|
126
|
+
expect(await value(`return Number.isNaN(NaN)`)).toBe(true)
|
|
127
|
+
expect(await value(`return Infinity > 1e9`)).toBe(true)
|
|
128
|
+
expect(await value(`return Number.isFinite(1/0)`)).toBe(false)
|
|
129
|
+
expect(await value(`return [3,1,2].reduce((a,b)=>Math.max(a,b), -Infinity)`)).toBe(3)
|
|
130
|
+
// JSON.stringify inside the sandbox matches JS: non-finite serializes to null
|
|
131
|
+
expect(await value(`return JSON.stringify({ x: Number("z") })`)).toBe('{"x":null}')
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
test("copyOut normalizes non-finite numbers to null (the shared return + tool-arg boundary)", () => {
|
|
135
|
+
// Tool-call arguments funnel through copyOut too, so this one function pins both boundaries.
|
|
136
|
+
expect(ToolRuntime.copyOut(NaN)).toBeNull()
|
|
137
|
+
expect(ToolRuntime.copyOut(Infinity)).toBeNull()
|
|
138
|
+
expect(ToolRuntime.copyOut(-Infinity)).toBeNull()
|
|
139
|
+
expect(ToolRuntime.copyOut(42)).toBe(42)
|
|
140
|
+
expect(ToolRuntime.copyOut({ a: NaN, b: [Infinity, 1] })).toEqual({ a: null, b: [null, 1] })
|
|
141
|
+
})
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
describe("Error values and instanceof", () => {
|
|
145
|
+
test("new Error carries name/message and is instanceof Error", async () => {
|
|
146
|
+
expect(await value(`const e = new Error("boom"); return [e instanceof Error, e.name, e.message]`)).toEqual([
|
|
147
|
+
true,
|
|
148
|
+
"Error",
|
|
149
|
+
"boom",
|
|
150
|
+
])
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
test("Error without new behaves like new Error", async () => {
|
|
154
|
+
expect(await value(`const e = Error("plain"); return [e instanceof Error, e.name, e.message]`)).toEqual([
|
|
155
|
+
true,
|
|
156
|
+
"Error",
|
|
157
|
+
"plain",
|
|
158
|
+
])
|
|
159
|
+
expect(await value(`const e = new Error(); return [e.name, e.message, e instanceof Error]`)).toEqual([
|
|
160
|
+
"Error",
|
|
161
|
+
"",
|
|
162
|
+
true,
|
|
163
|
+
])
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
test("specific error types are instanceof themselves and Error, not each other", async () => {
|
|
167
|
+
expect(
|
|
168
|
+
await value(
|
|
169
|
+
`const e = new TypeError("t"); return [e instanceof TypeError, e instanceof Error, e instanceof RangeError]`,
|
|
170
|
+
),
|
|
171
|
+
).toEqual([true, true, false])
|
|
172
|
+
expect(await value(`return new Error("e") instanceof TypeError`)).toBe(false)
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
test("thrown errors keep instanceof through try/catch", async () => {
|
|
176
|
+
expect(await value(`try { throw new Error("x") } catch (e) { return [e instanceof Error, e.message] }`)).toEqual([
|
|
177
|
+
true,
|
|
178
|
+
"x",
|
|
179
|
+
])
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
test("interpreter runtime failures are caught as Error values", async () => {
|
|
183
|
+
expect(await value(`try { JSON.parse("nope") } catch (e) { return e instanceof Error }`)).toBe(true)
|
|
184
|
+
expect(await value(`try { undeclared() } catch (e) { return e instanceof Error }`)).toBe(true)
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
test("caught failures carry the constructor name the real-JS failure would have", async () => {
|
|
188
|
+
// JSON.parse throws SyntaxError: name and specific-instanceof both carry through, and the
|
|
189
|
+
// message keeps the engine's position detail.
|
|
190
|
+
expect(
|
|
191
|
+
await value(`
|
|
192
|
+
try { JSON.parse("{oops") } catch (e) {
|
|
193
|
+
return [e.name, e instanceof SyntaxError, e instanceof Error, e instanceof TypeError, e.message.includes("JSON")]
|
|
194
|
+
}
|
|
195
|
+
`),
|
|
196
|
+
).toEqual(["SyntaxError", true, true, false, true])
|
|
197
|
+
expect(await value(`try { undeclared() } catch (e) { return [e.name, e instanceof ReferenceError] }`)).toEqual([
|
|
198
|
+
"ReferenceError",
|
|
199
|
+
true,
|
|
200
|
+
])
|
|
201
|
+
expect(await value(`try { const c = 1; c = 2 } catch (e) { return [e.name, e instanceof TypeError] }`)).toEqual([
|
|
202
|
+
"TypeError",
|
|
203
|
+
true,
|
|
204
|
+
])
|
|
205
|
+
expect(await value(`try { "a".normalize("NOPE") } catch (e) { return [e.name, e instanceof RangeError] }`)).toEqual(
|
|
206
|
+
["RangeError", true],
|
|
207
|
+
)
|
|
208
|
+
expect(await value(`try { "a".match("(") } catch (e) { return [e.name, e instanceof SyntaxError] }`)).toEqual([
|
|
209
|
+
"SyntaxError",
|
|
210
|
+
true,
|
|
211
|
+
])
|
|
212
|
+
expect(await value(`try { new RegExp("(") } catch (e) { return [e.name, e instanceof SyntaxError] }`)).toEqual([
|
|
213
|
+
"SyntaxError",
|
|
214
|
+
true,
|
|
215
|
+
])
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
test("diagnostics without a specific real-JS analogue are named plain Error", async () => {
|
|
219
|
+
expect(await value(`try { JSON.parse(5) } catch (e) { return [e.name, e instanceof Error] }`)).toEqual([
|
|
220
|
+
"Error",
|
|
221
|
+
true,
|
|
222
|
+
])
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
test("Promise.allSettled rejection reasons are Error values", async () => {
|
|
226
|
+
expect(
|
|
227
|
+
await value(`
|
|
228
|
+
const settled = await Promise.allSettled([Promise.reject(new Error("b"))])
|
|
229
|
+
return [settled[0].reason instanceof Error, settled[0].reason.message]
|
|
230
|
+
`),
|
|
231
|
+
).toEqual([true, "b"])
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
test("non-error thrown values are not instanceof Error", async () => {
|
|
235
|
+
expect(await value(`try { throw "raw" } catch (e) { return e instanceof Error }`)).toBe(false)
|
|
236
|
+
expect(await value(`try { throw { message: "shaped" } } catch (e) { return e instanceof Error }`)).toBe(false)
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
test("plain data is never instanceof Error", async () => {
|
|
240
|
+
expect(await value(`return [({}) instanceof Error, "s" instanceof Error, null instanceof Error]`)).toEqual([
|
|
241
|
+
false,
|
|
242
|
+
false,
|
|
243
|
+
false,
|
|
244
|
+
])
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
test("error values still serialize as plain { name, message } data", async () => {
|
|
248
|
+
expect(await value(`return new Error("m")`)).toEqual({ name: "Error", message: "m" })
|
|
249
|
+
expect(await value(`return JSON.stringify(new Error("m"))`)).toBe('{"name":"Error","message":"m"}')
|
|
250
|
+
expect(await value(`try { throw new Error("m") } catch (e) { return Object.keys(e) }`)).toEqual(["name", "message"])
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
test("spreading an error loses the brand, like losing the prototype in JS", async () => {
|
|
254
|
+
expect(await value(`const e = new Error("m"); return ({ ...e }) instanceof Error`)).toBe(false)
|
|
255
|
+
expect(await value(`const e = new Error("m"); return { ...e }`)).toEqual({ name: "Error", message: "m" })
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
test("typeof Error is function; an unknown instanceof right-hand side is a catchable error", async () => {
|
|
259
|
+
expect(await value(`return typeof Error`)).toBe("function")
|
|
260
|
+
expect(await value(`try { return 1 instanceof 5 } catch (e) { return "caught" }`)).toBe("caught")
|
|
261
|
+
const err = await error(`return 1 instanceof 5`)
|
|
262
|
+
expect(err.message).toContain("right-hand side of 'instanceof'")
|
|
263
|
+
})
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
describe("array methods: splice, fill, copyWithin, keys/values/entries", () => {
|
|
267
|
+
test("splice removes in place and returns the removed elements", async () => {
|
|
268
|
+
expect(await value(`const a = [1,2,3,4]; const removed = a.splice(1, 2); return { removed, a }`)).toEqual({
|
|
269
|
+
removed: [2, 3],
|
|
270
|
+
a: [1, 4],
|
|
271
|
+
})
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
test("splice inserts new elements at the cut", async () => {
|
|
275
|
+
expect(await value(`const a = ["a","d"]; a.splice(1, 0, "b", "c"); return a`)).toEqual(["a", "b", "c", "d"])
|
|
276
|
+
expect(await value(`const a = [1,2,3]; const removed = a.splice(1, 1, "x"); return { removed, a }`)).toEqual({
|
|
277
|
+
removed: [2],
|
|
278
|
+
a: [1, "x", 3],
|
|
279
|
+
})
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
test("splice with one argument removes to the end; negative start counts back", async () => {
|
|
283
|
+
expect(await value(`const a = [1,2,3]; const removed = a.splice(1); return { removed, a }`)).toEqual({
|
|
284
|
+
removed: [2, 3],
|
|
285
|
+
a: [1],
|
|
286
|
+
})
|
|
287
|
+
expect(await value(`const a = [1,2,3]; const removed = a.splice(-1); return { removed, a }`)).toEqual({
|
|
288
|
+
removed: [3],
|
|
289
|
+
a: [1, 2],
|
|
290
|
+
})
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
test("splice rejects inserting a container into itself", async () => {
|
|
294
|
+
const err = await error(`const a = [1]; a.splice(0, 0, [a]); return a`)
|
|
295
|
+
expect(err.kind).toBe("InvalidDataValue")
|
|
296
|
+
expect(err.message).toContain("circular")
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
test("fill overwrites a range and returns the mutated array", async () => {
|
|
300
|
+
expect(await value(`const a = [1,2,3,4]; return a.fill(0, 1, 3)`)).toEqual([1, 0, 0, 4])
|
|
301
|
+
expect(await value(`return [1,2,3].fill("z")`)).toEqual(["z", "z", "z"])
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
test("copyWithin copies a range in place", async () => {
|
|
305
|
+
expect(await value(`return [1,2,3,4,5].copyWithin(0, 3)`)).toEqual([4, 5, 3, 4, 5])
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
test("keys/values/entries return arrays usable with for...of and spread", async () => {
|
|
309
|
+
expect(await value(`return [...["x","y","z"].keys()]`)).toEqual([0, 1, 2])
|
|
310
|
+
expect(await value(`return ["x","y"].values()`)).toEqual(["x", "y"])
|
|
311
|
+
expect(
|
|
312
|
+
await value(`
|
|
313
|
+
const out = []
|
|
314
|
+
for (const [index, item] of ["a","b"].entries()) out.push(index + ":" + item)
|
|
315
|
+
return out
|
|
316
|
+
`),
|
|
317
|
+
).toEqual(["0:a", "1:b"])
|
|
318
|
+
expect(await value(`return [...[7].entries()]`)).toEqual([[0, 7]])
|
|
319
|
+
})
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
describe("string methods: localeCompare, normalize, trim aliases", () => {
|
|
323
|
+
test("localeCompare orders strings for sorting", async () => {
|
|
324
|
+
expect(await value(`return ["b","a","c"].sort((x, y) => x.localeCompare(y))`)).toEqual(["a", "b", "c"])
|
|
325
|
+
expect(await value(`return "a".localeCompare("a")`)).toBe(0)
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
test("normalize applies unicode normalization forms", async () => {
|
|
329
|
+
expect(await value(`return "\\u0065\\u0301".normalize("NFC").length`)).toBe(1)
|
|
330
|
+
expect(await value(`return "\\u00e9".normalize("NFD").length`)).toBe(2)
|
|
331
|
+
expect(await value(`return "x".normalize() === "x"`)).toBe(true)
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
test("an invalid normalize form is a clear catchable error", async () => {
|
|
335
|
+
expect(await value(`try { "x".normalize("nope"); return "no" } catch (e) { return e.message }`)).toContain('"NFC"')
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
test("trimLeft/trimRight alias trimStart/trimEnd", async () => {
|
|
339
|
+
expect(await value(`return " x ".trimLeft()`)).toBe("x ")
|
|
340
|
+
expect(await value(`return " x ".trimRight()`)).toBe(" x")
|
|
341
|
+
})
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
describe("compound assignment matches its binary operator", () => {
|
|
345
|
+
// `x op= y` must behave exactly like `x = x op y`, sharing the binary operator's coercion
|
|
346
|
+
// semantics (Dates string-coerce for `+` and use their time value for arithmetic; data
|
|
347
|
+
// objects/arrays coerce to their JS string form).
|
|
348
|
+
const pair = async (compound: string, expanded: string) => {
|
|
349
|
+
const [a, b] = await Promise.all([value(compound), value(expanded)])
|
|
350
|
+
expect(a).toEqual(b)
|
|
351
|
+
return a
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
test("sandbox Date += concatenates its string form, like d = d + 1", async () => {
|
|
355
|
+
const result = await pair(`let d = new Date(1000); d += 1; return d`, `let d = new Date(1000); d = d + 1; return d`)
|
|
356
|
+
expect(result).toBe("1970-01-01T00:00:01.000Z1")
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
test("sandbox Date numeric compound ops use its time value", async () => {
|
|
360
|
+
expect(
|
|
361
|
+
await pair(`let d = new Date(1000); d -= 400; return d`, `let d = new Date(1000); d = d - 400; return d`),
|
|
362
|
+
).toBe(600)
|
|
363
|
+
expect(await pair(`let d = new Date(1000); d /= 4; return d`, `let d = new Date(1000); d = d / 4; return d`)).toBe(
|
|
364
|
+
250,
|
|
365
|
+
)
|
|
366
|
+
})
|
|
367
|
+
|
|
368
|
+
test("string += object/array matches x = x + obj", async () => {
|
|
369
|
+
expect(await pair(`let x = "a"; x += { b: 1 }; return x`, `let x = "a"; x = x + { b: 1 }; return x`)).toBe(
|
|
370
|
+
"a[object Object]",
|
|
371
|
+
)
|
|
372
|
+
expect(await pair(`let x = "a"; x += [1, 2]; return x`, `let x = "a"; x = x + [1, 2]; return x`)).toBe("a1,2")
|
|
373
|
+
})
|
|
374
|
+
|
|
375
|
+
test("compound assignment through a member target coerces the same way", async () => {
|
|
376
|
+
expect(
|
|
377
|
+
await pair(
|
|
378
|
+
`const o = { s: "t" }; o.s += new Date(0); return o.s`,
|
|
379
|
+
`const o = { s: "t" }; o.s = o.s + new Date(0); return o.s`,
|
|
380
|
+
),
|
|
381
|
+
).toBe("t1970-01-01T00:00:00.000Z")
|
|
382
|
+
})
|
|
383
|
+
|
|
384
|
+
test("numeric and string compound operators sweep identically to their expansions", async () => {
|
|
385
|
+
const cases: Array<[string, number | string]> = [
|
|
386
|
+
[`let x = 7; x += 3; return x`, 7 + 3],
|
|
387
|
+
[`let x = 7; x -= 3; return x`, 7 - 3],
|
|
388
|
+
[`let x = 7; x *= 3; return x`, 7 * 3],
|
|
389
|
+
[`let x = 7; x /= 2; return x`, 7 / 2],
|
|
390
|
+
[`let x = 7; x %= 3; return x`, 7 % 3],
|
|
391
|
+
[`let x = 7; x **= 2; return x`, 7 ** 2],
|
|
392
|
+
[`let x = 7; x &= 3; return x`, 7 & 3],
|
|
393
|
+
[`let x = 7; x |= 8; return x`, 7 | 8],
|
|
394
|
+
[`let x = 7; x ^= 2; return x`, 7 ^ 2],
|
|
395
|
+
[`let x = 7; x <<= 2; return x`, 7 << 2],
|
|
396
|
+
[`let x = -7; x >>= 1; return x`, -7 >> 1],
|
|
397
|
+
[`let x = -7; x >>>= 1; return x`, -7 >>> 1],
|
|
398
|
+
[`let x = "a"; x += "b"; return x`, "ab"],
|
|
399
|
+
]
|
|
400
|
+
for (const [compound, expected] of cases) {
|
|
401
|
+
expect(await value(compound)).toBe(expected)
|
|
402
|
+
expect(await value(compound.replace(/x (\S+)= /, (_, op) => `x = x ${op} `))).toBe(expected)
|
|
403
|
+
}
|
|
404
|
+
})
|
|
405
|
+
})
|
|
406
|
+
|
|
407
|
+
describe("H5: builtin coercion functions work as array callbacks", () => {
|
|
408
|
+
test("filter(Boolean) drops falsy values", async () => {
|
|
409
|
+
expect(await value(`return [0, 1, "", 2, null, 3].filter(Boolean)`)).toEqual([1, 2, 3])
|
|
410
|
+
})
|
|
411
|
+
|
|
412
|
+
test("map(String) coerces each element", async () => {
|
|
413
|
+
expect(await value(`return [1, 2, 3].map(String)`)).toEqual(["1", "2", "3"])
|
|
414
|
+
})
|
|
415
|
+
|
|
416
|
+
test("arrow callbacks still work (no regression)", async () => {
|
|
417
|
+
expect(await value(`return [1, 2, 3, 4].filter(x => x % 2 === 0)`)).toEqual([2, 4])
|
|
418
|
+
expect(await value(`return [1, 2, 3].reduce((a, b) => a + b, 0)`)).toBe(6)
|
|
419
|
+
})
|
|
420
|
+
|
|
421
|
+
test("a non-callable callback is still rejected", async () => {
|
|
422
|
+
const err = await error(`return [1,2,3].map(42)`)
|
|
423
|
+
expect(err.message).toContain("callback")
|
|
424
|
+
})
|
|
425
|
+
})
|