Agent 框架后端低代码RAG【免费下载链接】yao✨ All your agents and workspaces in one place, on every device you own. Track tasks on a board, accessible from desktop, mobile, browser, or API. Self-hosted.项目地址https://gitcode.com/gh_mirrors/ya/yao点击查看免费下载导读本文是 Yao 开源仓库 sandbox/v2/TEST.md 的完整技术解读全面覆盖 Sandbox V2 容器沙箱模块的测试设计从黑盒测试原则、Docker/Tai 环境准备、统一测试辅助函数到 Manager、Box、gRPC 等各层的必测用例、Makefile 集成与 CI 流水线编排。读完本文你将掌握如何为本模块编写可复用、可跳过、可覆盖远程节点的集成测试并理解底层源码中容器生命周期、心跳恢复、OAuth 令牌注入等机制为何如此设计。一、测试设计五原则黑盒、真实容器、可跳过Sandbox V2 的测试策略在 TEST.md 中确立了五条核心原则每一条都能在源码中找到对应依据黑盒测试Black-box testing所有*_test.go文件使用package sandbox_test而非内部包package sandbox测试只能访问导出的公开 API。这样保证测试不依赖未导出的内部实现细节重构内部代码时测试无需改动。真实容器Real containers测试通过 tai SDK 创建真实的 Docker 容器不进行任何 mock。这意味着测试结果就是生产环境真实行为的缩影。条件跳过Skip when unavailable通过skipIfNoDocker(t)/skipIfNoTai(t)两个辅助函数在本地开发环境缺少 Docker 或 Tai 节点时自动跳过相关测试保证 CI 有完整环境、本地开发无环境也能跑通其余用例。测试跟随实现Tests follow implementation*_test.go与被测代码文件放在同一目录下例如sandbox_test.go紧挨sandbox.go、manager_test.go紧挨manager.go。覆盖率门槛Coverage 80%单文件与整体覆盖率目标均为 80% 以上。从源码看黑盒原则的一个实际体现是 sandbox.go 只暴露两个函数Init()初始化全局 Manager与M()获取全局单例未调用 Init 时直接 panic。测试正是围绕这两个公开入口展开例如sandbox_test.go需要验证Init成功、M()在未初始化前 panic、以及重复Init的安全性。而skipIfNoDocker(t)等辅助函数集中定义在 testutils_test.go 中。该文件同时定义了TestMain在全部测试启动前执行两项关键准备工作func TestMain(m *testing.M) { testprepare.MustLoadEnv() // 加载 YAO_TEST_APPLICATION 等环境变量 sandboxtest.PurgeStaleContainers(sb-) // 清理上次运行残留的 sb- 前缀容器 code : m.Run() testprepare.Cleanup() os.Exit(code) }其中PurgeStaleContainers(sb-)会清理所有以sb-开头的遗留容器保证测试从干净状态开始——这正是「真实容器」原则下的必要前置清理。二、环境准备Docker、Tai 与环境变量2.1 加载环境所有测试开始前需要先加载 Yao 的开发环境变量source $YAO_SOURCE_ROOT/env.local.sh2.2 Docker所有容器测试必需Docker daemon 必须处于运行状态测试通过默认 socket 连接。没有 Docker 时容器类测试会被skipIfNoDocker跳过。2.3 Tai仅远程模式测试必需远程模式测试需要一个 Tai 节点容器它同时提供远端容器运行时、gRPC 转发、WebSocket/SSE 代理等能力docker run -d --name tai \ -v /var/run/docker.sock:/var/run/docker.sock \ -p 2375:2375 -p 9100:9100 -p 8080:8080 -p 6080:6080 \ yaoapp/tai:latest端口说明2375为 Docker 远程 API9100为 Tai 服务端口8080为 HTTP 代理6080为 VNC/WebSocket 相关端口。2.4 环境变量一览变量用途默认值YAO_TEST_APPLICATIONyao-dev-app的路径—必填YAO_DB_DRIVER/YAO_DB_PRIMARY数据库连接—必填YAO_JWT_SECRET/YAO_DB_AESKEY加密密钥用于创建 OAuth 令牌—必填SANDBOX_TEST_IMAGE测试用容器镜像yaoapp/sandbox-v2-test:latestSANDBOX_TEST_REMOTE_ADDRTai 远程地址如tai://127.0.0.1—为空则跳过远程测试TAI_TEST_HOSTTai HTTP 代理主机127.0.0.1注意SANDBOX_TEST_REMOTE_ADDR是「远程测试的开关」为空时skipIfNoTai直接跳过远程相关用例。这与 Makefile 中test-remote目标的逻辑一致——未设置该变量时直接输出跳过提示并退出。三、目录结构与测试文件职责sandbox/v2/ ├── sandbox.go ├── sandbox_test.go # Init/M 单例测试 ├── manager.go ├── manager_test.go # Create/Get/GetOrCreate/List/Remove、池管理 ├── manager_lifecycle_test.go # 启动恢复、Cleanup、空闲跟踪、心跳 ├── box.go ├── box_test.go # Exec/Stream/Workspace/Proxy/VNC、生命周期 ├── box_attach_test.go # Attach WS/SSE需要容器内服务 ├── config.go ├── types.go ├── errors.go ├── grpc.go ├── grpc_test.go # OAuth 令牌创建/吊销、环境变量构建 ├── testutils_test.go # 共享测试辅助函数未导出package sandbox_test └── DESIGN.md当前仓库 sandbox/v2 实际还包含watcher.go、host.go、convert.go、export_test.go等文件且测试文件按模式拆分更细如manager_unit_test.go、box_unit_test.go、manager_sandbox_test.go、host_sandbox_test.go等。文档中的这份结构图是模块的经典划分实际文件在演进中进一步细分了「纯单元测试」与「需真实容器的沙箱集成测试」。四、testutils共享测试辅助函数testutils_test.go不是独立包而是直接位于package sandbox_test内的共享辅助代码。核心函数签名与用途如下// skipIfNoDocker skips the test if Docker is not available. func skipIfNoDocker(t *testing.T) // skipIfNoTai skips the test if SANDBOX_TEST_REMOTE_ADDR is empty. func skipIfNoTai(t *testing.T) // testImage returns SANDBOX_TEST_IMAGE or yaoapp/sandbox-v2-test:latest. func testImage() string // setupManager initializes sandbox with a local pool, returns cleanup func. // Calls sandbox.Init sandbox.M().Start. func setupManager(t *testing.T) func() // setupManagerWithRemote initializes sandbox with local remote pools. func setupManagerWithRemote(t *testing.T) func() // createTestBox creates a box with defaults and returns it. Registers t.Cleanup for removal. func createTestBox(t *testing.T, opts ...sandbox.CreateOption) *sandbox.Box这些辅助函数奠定了「三行起步」的测试范式跳过判断 初始化 Manager 创建 Box。setupManager内部调用sandbox.Init()sandbox.M().Start()对应源码 manager.go 中Start的行为从 tai/registry 中枚举已注册节点通过recoverBoxes恢复既有容器。也就是说每次测试的 Manager 启动都是一次完整的「容器发现 状态恢复」演练。createTestBox注册了t.Cleanup测试结束后自动移除容器避免资源泄漏。五、如何编写测试六类典型模式5.1 标准模式Manager 层// manager_test.go package sandbox_test import ( context testing github.com/stretchr/testify/assert github.com/stretchr/testify/require sandbox github.com/yaoapp/yao/sandbox/v2 ) func TestCreate(t *testing.T) { skipIfNoDocker(t) cleanup : setupManager(t) defer cleanup() box, err : sandbox.M().Create(context.Background(), sandbox.CreateOptions{ Image: testImage(), Owner: test-user, }) require.NoError(t, err) defer box.Remove(context.Background()) assert.NotEmpty(t, box.ID()) assert.Equal(t, test-user, box.Owner()) } func TestCreate_NoImage(t *testing.T) { skipIfNoDocker(t) cleanup : setupManager(t) defer cleanup() _, err : sandbox.M().Create(context.Background(), sandbox.CreateOptions{}) assert.Error(t, err) // Image is required }源码佐证Create在 manager.go 中第一行就校验opts.Image 并返回错误「sandbox: image is required」。Create的完整链路是解析节点 → 生成 ID缺省为sb-纳秒时间戳→ 调用 tai Runtime 创建容器 → 启动容器 →waitEntrypoint等待入口点完成 UID 映射 → 构造Box存入boxesmap。其中waitEntrypoint的实现很能体现测试价值它轮询探测容器内/workspace属主 UID 是否与sandbox用户 UID 一致兼容 Docker Desktop VirtioFS 下 UID 0 的情况最多重试 9 次、退避递增至 2 秒。如果测试容器镜像的 entrypoint 行为异常这类用例会第一时间暴露。5.2 容器执行测试Exec// box_test.go package sandbox_test func TestExec(t *testing.T) { skipIfNoDocker(t) cleanup : setupManager(t) defer cleanup() box : createTestBox(t) result, err : box.Exec(context.Background(), []string{echo, hello}) require.NoError(t, err) assert.Equal(t, 0, result.ExitCode) assert.Equal(t, hello\n, result.Stdout) assert.Empty(t, result.Stderr) } func TestExec_NonZeroExit(t *testing.T) { skipIfNoDocker(t) cleanup : setupManager(t) defer cleanup() box : createTestBox(t) result, err : box.Exec(context.Background(), []string{sh, -c, exit 42}) require.NoError(t, err) assert.Equal(t, 42, result.ExitCode) }源码佐证Box.Exec位于 box.go内部会先touch()刷新活跃时间戳再通过injectGRPCAddr注入YAO_GRPC_ADDR等环境变量最后以User: sandbox身份在容器内执行命令。值得注意的细节Exec 对 Docker 的 409 冲突做了最多 3 次幂等重试每次间隔 500ms因为 Docker 在上一 exec 会话尚未排空时可能返回 409。这正是TestExec这类用例能稳定通过的原因之一。5.3 流式测试Streamfunc TestStream(t *testing.T) { skipIfNoDocker(t) cleanup : setupManager(t) defer cleanup() box : createTestBox(t) s, err : box.Stream(context.Background(), []string{sh, -c, echo a; sleep 0.1; echo b}) require.NoError(t, err) out, _ : io.ReadAll(s.Stdout) code, err : s.Wait() assert.NoError(t, err) assert.Equal(t, 0, code) assert.Contains(t, string(out), a\n) assert.Contains(t, string(out), b\n) } func TestStream_Cancel(t *testing.T) { skipIfNoDocker(t) cleanup : setupManager(t) defer cleanup() box : createTestBox(t) s, err : box.Stream(context.Background(), []string{sleep, 60}) require.NoError(t, err) s.Cancel() code, _ : s.Wait() assert.NotEqual(t, 0, code) // killed } func TestStream_Stdin(t *testing.T) { skipIfNoDocker(t) cleanup : setupManager(t) defer cleanup() box : createTestBox(t) s, err : box.Stream(context.Background(), []string{cat}) require.NoError(t, err) s.Stdin.Write([]byte(hello\n)) s.Stdin.Close() out, _ : io.ReadAll(s.Stdout) code, _ : s.Wait() assert.Equal(t, 0, code) assert.Equal(t, hello\n, string(out)) }ExecStream类型见 types.go暴露Stdout/Stderr/Stdin三个 I/O 句柄与Wait()、Cancel()两个控制方法覆盖了「读输出、取消进程、写标准输入」三条主路径。源码中Stream还有一处健壮性处理当底层返回「500 Internal Server Error」时会先检查容器真实状态inspectStatus若容器仍在运行则等待 2 秒后重试一次避免瞬时错误误伤测试。5.4 工作区测试Workspacefunc TestWorkspace_ReadWrite(t *testing.T) { skipIfNoDocker(t) cleanup : setupManager(t) defer cleanup() box : createTestBox(t) ws : box.Workspace() err : ws.WriteFile(test.txt, []byte(hello), 0644) require.NoError(t, err) data, err : fs.ReadFile(ws, test.txt) require.NoError(t, err) assert.Equal(t, hello, string(data)) } func TestWorkspace_MkdirAll(t *testing.T) { skipIfNoDocker(t) cleanup : setupManager(t) defer cleanup() box : createTestBox(t) ws : box.Workspace() err : ws.MkdirAll(a/b/c, 0755) require.NoError(t, err) info, err : fs.Stat(ws, a/b/c) require.NoError(t, err) assert.True(t, info.IsDir()) } func TestWorkspace_WalkDir(t *testing.T) { skipIfNoDocker(t) cleanup : setupManager(t) defer cleanup() box : createTestBox(t) ws : box.Workspace() ws.MkdirAll(src, 0755) ws.WriteFile(src/main.go, []byte(package main), 0644) ws.WriteFile(src/util.go, []byte(package main), 0644) var files []string fs.WalkDir(ws, src, func(path string, d fs.DirEntry, err error) error { if !d.IsDir() { files append(files, path) } return nil }) assert.Len(t, files, 2) }Workspace()返回的是fs.FS兼容的文件系统接口见 box.go底层由taiworkspace.New(res.Volume, sessionID)实现。关键设计是session 的选择优先使用挂载的workspaceID否则回退到sandboxID向后兼容。因此测试可以直接用标准库io/fs的函数操作容器内文件系统——这本身就是对接口设计的验证。5.5 生命周期测试Lifecyclefunc TestIdleCleanup_Session(t *testing.T) { skipIfNoDocker(t) cleanup : setupManager(t) defer cleanup() box, err : sandbox.M().Create(context.Background(), sandbox.CreateOptions{ Image: testImage(), Policy: sandbox.Session, IdleTimeout: 2 * time.Second, }) require.NoError(t, err) // Box exists _, err sandbox.M().Get(context.Background(), box.ID()) assert.NoError(t, err) // Wait for idle cleanup cycle time.Sleep(4 * time.Second) sandbox.M().Cleanup(context.Background()) // Box should be gone _, err sandbox.M().Get(context.Background(), box.ID()) assert.ErrorIs(t, err, sandbox.ErrNotFound) } func TestStartRecovery(t *testing.T) { skipIfNoDocker(t) // Phase 1: create a box, then shut down Manager cleanup1 : setupManager(t) box, err : sandbox.M().Create(context.Background(), sandbox.CreateOptions{ Image: testImage(), Owner: recovery-test, }) require.NoError(t, err) boxID : box.ID() cleanup1() // closes Manager, but container stays // Phase 2: new Manager, Start should discover the container cleanup2 : setupManager(t) defer cleanup2() recovered, err : sandbox.M().Get(context.Background(), boxID) require.NoError(t, err) assert.Equal(t, boxID, recovered.ID()) assert.Equal(t, recovery-test, recovered.Owner()) // Clean up recovered.Remove(context.Background()) } func TestHeartbeat(t *testing.T) { skipIfNoDocker(t) cleanup : setupManager(t) defer cleanup() box : createTestBox(t) // Simulate heartbeat err : sandbox.M().Heartbeat(box.ID(), true, 3) assert.NoError(t, err) info, _ : box.Info(context.Background()) assert.Equal(t, 3, info.ProcessCount) } func TestHeartbeat_NotFound(t *testing.T) { skipIfNoDocker(t) cleanup : setupManager(t) defer cleanup() err : sandbox.M().Heartbeat(nonexistent, true, 1) assert.ErrorIs(t, err, sandbox.ErrNotFound) }这三个用例分别验证了生命周期的三大机制Session 空闲回收Policy: sandbox.Session配合 2 秒空闲超时4 秒后调用Cleanup容器应被移除。源码中空闲回收的真实执行者是 watcher.go 注册的sandboxWatcher——它以 30 秒为间隔轮询所有 Box对Session策略的空闲容器执行mgr.Remove对LongRunning策略的空闲容器执行b.Stop停止而非删除对OneShot策略则保留最长DefaultOneShotMaxAge8 小时作为清理失败的安全网。四种策略的默认超时定义在 types.goDefaultStopTimeout2s、DefaultSessionIdleTimeout30min、DefaultLongRunningIdleTimeout2h、DefaultOneShotMaxAge8h、DefaultLifetimeGrace5min。启动恢复recoverBoxesmanager.go通过查询标签managed-byyao-sandbox的容器读取sandbox-id、sandbox-owner、sandbox-policy等标签重建 Box。这就是TestStartRecovery中「Manager 关闭后容器仍在、重启后能找回」的底层原理。另外值得注意的是systemInfoFromLabels——恢复时优先从容器标签还原系统信息不依赖 Tai 节点在线。心跳更新Heartbeat(sandboxID, active, processCount)manager.go在activetrue时刷新lastHeartbeat并更新进程数Box 不存在时返回ErrNotFound。5.6 池管理测试Pool Managementfunc TestPoolLimits_MaxTotal(t *testing.T) { skipIfNoDocker(t) // Init with MaxTotal1 err : sandbox.Init(sandbox.Config{ Pool: []sandbox.Pool{{ Name: limited, Addr: local, MaxTotal: 1, }}, }) require.NoError(t, err) sandbox.M().Start(context.Background()) defer sandbox.M().Close() box1, err : sandbox.M().Create(context.Background(), sandbox.CreateOptions{ Image: testImage(), }) require.NoError(t, err) defer box1.Remove(context.Background()) _, err sandbox.M().Create(context.Background(), sandbox.CreateOptions{ Image: testImage(), }) assert.ErrorIs(t, err, sandbox.ErrLimitExceeded) } func TestAddPool(t *testing.T) { skipIfNoDocker(t) cleanup : setupManager(t) defer cleanup() err : sandbox.M().AddPool(context.Background(), sandbox.Pool{ Name: new-pool, Addr: local, }) assert.NoError(t, err) pools : sandbox.M().Pools() names : make([]string, len(pools)) for i, p : range pools { names[i] p.Name } assert.Contains(t, names, new-pool) } func TestRemovePool_InUse(t *testing.T) { skipIfNoDocker(t) cleanup : setupManager(t) defer cleanup() box : createTestBox(t) _ box err : sandbox.M().RemovePool(context.Background(), local, false) assert.ErrorIs(t, err, sandbox.ErrPoolInUse) }池管理测试验证了三类约束容量上限MaxTotal1时第二个容器创建返回ErrLimitExceeded、动态增池AddPool后Pools()列表包含新池、在用保护池内存在容器时RemovePool返回ErrPoolInUse。5.7 多池测试Multi-Pool / 远程模式func TestMultiNode(t *testing.T) { skipIfNoDocker(t) skipIfNoTai(t) cleanup : setupManagerWithRemote(t) defer cleanup() // Create on local local, err : sandbox.M().Create(context.Background(), sandbox.CreateOptions{ Image: testImage(), Pool: local, }) require.NoError(t, err) defer local.Remove(context.Background()) // Create on remote remote, err : sandbox.M().Create(context.Background(), sandbox.CreateOptions{ Image: testImage(), Pool: remote, }) require.NoError(t, err) defer remote.Remove(context.Background()) // Both should exec r1, _ : local.Exec(context.Background(), []string{echo, local}) r2, _ : remote.Exec(context.Background(), []string{echo, remote}) assert.Equal(t, local\n, r1.Stdout) assert.Equal(t, remote\n, r2.Stdout) }这是唯一同时调用skipIfNoDocker与skipIfNoTai的用例本地与远程两个池各自创建容器并执行命令。它完整验证了「一个 Manager 管理多个节点容器」的核心能力对应 manager.go 中节点连接委托给tai/registry的设计——每个节点提供独立的 Runtime、Volume、Proxy、VNC 能力。5.8 OAuth / gRPC 环境注入测试func TestBuildGRPCEnv_Local(t *testing.T) { env : sandbox.BuildGRPCEnv(sandbox.Pool{Addr: local}, sb-001, tok, ref) assert.Equal(t, sb-001, env[YAO_SANDBOX_ID]) assert.Equal(t, tok, env[YAO_TOKEN]) assert.Equal(t, ref, env[YAO_REFRESH_TOKEN]) assert.NotEmpty(t, env[YAO_GRPC_ADDR]) } func TestBuildGRPCEnv_Remote(t *testing.T) { env : sandbox.BuildGRPCEnv(sandbox.Pool{Addr: tai://gpu.internal}, sb-002, tok, ref) assert.NotEmpty(t, env[YAO_GRPC_ADDR]) } func TestCreateContainerTokens(t *testing.T) { // Requires Yao runtime for OAuth cleanup : setupManager(t) defer cleanup() access, refresh, err : sandbox.CreateContainerTokens(sb-test, user-1) require.NoError(t, err) assert.NotEmpty(t, access) assert.NotEmpty(t, refresh) }gRPC 环境注入是 Sandbox V2 的「身份打通」机制。源码 grpc.go 的BuildGRPCEnv展示了完整逻辑所有容器固定通过host.tai.internal访问宿主机由 Tai 在创建容器时注入该 DNSlocal 模式YAO_GRPC_ADDR host.tai.internal:Yao gRPC 端口缺省 9099且当 Yao 配置了 gRPC 证书时额外注入YAO_GRPC_TLStruetunnel/cloud 模式使用节点注册时上报的Ports.GRPC由 Tai Gateway 转发到 Yao同时注入YAO_SANDBOX_ID、CTX_CHAT_ID、CTX_WORKSPACE_ID供容器内进程识别上下文。Exec时还有一层动态注入injectGRPCAddrbox.go每次执行前从 registry 读取最新的 gRPC 端口即使 Tai 重启换端口也不会失联——但若调用方显式设置了YAO_GRPC_ADDR则尊重显式值不覆盖。这是文档测试用例背后的深层设计逻辑。六、必测用例清单Required Test Cases文件必测用例sandbox_test.goInit成功 /M()在 Init 前 panic / 重复 Init 安全manager_test.goCreate / 显式 ID 创建 / 无镜像创建报错/ Get / Get 未找到 / GetOrCreate / List / 按 Owner 过滤 List / Remove / 池上限 MaxTotal / 池上限 MaxPerUser / AddPool / RemovePool / 在用池 RemovePool报错/ Poolsmanager_lifecycle_test.go从标签启动恢复 / Cleanup Session 空闲 / Cleanup LongRunning 停止后移除 / Persistent 永不清理 / 心跳更新 / 心跳未找到 / OneShot Exec 后自动移除box_test.goExec 成功 / Exec 非零退出 / Exec 指定 WorkDir / Exec 指定 Env / Exec 超时 / Stream 读取 / Stream 取消 / Stream 标准输入 / Workspace 读写 / Workspace MkdirAll / Workspace Remove / Workspace Rename / Workspace WalkDir / VNC无 VNC 镜像则跳过/ Proxy URL / StartStopStart / Infobox_attach_test.goAttach WS无 WS 服务器镜像则跳过/ Attach SSE无 SSE 服务器镜像则跳过grpc_test.goBuildGRPCEnv 本地 / BuildGRPCEnv 远程 / CreateContainerTokens / RevokeContainerTokens注意「无则跳过」的用例VNC、WS、SSE 三类依赖测试镜像内置的特殊服务若镜像中不存在对应服务则跳过避免假失败。这体现了文档「测试跟随能力」的设计思想。七、Makefile 集成覆盖率聚合TEST.md 给出了在仓库根 Makefile 中添加unit-test-sandbox-v2目标的标准模板TESTFOLDER_SANDBOX_V2 : $(shell $(GO) list ./sandbox/v2/...) .PHONY: unit-test-sandbox-v2 unit-test-sandbox-v2: echo mode: count coverage.out for d in $(TESTFOLDER_SANDBOX_V2); do \ $(GO) test -tags $(TESTTAGS) -v -timeout10m \ -covermodecount -coverprofileprofile.out \ -coverpkg$$d \ $$d tmp.out; \ cat tmp.out; \ if grep -q ^--- FAIL tmp.out; then \ rm tmp.out; \ exit 1; \ elif grep -q build failed tmp.out; then \ rm tmp.out; \ exit 1; \ fi; \ if [ -f profile.out ]; then \ cat profile.out | grep -v mode: coverage.out; \ rm tmp.out; \ fi; \ done该目标的关键设计-timeout10m容器测试耗时较长需放宽超时-covermodecount -coverpkg$$d按包统计覆盖率多包结果聚合到coverage.outgrep ^--- FAIL与grep build failed双保险任一测试失败或编译失败立即退出并返回非零码保证 CI 中断。仓库实际已实现该目标根 Makefile 中unit-test-sandbox-v2: unit-test-tai unit-test-workspace声明了依赖顺序先跑 tai 与 workspace 的测试。sandbox/v2/Makefile 还提供了更细粒度的本地目标make test-local仅本地 Docker 模式-run /localmake test-remote远程模式未设置SANDBOX_TEST_REMOTE_ADDR时自动跳过make test双模式全量测试make test-ci带覆盖率聚合的 CI 版本。八、CI 集成GitHub Actions 编排TEST.md 给出了完整的sandbox-v2-testjob 定义用于unit-test.yml与pr-test.ymlsandbox-v2-test: runs-on: ubuntu-latest services: mongodb: image: mongo:6.0 ports: - 27017:27017 env: MONGO_INITDB_ROOT_USERNAME: root MONGO_INITDB_ROOT_PASSWORD: 123456 MONGO_INITDB_DATABASE: test strategy: matrix: go: [1.25] steps: - uses: actions/checkoutv4 - uses: actions/setup-gov5 with: go-version: ${{ matrix.go }} - name: Start Tai container run: | docker run -d --name tai \ -v /var/run/docker.sock:/var/run/docker.sock \ -p 2375:2375 -p 9100:9100 -p 8080:8080 -p 6080:6080 \ yaoapp/tai:latest sleep 3 - name: Build V2 test image run: | cd sandbox/docker bash build.sh v2 - name: Setup ENV run: | echo YAO_DB_DRIVERsqlite3 $GITHUB_ENV echo YAO_DB_PRIMARY${{ github.WORKSPACE }}/../app/db/yao.db $GITHUB_ENV echo SANDBOX_TEST_IMAGEyaoapp/sandbox-v2-test:latest $GITHUB_ENV echo SANDBOX_TEST_REMOTE_ADDRtai://127.0.0.1 $GITHUB_ENV echo TAI_TEST_HOST127.0.0.1 $GITHUB_ENV mkdir -p ${{ github.WORKSPACE }}/../app/db - name: Run Sandbox V2 Tests run: make unit-test-sandbox-v2 - name: Codecov Report uses: codecov/codecov-actionv4 with: token: ${{ secrets.CODECOV_TOKEN }}CI 编排中的关键决策原文明确列出仅 SQLite沙箱是基础设施而非数据模型测试不需要多数据库矩阵用 SQLite 降低成本与抖动Tai 容器提供远程模式完整走一遍代理链路proxy path让TestMultiNode等远程用例在 CI 中真实执行sandbox-v2-test作为默认测试镜像内置tai心跳上报、openai-proxy、Nginx、WS echo 与 SSE 测试服务一次拉起即可覆盖 Exec、Attach、Proxy、Heartbeat 全部能力CI 从源码构建测试镜像StepBuild V2 test imagebash build.sh v2保证测试镜像与最新 tai SDK 变更保持二进制兼容避免「镜像旧、代码新」导致的假阴性**Attach 测试WS/SSE**直接复用测试镜像的内置服务无需额外起服务容器。九、覆盖率策略覆盖率目标单文件 80%、整体 80%。TEST.md 对每个文件的覆盖路径做了明确规划sandbox.go单例通过sandbox_test.go覆盖——Init、M、panic 路径、重复 Initmanager.go最重的文件必须有独立的manager_test.gomanager_lifecycle_test.go双文件覆盖因为Create的完整链路涉及节点解析、镜像检查、容器创建、entrypoint 等待、标签写入等多个分支box.go通过box_test.go覆盖所有 tai SDK 集成点Exec/Stream/Workspace/VNC/Proxy/Start/Stop/Infogrpc.go纯单元测试即可覆盖令牌生成、环境构建无需真实容器——这正是「按测试成本分级」的体现。值得补充的是当前仓库在演进中还出现了export_test.go导出内部函数供黑盒包测试与各类*_unit_test.go/*_sandbox_test.go拆分——例如manager_unit_test.go直接对ExportDefaultWorkspaceName、ExportSystemInfoFromLabels做纯单元验证无需 Docker。这种「无环境可跑的单元测试 需环境的沙箱测试」分层正是达成 80% 覆盖率且不让 CI 过度依赖外部资源的实践路径。十、运行测试的完整命令# 所有 sandbox v2 测试仅本地 Docker make unit-test-sandbox-v2 # 带远程模式先启动 Tai SANDBOX_TEST_REMOTE_ADDRtai://127.0.0.1 make unit-test-sandbox-v2 # 单文件 go test -v ./sandbox/v2/ -run TestCreate # 单个测试 go test -v ./sandbox/v2/ -run TestExec_NonZeroExit # 竞态检测 go test -race -v ./sandbox/v2/实践建议本地开发阶段优先用go test -v ./sandbox/v2/ -run TestCreate之类的定向运行秒级反馈提交前用make test-local验证本地池路径若配置了 Tai 节点再加SANDBOX_TEST_REMOTE_ADDR跑make test双模式涉及并发与心跳的用例建议加-race因为Box内部大量使用atomic.Int64/atomic.Int32/sync.Mapmanager.go、box.go竞态检测能捕获lastCall、processCount等并发更新的隐患。十一、与源码的对应关系速查测试关注点底层实现单例初始化 / panic 语义sandbox.goInit/M创建、恢复、心跳、节点解析manager.goCreate/recoverBoxes/Heartbeat/getNode生命周期清理、OneShot 安全网watcher.gosandboxWatcher.CheckExec/Stream/Workspace/VNC/Proxybox.goExec/Stream/Workspace/VNC/ProxygRPC 环境注入、TLS 判定grpc.goBuildGRPCEnv/resolveGRPCAddr/resolveGRPCTLS生命周期策略与默认超时types.goOneShot/Session/LongRunning/Persistent及默认值错误语义ErrNotFound等errors.go测试辅助函数与 TestMain 清理testutils_test.go本地/远程/CI 测试目标Makefile 与根 Makefile 的unit-test-sandbox-v2十二、设计思路总结Sandbox V2 的测试体系可以概括为三个层次无环境层grpc_test.go中的环境构建、manager_unit_test.go中的标签解析等纯单元测试任何机器可跑是覆盖率的基本盘单机容器层依赖 Docker 的box_test.go、manager_test.go通过skipIfNoDocker优雅降级多节点层依赖 Tai 的远程用例TestMultiNode、Attach 等通过skipIfNoTai与 CI 中的 Tai 容器保证在流水线内真实执行。这种分层配合「真实容器、无 mock、黑盒访问、覆盖率 80%」的原则既保证了测试结果与生产行为高度一致又让本地开发、PR 校验、发布流水线各取所需。对希望为本模块贡献测试的开发者而言最快路径是阅读 TEST.md 的必测用例表对照 manager_test.go、box_test.go、grpc_test.go 的既有模式套用setupManagercreateTestBox三板斧即可写出风格一致的用例。更深入的设计背景可进一步阅读同目录的 DESIGN.md 与 IMPL.md。赞分享Agent 框架后端低代码RAG【免费下载链接】yao✨ All your agents and workspaces in one place, on every device you own. Track tasks on a board, accessible from desktop, mobile, browser, or API. Self-hosted.项目地址https://gitcode.com/gh_mirrors/ya/yao点击查看免费下载相关推荐linkding 网页归档实战服务端 HTML 快照与 Singlefile 扩展集成指南linkding 网页归档实战服务端 HTML 快照与 Singlefile 扩展集成指南 导读 本文基于 linkding 官方文档 archiving.mAgent 框架后端低代码RAGRancher 集成测试完全指南从 make ci 全量验证到本地调试tests/v2/integration 深度解析Rancher 集成测试完全指南从 make ci 全量验证到本地调试tests/v2/integration 深度解析 导读 本文围绕 Rancher云原生容器编排集群管理后端phpMyAdmin 运行环境要求与 PHP 扩展配置完整指南基于 docs/require.rstphpMyAdmin 运行环境要求与 PHP 扩展配置完整指南基于 docs/require.rst 本文以 phpMyAdmin 官方文档 docs/re数据库后端上一篇TREK 旅行预算与费用分摊完全指南多币种记账、成员分摊与自动结算Costs / Budget下一篇Dozzle 简单认证Simple Auth完全指南users.yml 用户管理、角色权限与安全配置创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
企业数字化 ERP 产品动态
相关推荐
网站建设公司推广方式实战案例:告别拖沓,提升转化 网站建设公司推广方式实战案例:告别拖沓,提升转化 改个需求建站公司拖一周,客户投诉率飙升,你的推广费全打水漂了?这种痛,做网站建设的朋友太懂。很多同行还在靠刷脸或硬广,效率低且不稳定。我见过太多 实战案例… · 2026/9/27 21:59:06
【VLA工程】(7)—— 边缘侧推理延迟与控制周期对齐 【VLA工程】(7)—— 边缘侧推理延迟与控制周期对齐 文章目录【VLA工程】(7)—— 边缘侧推理延迟与控制周期对齐1. 延迟与控制周期先分别说明2. 周期内链路与预算2.1 控制周期选型的起步对照3. 观测时刻与动作生效要对齐4. 超限时的… · 2026/9/27 21:59:06
网站开发引发的官司复盘图解步骤避坑指南 网站开发引发的官司复盘图解步骤避坑指南 ICP备案流程一头雾水,导致网站上线延期三个月,进而引发合同纠纷索赔,这是去年我接手的一个典型烂摊子。很多甲方觉得建站就是写代码,殊不知合规与部署细节才是雷区。今天不聊虚的,直接拆解这个【网站开发引发… · 2026/9/28 0:20:29
网站后台系统有哪些?懂性能优化才不踩坑 网站后台系统有哪些?懂性能优化才不踩坑 模板网站太丑,更别提后台管理混乱,这是很多站长和项目经理的噩梦。你以为买个模板就能省事?结果上线后才发现,改个文案要等半天,加个功能得加钱,最要命的是 性能优化 几乎无从下手。… · 2026/9/28 0:19:53
学编程做网站自研比外包省多少钱3步搞定域名服务器 学编程做网站自研比外包省多少钱3步搞定域名服务器 改个需求建站公司拖一周,你盯着邮件干着急,心里盘算着这单外包费到底值不值。很多项目经理朋友问我,自己 学编程做网站 到底 多少钱… · 2026/9/28 0:19:47
不会代码想建站?网站开发包括哪些环节全解析 不会代码想建站?网站开发包括哪些环节全解析 自己不会代码想做网站,这是无数老板和创业者最头疼的坎。别被那些高大上的术语吓住,其实 网站开发包括哪些 环节,核心就是解决从想法到落地的过程。很多新手在 对比评测… · 2026/9/28 0:19:35
从零搭建安全防线:如何入侵自己做的网站防挂马实战 从零搭建安全防线:如何入侵自己做的网站防挂马实战 网站被黑挂马却毫无头绪?别慌,这行干久了谁没遇到过。很多站长从 从零搭建 网站开始,只盯着功能实现,却忽略了安全漏洞,结果上线没几天,页面就被塞满博彩广告或木马文件。今天不讲虚的,直接拆解… · 2026/9/28 0:19:35
MATLAB雷达信号脉冲压缩仿真:LFM线性调频、匹配滤波与距离分辨率实现 简介:这套Matlab仿真工具完整呈现雷达信号脉冲压缩过程,从线性调频(LFM)信号生成、目标回波仿真到匹配滤波压缩处理均有可运行代码支撑,面向电子信息工程、计算机、数学等专业学生,适用于课程设计、期末大作… · 2026/9/27 0:00:01
汕头网站建设制作厂家避坑指南:5大注意事项救急 汕头网站建设制作厂家避坑指南:5大注意事项救急 改个需求建站公司拖一周,这种憋屈事我见得太多了。 很多汕头老板找本地建站团队,签合同前看着方案挺美,一上线就变脸。 今天不聊虚的,直接拆解找 汕头网站建设制作厂家 时的5个核心 注意事项… · 2026/9/27 0:00:01
多模态虚假新闻检测实战:BERT+ResNet双塔与对比学习 简介:基于PyTorch的多模态虚假新闻检测项目完整代码包,面向自然语言处理与计算机视觉交叉方向的开发者、科研人员及毕业设计选题者,解决社交媒体中文本与图像联合识别虚假新闻的问题。系统以BERT预训练模型提取文本语义特征,以Res… · 2026/9/27 0:00:01
制作网页比较方便的软件怎么选?一文搞懂避坑指南 制作网页比较方便的软件怎么选?一文搞懂避坑指南 很多老板一上来就问:做个网站多少钱?但我反问他:你的域名买了吗?服务器租了吗?他一脸懵。这就是典型的“域名服务器搞不懂”。别急,今天咱们不聊虚的,直接 一文搞懂 那些让你头秃的技术名词。… · 2026/9/28 0:00:06
婚恋网站实战案例:避开3个高价坑,省钱50%还能跑赢流量 婚恋网站实战案例:避开3个高价坑,省钱50%还能跑赢流量 找婚恋网站建站公司,最怕的就是被坑高价。很多同行跟我吐槽,报价单上写得模棱两可,功能栏里全是“高级定制”、“专属UI”,结果落地全是套壳。今天不聊虚的,直接甩几个我经手的 实战案例… · 2026/9/28 0:00:19
济南做网站多少钱:3个案例拆解,防黑源码下载全攻略 济南做网站多少钱:3个案例拆解,防黑源码下载全攻略 上周济南一个做建材的老板找我,脸都绿了。他的官网首页弹出了赌博广告,后台被植入了挖矿脚本。他慌得问我:“网站被黑挂马不知道怎么办?能不能直接找之前的外包公司要源码下载,看看哪里被动了手脚?… · 2026/9/28 0:00:25