- CLI
- AI 技能
【免费下载链接】cli
The 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.
本篇技术指南围绕 tests/cli_e2e/slides/coverage.md 展开,系统梳理 lark-cli 中 Slides(幻灯片)域的端到端(E2E)测试覆盖矩阵:7 个叶命令全部覆盖、dry-run 层与 live 层双轨验证、错误信封与结构化输出的断言方式。读完本文,你将理解为什么 slides 的命令测试必须跑在"真实二进制"这一层,以及+create、+add-slide、+update-slide、+replace-slide、+media-upload、+media-download各自的覆盖要点与尚未覆盖的边界。
覆盖矩阵总览
根据 coverage.md 的 Metrics 小节,Slides 域共有7 个叶命令(leaf commands),已覆盖 7 个,覆盖率为 100%。这 7 个叶命令全部是 shortcut(快捷命令),而非直接暴露的原始 API 命令:
| 命令 | 类型 | 覆盖方式 |
|---|---|---|
slides +create | shortcut | dry-run 文件输入 + live 创建回读 |
slides +add-slide | shortcut | dry-run 请求形状 + live 增删往返 |
slides +delete-slide | shortcut | dry-run 请求形状(含 wiki URL)+ live 增删往返 |
slides +update-slide | shortcut | dry-run 请求形状 + live 就地更新 + 可选历史回退 |
slides +replace-slide | shortcut | dry-run 规范化 + live 别名替换/插入 |
slides +media-upload | shortcut | dry-run parent_type 拆分 + live 上传下载往返 |
slides +media-download | shortcut | dry-run 两步下载计划 + live 上传下载往返 |
每个命令的覆盖都遵循同一条铁律:dry-run 层通过真实编译出的二进制验证请求构造,live 层通过一次性演示文稿验证后端真实行为。这两层各自承担着单元测试(package test)无法证明的东西。
测试分层的设计动机:为什么单元测试不够
coverage.md 中反复强调一个核心论点:只有"真实二进制"这一层才能证明某个特性。以+create的文件输入为例(slides_create_slide_inputs_dryrun_test.go):
- 一个多行、引号密集的
<slide>XML 页面要进入 JSON 数组,在 shell 里必须做 JSON 转义。测试夹具page1刻意设计成这种形态(<slide xmlns="https://www.larkoffice.com/sml/2.0">\n <data>\n <shape type="text"><content>Q1 "results" & outlook</content></shape>\n </data>\n</slide>),因为"从 shell 组装这个数组"正是文件输入存在的全部理由——调用方此前靠 jq 做转义,而没有 jq 的环境会把命令替换变成空参数。 - dry-run 测试断言
data.api.1.body.slide.content与page1逐字节相等,证明文件字节在没有调用方手里的 JSON 编码器的情况下原样到达请求体;--slide的重复顺序即页面顺序(--slide @./slide-01.xml在前、--slide @./slide-02.xml在后,API 1 是 page1、API 2 是 page2,且不存在 API 3)。 - 拒绝场景
TestSlidesCreateRejectsBothSlideFormsDryRunE2E固定了exit code 2的错误信封:error.type = "validation"、error.subtype = "invalid_argument"、error.param = "--slide"、message 含 "cannot be combined"——这正是 Agent 解析后用于自我修复命令的机器可读结构。coverage.md 特别指出:包级测试从不运行 dispatcher,所以看不到这个信封。
从源码看,这一行为的底层实现在 shortcuts/slides/slides_create.go 的createSlideContents中:--slides(成品 JSON 数组)与--slide(可重复的单页 XML 或@path)互斥;--slide不支持 stdin(-),因为一个进程只有一个 stdin,-无法表达"这一次出现";--slides空值与 JSONnull字面量都会被拒绝(前者是失败的命令替换形态,后者会被误读为"无页面"导致空演示文稿被报告为成功),只有显式[]合法。readSlideArgs还自行处理了@前缀的文件读取与 UTF-8 BOM 剥离,与框架对单值 Input flag 的处理保持一致。
+create:从空壳到分页添加的编排
TestSlides_CreateWorkflowAsUser 是+create的 live 主路径:以用户身份创建演示文稿,断言返回的xml_presentation_id、title、slides_added、slide_ids,随后通过原始 slides API(api get /open-apis/slides_ai/v1/xml_presentations/<id>)读回 XML 内容,证明标题与页面正文确实持久化——而非仅信任写请求自身的响应。测试自清理:在 cleanup 中用drive +delete --file-token <id> --type slides --yes删除演示文稿。
coverage.md 同时点明了 live 测试环境依赖:TestSlides_CreateWorkflowAsUser需要真实用户 token(源码中clie2e.SkipWithoutUserToken(t)会跳过无 token 的环境)。dry-run 测试则通过setSlidesDryRunEnv(t)固定环境,不依赖任何后端。
从实现看(slides_create.go),+create是一个典型的多步编排:先POST /open-apis/slides_ai/v1/xml_presentations创建空壳(buildPresentationXML生成 960×540 的<presentation>标题壳,标题缺省为 "Untitled"),再逐个POST .../slide添加页面;每页进入时单独 lint,坏页面会带着该页的 findings 被拒绝,运行在失败点停止,错误信息会说明已存在的演示文稿和已添加的页数(appendSlidesProgressHint)。createSlideQuery把revision_id钉死为-1(latest)而非暴露给调用方——因为演示文稿刚由同一命令创建,不存在更早的版本可供锁定。+create还声明了docs:document.media:uploadscope(用于@图片占位符上传)并刻意不声明 drive scope(创建从不触碰 drive,URL 在本地BuildResourceURL构建)。每次 create 最多 10 页(maxSlidesPerCreate = 10),更多页面须先创建再逐个+add-slide。
+add-slide/+delete-slide:追加、插入与无--yes的删除
slides_slide_add_delete_dryrun_test.go 中的TestSlidesAddSlideDryRunE2E固定了两个请求形状:
- 追加到末尾:不传
before_slide_id(--slide携带完整<slide>XML,空字符串会被后端当作未知 slide 拒绝,因此省略而非置空);revision_id为-1;images_to_upload为 0。 - 插入到指定页之前:
--before-slide-id slide_target+--revision-id 12(乐观锁示例),并通过--presentation-id别名传参,覆盖了 presentation 参数别名解析。
两个用例都断言POST /open-apis/slides_ai/v1/xml_presentations/presAddDryRun/slide这一 URL 形状。coverage.md 强调:单测已覆盖同样的形状,但只有真实二进制能证明完整<slide>XML 文档在 flag 解析后引号与尖括号原样保留——这恰恰是最容易产生 3350001 报错的一层。
TestSlidesDeleteSlideDryRunE2E则验证删除的请求形状(DELETE .../slide+slide_id+revision_id=-1),并额外证明shortcut 无需--yes即可运行:原始xml_presentation.slide.delete命令属于 high-risk-write,会以 exit 10 + confirmation_required 退出;shortcut 特意把 Risk 定为 "write"(见 slides_delete_slide.go)。TestSlidesDeleteSlideWikiDryRunE2E还证明 wiki URL 会被声明为第一步解析:dry-run 的第一段是GET /open-apis/wiki/v2/spaces/node_by_token?token=wikcnE2ETOKEN,第二段才是删除,而非把 wiki 令牌直接发给 slides API。
live 侧 TestSlides_SlideAddDeleteWorkflowAsUser 在一次性演示文稿上做真实增删往返,且针对回读而非写响应本身断言——slide_id确实指向真实页面、--before-slide-id真的把页面定位在邻居之间而非仅仅被转发、删除后页面消失而两个邻居存活。
+update-slide:单 part 的block_replace设计与历史回退
coverage.md 对+update-slide的 dry-run 覆盖做了精确描述:一次请求携带一个block_replacepart,其block_id是PAGE id——这是整个设计的核心,若放元素 id 只会替换一个元素而留下页面其余部分。同时覆盖了slide服务别名、+update命令别名、--xml拼写,以及两个"必须不产生请求"的拒绝场景:裸元素根、根 id 指向另一页。
live 侧TestSlidesUpdateSlideLiveE2E(slides_update_slide_workflow_test.go)是+update-slide必需的后端断言:用 lane 的 bot 凭证创建一次性演示文稿,用返回的 page id 作为block_id替换其页面,再通过+xml-get读回,验证新标记替换了旧标记且slide_id未变,最后自清理。
TestSlides_HistoryWorkflow(slides_history_workflow_test.go)是+update-slide的可选 live 覆盖:创建演示文稿、就地更新页面、断言返回的slide_id、持久化的标记、以及用原始 id 写回的元素保持该 id,然后通过幻灯片历史回退并自清理。coverage.md 明确标注:它仅在LARK_SLIDES_HISTORY_E2E=1时运行,因此尚未进入默认 live 通道——这是文档诚实记录的"未覆盖边界"之一。
+replace-slide:规范化、严格边界与别名工作流
slides_replace_slide_dryrun_test.go 的三个用例:
- 规范化用例
TestSlidesReplaceSlideNormalizationDryRunE2E:证明replace/insert、target_id、content、element这些兼容别名会被规范化为标准请求,且结构化输出记录所有转换(dry-run 的normalizations字段)。 - 空替换用例
TestSlidesReplaceSlideEmptyReplacementDryRunE2E:真正空的规范化载荷依然失败,保持严格边界。 - 常规用例
TestSlidesReplaceSlideDryRunE2E:合法的混合block_replace+block_insert批处理保持不变。
从实现看(slides_replace_slide.go),+replace-slide相对原始命令有五个价值点:--presentation接受 token / slides URL / wiki URL(wiki 需解析并声明条件 scopewiki:node:read);对每个block_replacepart 自动向 replacement 的根元素注入id="<block_id>"(后端要求根携带该 id,否则返回 3350001,该要求未文档化且反复绊倒调用方);对<shape>元素缺<content/>时自动注入(SML 2.0 schema 要求每个 shape 必须携带 content 子节点);3350001 错误时附带场景化 hint 供 AI Agent 自纠;后端对 parts 产生的页面做 lint,--no-lint可退出。str_replace刻意不暴露——产品方向是幻灯片编辑只走结构性(block 级)操作。parts 上限 200(与 API catalog 声明的服务端上限一致),客户端先校验可快速失败。live 侧TestSlides_ReplaceSlideAliasWorkflowAsUser(slides_replace_slide_workflow_test.go)读取服务端分配的 block ID,通过replace/target_id/content替换一个目标、通过insert/element插入另一个,读回整副演示文稿证明两次写入都持久化在请求位置且控制块存活,清理时删除演示文稿。
+media-upload:native/office 的parent_type拆分
TestSlides_ImageUploadDryRunParentType(slides_image_upload_dryrun_test.go)覆盖的是 driveparent_type的拆分:+media-upload --file以及+add-slide/+update-slide背后@path图片占位符管道,都必须按演示文稿类型选择正确的parent_type——原生(API 创建)演示文稿上传为slide_file,导入的 "office" 演示文稿上传为office_slide_file。
coverage.md 强调负例部分最重要:后端不校验parent_node与parent_type的对应关系,错误的值也能上传成功,只会在之后表现为渲染不出的图片。因此三个入口都以 wiki--presentation补充测试,其真实 token 是预览(dry-run)绝不能解析的;这些用例钉死slide_file是"基于构造的正确"而非猜测——resolvePresentationID拒绝任何obj_type非slides的 wiki 节点(见 helpers.go),而导入的 office 演示文稿是 drivefile节点,永远过不了这道闸。它们存在的原因正是生产代码现在直接断言该值,而不再靠把占位符跑过 office 检查来推导。+create刻意缺席此通道:它没有--presentationflag,总是上传进刚通过 API 创建的演示文稿,其 native-only 期望在单测通道中明确声明。
实现佐证在 slides_media_upload.go:slidesMediaParentType是唯一把 presentation token 映射为parent_type的地方,common.IsLocalOfficeToken识别 office token(token 形态是 drive 级属性,映射才是领域差异);dry-run 的slidesDryRunParentType单独存在,因为占位符 token 不该侥幸落入slideFileParentType分支(那只是占位符拼写碰巧不匹配 office 形态)。注释还记录了实测结论:slide_image返回 1061001、slides_image/slides_file返回 1061002,而slide_file返回可用作<img src="...">的有效 file_token;两个值都不被 multipartupload_prepare端点接受(99992402),因此图片上传统一封顶 20 MB。
+media-download:direct→preview 的两步回退计划
slides_media_download_dryrun_test.go 的三个用例固定了 shortcut 规划的两个请求:先GET /open-apis/drive/v1/medias/<file_token>/download(直接 Drive media 下载),失败后回退GET .../preview_download?preview_type=16(源文件预览制品),以及file_token与解析后的output字段在--output、--output-dir、默认 output-dir 三种形态下的取值。校验用例通过真实命令注册固定了空 token 信封与 output/output-dir 互斥信封——这是包级测试(在 HTTP 层打桩)够不到的层。
实现上(slides_media_download.go),+media-download的 Risk 是 "read",scope 为docs:document.media:download,回退分支才触发条件 scopedrive:file:download;默认输出目录为.lark-slides/media(defaultSlidesMediaDownloadDir),--output指定单文件相对路径且扩展名可选(.png/.jpg/.jpeg),与--output-dir互斥;Execute在直接下载返回权限错误(isSlidesMediaDownloadPermissionError)时才切换 preview 通道,并将source标记为"download"或"preview"。
live 媒体往返:唯一能证明真实媒体 token 的层
TestSlidesMediaUploadDownloadLiveE2E(slides_media_workflow_test.go)是媒体链路必需的后端断言:创建一次性演示文稿、生成确定性的 8×8 PNG 夹具、经+media-upload上传,验证返回的file_token、file_name、presentation_id、size与本地夹具一致;再经+media-download下载回来,验证保存路径落在--output-dir下、content_type是 image/*、source为"download"或"preview"、磁盘字节数与报告的size一致,最后清理演示文稿。coverage.md 的结论是:这是唯一能证明真实 Slides 媒体 token 能穿越 direct-to-preview 过渡并返回可解码图片字节的层。
阅读与验证建议
- 覆盖矩阵本身见 tests/cli_e2e/slides/coverage.md 的 Command Table,表格中每一行都标注了测试用例文件与"未覆盖原因"(如 history workflow 需要
LARK_SLIDES_HISTORY_E2E=1)。 - 想复现 dry-run 断言,可查看 slides_create_slide_inputs_dryrun_test.go 的
--slide @page.xml/--slides @deck.json/--slides -三种输入与拒绝信封;live 用例需要真实 token(SkipWithoutUserToken),历史用例需要额外环境变量。 - 命令实现与测试的对应关系:
+create见 slides_create.go、媒体上传见 slides_media_upload.go、媒体下载见 slides_media_download.go、结构化替换见 slides_replace_slide.go。
这套覆盖设计的核心方法论值得借鉴:dry-run 层证明"命令会构造出什么",live 层证明"后端真正发生了什么",而两层都跑在真实二进制上,才能覆盖到 shell 转义、dispatcher 错误信封、flag 解析保真这类单元测试天然够不到的层。对于以 Agent 为目标的 CLI,后者(机器可解析的error.param信封、normalizations结构化输出)与前者同样重要——它们正是 Agent 自我修复命令的接口契约。
- CLI
- AI 技能
【免费下载链接】cli
The 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.
相关推荐
iptvnator测试策略:单元测试与E2E测试覆盖
iptvnator测试策略:单元测试与E2E测试覆盖 还在为IPTV播放器应用的稳定性担忧?iptvnator采用全面的测试策略,确保应用在各种场景下都能稳定运
音视频视频桌面应用前端Headscale测试覆盖:测试策略与覆盖率分析
Headscale测试覆盖:测试策略与覆盖率分析 概述 Headscale作为Tailscale控制服务器的开源自托管实现,其测试策略和覆盖率直接关系到项目的稳
后端网络认证鉴权OHIF Viewer 测试覆盖率指南:Playwright E2E 测试体系与覆盖率统计实战
OHIF Viewer 测试覆盖率指南:Playwright E2E 测试体系与覆盖率统计实战 本文档围绕 OHIF Viewer 官方 3.11 版测试覆盖率
医疗健康前端音视频
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考