OBS Studio CLI Agent Harness 测试体系全解:153 项零安装用例如何保障 JSON 场景集合编辑的正确性
导读
本文聚焦 obs-studio/agent-harness/cli_anything/obs_studio/tests/TEST.md 所记载的 OBS Studio CLI Agent Harness 测试体系——它面向一个“无需安装 OBS Studio 即可通过命令行编辑场景集合”的 Python 工具链,用 117 项单元测试 + 36 项端到端测试(共 153 项) 覆盖项目、场景、源、滤镜、音频、转场、输出与撤销会话八大模块。读完本文,你将掌握这套测试清单的全貌、各测试类的设计意图、其背后的 JSON 数据模型与参数校验实现(含全部合法值与取值范围),以及如何在本仓库中一键复现 153 passed 的测试结果。
一、测试文档定位:给“无头 OBS 编辑”立下验收标准
OBS Studio CLI Agent Harness 是一个有状态命令行接口,它把 OBS 的“场景集合(Scene Collection)”抽象为一份纯 JSON 文档,让 LLM / Agent 或脚本可以像操作文件一样编排直播与录制流程,而编辑全程不需要运行 OBS Studio。相关说明见 README.md,命令入口为 obs_studio_cli.py,核心业务逻辑全部集中在 core 目录下的 8 个模块。
在这种设计下,测试不再依赖 GUI 与真实采集设备——这正是 TEST.md 的立身之本。它既是测试资产的目录清单,也是这套“JSON 数据模型 + 纯函数操作”设计的回归契约:
- 单元测试全部使用合成 / 内存数据,不要求安装 OBS Studio;
- E2E 测试把多个核心操作编排成完整的直播搭建、录制配置、保存载回等真实工作流,同样在无 OBS 环境验证;
- 测试结果可在 0.19s 内完成(Python 3.13.11 / pytest 9.0.2 环境),使每次迭代都能获得近乎即时的回归反馈。
二、测试资产总览:两组文件、16 个测试类、153 项用例
TEST.md 开篇给出测试清单总表,这也是理解整套测试的目录索引:
| 文件 | 测试类 | 用例数 | 关注点 |
|---|---|---|---|
test_core.py |
8 | 117 | project、scenes、sources、filters、audio、transitions、output、session 的单元测试 |
test_full_e2e.py |
8 | 36 | E2E 工作流:直播搭建、源操作、场景管理、滤镜链、输出配置、保存/加载、撤销/重做、边界用例 |
| 合计 | 16 | 153 |
两份测试文件均位于 tests 目录,与被测的 core 模块一一对应。需要补充说明的是:该目录中还实际存在 test_validate_geometry_nan.py 与 test_validate_range_nan.py 两个未计入 TEST.md 清单的专项文件;从文件名与 obs_utils.py 的实现来看,它们专门验证 _finite_number / _finite_int / validate_range 对 NaN、Infinity 等非有限数值的拒绝逻辑(例如 math.isfinite 检查),说明仓库的校验回归在 TEST.md 定稿后仍在持续加强。
运行方式与可复现基线
TEST.md 末尾给出一次完整运行记录,可直接作为“必须保持绿色”的基线:
============================= test session starts ==============================
platform linux -- Python 3.13.11, pytest-9.0.2, pluggy-1.5.0
rootdir: /root/cli-anything
plugins: langsmith-0.5.1, anyio-4.12.0
collected 153 items
test_core.py 117 passed
test_full_e2e.py 36 passed
============================= 153 passed in 0.19s ==============================
按 README.md 的说明,在 agent-harness 目录下运行 python3 -m pytest cli/tests/ -v 可复现全部用例;test_core.py 与 test_full_e2e.py 也支持单独运行,便于在开发某个模块时只跑对应回归。
三、单元测试分层解剖(test_core.py · 8 类 117 项)
单元测试的导入清单(test_core.py)直接揭示了被测 API 面:create_project/open_project/save_project、场景与源管理、滤镜与参数设置、音频混音、转场、输出配置、Session。下面按 TEST.md 的 8 个测试类逐一展开,并结合源码说明“为什么这样测”。
3.1 TestProject(16 项):项目生命周期与输入闸门
覆盖点包括:默认参数创建、自定义分辨率/FPS/编码器/项目名、非法参数拒绝、JSON 存取往返、打开不存在或非法文件、查询项目信息、默认项目自带头尾转场与推流/录制配置。
源码印证见 project.py:create_project() 是全部校验的第一道闸门,默认值为 1920x1080 @ 30fps、x264 编码、视频码率 6000、音频码率 160;并强制以下约束(与测试的 pytest.raises 断言一一对应):
- 分辨率必须为正整数(
output_width < 1 or output_height < 1→ ValueError); - FPS 必须为正(
fps < 1→ ValueError); - 视频码率 ≥ 100、音频码率 ≥ 32;
- encoder 必须是
x264, x265, nvenc, qsv, amd, svt-av1之一。
默认项目结构(project.py#L13-L54)预置 1 个场景(id=0)、Cut/Fade 两个转场、active_scene=0、空的 audio_sources、Twitch 推流占位与 MKV 录制占位——这正是“默认项目自带转场、推流与录制配置”这一断言的直接来源。打开文件时要求 JSON 必须含 version 与 scenes 字段(project.py#L92-L100),否则即便文件合法也会被判为非法项目文件。
3.2 TestScenes(11 项):场景数量的不变量
覆盖:增删场景、自动去重命名、唯一 ID、复制场景生成独立副本、切换活动场景、非法索引拒绝、列表查询,以及“删除当前活动场景后活动索引自动校正”的状态联动。
场景操作的核心不变式在源码中体现为至少保留 1 个场景与索引校验:remove_scene 会拒绝移除最后一个场景,索引访问统一走 obs_utils.py 的 get_item,空集合抛 ValueError、越界抛 IndexError。活动场景的调整逻辑保证项目始终处于“存在一个活动场景”的合法状态,这也使得后续所有场景级操作(增源、挂滤镜)都有稳定锚点。
3.3 TestSources(19 项):源是场景编辑的主力对象
覆盖:添加 video_capture 及全部 12 种源类型、非法类型拒绝、带位置/尺寸/settings 添加、负尺寸拒绝、自动唯一命名、删除/复制源、可见性与透明度等属性设置(含非法属性与越界透明度拒绝)、position/size/crop/rotation 变换、按索引查询与列表、源默认属性自动填充。
12 种源类型及其“类别 + 默认设置”注册在 sources.py#L17-L78 的 SOURCE_TYPES 字典中:
| 类型 | 类别 | 默认设置要点 |
|---|---|---|
video_capture |
video | device、1920x1080、30fps |
display_capture |
video | display=0、capture_cursor |
window_capture |
video | window、capture_cursor |
image |
media | file、unload_when_hidden |
media |
media | local_file、looping、restart_on_activate |
browser |
web | url、800x600、css |
text |
text | Sans Serif、36 号、白色 |
color |
utility | 黑色 1920x1080 |
audio_input / audio_output |
audio | device |
group / scene |
utility | items / scene_name |
新源的默认结构(sources.py#L88-L105)固定含 position{x,y}、size{1920,1080}、crop{0,0,0,0}、rotation=0、opacity=1.0、空 filters 与类型默认 settings——这就是“默认属性已填充”断言的依据。对 size 的负值拒绝与 crop 的非负约束,实际由 obs_utils.py 的 validate_size / validate_crop 完成。
3.4 TestFilters(14 项):滤镜参数规格是“自带 schema”的注册表
覆盖:给源挂滤镜、带参数挂滤镜、非法滤镜类型/非法参数/越界参数拒绝、chroma_key 与 noise_suppress 专项、从源移除滤镜、设置滤镜参数、列出源上滤镜、列出全部可用滤镜及按类别过滤、所有滤镜类型都有参数定义。
最后一类断言意义重大:FILTER_TYPES 注册表(filters.py#L8-L127)为每种滤镜声明的 params 自带 type / default / min / max / values,因此测试可以遍历注册表验证每项都定义了参数规格,保证“注册即可用”。参数校验函数 _validate_filter_params(filters.py#L139 起)会拒绝未知参数名并补齐默认值、执行范围检查,例如:
chroma_key:similarity/smoothness/spill取值范围 1–1000,key_color_type限定green/blue/magenta/custom;color_correction:gamma ∈ [-3,3]、contrast ∈ [-4,4]、hue_shift ∈ [-180,180] 等;noise_suppress:method 限定rnnoise/speex/nvafx,suppress_level ∈ [-60,0];compressor:ratio ∈ [1,32]、threshold ∈ [-60,0]、attack ∈ [1,500] 等;- 还有
sharpen、noise_gate、gain、limiter、crop_pad、lut、image_mask、scroll、color_key。
3.5 TestAudio(13 项):全局混音轨的完整状态机
覆盖:添加输入/输出型音频源、非法类型拒绝、音量设置与越界拒绝、静音/取消静音、监听模式设置、声道平衡与同步偏移、移除、列表与唯一命名。
约束来自 audio.py:audio_type 仅允许 input/output;MONITOR_TYPES(audio.py#L8)限定监听模式为 none / monitor_only / monitor_and_output;音量合法区间为 0.0–3.0(1.0 即 100%,支持超过 100% 的增益),声道平衡合法区间为 [-1.0, 1.0],同步偏移以毫秒计。这些范围正是测试中断言“越界即 ValueError”的规则来源。
3.6 TestTransitions(11 项):默认转场之外的类型与时长
覆盖:增删转场、自定义时长、非法类型与负时长拒绝、删除最后一个转场拒绝、设置时长、设置活动转场、列表与“所有转场类型均合法”。
七种转场类型及默认时长注册在 transitions.py#L8-L16:cut(0ms)、fade(300ms)、swipe(500ms)、slide(500ms)、stinger(1000ms)、fade_to_color(300ms)、luma_wipe(500ms)。删除最后一个转场会被 remove_transition 拒绝——与场景的“至少一个”不变式同一思路。
3.7 TestOutput(14 项):推流、录制与编码的三层配置
覆盖:推流服务与配置、非法服务拒绝、设置推流密钥;录制路径/格式/质量、非法格式与质量拒绝;输出分辨率/FPS/码率设置、按预设置档配置、非法预设置档拒绝、非法输出宽度与编码器拒绝;输出信息查询、预设置档列表、合法服务与格式列表。
output.py 集中了三张白名单与 8 个内置编码预设置档:
VALID_SERVICES = twitch, youtube, facebook, custom;VALID_RECORDING_FORMATS = mkv, mp4, mov, flv, ts;VALID_RECORDING_QUALITIES = low, medium, high, lossless;ENCODING_PRESETS:ultrafast / fast / balanced / quality / high_quality(x264 系)、nvenc_fast / nvenc_quality(NVENC 系)与recording_high(x264、20Mbps)。
set_output_settings(output.py#L67-L114)支持“先应用 preset,再覆盖单项”的叠加语义——这正是 E2E 中“预设置档后被单一参数覆盖、encoder 保留自预设置档”断言的实现基础。
3.8 TestSession(13 项):快照式撤销/重做的状态机
覆盖:会话创建、绑定项目、未绑定项目时取项目报错、撤销/重做往返、空撤销/重做、新快照清空 redo 栈、状态上报深度、保存会话到文件、历史列表、最大撤销数强制、撤销回退源添加与场景添加。
实现集中在 session.py 的 Session 类:MAX_UNDO = 50(session.py#L39),每次变更前由 snapshot() 深度拷贝当前项目入 undo 栈并清空 redo 栈;undo() 把当前状态压入 redo 后回弹 undo 栈顶;save_session 通过 _locked_save_json(session.py#L10-L33)用 fcntl 独占锁实现原子写盘。测试验证的“新快照清空 redo”“undo 上限 50”“保存无路径抛 ValueError”等,都能在此逐行找到对应分支。
四、E2E 工作流解剖(test_full_e2e.py · 8 类 36 项)
单元测试验证“每个原子操作正确”,E2E 测试则把原子操作编排成 Agent / 脚本真实会执行的整段流程,验证跨模块协作后的最终状态。全部 9 组场景的编排风格可参看 test_full_e2e.py#L30-L60(创建项目→加 3 个场景→往不同场景放源→配置 Twitch 推流),逐类要点如下:
| E2E 测试类 | 用例数 | 编排要点 |
|---|---|---|
| TestStreamSetupWorkflow | 3 | 完整直播搭建(4 场景 + webcam/display/overlay/BRB 图 + Twitch + balanced 预置);绿幕机位(video_capture + chroma_key/color_correction);音频调音台(麦克风输入 + 桌面输出 + 音量 + 监听模式) |
| TestSourceManipulation | 4 | 四层叠加场景(游戏捕获/边框/webcam/text)自定义位置;位置-缩放-裁剪-旋转全变换;复制文本源并独立控制可见性;显隐切换 |
| TestSceneWorkflow | 4 | 多场景各自承载不同源并核对数量;切换活动场景;带源复制场景并验证副本独立;删场景不影响其他场景 |
| TestFilterChains | 4 | 音频五连滤镜链(noise_suppress→noise_gate→compressor→gain→limiter);视频三连滤镜链(chroma_key→color_correction→sharpen);链中改参;从链中间删滤镜且保持顺序 |
| TestTransitionWorkflow | 2 | 在默认 Cut/Fade 之外追加 stinger 与 slide;修改转场时长 |
| TestOutputConfiguration | 2 | 完整输出配置(YouTube 推流 + MP4 高质量录制 + 1080p60/8Mbps);先 preset 后覆盖单项 |
| TestSaveLoadRoundtrip | 2 | 全量往返(场景/源/滤镜/音频/转场/推流/录制);保存/载回后源 transform 保持一致 |
| TestSessionUndoRedo | 5 | 撤销源添加、场景添加、双滤镜两步撤销、音量变更撤销、3 次改名 + 3 撤 + 2 重做的复合序列 |
| TestEdgeCases | 10 | 空项目信息、对不存在场景取源(IndexError)、对不存在源挂滤镜(ValueError)、空场景删源、变换不存在源、负裁剪值、全部源/滤镜类型均可添加、chroma key 拒绝非法颜色类型、会话无路径保存、20 场景 + 21 源的大集合 |
值得强调的是 TestEdgeCases:它把单元测试里的“非法入参拒绝”升级为跨层行为契约——例如“源挂在不存在场景上抛 IndexError、滤镜挂在不存在源上抛 ValueError”,直接约束了 scenes → sources → filters 的索引链错误类型划分(场景层越界抛 IndexError、资源层空/缺失抛 ValueError)。这类断言对 Agent 非常重要:LLM 在生成 CLI 操作序列时可以根据异常类型精确判断“是索引写错了还是资源不存在”。
五、为什么“不装 OBS 也能测”:源码结构的可测性设计
153 项用例在 0.19 秒内跑完,根本原因是被测对象与 OBS 本体彻底解耦:
- 数据即 JSON:项目、场景、源、滤镜、转场、推流/录制配置全部是纯 Python
dict,任何操作都以dict为入参与返回值,天然可用内存构造、用json.dumps快照对比; - core 层为纯函数模块:core 下每个模块都是“
(project, 参数...) → 变更后 dict”的无副作用函数,不触碰硬件采集、不依赖 X11/GPU,可测性与可移植性由此而来; - 校验集中在 utils:
generate_id / unique_name / get_item / validate_range / validate_position / validate_size / validate_crop / _finite_number / _finite_int等通用函数(obs_utils.py)统一处理边界、越界、非有限数值(NaN/Infinity),使各模块的错误语义保持一致,测试可批量覆盖; - CLI 只是薄壳:obs_studio_cli.py 通过 Click 将 core 函数映射成命令,会话状态由全局
Session持有,并支持--json结构化输出与 REPL 交互模式,因此测试直接打 core 层即可获得完整逻辑覆盖,无需启动进程。
从源码结构可以推断,这套设计把“编辑场景集合”压缩成了可复现、可版本控制、可被语言模型直接操作的纯数据处理问题——这正是其测试可以不依赖任何 OBS 运行时的根本原因,也解释了为什么文档反复强调 “No OBS Studio installation required”。
六、从测试反推 API 契约:一份可复用的参数边界速查
综合 TEST.md 与各 core 模块源码,可以得到一张面向开发与 Agent 调用的契约速查表:
| 领域 | 参数 | 合法范围 / 白名单 |
|---|---|---|
| 项目 | resolution / fps / 码率 | 宽高 ≥ 1、fps ≥ 1、video ≥ 100kbps、audio ≥ 32kbps |
| 项目 | encoder | x264 / x265 / nvenc / qsv / amd / svt-av1 |
| 场景 | 数量 | 至少保留 1 个;索引越界抛 IndexError |
| 源 | 类型 | 12 种(见 SOURCE_TYPES) |
| 源 | size / crop | 宽高 ≥ 1;crop 各边 ≥ 0 |
| 源 | opacity | [0.0, 1.0] |
| 滤镜 | 参数 | 按 FILTER_TYPES 注册表的 type/min/max/values 校验,未知参数拒绝 |
| 音频 | volume / balance | volume ∈ [0.0, 3.0];balance ∈ [-1.0, 1.0] |
| 音频 | 监听模式 | none / monitor_only / monitor_and_output |
| 转场 | 类型 / 时长 | 7 种类型;时长 ≥ 0ms;至少保留 1 个 |
| 推流 | 服务 | twitch / youtube / facebook / custom |
| 录制 | 格式 / 质量 | mkv / mp4 / mov / flv / ts;low / medium / high / lossless |
| 编码 | 预设置档 | 8 个内置档(ultrafast → recording_high),可单项覆盖 |
| 会话 | undo 深度 | MAX_UNDO = 50,新快照清空 redo |
测试用例对“错误类型”的划分也值得直接复用:文件层用 FileNotFoundError(如 test_core.py 的 open 测试),场景索引越界用 IndexError,资源空集合 / 参数越界 / 非法白名单值统一用 ValueError。CLI 层的 handle_error 包装器 会把这些异常转换成 {"error": ..., "type": ...} 结构化输出或非零退出码,供上层 Agent 判断。
七、回归运行建议与结语
在开发任一 core 模块(例如新增源类型、扩展滤镜参数、调整编码预置)后,建议按以下节奏回归:
# 在 obs-studio/agent-harness 目录下
python3 -m pytest cli/tests/ -v # 全量 153 项
python3 -m pytest cli/tests/test_core.py -v # 仅单元测试(117 项)
python3 -m pytest cli/tests/test_full_e2e.py -v # 仅 E2E(36 项)
每次新增注册表条目(SOURCE_TYPES / FILTER_TYPES / TRANSITION_TYPES / ENCODING_PRESETS)时,记得补一条“全部类型可添加 / 均有参数定义”的断言——这正是 TEST.md 中最具“契约自检”价值的测试思路。此外,已存在的 test_validate_geometry_nan.py 与 test_validate_range_nan.py 提示:对任何接受数值入参的新 API,都应把 NaN/Infinity 拒绝纳入测试面,与 obs_utils.py 的 _finite_number 保持同一防线。
总而言之,这套测试体系的价值不止于“153 passed”:它以纯 JSON 数据模型 + 纯函数 core + 集中式校验为前提,把一份庞大的 OBS 场景集合编辑能力压缩为毫秒级、零依赖、Agent 可调用的可靠契约。对任何想在 CI 中验证“直播/录制配置生成逻辑”或让 LLM 安全编排 OBS 工作流的开发者而言,TEST.md 与其背后的 test_core / test_full_e2e 就是最直接的范式参考。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00