test: 引擎核心的测试与基准

除了常规用例,有几组是专门钉住「引擎的既定事实」的,改动时会先红:

  - vmstate_test  goja 的硬限制:非原始值不是 goroutine 安全的、对象跨不了
                  Runtime。其中一条是哨兵——goja 哪天允许对象跨 Runtime 了,
                  它会失败,提醒我们可以简化设计。
  - govalue_test  Go 的切片/map 进到脚本里长什么样。这不是本库的行为而是
                  goja 的,但脚本作者照着它写代码,变了会静默走错分支。
  - safety_test   并发下坏 VM 不会被别的 goroutine 捡到。
  - bench_gobind  脚本碰 Go 对象的单次开销,README 性能一节的数据来源。

missing_global_test 里有一条断言 typeof setTimeout === "undefined":
定时器必须保持未定义,库里的特性探测才能正常降级,防止以后有人把桩加回来。
This commit is contained in:
2026-09-05 22:13:57 +08:00
parent 6ce9b483fd
commit 7f484d7a71
20 changed files with 4660 additions and 0 deletions
+96
View File
@@ -0,0 +1,96 @@
package jscriptx_test
import (
"context"
"testing"
"git.fsdpf.net/go/jscriptx"
)
// Go 的切片和 map 进到脚本里长什么样。
//
// 这不是 jscriptx 的行为,是 goja 的——但脚本作者是照着这个写代码的,一旦变了,
// 业务脚本会**静默**走错分支(比如查出来的记录突然 Array.isArray 为 false
// 整段列表被当成空的),所以在这里钉住。
//
// 之所以走 jscriptx 而不是直接用 goja:注入前还经过一层 freeze,得连那层一起验。
func TestGo值到脚本里的形状(t *testing.T) {
e, err := jscriptx.New(jscriptx.WithLogger(nil))
if err != nil {
t.Fatal(err)
}
defer e.Close()
// 照着资源查询的返回长的:[]map[string]any,里面混着切片、字符串和 nil
records := func() []map[string]any {
return []map[string]any{{
"role": "user",
"content": "你好",
"attempts": []any{map[string]any{"name": "a.pdf"}, map[string]any{"url": "http://x"}},
"tool_calls": `[{"id":"1"}]`, // 列里存的是 JSON 字符串
"reasoning": nil,
}}
}
s, err := e.Compile("shape.ts", `
export default class C {
Run() {
const rows = getRecords()
const r = rows[0]
return {
// 切片是真数组:length / for-of / map 都能用
rowsIsArray: Array.isArray(rows),
rowsLen: rows.length,
attemptsIsArray: Array.isArray(r.attempts),
attemptsLen: r.attempts.length,
mapped: r.attempts.map(a => a.name || a.url).join(","),
// map[string]any 的值按 Go 的类型各自转换,不会统一成字符串
contentType: typeof r.content,
toolCallsType: typeof r.tool_calls,
// Go 的 nil 是 null,不是 undefined——判空要用 == null 或 === null
reasoningIsNull: r.reasoning === null,
reasoningIsUndef: r.reasoning === undefined,
// 没有的列是 undefined,不会报错
missingIsUndef: r.nope === undefined,
}
}
}`)
if err != nil {
t.Fatal(err)
}
ctx := jscriptx.WithScope(context.Background(),
jscriptx.ScopeGlobals(map[string]any{"getRecords": records}))
obj, err := s.New(ctx)
if err != nil {
t.Fatal(err)
}
defer obj.Close()
var got map[string]any
if err := obj.CallInto(ctx, "Run", &got); err != nil {
t.Fatal(err)
}
want := map[string]any{
"rowsIsArray": true,
"rowsLen": int64(1),
"attemptsIsArray": true,
"attemptsLen": int64(2),
"mapped": "a.pdf,http://x",
"contentType": "string",
"toolCallsType": "string",
"reasoningIsNull": true,
"reasoningIsUndef": false,
"missingIsUndef": true,
}
for k, w := range want {
if got[k] != w {
t.Errorf("%s = %#v,该是 %#v", k, got[k], w)
}
}
}