ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

jcode iOS TestHarness:面向 iOS 客户端的确定性 E2E 测试与 UI 奖励度量框架

jcode iOS TestHarness:面向 iOS 客户端的确定性 E2E 测试与 UI 奖励度量框架 jcode iOS TestHarness面向 iOS 客户端的确定性 E2E 测试与 UI 奖励度量框架【免费下载链接】jcodeThe most RAM efficient harness项目地址: https://gitcode.com/GitHub_Trending/jcod/jcodeios/TestHarness是 jcode 仓库中专门用于开发和验证 iOS 客户端JCodeMobile的端到端测试装置它用一个零第三方依赖的 Python mock 网关在本地复现 jcode 服务端的真实线协议把构建应用 → 启动模拟器 → 注入配对凭据 → 启动 → 截图压缩成一条命令并在此之上叠加了一套把这个 UI 不好看量化为 0-100 可爬山hill-climb奖励的度量框架。读完本文你将掌握如何在本机不依赖真机、网络和 LLM 的情况下对 jcode iOS 客户端做可重复的协议级 E2E 验证以及如何用ui_matrix.pyreward/聚合器对 UI 改动做回归门控。1. 整体定位一个诚实且可重复的本地服务端ios/TestHarness/README.md对该装置的定位非常明确它是一个确定性deterministic、无 LLM 的 harness用来替代旧版 Rust 模拟器——为客户端提供一个诚实、可重复的服务端行为来源让开发机上的构建可以对着它工作无需真机、无需网络、无 provider 成本。它由三层组成协议层mock_gateway.pymock 服务端protocol_smoke_test.py纯标准库的协议断言客户端E2E 层run_e2e.sh一条命令跑通swift test→ 构建应用 → 启动 mock → 冒烟测试 → 启动模拟器 → 注入凭据 → 启动应用 → 截图度量层ui_metrics.py/ui_lint.py/ui_matrix.pyreward/奖励框架把 UI 质量变成单个可爬山的数字。README 说明其动机JCodeKit平台无关的客户端核心已经用swift test做了完整的单元测试而这层 harness 证明的是真正运行在模拟器里的 SwiftUI 应用能通过真实的 WebSocket 连接并渲染出真实的会话记录。两层合起来让客户端行为可以脱离真机持续改进。2. mock_gateway.py在同一端口上说真实线协议的 mock 服务mock_gateway.py是纯标准库实现仅asyncio/json/hashlib/struct等无第三方依赖在单个 TCP 端口上按crates/jcode-base/src/gateway.rs定义的线协议路由请求。它与真实网关的结构对应关系可以从 gateway.rs 看到真实网关用peek检查首个数据块中的Upgrade: websocket头来决定走 HTTP 还是 WebSocket 分支mock 网关同样通过嗅探请求行来路由路由行为GET /health返回{status: ok, version, gateway: true}POST /pair配对码换 token默认码123456码错返回401GET /wsWebSocket 升级承载换行分隔的 JSON 事件协议Authorization: Bearer token鉴权真实的/health、/pair、/ws路由在 gateway.rs 中同样存在且真实网关在 WebSocket 握手阶段就完成 token 校验token 无效会收到明确的 401——mock 的鉴权行为Bearer {token}匹配与之保持一致。2.1 命令参数mock_gateway.py 的 CLImain()中的 argparse 定义python3 mock_gateway.py [--port 7643] [--host 127.0.0.1] [--code 123456] \ [--token mocktoken0123456789abcdef] [--push-demo] \ [--scenario empty|short|tool|long|code]--port/--host监听地址默认127.0.0.1:7643--code配对码默认123456/pair请求必须携带相同 code 才返回 token--token配对成功签发的 token后续/ws升级用Bearer校验--push-demo连接建立后主动out-of-band推送一条notificationbuild finished和一条compaction通知用于验证 App 的 toast UI--scenario预置五类确定性会话内容scenario_messages()中定义empty空会话、short一问一答、tool带一次 bash 工具调用、long6 轮、含滚动压力、code含 Python 代码块。这是ui_matrix.py布局矩阵的内容轴来源。2.2 脚本化的message响应流mock 不调用任何 LLM收到message请求后stream_response()输出一条固定事件序列覆盖客户端需要渲染的全部事件类型ack→reasoning_delta× N →reasoning_done→text_delta× N6 字符一片→tool_startbash→tool_input增量 →tool_exec→tool_done输出hello\n→text_delta× N含 代码围栏、粗体与行内代码用于验证 markdown 渲染→message_end→tokens累加 input/output→done。其余请求分支同样全部脚本化包括subscribeack session state、get_history带available_models、all_sessions、display_title、reasoning_effort的完整 history 载荷、soft_interrupt先回soft_interrupt_injected再流式响应镜像真实服务端行为、cancel、ping、set_model、set_reasoning_effort、compact、rename_session、resume_session、clear等。值得注意的是 mock 中手写了 WebSocket 帧编解码encode_text_frame/read_frame/WSConn支持 126/127 扩展长度与 mask 解包并有 20 秒周期的 ping keepalive——这意味着协议保真度是逐字节的而非语义近似。3. protocol_smoke_test.py对 happy-path 事件序列的完整断言protocol_smoke_test.py是纯标准库的 WebSocket/HTTP 客户端http.client 手写 socket 帧处理可对 mock 网关或真实 jcode 网关运行断言完整 happy path。它的检查项main()内逐项check()可归纳为/health返回 200、status ok、gateway标志为 true错误配对码 →401正确配对码 → 返回 token 与server_name mock-jcodeWS 连接 subscribe→ 依次出现ack、session、stateget_history→history事件含非空的available_models与all_sessionsmessage→ 依次断言ack / reasoning_delta / text_delta / tool_start / tool_exec / tool_done / message_end / tokens / done全部出现且流式拼接后的文本包含对输入的回显与 代码围栏set_model→model_changed事件且模型值正确history 载荷携带display_title与reasoning_effort会话标题 UI 所依赖;soft_interrupt→ 断言soft_interrupt_injected出现、内容回显、且注入确认先于流式响应set_reasoning_effort/compact/rename_session各自的确认事件。任何一项失败都会打印FAILED (n): [...]并以退出码 1 结束全部通过则打印ALL CHECKS PASSED。单独运行协议断言README 的用法针对 mock 或真实网关均可python3 TestHarness/mock_gateway.py # 或运行一个真实的 jcode 网关 python3 TestHarness/protocol_smoke_test.py --port 76434. run_e2e.sh一条命令的完整 E2E 流水线run_e2e.sh 把整条链路串起来支持--device iPhone 17默认与--push-demo两个参数脚本开头cd到ios/目录执行。七个步骤脚本中按编号注释swift test先跑 headless 行为层单测JCodeKit的平台无关测试构建xcodegen generatexcodebuild build -project JCodeMobile.xcodeproj -scheme JCodeMobile -destination platformiOS Simulator,name$DEVICE -derivedDataPath .build-ios启动 mock 网关python3 TestHarness/mock_gateway.py --port 7643 --host 127.0.0.1 [--push-demo]日志落盘到$TMPDIR/jcode-ios-e2e/mockgw.log并用trap在退出时清理进程协议冒烟测试protocol_smoke_test.py --port 7643启动模拟器xcrun simctl boot $DEVICE幂等全新安装 注入配对凭据simctl uninstallsimctl install然后通过simctl get_app_container拿到数据容器直接写入Library/Application Support/jcode-servers.json启动 截图simctl launch后等待 6 秒simctl io screenshot产出$TMPDIR/jcode-ios-e2e/chat.png。# 完整流水线截图落在 $TMPDIR/jcode-ios-e2e/chat.png ./TestHarness/run_e2e.sh # 同时验证 out-of-band 通知 toast ./TestHarness/run_e2e.sh --push-demo4.1 自动连接凭据是如何种进去的步骤 6 是整个流水线里最巧妙的部分。App 把配对过的服务器保存在 Keychain当 Keychain 不可用时未签名的模拟器构建回退到Library/Application Support/jcode-servers.json。这一点在 CredentialStore.swift 中有直接证据Keychain 写入失败会打印keychain write failed (%d), using file fallback并调用persistToFile()而fallbackURL正是applicationSupportDirectory下的jcode-servers.json。harness 利用的就是这条回退路径——直接往 App 数据容器写一份 JSON使应用启动时自动连接从而绕开 SpringBoard 的 Open in app? 深度链接确认该确认无法脚本化。脚本写入的内容run_e2e.sh[{host:127.0.0.1,port:7643,token:mocktoken0123456789abcdef, serverName:mock-jcode,serverVersion:mock-0.32.0,pairedAt:770000000}]ui_matrix.py中复用同一份凭据模板CRED常量字段一一对应host/port指向本机 mocktoken与 mock 的--token默认值一致serverName与 mock 的SERVER_NAME mock-jcode一致。5. UI 度量三件套从单张截图到布局矩阵这一层的设计哲学README 原话This looks ugly 被转化为一个可爬山的单一数字。三件套分工明确——ui_metrics.py打分渲染结果像素ui_lint.py打分源码设计 token 纪律ui_matrix.py把两者扩展到内容场景 × 设备 × Dynamic Type的矩阵。5.1 ui_metrics.py像素级评分卡ui_metrics.py对单张simctl截图打分输出四项 0-100 轴分 总分权重为0.40 * space 0.30 * consistency 0.20 * legibility 0.10 * rhythmui_metrics.pyspace内容填充率理想 chat 填充带约 35-65%以 45% 为峰值、垂直平衡质心是否居中、最大死区占比consistency主色数量5-bit/通道量化后4-9 个主色为健康区间 左 margin 聚类数纪律良好的布局应只有 1-3 个左边距legibility最亮内容近似文字对背景的 WCAG 对比度1:1 映射 0 分、7:1 映射 100 分rhythm内容带之间的垂直间隙是否贴合 8pt 网格。两个值得注意的实现细节它先裁剪掉状态栏5.5%与 home indicator2.5%再评分评的是 App 的内容区不是 Apple 的时钟设计 tokenbackground 0x0F0F14、surface 0x1A1A1F、mint 0x4DD9A6等注释标明必须与Sources/JCodeMobile/Theme.swift保持镜像。它是刻意渲染器无关的——只读像素、不读视图树因此同一工具可以给任意截图打分不依赖对代码的信任。python3 ui_metrics.py SHOT.png [--scale 3] [--json] [--annotate OUT.png] python3 ui_metrics.py --baseline a.png --candidate b.png # 对比两张delta -0.5 时退出码 15.2 ui_lint.py源码级设计 token 纪律ui_lint.py扫描 SwiftUI 源码默认根Sources/JCodeMobile检查五类问题并各带惩罚权重检查项规则每次扣分raw_colorColor(red:/white:/.sRGB/hue/displayP3)字面量及Color(hex:)应走 Theme token6raw_font.font(.system(size:))字面量应走Theme.mono(..)4magic_radiuscornerRadius:不在{0, 8, 10, 12, 14, 16, 20, 999}刻度上2magic_pad.padding(字面量)偏离 4pt 间距网格{0,2,4,...,64}1long_view单个视图文件超过 220 行预算提示拆分5Theme.swift本身豁免颜色/字体检查token 定义处。总体分数为100 - 总惩罚 / 文件数按文件数归一避免新增干净文件稀释信号--min参数可在基线清理完后用于 CI 门控低于阈值退出码 1默认 0 即只报告。5.3 ui_matrix.py设备 × Dynamic Type × 场景 的布局矩阵单张截图只测一个内容状态ui_matrix.py把矩阵轴定为设备 × Dynamic Type 尺寸 × 场景设备默认iPhone 17大屏 3xiPhone SE (3rd generation)小屏 2x可用--devices覆盖让布局鲁棒性在真实的宽高压力的两端都被测到Dynamic Type主设备额外以accessibility-large复跑一遍通过xcrun simctl ui dev content_size设置让文字缩放破坏暴露在矩阵里--a11y-size 可禁用该变体场景默认empty,short,tool,long,code每个场景对应一个预置了相应会话内容的 mock 网关实例即第 2.1 节的--scenario。每个单元cell的流程杀掉旧 mock → 以该场景启动 mock 网关 →simctl uninstall install保证每次都是真·冷启动→ 写入凭据 →simctl launch→ 截图 → 交给ui_metrics.analyze()打分 → 结束前把设备 Dynamic Type 恢复默认值绝不把模拟器留在 accessibility 尺寸上。运行时性能指标README 与 ui_matrix.py 一致每个单元在 schema 上记录尽力而为best-effort的两项指标供reward/scorers/perf.py消费cold_launch_ms全新安装后simctl launch的墙钟耗时进程拉起并返回 pid是冷启动到进程就绪最廉价的近似first_frame_ms轮询截图直到屏幕上约 30% 像素接近 App 背景色0x0F0F14与 Theme 一致为止的耗时超时 12 秒。README 对此有明确的诚实性限定测量包含 harness 自身开销应视为一致的相对信号而非绝对真值若测量失败单元直接省略runtime字段perf 评分器降级为不可用权重重新归一缺失数据永远不会拉垮奖励。--no-perf可整体跳过滚动卡顿采集尚未实现scroll_jank_frac保持缺省。矩阵跑完后输出聚合表含MEAN overall与WORST cell均值就是要爬的那座山一个布局改动只有在整个矩阵的均值上提升才算改进而不是某一张幸运截图上的提升。python3 ui_matrix.py [--devices iPhone 17,iPhone SE (3rd generation)] \ [--scenarios empty,short,tool,long,code] [--a11y-size accessibility-large] \ [--no-perf] [--out DIR] [--json] # 回归门控候选均值不低于基线 0.5 分以内才通过 python3 ui_matrix.py --baseline-json before.json --candidate-json after.json6. reward/完整的 UX 奖励框架reward/目录把上面的度量聚合为一个 0-100 的单一奖励。reward/REWARD_SPEC.md给出生成管线截图(s) 源码树 (可选) AX 树 / 运行时 traces - 每个类别独立的 scorer - CategoryScore(0..100, evidence) - 加权和归一 - 总体奖励 (0..100) 分类别分解 最差单元标注一切在本机 headless 运行mock 场景 simctl截图无 LLM、无真机、无网络。6.1 类别与权重REWARD_SPEC 的核心原则是以真实用户每分钟的感知优先1长时间阅读流式文字与工具输出 → 可读性优先2高频流程发送、打断、切会话的点击成本 → 人机工效次之抽象的像素几何美学重要但不能盖过读得清、够得着。当前规范的六个类别权重合计 1.0A. Space density (0.15)没有浪费的像素——space_efficiency(0.05)、information_density(0.05)、content_safety(0.05无裁切/溢出/截断)前两者对场景感知empty场景按安静的空态 可见的起始入口评分而不是按 30-60% 的会话填充带评分。B. Ergonomics interaction (0.30)权重最高因为用户每会话要触摸这个 App 几十次——touch_targets(0.10 44×44pt)、reachability(0.08拇指舒适区)、interaction_cost(0.12基于 KLM/Fitts 模型、以真实使用日志为据的每动作期望秒数)。C. Visual clarity (0.12)visual_hierarchy(0.04)、consistency(0.04)、rhythm(0.048pt 网格)。D. Legibility accessibility (0.22)对这类产品杠杆最高的类别——contrast(0.14真实渲染内容的 WCAG 对比度含暗色二级/工具输出层)、accessibility(0.08VoiceOver、Dynamic Type、reduce-motion、语义角色)。E. Responsiveness (0.09)layout_robustness(0.05矩阵内方差惩罚脆弱布局)、perf(0.04冷启动到首帧 滚动流畅度best-effort可降级 N/A)。F. Design authenticity craft (0.12)设计过而非生成出来的——styling、simplicity、ai_patterns反AI 味惩罚紫/靛/青 slop 配色、渐变文字、玻璃拟态滥用、通用字体等0 个 tell 即 100 分依据reward/AI_SLOP_RESEARCH.md。这里有一处值得指出的仓库内部不一致ios/TestHarness/README.md仍描述为13 个评分器跨 5 个加权类别A .30 / B .25 / C .20 / D .15 / E .10而仓库当前reward/scorers/下实际有 16 个评分器模块REWARD_SPEC.md定义的也是上表 6 类别的新权重分布。从两者对照看README 记录的是框架的较早快照而REWARD_SPEC.md与评分器目录是当前的权威状态实际运行时权重以各 scorer 模块的WEIGHT常量为准聚合器读取模块属性而非硬编码类别表。6.2 评分器契约与聚合每个 scorer 是reward/scorers/name.py下的一个 Python 模块暴露固定契约NAME space_efficiency # 唯一 id对应分类法 CATEGORY A # 分类法组字母 WEIGHT 0.12 # 框架内相对权重 def score(ctx: Context) - CategoryScore: 纯函数读 ctx返回 CategoryScore。无全局可变状态。Contextreward/context.py提供评分器可能需要的全部输入截图路径 解码后的 numpy 数组、设备与场景、px-per-point scale、源码根目录、可选 AX 树 JSON、可选运行时指标字典CategoryScorereward/types.py为{name, category, weight, value: 0..100, evidence: dict, available: bool}availableFalse表示此处测不了例如没有 Instruments 时的 perf聚合器会丢弃并重新归一权重缺失数据不会悄悄拉垮奖励场景感知通过ctx.scenario实现例如empty是刻意设计的空态场景感知型评分器应在 evidence 里报告mode键便于逐单元审计评分器必须确定、无副作用由reward/test_determinism.py强制同输入 → 同输出。聚合器reward/aggregate.py的行为main()与aggregate()discover_scorers()通过pkgutil.iter_modules自动发现scorers/下满足契约的模块缺失契约属性的模块发警告并跳过逐单元加权平均仅 available 的评分器权重归一单个评分器抛异常会被捕获并降级为availableFalse不会杀死整轮运行总体 跨单元均值同时给出最差单元device/size/scenario与最差类别——告诉你下一步该修什么支持--baseline/--candidate回归门控delta -0.5 则退出码 0否则 1可直接进 CI并行安全是显式设计目标一评分器一文件意味着 swarm 工作流中的多个 worker 永远不会编辑同一文件契约 types.py/context.py是唯一共享 API诚实性则是评分器读渲染出的像素与真实源码而非 view model无法通过谎报状态来作弊。6.3 典型爬山循环README 原样流程# 1. 采集截图矩阵并打分 python3 ui_matrix.py --json /tmp/before.json python3 -m reward.aggregate --matrix-json /tmp/before.json --out-json /tmp/before_reward.json # 2. 做一处 UI 改动重新构建重新测量 python3 ui_matrix.py --json /tmp/after.json python3 -m reward.aggregate --matrix-json /tmp/after.json --out-json /tmp/after_reward.json # 3. 门控仅当奖励未回退时才保留改动 python3 -m reward.aggregate --baseline /tmp/before_reward.json --candidate /tmp/after_reward.json # 评分器必须保持纯函数/确定性 python3 -m reward.test_determinismaggregate.py也支持对单张截图打分--shot a.png --device iPhone 17 --scenario short --scale 3--source-root默认指向ios/Sources/JCodeMobile。6.4 扩展方式一文件即插即用新增一个类别是往reward/scorers/丢一个文件的事满足契约NAME、CATEGORY、WEIGHT、score(ctx) - CategoryScore即可被聚合器自动发现。README 与 REWARD_SPEC 对此的表述一致aggregate.py的发现逻辑检查四个契约属性齐备也印证了这一点——没有任何注册表需要维护。7. 适用前提与运行环境平台E2E 流水线与 UI 矩阵依赖xcodegen、xcodebuild、xcrun simctl只能在 macOS Xcode 环境运行mock_gateway.py与protocol_smoke_test.py为纯 Python 标准库可在任意平台独立运行对 mock 或真实网关做协议断言依赖ui_metrics.py/ui_matrix.py的像素分析需要numpy与Pillowui_matrix.py中背景占比检测对两者做了try/except保护缺失时降级为 0.0端口与设备默认端口7643、默认设备iPhone 17、Bundle IDcom.jcode.mobile均可通过参数覆盖模拟器限制凭据注入依赖 Keychain 不可用时的文件回退路径未签名模拟器构建这是该机制成立的前提诚实性边界cold_launch_ms/first_frame_ms含 harness 开销是相对信号perf 类评分缺失时降级而非记零。8. 小结ios/TestHarness的价值在于把三件通常彼此分离的事焊成了一条确定性流水线线协议级 mock 服务端逐字节对齐crates/jcode-base/src/gateway.rs的/health、/pair、/ws协议、一条命令的模拟器 E2E利用jcode-servers.json文件回退路径注入凭据绕开不可脚本化的系统确认弹窗、以及可门控的 UI 奖励度量像素评分 源码 lint 矩阵均值 16 个可插拔评分器聚合为 0-100 奖励--baseline/--candidate让每次改动都有可测的 /- delta。对任何想在不碰真机的前提下让移动客户端行为可爬山的团队这套mock 网关 场景矩阵 奖励聚合的组合本身就是一个可参考的工程范式。【免费下载链接】jcodeThe most RAM efficient harness项目地址: https://gitcode.com/GitHub_Trending/jcod/jcode创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表