lark-cli Contact 通信录命令 E2E 覆盖率解读:从覆盖率报告到 `+get-user` / `+search-bot` 的实测验证
lark-cli Contact 通信录命令 E2E 覆盖率解读从覆盖率报告到get-user/search-bot的实测验证【免费下载链接】cliThe official Lark/飞书 CLI tool, maintained by the larksuite team — built for humans and AI Agents. Covers core business domains including Messenger, Docs, Base, Sheets, Calendar, Mail, Tasks, Meetings, and more, with 200 commands and 20 AI Agent Skills.项目地址: https://gitcode.com/gh_mirrors/cli414/cli本篇技术指南以 lark-cli 仓库中 tests/cli_e2e/contact/coverage.md 这份 Coverage 报告为核心梳理 Contact通信录域三个叶子短命令的端到端E2E测试覆盖现状哪些命令被覆盖、用怎样的断言证明、哪些命令因租户数据不稳定而刻意未覆盖。读完本文你将理解 lark-cli E2E 测试的覆盖度量口径、与租户数据无关tenant-independent的断言设计哲学以及get-user、search-bot背后真实的 API 调用链与参数契约。一、这份 Coverage 报告是什么coverage.md是 contact 域 E2E 测试的覆盖率快照位于 tests/cli_e2e/contact/coverage.md与测试用例contact_lookup_workflow_test.go、contact_search_bot_workflow_test.go同目录存放。它的作用是让维护者一眼看清通信录域的每个叶子命令是否被真实的编译后二进制 真实租户验证过以及某个命令没有被覆盖时原因是测试遗漏还是客观上无法稳定验证。报告的核心度量如下指标数值分母叶子命令数3已覆盖命令数2覆盖率66.7%三个叶子命令分别是contact get-user查询用户信息、contact search-bot搜索机器人/应用、contact search-user搜索用户均注册在 shortcuts/contact/shortcuts.go 中func Shortcuts() []common.Shortcut { return []common.Shortcut{ ContactSearchUser, ContactSearchBot, ContactGetUser, } }二、覆盖度量口径以叶子命令为分母报告使用叶子命令leaf commands而非测试用例数作为分母说明覆盖度量关注的是用户可调用的顶层命令是否获得端到端验证而非测试数量。一个命令只要有一条能稳定通过的真实调用链测试就算作已覆盖反之即使写了测试但依赖租户数据的波动断言flaky也不能计入覆盖。这与tests/cli_e2e模块的定位一致。tests/cli_e2e/README.md 明确说明该模块通过运行编译后的二进制、从用户视角执行命令来验证真实工作流捕获单测难以发现的回归。运行方式为make build go test ./tests/cli_e2e/... -count1三、已覆盖contact get-user的用户态与机器人态工作流报告将get-user标记为 ✓由TestContact_LookupWorkflowAsUser和TestContact_LookupWorkflowAsBot两个用例共同证明见 tests/cli_e2e/contact/contact_lookup_workflow_test.go。3.1 用户态先取自己再把open_id回灌TestContact_LookupWorkflowAsUser采用先读后写write-after-read 风格的闭环验证get self as user以用户身份执行contact get-user省略--user-id从 stdout 的data.user.open_id取出当前用户自己的open_idget self by open id as user把上一步得到的open_id作为--user-id再执行一次contact get-user断言返回结果中data.user.user_id与第一步的selfOpenID相等。这个闭环的价值在于不依赖任何预置数据纯粹用命令自己产出的标识符去查询自己来证明查询链路自洽。对应的底层实现见 shortcuts/contact/contact_get_user.go省略--user-id时走GET /open-apis/authen/v1/user_info获取当前登录用户信息携带--user-id且为 user 身份时走POST /open-apis/contact/v3/users/basic_batch轻量批量接口仅返回姓名与user_id这正是测试断言data.user.user_id而非open_id的原因——两种身份路径返回的字段形态不同。3.2 机器人态经api get发现目标再查询TestContact_LookupWorkflowAsBot同样分两步discover user via api as bot先通过通用命令api get /open-apis/contact/v3/users --params {department_id:0,page_size:10}以 bot 身份拉取用户列表从data.items.0.open_id取一个目标open_id。报告特别注明这一步只是 fixture 准备发现测试目标不计入 contact 域的分母并且如果返回权限错误permission denied或错误码99991679测试会直接t.Skipf跳过——因为那是租户缺少 bot 通讯录权限与 CLI 实现无关get user by open id as bot以 bot 身份执行contact get-user --user-id open_id断言返回的data.user.open_id与发现阶段一致。在源码中bot 身份的按 ID 查询走GET /open-apis/contact/v3/users/:user_id全量档案接口并携带user_id_type参数contact_get_user.go。get-user的身份能力差异如下表维度user 身份bot 身份省略--user-id查自己支持GET /open-apis/authen/v1/user_info不支持校验阶段直接报错提示必须指定--user-id携带--user-idPOST /open-apis/contact/v3/users/basic_batch轻量GET /open-apis/contact/v3/users/:user_id全量权限 scopecontact:user.basic_profile:readonlycontact:user.base:readonly、contact:contact.base:readonly--user-id-type默认open_id可选union_id/user_id同左机器人身份无法查询自己这一点在源码Validate回调中有硬性约束if runtime.Str(user-id) runtime.IsBot() { return common.ValidationErrorf(bot identity cannot get current user info, specify --user-id). WithParam(--user-id) }四、已覆盖contact search-bot的租户无关断言search-bot由TestContactSearchBotWorkflowAsUser与TestContactSearchBotRejectsFilterOnlyAsUser共同证明见 tests/cli_e2e/contact/contact_search_bot_workflow_test.go。该命令的底层实现是POST /open-apis/bot/v4/bot/search常量botSearchURL见 shortcuts/contact/contact_search_bot.go且仅支持 user 身份AuthTypes: []string{user}scope 为search:bot。4.1 核心思想不做最低行数断言TestContactSearchBotWorkflowAsUser的注释明确记录了一段设计取舍早期版本曾用硬编码关键词要求至少命中一个机器人但这引入了租户依赖——某个租户恰好没有匹配该词的机器人时测试就会无辜失败。因此现在的版本执行contact search-bot --query 助 --format json--format json是为了让 envelope 元数据进入 stdout供gjson解析只断言返回信封的形状不对行数做任何要求data.bots必须是数组即使为空也要是数组而非 nulldata.has_more字段必须存在对返回的每一行open_id非空且以ou_前缀开头chat_id字段必须存在允许为空字符串match_segments必须是数组、绝不能为 null。零行即通过zero rows is a pass是这份覆盖报告最重要的设计哲学把断言收敛到任何租户都成立的不变量上——命令能鉴权、服务端接受请求、信封形状稳定、字段契约完整——而不是绑定某个租户的具体数据。4.2 字段契约背后的源码证据测试断言的三个字段在源码searchBot结构体中都有对应contact_search_bot.gotype searchBot struct { OpenID string json:open_id ChatID string json:chat_id // P2P chat with the bot允许为空 MatchSegments []string json:match_segments // ... }其中match_segments来自对 API 返回的display_info中h.../h高亮标签的解析parseBotDisplayInfo源码保证初始化时matchSegments make([]string, 0)而非 nil这与测试never null的断言完全对应。而open_id前缀ou_是因为projectBots直接把 API item 的ID原样透出为OpenID。4.3 纯过滤请求的本地拒绝零 API 调用TestContactSearchBotRejectsFilterOnlyAsUser钉死了另一个契约--has-chatted单独出现不带--query必须被本地校验拒绝因为该请求不会真正发出、不消耗 API 配额所以在任何租户都能稳定复现。测试断言退出码非 0stderr 中error.type为validation错误信息同时命名--query与--queries两个参数error.params[].name的集合等于[--query, --queries]。源码中对应的错误构造在botSearchKeywordRequiredError()contact_search_bot.goreturn common.ValidationErrorf( specify --query or --queries: --chat-ids and --has-chatted shape a keyword search but cannot enumerate bots on their own). WithParams( errs.InvalidParam{Name: --query, Reason: required unless --queries is given}, errs.InvalidParam{Name: --queries, Reason: required unless --query is given}, )故意同时命名两个参数是为了避免 Agent 只看到--query就以为--queries不是合法的关键词入口。search-bot的完整参数表如下参数类型默认值约束--querystring—≤ 50 字符与--queries互斥二选一必填--queriesstring—逗号分隔多关键词并行搜索去重后 ≤ 上限条数--chat-idsstring—CSV≤ 100 个 chat_id先归一化再去重--has-chattedbool未设置显式传false被拒绝防止静默错误结果--page-sizeint201–304.4 单查询与多查询 fanout 分发search-bot的执行入口executeBotSearch会根据是否提供--queries分发到单查询或 fanout 模式func executeBotSearch(ctx context.Context, runtime *common.RuntimeContext) error { if strings.TrimSpace(runtime.Str(queries)) ! { return executeBotSearchFanout(ctx, runtime) } return executeBotSearchSingle(ctx, runtime) }fanout 模式实现见 contact_search_bot_fanout.go对每个关键词并行发出一次POST /open-apis/bot/v4/bot/search最终拼成扁平的bots[]并附queries[]sidecar。另外值得注意的一个细节响应中的page_token被刻意不暴露给用户——注释说明两个搜索命令都不支持分页与其给一个没有任何 flag 能消费的 token 误导调用者不如让用户通过收窄关键词来获得更精确的结果。五、未覆盖contact search-user的客观原因报告将search-user标记为 ✕未覆盖原因写得非常坦诚UAT用户验收测试环境中即使使用 self-derived 标识符即从get-user取回的自己的open_id作为查询条件contact search-user也无法可靠返回当前用户因此无法提供稳定的 write-after-read 式证明。换句话说团队并非没有尝试。search-user的底层是POST /open-apis/contact/v3/users/searchcontact_search_user.go它同样只支持 user 身份scopecontact:user:search。测试者原本可以仿照get-user的闭环先取自己的open_id再用--user-ids me或直接回灌该open_id搜索自己并断言结果包含自己。但在 UAT 租户上这一行为不稳定若把这种波动的断言计入覆盖就会得到一个随时可能红掉的测试套件。这恰好与search-bot测试注释中tenant dependency that kept search-user out of live coverage互相印证一个依赖租户数据波动的断言宁可标记为未覆盖也不能算作覆盖。报告因此把search-user留在 Blocked area等待其自查询行为在 UAT 稳定后再补充测试。search-user本身的参数能力作为未覆盖命令的背景知识非常丰富参数说明--query/--queries关键词搜索 / 逗号分隔多关键词并行搜索≤ 50 字符--user-idsopen_id 列表CSV≤ 100me代表调用者用于直接查人或限定--query范围--has-chatted限定为聊过天的用户API 字段has_contact--has-enterprise-email限定有企业邮箱的用户API 字段has_enterprise_email--exclude-external-users排除跨租户外部用户API 字段exclude_outer_contact--left-organization限定已离职用户API 字段is_resigned--lang覆盖localized_name的语言如zh_cn、en_us--page-size默认 201–30源码searchUserBoolFilters表说明了 CLI 参数名与 API 字段名的映射关系has-chatted→has_contact、exclude-external-users→exclude_outer_contact、left-organization→is_resigned并注明阅读 API 文档时需要借助此表进行翻译。四个布尔过滤器的共同约束与search-bot一致显式传false一律拒绝——因为 Agent 传false的意图几乎总是不要过滤而 API 会将其理解为必须不匹配硬报错可以阻止静默的错误结果。六、覆盖哲学总结什么才算已覆盖综合这份报告lark-cli contact 域 E2E 覆盖遵循三条可复用的原则以叶子命令为度量单位而不是测试条数命令级闭环验证如get-user的 self round-trip优先于散点断言。只断言租户无关的不变量信封形状数组 vs null、字段存在性、前缀契约ou_、本地校验行为validation 错误、零 API 调用这些都是任何租户都必须成立的性质。拒绝 flaky 证明数据波动导致无法稳定复现的场景如search-user的 UAT 自查询明确标记为 Blocked 而非虚报覆盖保持覆盖率报告的可信度。七、如何在本仓库复现这些测试contact 域 E2E 测试位于 tests/cli_e2e/contact/依赖共享测试框架 tests/cli_e2e/core.go。框架通过环境变量控制测试前提TEST_USER_ACCESS_TOKEN/LARKSUITE_CLI_USER_ACCESS_TOKEN控制用户态用例SkipWithoutUserTokenTEST_TENANT_ACCESS_TOKEN/LARKSUITE_CLI_TENANT_ACCESS_TOKEN控制机器人态用例SkipWithoutTenantAccessToken未设置对应 token 时用例自动跳过不会因缺少凭据而失败。Request结构体支持Args、Params、Data、Stdin、DefaultAsuser/bot 身份切换等字段Result提供ExitCode、Stdout、Stderr配合AssertExitCode、AssertStdoutStatus检查ok或code字段等断言方法完成结果校验。复现步骤# 1. 构建二进制 make build # 2. 准备真实租户凭据用户 token、租户 token按需设置环境变量 # 3. 仅运行 contact 域 E2E go test ./tests/cli_e2e/contact/... -count1 -v若需查看或维护测试用例仓库还提供了本地技能tests/cli_e2e/cli-e2e-testcase-writer安装方式见 tests/cli_e2e/README.md。覆盖报告应随测试用例的增删同步更新保持 66.7% 或更高数值背后的事实与源码、测试三方一致。八、延伸阅读覆盖率报告原文tests/cli_e2e/contact/coverage.md用户/机器人查询工作流测试tests/cli_e2e/contact/contact_lookup_workflow_test.go机器人搜索工作流测试tests/cli_e2e/contact/contact_search_bot_workflow_test.go命令实现get-user见 shortcuts/contact/contact_get_user.gosearch-bot见 shortcuts/contact/contact_search_bot.gosearch-user见 shortcuts/contact/contact_search_user.goE2E 测试共享框架tests/cli_e2e/core.go【免费下载链接】cliThe official Lark/飞书 CLI tool, maintained by the larksuite team — built for humans and AI Agents. Covers core business domains including Messenger, Docs, Base, Sheets, Calendar, Mail, Tasks, Meetings, and more, with 200 commands and 20 AI Agent Skills.项目地址: https://gitcode.com/gh_mirrors/cli414/cli创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考