Lark CLI Note 域 E2E 测试覆盖率解析:`note +detail` 与 `note +transcript` 的 Dry-run 验证实践
Lark CLI Note 域 E2E 测试覆盖率解析note detail与note transcript的 Dry-run 验证实践【免费下载链接】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/飞书 CLIlarksuite/cli中 Note 域的 E2E 测试覆盖清单 tests/cli_e2e/note/coverage.md深入剖析 Note 域两条叶子命令note detail、note transcript的端到端E2E测试策略为何目前采用 dry-run干跑覆盖而非 live真实 API覆盖、每个测试用例断言了什么、底层命令实现如何支撑这些断言以及后续补齐 live 覆盖需要满足的前提。读完本文你将掌握该仓库 Note 域的测试骨架、dry-run 输出协议data.api[]identity字段与 gjson 断言方法并能在自己的 CLI 项目中复刻同样的无凭证、无工件即可回归的 E2E 覆盖模式。覆盖率总览100% 的 dry-run 与 0% 的 live 背后的设计取舍coverage.md开篇用一组 Metrics 数字定义了 Note 域 E2E 的覆盖现状指标数值叶子命令总数Denominator2Dry-run 已覆盖Dry-run covered2Dry-run 覆盖率100.0%Live 已覆盖Live covered0Live 覆盖率0.0%文档明确解释了这个看似一边倒的格局live E2E 之所以暂未计入是因为两条命令都依赖会议生成的 note 工件meeting-generated note artifacts——即真实可用的 note 资源必须来自一次真实的会议录制/妙记流程而在当前测试套件中还没有稳定的 create / use / cleanup 夹具fixtures来提供这类工件。因此测试团队选择了先 dry-run 全覆盖、后 live 补充的渐进路线。从仓库结构看Note 域的两条叶子命令集中注册在 shortcuts/note/shortcuts.goShortcuts()返回且仅返回NoteDetail与NoteTranscript两个条目——这正与 coverage 文档中 Denominator: 2 leaf commands 一一对应是覆盖率分母的源码级依据。Dry-run E2E 的测试基础设施RunCmd 与 DryRunGet理解三个测试用例之前需要先了解 Note dry-run 测试依赖的两个基础设施函数它们都位于 E2E 测试公共库 tests/cli_e2e/core.goRunCmd(ctx, Request)执行真实的lark-cli二进制子进程捕获 stdout / stderr / exit code。Request支持Args、DefaultAs对应--as value、Format对应--format format等字段。默认会套用ResultHasRetryableError的有界指数退避重试用于吸收瞬时服务端抖动。DryRunGet(stdout, path)先用 gjson 在标准成功信封的data.path中读取字段若不存在则回退到stdout.path兼容断言直接读取顶层字段如identity的写法。三个测试还共用setNoteDryRunEnv(t)辅助函数通过环境变量为每个测试建立隔离的沙箱t.Setenv(LARKSUITE_CLI_CONFIG_DIR, t.TempDir()) // 独立的配置目录互不污染 t.Setenv(LARKSUITE_CLI_APP_ID, note_dryrun_test) t.Setenv(LARKSUITE_CLI_APP_SECRET, note_dryrun_secret) t.Setenv(LARKSUITE_CLI_BRAND, feishu)这样每个测试都在全新配置目录中以固定 app 凭证运行 dry-run不需要任何网络请求与租户凭证这正是 dry-run 覆盖率可以做到 100% 的根本原因。用例一TestNoteDetailDryRun —— 锚定note detail的请求形状测试断言内容来自 tests/cli_e2e/note/note_dryrun_test.go 的TestNoteDetailDryRunresult, err : clie2e.RunCmd(ctx, clie2e.Request{ Args: []string{ note, detail, --note-id, note_dryrun, --dry-run, }, DefaultAs: user, }) // 断言 1退出码为 0 result.AssertExitCode(t, 0) // 断言 2第一个也是唯一一个API 的 HTTP 方法为 GET // 断言 3请求 URL 精确等于 /open-apis/vc/v1/notes/note_dryrun该用例覆盖的要点可归纳为参数形状--note-id是note detail的唯一必填参数。身份形状通过DefaultAs: user以用户身份执行dry-run 输出会携带identity信息。请求形状仅发起一次GET /open-apis/vc/v1/notes/{note_id}调用不触发任何真实网络请求。底层实现印证note detail的 DryRun 钩子定义在 shortcuts/note/note_detail.govar NoteDetail common.Shortcut{ Service: note, Command: detail, Description: Get note detail (display type, document tokens) by note_id, Risk: read, Scopes: []string{vc:note:read}, AuthTypes: []string{user, bot}, Flags: []common.Flag{ {Name: note-id, Desc: note ID, Required: true}, }, DryRun: func(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { noteID : strings.TrimSpace(runtime.Str(note-id)) return common.NewDryRunAPI(). GET(fmt.Sprintf(/open-apis/vc/v1/notes/%s, validate.EncodePathSegment(noteID))) }, ... }可以看到 dry-run 输出中的api[0].url就是这里用validate.EncodePathSegment对 note ID 做路径段编码后拼出的 URL。真实执行时Execute会调用FetchDetail定义于 shortcuts/note/note.go其请求路径与 dry-run 完全一致GET /open-apis/vc/v1/notes/{note_id}解析出Detail结构体note_id、creator_id、create_time、note_display_type、note_doc_token、verbatim_doc_token、shared_doc_tokens再经runtime.OutFormat输出。dry-run 与 live 使用同一份 URL 构造逻辑因此 dry-run 断言等价于对真实请求路径的回归保护。值得注意的一个健壮性细节FetchDetail对note_display_type的读取同时兼容note_display_type与旧键display_type两种字段见displayTypeValue并把整型枚举映射为稳定字符串normal/unified/unknown方便 Agent 按名称而非魔法数字路由。用例二TestNoteDetailDryRunAsBot —— 钉死 bot 身份接受行为测试断言内容result, err : clie2e.RunCmd(ctx, clie2e.Request{ Args: []string{ note, detail, --note-id, note_dryrun_bot, --as, bot, --dry-run, }, }) // 断言 1dry-run 输出的 identity 字段 bot // 断言 2请求 URL 精确等于 /open-apis/vc/v1/notes/note_dryrun_bot该用例覆盖的要点身份形状note detail显式支持--as bot且 dry-run 输出会原样呈现identity: bot证明命令对 bot 身份是接受并透传的。请求形状即使切换为 bot 身份请求 URL 与方法保持不变仍是一次GET /open-apis/vc/v1/notes/{note_id}。底层实现印证NoteDetail的AuthTypes: []string{user, bot}见 shortcuts/note/note_detail.go决定了它允许 bot 身份而note transcript的AuthTypes仅为[user]。这一差异被单元测试TestNoteDetailAuthTypesIncludeBot与TestNoteTranscriptRejectsBotIdentity显式钉死见 shortcuts/note/note_test.godry-run E2E 则从进程级再确认一次--as bot参数确实被接受且不会改变请求形状。用例三TestNoteTranscriptDryRun —— 两步请求链与参数透传测试断言内容这是三个用例中信息量最大的一个它验证了note transcript的两步请求形状与全部关键参数result, err : clie2e.RunCmd(ctx, clie2e.Request{ Args: []string{ note, transcript, --note-id, note_dryrun, --transcript-format, plain_text, --dry-run, }, DefaultAs: user, Format: json, // 全局 --format json }) // 断言 1api 请求数组长度为 2 // 断言 2api[0] GET /open-apis/vc/v1/notes/note_dryrunnote detail 预检 // 断言 3api[1] GET /open-apis/vc/v1/notes/note_dryrun/unified_note_transcript // 断言 4api[1].params.format plain_text // 断言 5api[1].params.page_size 200 // 断言 6api[1].params.locale zh_cn // 断言 7顶层 transcript_format plain_text该用例覆盖的要点两步请求链先note detail预检再拉取unified_note_transcript。transcript API 查询参数format内容格式、page_size默认 200、locale默认跟随品牌feishu 为zh_cn。--transcript-format与全局--format共存命令级--transcript-format控制转录内容格式markdown/plain_text全局--format控制CLI 输出格式此处为json两者互不遮蔽。这一点也有对应的单元测试TestNoteTranscriptFormatFlagDoesNotShadowOutputFormat见 shortcuts/note/note_transcript_test.go专门断言输出信封中不得出现歧义的format顶层字段。底层实现印证note transcript的 DryRun 钩子定义在 shortcuts/note/note_transcript.goDryRun: func(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { noteID : strings.TrimSpace(runtime.Str(note-id)) transcriptFormat : runtime.Str(transcript-format) locale : resolveTranscriptLocale(runtime) return common.NewDryRunAPI(). GET(fmt.Sprintf(/open-apis/vc/v1/notes/%s, validate.EncodePathSegment(noteID))). Desc([1] Check note_display_type and verbatim_doc_token before transcript fetch). GET(fmt.Sprintf(/open-apis/vc/v1/notes/%s/unified_note_transcript, validate.EncodePathSegment(noteID))). Desc([2] Fetch unified note transcript pages; subsequent pages add cursor_id internally). Params(map[string]interface{}{ format: transcriptFormat, page_size: transcriptPageSize, // 常量 200 locale: locale, }). Set(transcript_format, transcriptFormat). Set(locale, locale). Set(note, CLI first checks note_display_type via note detail, then paginates internally (cursor_id) and saves the full unified transcript to a file) },代码中几个值得展开的常量与逻辑maxTranscriptPages 500、transcriptPageSize 200、pageDelay 100ms预检的必要性Execute阶段会先调用ensureUnifiedNote只有note_display_type unified的妙记才允许转录普通文档normal会返回FailedPrecondition错误并给出提示——若存在verbatim_doc_token建议改用docs fetch --doc token。dry-run 的第一步请求正是这个预检的可观测化。内部游标分页真实拉取时fetchUnifiedTranscript会以cursor_id循环翻页每页 200 条has_more为真且next_cursor_id合法时继续页间间隔pageDelay100ms以温和地访问下游单页失败即整体失败避免保存不完整的转录超过 500 页或游标不前进死循环时主动中止。默认保存路径转录内容拼接后写入文件默认路径为./notes/{note_id}/unified_transcript.mdplain_text 时扩展名为.txt文件已存在且未传--overwrite时会报FailedPrecondition并提示--overwrite。locale 解析优先级--locale显式值 当前 profile 语言 品牌兜底Lark 品牌默认en_us飞书品牌默认zh_cn这正是 dry-run 断言中zh_cn的来源。命令覆盖矩阵从文档到源码的对照coverage.md的 Command Table 可以结合源码整理为更完整的对照表状态命令类型测试用例tests/cli_e2e/note/note_dryrun_test.go关键参数形状未覆盖原因dry-run ✓ / live ✕note detailshortcutTestNoteDetailDryRun--note-id用户身份live note 夹具依赖会议生成的工件dry-run ✓ / live ✕note detailshortcutTestNoteDetailDryRunAsBot--note-id--as botlive note 夹具依赖会议生成的工件dry-run ✓ / live ✕note transcriptshortcutTestNoteTranscriptDryRun--note-id--transcript-format--format jsontranscript API 的format/page_size/locale参数live unified-note 夹具依赖生成的 VC note 工件对应命令的完整参数表以 shortcuts/note/note_detail.go 与 shortcuts/note/note_transcript.go 为准命令参数必填默认值取值/说明note detail--note-id是—经过validate.ResourceName校验URL 中做路径段编码note detail--as全局否user支持user/botAuthTypes限定note transcript--note-id是—同上note transcript--transcript-format否markdown枚举markdown/plain_textnote transcript--locale否跟随 profile 语言或品牌如zh_cn、en_us、ja_jpnote transcript--output否./notes/{note_id}/unified_transcript.{md,txt}经ValidateSafePathTyped校验note transcript--overwrite否falsebool覆盖已存在的输出文件note transcript--format全局否—控制 CLI 输出格式与--transcript-format正交note transcript--as全局否user仅userbot 会被拒绝Dry-run 输出协议测试断言依赖的稳定契约三个测试都通过 gjson 路径从标准成功信封中读取断言字段这构成了 dry-run 输出的稳定契约可对照 tests/cli_e2e/core.go 中DryRunGet的实现理解data.api[]按顺序排列的 API 请求数组每个元素至少含method如GET与url如/open-apis/vc/v1/notes/{note_id}可带params查询参数对象与body。data.api.#数组长度note transcript为 2预检 转录note detail为 1。data.transcript_format/data.locale由Set(...)注入的顶层语义字段便于断言内容格式与输出格式不混淆。identity命令实际生效的身份user/bot顶层读取。该契约的价值在于即使未来 API 返回结构变化只要 dry-run 协议稳定E2E 断言就能持续捕捉 URL 拼接、参数透传、身份接受规则等最容易回归的环节而真实的响应解析与错误映射则由 shortcuts/note/note_test.go 与 shortcuts/note/note_transcript_test.go 中的 httpmock 单测补充覆盖例如空 detail 响应触发ErrEmptyDetail、无权限码 121005 被映射为带vc:note:read与修复提示的PermissionError、空转录与游标循环均拒绝落盘等。边界与防护为什么部分错误路径无需 live 也能被保障coverage.md只统计了 dry-run 的请求形状覆盖但 Note 域的错误处理正确性并不依赖 live 环境——它由internal/httpmock驱动的单元测试独立保障。值得注意的防护点包括无权限场景note detail API 返回错误码121005NoNoteReadPermissionCode时mapNoteError将其规范化为PermissionError消息统一为 no read permission for this note并附带 hint Ask the note owner to grant read permission, then retry同时保留原始错误为 causeerrors.Is可穿透。非 unified 妙记note transcript遇到normal类型时拒绝转录并引导用户使用docs fetch或note detail检查文档 token。分页安全maxTranscriptPages、游标不前进检测、seenCursors去重共同防止死循环parseLooseCursorID对 float64 / json.Number / string 三种 JSON 数字形态做安全解析并拒绝精度超2^53-1的不安全值。上下文中断分页过程中context取消/超时会被转换为NetworkTransport/NetworkTimeout类型化错误并保留 cause。从 dry-run 到 live补齐覆盖的前置条件与建议根据coverage.md的说明live 覆盖率目前为 0 的原因是两条命令的输入note 资源必须来自会议流程生成的工件而测试套件尚未拥有稳定的 create / use / cleanup 夹具。要补齐 live 覆盖需要满足以下前置条件均属于测试基建工作不改变产品代码工件生成能力在测试环境中稳定触发一次真实的会议/妙记流程或在受控租户中预置一批可复用的 unified note 资源。身份与凭证提供可用的租户测试凭证类似 tests/cli_e2e/core.go 中auth status --verify探测并自动 skip 的模式让 live 用例在未配置凭证时优雅跳过不影响 dry-run 的确定性。清理策略建立 after-test 清理删除生成的 note/转录文件与资源回收机制避免测试相互污染。在此之前dry-run 全覆盖已经为 Note 域提供了零外部依赖、可随时回归的请求形状保障这与仓库中其余域的测试实践大量*_dryrun_test.go文件保持一致是值得在其他 CLI 项目中复用的低成本高确定性测试模式。参考文件导航覆盖清单tests/cli_e2e/note/coverage.mdDry-run E2E 用例tests/cli_e2e/note/note_dryrun_test.goE2E 公共基础设施RunCmd/DryRunGet/Request/Resulttests/cli_e2e/core.go命令注册与命令集合shortcuts/note/shortcuts.gonote detail实现shortcuts/note/note_detail.gonote transcript实现分页、落盘、locale 解析shortcuts/note/note_transcript.gonote detail 解析与错误映射shortcuts/note/note.go单元级防护测试shortcuts/note/note_test.go、shortcuts/note/note_transcript_test.go关联域vc notes会议上下文定位 note_id 后复用 note 解析逻辑shortcuts/vc/vc_notes.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),仅供参考