cli-anything-sbox 测试体系全解析:244 项测试的分层设计、退出码契约与地图测试编排
2026/9/11 22:51:54 网站建设 项目流程

cli-anything-sbox 测试体系全解析:244 项测试的分层设计、退出码契约与地图测试编排

【免费下载链接】CLI-Anything"CLI-Anything: Making ALL Software Agent-Native" -- CLI-Hub: https://clianything.cc/项目地址: https://gitcode.com/GitHub_Trending/cl/CLI-Anything

s&box(Facepunch Studios 基于 Source 2 的游戏引擎)的 CLI 自动化桥接层 cli-anything-sbox 在 sbox/agent-harness 中内置了一套 244 项测试的完整测试体系。本文以 sbox/agent-harness/cli_anything/sbox/tests/TEST.md 为主线,从测试清单、运行方式、逐类覆盖、模块映射到已知边界与平台怪癖,逐层拆解这套测试的设计意图,并配合 sbox_cli.py 与各 core 模块源码验证其底层契约。读完本文,你将掌握如何在无 s&box 安装的环境下跑通纯单元与子进程级 E2E 测试、如何配置环境变量切换测试目标、理解"一次性命令失败必须退出码 1"这一对 Agent 自动化至关重要的契约,以及地图生成测试编排管线的组合矩阵与哨兵轮询机制。

1. 测试清单总览

整个测试套件集中在 sbox/agent-harness/cli_anything/sbox/tests 目录下,由四个测试文件构成,全部可在agent-harness/目录下运行:

测试文件覆盖范围测试数
test_core.py全部 13 个 core 模块:project、scene、prefab、codegen、input_config、collision_config、material、sound、localization、session、export、validate,外加 s&box 后端解析器157
test_full_e2e.py通过子进程调用 CLI 表层、项目工作流端到端、sbox_backend 集成50
test_orchestrator.py地图生成测试编排器(组合矩阵、哨兵轮询、RGBA→PNG 转换、配置读写)17
test_exit_codes.py一次性 CLI 退出码契约、REPL 吸收 SystemExit、monkeypatch 覆盖的字典返回失败路径20
合计244

值得注意的是 test_core.py 的 157 项测试来自 36 个测试类,是纯单元测试;而 test_full_e2e.py 的 40 项TestCLISubprocess测试并不依赖 s&box 安装,它们通过子进程调用 CLI 并断言--json输出,专门验证 README.md 中记载的 79 个命令、14 个命令组的可用性。

2. 运行测试:分层命令与环境配置

2.1 按粒度运行的命令

agent-harness/目录执行(对应 sbox/agent-harness/setup.py 的包布局):

# 纯单元测试(速度快,无需安装 s&box) python -m pytest cli_anything/sbox/tests/test_core.py -v # 编排器测试(Pillow 为硬依赖,通常可直接运行) python -m pytest cli_anything/sbox/tests/test_orchestrator.py -v # 完整 E2E 套件(TestE2EBackend 在未安装 s&box 时自动跳过) python -m pytest cli_anything/sbox/tests/test_full_e2e.py -v # 全部测试 python -m pytest cli_anything/sbox/tests/ -v

2.2 环境变量契约

变量作用
SBOX_PATH将 harness 指向非 Steam 目录的 s&box 安装。当 s&box 不在标准 Steam 库中时,TestE2EBackend必须依赖它。
CLI_ANYTHING_FORCE_INSTALLED设为1/true/yes时,TestCLISubprocess改为针对已安装的cli-anything-sbox控制台脚本运行,而不是python -m

这两个变量的行为与 test_full_e2e.py 中的_resolve_cli辅助函数直接对应:它优先用shutil.which查找已安装命令,找不到时回退到python -m cli_anything.sbox.sbox_cli;只有当CLI_ANYTHING_FORCE_INSTALLED被设置为真值且命令不在 PATH 中时才会抛错。这套"先探测、后回退"策略保证了测试套件在只有源码、未做pip install -e .的裸环境下也能跑。

2.3 跳过(Skip)行为

  • TestE2EBackend(3 项测试)在主机上找不到 s&box 时整体跳过——不是失败、不是报错,而是带原因说明的干净跳过。
  • test_converts_rgba_bytes_to_png在 Pillow 不可导入时跳过(由于 Pillow 是硬依赖,正常情况下始终可导入)。

这种"环境不满足就跳过而非报错"的设计,使套件可以在仅具备 Python 的开发机、未安装引擎的 CI 以及完整 s&box 环境中三态运行。

3. test_core.py:157 项单元测试,36 个类

test_core.py 是套件的主体,覆盖 13 个 core 模块的全部纯逻辑。所有测试自包含,文件操作统一使用 pytest 的tmp_pathfixture(见文件头部注释),不触碰真实用户目录。以下按被测模块分组说明。

3.1 项目与校验(project / validate)

测试数被测模块与要点
TestProject7core/project.py:create、load/save、info、configure、find_sbproj
TestProjectPackages4add/remove package references
TestProjectValidate4core/validate.py:坏引用、重复 GUID、畸形输入

从源码签名看,core/project.py 的create_project支持nameproject_type(默认"game")、org(默认"local")、max_players(默认 64)、tick_rate(默认 50)、network_type(默认"Multiplayer")、startup_scene(默认"scenes/minimal.scene")等参数;_default_sbproj_default_minimal_scene负责生成工程骨架。validate 模块的validate_project(project_dir, check_refs, check_guids, check_inputs)是项目"体检"入口,由_build_asset_index_is_engine_builtin_collect_guids等内部函数配合完成引用与 GUID 检查。

3.2 场景(scene):最大的一簇,共 50 项

测试数要点
TestScene12create、list、增删 object/component、find、GUID 唯一性
TestSceneQuery9query_objects:按 component / tag / name / regex / bounds / enabled / 组合过滤
TestSceneRefs3extract_asset_refs:默认场景、去重排序、空场景
TestSceneBulkModify4bulk_modify_objects:位置、多字段、无匹配、需更新
TestSceneCloneAndGet6clone_object+get_object
TestSceneModify4modify_object:name、position、scale、tags
TestSceneModifyComponent2modify_component_properties
TestSceneInstantiatePrefab5instantiate_prefab:默认名、覆盖名、prefab 来源引用、父对象、非法父对象
TestSceneDiff5diff_scenes:相同、增删、位置、组件、场景属性

从 core/scene.py 的符号清单可见其实现厚度:query_objects支持has_componenthas_tagname_matchname_regexin_boundsenabled六个过滤维度,内部通过_object_has_component_object_has_tag_parse_position_bounds_object_in_bounds等谓词实现 AND 组合语义;diff_scenes依赖_diff_two_objects_object_summary生成结构化差异;extract_asset_refs通过_walk_for_refs递归遍历 JSON 节点,识别.vmdl/.vmat/.vsnd/.vtex/.vpcf/.prefab等资源引用,并调用_is_asset_ref_category_for_ref分类。

3.3 预制体(prefab):14 项

测试数要点
TestPrefab4create、带组件、info、from-scene
TestPrefabComponents2在 prefab 根上增删组件
TestPrefabRefs1extract_asset_refs
TestPrefabModifyComponent5按 type / 按 guid 修改组件、未找到、缺 id、缺 properties
TestPrefabDiff2diff_prefabs:相同、根变更

from_scene_object(scene_path, object_guid, output_path)(core/prefab.py)是从场景实体"抽出"预制体的关键函数,与场景侧的instantiate_prefab形成双向转换闭环。

3.4 代码生成(codegen):22 项

测试数要点
TestCodegen10组件基础、带属性、网络化、接口、生命周期方法、gameresource、编辑器菜单、代码风格(tabs / Allman / CRLF)
TestCodegenRazor5generate_razor
TestCodegenClass3generate_class:静态类、基类、多行方法体回归
TestCodegenPanelComponent5基础、含 ScreenPanel、命名空间、属性、GUID 唯一性

core/codegen.py 提供generate_component(支持lifecycle_methodsinterfacesis_networkedrpc_methods)、generate_gameresourcegenerate_editor_menugenerate_razorgenerate_panel_componentgenerate_class六个生成器,内部由_format_property_format_method_format_rpc_method等函数负责逐行格式化。网络化组件与 RPC 方法、Razor UI、PanelComponent+ScreenPanel 双件套这些"易错点"都有专门测试钉住输出格式。

3.5 输入与碰撞配置(input_config / collision_config):12 项

测试数要点
TestInputConfig6取默认、添加、重复、删除、设置、列出(core/input_config.py)
TestCollisionConfig5取默认、加层、删内置层、加规则、删规则
TestCollisionRemoveLayer1remove_layer

add_action(name, group="Other", keyboard_code="None", gamepad_code="None", title=None)add_layer(name, default="Collide")add_rule(layer_a, layer_b, result="Collide")的默认参数均在单元测试中被逐一验证。

3.6 材质 / 声音 / 本地化:19 项

测试数要点
TestMaterial5new、load/save、info、list、presets(core/material.py)
TestMaterialUpdate2update_material:shader、颜色贴图
TestSound4new、info、list、presets(core/sound.py)
TestSoundUpdate2update_sound_event
TestLocalization5new、set、get、list、remove(core/localization.py)
TestLocalizationBulkSet1bulk_set

create_material的默认参数(shader="complex"metalness=0.0tint="1 1 1 0")、create_sound_event的默认参数(volume="1"pitch="1"decibels=70selection_mode="Random"occlusion=True)都在这一层被固定下来。

3.7 会话 / 导出 / 引用图:30 项

测试数要点
TestSession6create、set project、undo/redo、save/load、clear、损坏文件警告 + 时间戳备份(core/session.py)
TestExport3列资源、过滤列表、查找工程目录
TestAssetRefGraph5find_asset_refs+find_unused_assets
TestAssetRenameMove7rename_asset+move_asset:均含 dry-run、目标已存在、源缺失、跨目录

core/export.py 的rename_asset/move_asset是"重命名/移动资源并同步更新所有场景与预制体引用"的高危操作,测试对 dry-run、目标冲突、跨目录三种边界都做了覆盖;_rewrite_string_refs_rewrite_refs_in_project负责在项目内重写引用字符串。

3.8 组件预设(presets):3 项

测试数要点
TestComponentPresets2COMPONENT_PRESETS数量与标准名称
TestJointPresets1关节预设覆盖

sandboxCOMPONENT_PRESETS常量(位于 core/scene.py)被_resolve_component_type使用,用于把--components中的短名(如box_colliderrigidbody)解析为完整组件类型。README 中记载的 29 个预设(model、box_collider、sphere_collider、rigidbody、camera、各类灯光、joint 系列等)即由此常量驱动。

4. test_full_e2e.py:50 项端到端测试,3 个类

4.1 TestCLISubprocess(40 项)

以子进程方式启动cli-anything-sbox(未安装时回退为python -m cli_anything.sbox.sbox_cli),对 README 记载的每个命令组断言--json输出结构。它验证的是"命令真实可执行、输出可被机器解析",而非仅仅存在于 help 文本中。

4.2 TestE2EProjectWorkflow(7 项)

使用进程内 Click runner 跑完整项目工作流。test_full_project_creation(见 test_full_e2e.py)是典型代表,它断言一次create_project必须同时产出:

  • .sbproj文件且为合法 JSON(TitleType字段正确);
  • Assets/scenes/minimal.scene(含GameObjectsSceneProperties两个键);
  • Code/Assembly.csEditor/Assembly.cs
  • ProjectSettings/Input.configProjectSettings/Collision.config,且均为合法 JSON。

test_scene_manipulation_workflow则验证 create → add 3 个对象(不同组件组合)→ 校验对象计数与 GUID 互异的完整链路。整个工作流覆盖 create → add objects → generate code → validate 的闭环。

4.3 TestE2EBackend(3 项)

真实 s&box 后端集成,覆盖 utils/sbox_backend.py 的三个探测函数:find_sbox_installation(定位安装)、get_sbox_version(读取版本)、find_server_executable(定位服务端可执行文件)。未安装 s&box 时这 3 项干净跳过。

5. test_orchestrator.py:17 项地图测试编排器测试

这组测试验证 core/test_orchestrator.py(地图生成测试管线)的五个核心机制:

测试数覆盖
TestComboMatrix7Strategy × Size × Seed 组合矩阵
TestSentinelPolling3文件系统哨兵轮询(用于检测引擎内测试是否完成)
TestConfigIO3test_config.json往返(读、校验、写)
TestRgbaConversion2RGBA 字节数组 → PNG(经 Pillow)
TestDataPathResolution2FileSystem.Data路径在编辑器与独立模式下的解析

从测试断言可以看出组合矩阵的默认形态:build_combo_matrix()默认产生 12 个组合,即 4 种策略 × 3 种尺寸 × 1 个种子;策略集合为{"Serpentine", "Gilbert", "SpanningTree", "Backbite"},尺寸集合为{"Small", "Medium", "Large"}seeds=[42, 99]时矩阵扩为 24 项,seed_count=3时扩为 36 项,策略/尺寸均可单独过滤——这意味着管线可以按需组合出任意规模的测试矩阵。哨兵机制中,check_sentinel读取test_complete.json,存在则返回其内容(含success/error字段),不存在返回Nonepoll_for_sentinel在此基础上提供 60 秒默认超时。rgba_to_png则由screenshot.rgba原始像素缓冲转换为 PNG 输出。

run_test_pipeline是整个编排的入口:遍历组合矩阵 →swap_startup_scene切换启动场景 → 以run_single_combo逐个启动 s&box →collect_screenshot收集截图 →poll_for_sentinel等待引擎内测试完成哨兵 →cleanup_data_files清理test_config.jsonscreenshot.rgbametadata.jsontest_complete.json等中间产物。

6. test_exit_codes.py:20 项退出码契约测试

这组测试是本套件对 Agent 自动化最关键的贡献,其背景记录在文件 docstring 中:HKUDS/CLI-Anything PR #251 的评审反馈指出,handler 捕获异常后用_output_error()打印、随后正常返回,导致脚本和 Agent 把失败误判为成功。这组测试把契约钉死:一次性模式下任何失败命令必须以退出码 1 结束,成功路径保持退出码 0,REPL 模式则吸收退出让循环继续

6.1 五个测试类

测试数覆盖
TestOneShotFailureExitsNonZero11失败路径返回退出码 1:场景/工程文件缺失(人读 + JSON 双模式)、本地化 key 缺失、必填参数缺失、--properties传入非法 JSON、无 s&box 安装时执行 asset compile、缺失文件上的 scene list(except Exception裸路径的真门禁)、损坏 JSON 上的 asset info(json_info.error表层)。含--help退出码 0 基线。
TestOneShotSuccessExitsZero2确认退出码 1 改动没有回归成功路径
TestProjectValidateExitsNonZero2project validateok=False(坏引用)时退出 1 以便 CI 把关;干净工程仍退出 0
TestAuditedFailurePaths2monkeypatch 覆盖字典返回型失败路径:resourcecompiler 返回success=Falseasset compileget_sbox_version()error字段的server info
TestReplModeAbsorbsExit3_output_errorctx.obj["repl"] = True时不调用sys.exit;键缺失时安全默认一次性模式

6.2 底层实现

契约的实现在 sbox_cli.py 的_output_error:JSON 模式在 stdout 输出{"error": message},人读模式在 stderr 输出Error: ...,随后仅当ctx.obj["repl"]为假时sys.exit(1)。集中式退出逻辑让每个命令的 try/except 保持简洁,同时统一传播失败。

TestAuditedFailurePaths之所以需要 monkeypatch,是因为asset compile失败(resourcecompiler 返回success=False)和server info版本读取失败(返回error字段)这两条路径返回的是字典而非抛异常,标准的except Exception路径抓不到;审计补上了显式 success/error 检查,测试用CliRunner + monkeypatch组合把该契约钉死(断言输出含 "Resource compilation failed" 与 "Failed to read s&box version")。

6.3 子进程测试的可移植性

测试用python -m cli_anything.sbox而非安装后的cli-anything-sbox二进制,是为了在启动脚本行为可能不同的环境中保持可移植(如 Windows + Python 3.14 的原生 launcher 怪癖);_run辅助函数统一传入stdin=subprocess.DEVNULL规避平台句柄问题(详见第 8 节)。

7. 按源码模块的覆盖矩阵

TEST.md 给出了模块维度的三层覆盖(单元 / E2E / 子进程),此处是理解测试"密度"分布的关键:

模块单元E2E子进程
core/project.py7 + 4 packages2(创建、塔防)1(project new)
core/scene.py50(12+9+3+4+6+4+2+5+5)3(操作、塔防、工作流)6(query、refs、bulk-modify×2、diff×2、instantiate)
core/prefab.py14(4+2+1+5+2)1(from_scene)3(refs、modify-component、diff)
core/codegen.py22(10+5+2+5)1(codegen 工作流)3(panel-component 普通 + scene 追加)
core/input_config.py62(配置工作流、塔防)1(input list)
core/collision_config.py6(5+1)2(配置工作流、塔防)1(collision list)
core/material.py7(5+2)--
core/sound.py6(4+2)--
core/localization.py6(5+1)--
core/session.py5--
core/export.py15(3+5+7)-5(find-refs、find-unused、rename、rename-dry-run、move)
core/validate.py4-2(validate 干净、validate 损坏)
core/test_orchestrator.py17(整文件)--
utils/sbox_backend.py-3(安装、版本、可执行文件,无 s&box 时跳过)-
sbox_cli.py(CLI)--40(TestCLISubprocess 全部)+ 20(退出码契约 + audited 路径 monkeypatch + REPL 吸收)

这张矩阵揭示了一个清晰的分层策略:纯文件逻辑(material/sound/localization/session/export/validate)以单元测试为主,涉及引擎进程(sbox_backend、resourcecompiler)的部分以可跳过的 E2E 或 help-smoke 为主,而 CLI 表层则由 40 项子进程测试全量把守。

8. E2E 前置条件与已知测试环境怪癖

8.1 E2E 前置条件

  • TestCLISubprocess(40 项)与TestE2EProjectWorkflow(7 项)在无 s&box 安装时即可运行——它们针对内存态或临时目录的项目状态执行 CLI。
  • TestE2EBackend(3 项)要求 s&box 可被发现:标准 Steam 安装无需任何操作;非标准位置需export SBOX_PATH=/path/to/sbox;未安装则带原因说明跳过。

8.2 已知怪癖

  • Windows + Python 3.14 + pytest 的句柄问题subprocess.Popen在子进程启动前可能抛OSError: [WinError 6] The handle is invalid,原因是父进程 stdin 句柄不可继承。test_full_e2e.py 的TestCLISubprocess._run与 test_exit_codes.py 的_run都通过stdin=subprocess.DEVNULL规避。新增子进程调用时必须照此模式。
  • 显式 UTF-8:所有文件 I/O 均显式指定open(..., encoding="utf-8")。早期版本在 Windows 上默认平台编码,提交上游前已修复——这也是所有测试断言文件内容时统一使用 UTF-8 的原因。

9. 覆盖缺口:诚实声明的边界

TEST.md 明确列出了三处未覆盖区域,理解它们有助于正确解读测试结论:

  • server startlaunch命令仅经 help 文本冒烟测试;真正启动 s&box 服务端/编辑器进程超出范围(需要活网络套接字与运行中的游戏)。
  • session undo/redo仅在单元层测试;没有 E2E 工作流在"编辑-撤销-再编辑"的真实序列中走一遍撤销日志。
  • asset compile仅 help 测试;调用真实resourcecompiler.exe需要完整 s&box 安装,留给上游维护者的 CI。缺失/失败编译的非零退出码路径由test_exit_codes.py::test_asset_compile_missing_sbox_exits_one覆盖。

10. 总结:这套测试教你如何为 Agent 原生 CLI 设计质量门槛

从 s&box harness 的 244 项测试可以提炼出可复用的测试设计模式:

  1. 三层粒度:纯单元(fast、无外部依赖)→ 子进程级 CLI 冒烟(验证命令真实可执行且 JSON 可解析)→ 引擎集成(条件不满足时干净跳过),让不同 CI 环境各取所需;
  2. 退出码即契约:一次性模式下失败必须exit 1(JSON 错误对象在 stdout、人类可读错误在 stderr),成功保持exit 0,REPL 模式吸收退出保持交互循环——这正是 Agent 判断命令成败、决定是否重试的根基;
  3. 字典返回型失败路径单独审计:不抛异常而返回{"success": False}{"error": ...}的路径不会被except Exception捕获,必须显式检查并用 monkeypatch 测试钉死;
  4. 矩阵化编排:地图测试用策略×尺寸×种子的组合矩阵 + 文件哨兵轮询,把引擎内测试的结果以test_complete.json的方式异步回传,规避了进程内同步等待的脆弱性;
  5. 对平台怪癖给出可操作处方stdin=DEVNULL、显式 UTF-8,避免"能跑但不能移植"的隐性债务。

围绕 TEST.md 记载的清单,配合 sbox_cli.py 的实现与 README.md 的命令手册,读者既可以从零跑通整套测试,也能深入理解每条契约背后的工程动机。

【免费下载链接】CLI-Anything"CLI-Anything: Making ALL Software Agent-Native" -- CLI-Hub: https://clianything.cc/项目地址: https://gitcode.com/GitHub_Trending/cl/CLI-Anything

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询