
CopilotKit Slack 桥端到端测试框架在真实 Slack 工作区中对流式 Agent 回复做采样与验证【免费下载链接】CopilotKitThe Frontend Stack for Agents Generative UI. React, Angular, Mobile, Slack, and more. Makers of the AG-UI Protocol项目地址: https://gitcode.com/GitHub_Trending/co/CopilotKit本文围绕仓库中 examples/slack/e2e/README.md 展开CopilotKit Slack 桥Slack bridge在把 Agent 接入 Slack 时单元测试难以发现只在真实环境暴露的缺陷——例如流式输出时未闭合的代码围栏code fence泄漏到整条消息、mrkdwn 翻译在 Slack 客户端里渲染异常、Block Kit 限制被遗忘、某些配置下 Bolt 事件不触发等。为此仓库提供了examples/slack/e2e/这套“活体”端到端测试框架在真实 Slack 工作区发送真实用户消息在 Bot 回复流式进行中反复采样并在长流中截屏、最后核验落地内容。读完本文你将理解该框架的目录结构、运行方式、采样与断言机制、用例目录的组织方式以及如何为 Slack 桥添加自己的端到端用例。为什么单元测试锁不住“Slack 桥”examples/slack/e2e/README.md的开篇给出了这套框架存在的根本理由src/__tests__/下的单元测试只锁定每个模块的内部契约它们捕捉不到只在端到端场景才浮出水面的问题流式过程中一个未闭合的代码围栏 泄漏到 Slack 消息的其余部分一种 mrkdwn 翻译在测试里“看起来正确”但在 Slack 真实客户端中渲染怪异被遗忘的 Block Kit 限制例如附件中块数量上限、button.value的长度约束某些设置下 Bolt 事件不触发例如未订阅app_mentions:read时 Bot 永远收不到 mention。文档明确写道e2e/cases.ts里的用例目录是“什么算 feature-complete功能完备”的事实来源source of truth。也就是说这套 e2e 不只是回归测试它还充当 Slack 桥的功能验收清单——目录里没有的维度就不能宣称已被真实环境验证。背景提示按 examples/slack/README.md 的 Tests 章节说明该 Slack e2e 框架正处于迁移期目标是新版createChannelAPI当前run.ts主测试器已实现为 API-first 方式见下文而文档描述的部分浏览器路径登录、截图对应早期设计仓库中以grab-user-token.ts、restart-recovery.ts等辅助脚本形式保留。目录结构与职责划分e2e/目录下的文件职责非常清晰路径均相对仓库根目录examples/slack/e2e/ ├── README.md 本文档框架说明 ├── cases.ts 用例目录按技术轴向组织鼓励大量扩充 ├── slack-api.ts Slack Web API 帮助函数历史、线程回复、采样 ├── run.ts 框架入口——发送提示词、采样、生成报告 ├── grab-user-token.ts 一次性脚本自动获取用户 OAuth Tokenxoxp- ├── restart-recovery.ts 桥重启恢复专项 e2eHITL 组件 ├── telegram-cases.ts / telegram-api.ts / telegram-run.ts Telegram 对照框架 └── results/ 每次运行的输出截图 JSON 报告其中run.ts是主测试器入口cases.ts是测试用例的“目录数据库”slack-api.ts是纯 API 层不依赖浏览器grab-user-token.ts与restart-recovery.ts分别覆盖“令牌获取”与“重启恢复”两个特殊场景。package.json中对应的脚本为e2etsx e2e/run.ts与e2e:restarttsx e2e/restart-recovery.ts另有一个e2e:telegram对应 Telegram 框架。运行方式与环境准备一次性登录文档描述的浏览器路径原文档给出的首次运行步骤是用 Playwright 的持久化浏览器配置登录一次 Slack之后的运行复用该 profile# from examples/slack/ # one-time: log into Slack once in the playwright browser profile. # Subsequent runs reuse that profile. pnpm exec playwright open --browserchromium --user-data-dir./e2e/.chrome-profile \ https://app.slack.com/client/T05QFA4BW9X/C0B49MEJ1HQ # then: pnpm e2e浏览器登录之所以必要是因为早期设计的“发送”路径依赖用户在 Slack Web UI 里手动发消息用持久化 profile 里的会话 Cookie而不是通过 Bot 发——Bot 自己发消息会被环回保护loop guard忽略。需要说明的是当前run.ts源码已经改为 API-first——发送走chat.postMessage使用用户的xoxp-token见下文“令牌”一节浏览器只在grab-user-token.ts抓取令牌与旧式截图路径中使用README 里“通过 playwright 驱动的 Slack UI 发送”是对早期实现的描述两者代表了框架的演进状态。环境变量README 指出运行器期望.env已包含SLACK_BOT_TOKEN用于在 Bot 流式回复期间轮询频道历史。结合 slack-api.ts 的实现实际需要的变量更完整变量必需说明依据 slack-api.ts / run.tsSLACK_BOT_TOKEN是xoxb-Bot Token用于conversations.history、conversations.replies等读取操作缺失时slack-api.ts直接抛错SLACK_USER_TOKEN是运行 run.ts 时xoxp-用户 Token用于以真实用户身份chat.postMessage发送提示词缺失时run.ts会打印提示并退出BOT_USER_ID否Bot 的用户 ID默认U0B45V75NNR用于过滤 Bot 自己的回复E2E_CHANNEL否测试频道 ID默认C0B49MEJ1HQ即#ag-ui-bot-testCASE_FILTER否按用例名子串过滤便于单跑某条用例如CASE_FILTERB6 pnpm e2e其中SLACK_USER_TOKEN的获取已自动化运行pnpm exec tsx e2e/grab-user-token.ts会借助持久化浏览器 profile自动完成“更新 manifest 声明chat:writeuser scope → 保存 → 重装应用 → 从 OAuth Permissions 页读取令牌 → 写入.env”的全过程。slack-api.ts里postAsUser会带上link_names: 1参数让 Slack 把纯文本username解析成真正的 mention token否则 Bot 的app_mention事件不会触发——这是发送路径上一个很容易踩的细节。注意README 中“from packages/slack/”的目录说明已随仓库结构调整过期实际应位于examples/slack/脚本定义见 examples/slack/package.json。采样机制如何在流式回复中“偷看”中间状态README 用 5 步概括了每条用例的执行流程run.ts与slack-api.ts给出了精确实现发送提示词通过 Slack UI旧路径或chat.postMessageAPI-first 路径把用户消息发到测试频道。轮询回复按sampleIntervalMs间隔轮询conversations.replies或对 DM / 平铺回复用conversations.history直到maxWaitMs耗尽或回复趋于稳定。记录每次采样包括已用时间elapsedMs、Bot 回复文本快照、以及括号平衡检查结果isBalanced(text)。按截图时间点截屏在screenshots[i]指定的偏移毫秒处用 Playwright 对 Slack 线程面板截屏README 记载的设计当前run.ts的 API-first 实现主要保留采样快照见下文。产出报告运行结束后写入results/timestamp/report.json与截图。稳定判定与“三连稳”策略slack-api.ts中的watchForReply是采样核心它每次轮询都抓取线程中 Bot 的第一条回复调用onSample回调记录快照并维护一个“稳定计数”——当连续3 次采样文本长度都相同且大于 0 时认为流式回复已经稳定提前结束采样watchForReply 实现。watchForNextReply是它的变体用于**追问follow-up**场景先数清线程里已有的 Bot 回复数seenCount然后只等“第seenCount 1条”新回复。watchForChannelReply则是更宽松的兄弟函数用于回复落在频道顶层DM / 斜杠命令而非线程内的情形。每次采样记录什么run.ts的CaseResult.samples数组为每次采样记录elapsedMs距发送的毫秒数balanced当前文本的括号平衡状态len当前文本长度preview前 100 字符的预览full仅对不平衡的样本保存完整文本以便无需重跑即可诊断平衡样本不存全文保持报告体积可控。这种“逐样本断言括号平衡”的设计很有价值README 明确指出balancedBrackets要在每一个采样点上校验而不是只在最终文本上校验——也就是说流式过程中的任何时刻出现悬空围栏都会被抓住。括号平衡的微妙判定isBalancedisBalancedslack-api.ts处理了一个流式场景的经典难题Agent 刚刚打开一个围栏例如 python后面还没有内容时缓冲区里围栏标记数量是奇数但视觉上 Slack 会把它渲染成一个空的/临时代码块内容随即填充——此时 autoCloseOpenMarkdown 故意**不**补上闭合围栏因为追加反而会造成闪烁。因此该函数把“刚打开、还没有实质内容”的围栏视为平衡只有围栏后面存在真实内容语言行之后有非空白字符却缺少闭合时才判为不平衡。同样的逻辑也应用于围栏外的内联反引号奇数个且反引号后没有内容时视为“即将闭合”不算缺陷。用例目录cases.ts一张可扩充的“技术轴向”清单README 强调用例按技术轴向technical axis组织而非产品功能角度——每个 prompt 只是“能可靠触发我们要测的那个维度”的话术。E2ECase接口cases.ts定义了全部字段字段类型作用namestring人类可读标签如B6 — long response, multi-paragraphpromptstring发送到#ag-ui-bot-test的用户消息文本sampleIntervalMsnumber可选流式期间轮询间隔默认 1000maxWaitMsnumber可选采样超时上限默认 30_000screenshotsnumber[]可选发送后若干毫秒处的截屏时间点followUp对象可选不重新 mention 就向同线程发送的追问轮带自己的 prompt 与 expectationsinterrupt对象可选流式中断场景在afterMs后向同一线程发第二条消息验证正在进行的回复被中止并标记expectations对象可选对最终文本的断言集合见下表expectations支持的断言类型均在run.ts的runExpectations与runCase中实现finalContains/finalNotContains最终回复必须包含 / 不得包含的子串大小写不敏感balancedBrackets最终文本括号平衡minLength回复最小字符数用于抓住截断回归monospaceAlignedTable验证最终文本包含一个等宽表格且所有表格行等长列对齐而不是“管道符乱炖”expectedChunkCount统计本条用例产生的 Slack 消息条数验证“长围栏块整体落进一条消息而不被拆分”的分块行为perReplyChecks自定义谓词接收线程内 Bot全部回复的文本数组与原始 Slack 消息对象含blocks、ts等可断言跨分块的属性例如“恰好一条消息包含围栏开启符”或“没有任何消息文本含悬空 ”。用例分组速览CASES数组cases.ts按轴向分组示例包括A. 触发面A1 — top-level mention断言最终回复含 HOTEL 且长度 ≥5。/agent斜杠命令用例被注释掉原因写得很实在斜杠命令需要真实的斜杠命令调用无法用chat.postMessage触发只能手动。B. 响应长度/形态B2针对历史上“单 token 回复 ECHO/AL 吞字”的 bugB6/B7是长回复用例8 段 essay配多时间点截屏并断言minLength 括号平衡B11加粗/斜体翻译成*/_且不得残留**、B13markdown 子弹列表翻译成 mrkdwn 的•、B16围栏代码块流式输出、B17表格退化为等宽且列对齐B-chunk-spill用perReplyChecks验证“整个长围栏块落在同一条 Slack 消息里”。C. 流式动态C-stream-1以 500ms 采样间隔盯住流式中打开的围栏验证中间状态始终平衡。D. 会话状态D-state-1验证不重新 mention也能在线程内续聊追问 BRAVO。Interrupt中断3.5 秒后向同线程发“算了就说 PONG”断言被中断的第一条回复带有(interrupted)标记、第二条新回复包含 PONG。E. 前端工具与上下文E-tag-1Agent 必须调用lookup_slack_user得到真实USERID才能 人禁止写纯文本atai、E-component-1渲染issue_list组件成含CPK-NNN的 Block Kit section、E-notion-1渲染page_list、E-restart-1confirm_write选择器的每个按钮必须在value字段携带 JSON 编码的 resume 载荷——这正是桥重启后仍可点击的“持久动作”故事、E-hitl-1HITL 门的回退文本 Approve、E-context-1验证 Slack 使用上下文真的以系统消息注入到了 LLM方法是让 Agent 逐字引用上下文里的特征短语。F. 环回/echo/子类型过滤F-edit验证编辑已发送的消息不得再次触发Bot。中断、续聊与重启恢复三个特殊场景的验证设计流式中断interruptrun.ts对带interrupt的用例会先setTimeout在afterMs后向同线程发第二条用户消息随后等待 10 秒让第二条回复落地再读取线程内全部 Bot 回复。这里有一个工程细节Bot 会发布:warning:、:wrench:、:white_check_mark:等中间状态消息它们不是真正的回复因此代码先用isStatus过滤掉这类消息再以“第一条”和“最后一条”有意义回复分别对应“被中断的回复”与“新回复”来断言。线程续聊followUp带followUp的用例在首轮回复完成后向同一线程threadTs: parentTs发送第二条消息且不带 mention并用watchForNextReply精确等待“新增的那条” Bot 回复而不是误把首轮回复当结果。桥重启恢复restart-recovery.tsexamples/slack/e2e/restart-recovery.ts是独立于run.ts的专项 e2e脚本e2e:restart验证的是“选择器已发出、用户却在一场部署重启之后才点击”的恢复故事启动桥实例 #1进程内发提示词触发confirm_write选择器轮询 Slack 直到选择器落地断言每条消息的metadata.event_payload.handler存在、每个按钮value都是合法 JSON可JSON.parse停掉实例 #1——它内存中的HumanInTheLoopRegistry被丢弃启动实例 #2全新的内存注册表用app.processEvent把合成的block_actions事件注入实例 #2 的 Bolt 应用模拟“过期点击”轮询 Slack断言原选择器被就地替换为已决状态的渲染section 文本匹配/approved|declined/i且不再有按钮HITL 场景下 Agent 的自然语言回复不必出现LangGraph 线程已 RUN_FINISHED。这套设计的核心洞察是Slack 本身同时保存了绑定用的 resume 值button.value和分发上下文message.metadata.event_payload所以 Slack 才是“跨重启的真相来源”。makeBridge中装配了defaultSlackTools appTools、defaultSlackContext appContext、appComponents与appHitl与主应用的createChannel配置一一对应。结果报告与退出码每次运行run.ts会在examples/slack/e2e/results/ISO时间戳/report.json写入完整报告包含ranAt、botUserId与每条用例的结果对象状态pass/fail、错误列表、耗时、最终文本、不平衡采样数以及完整的采样序列followUp与interrupt场景则嵌套各自的子结果。控制台会逐条打印✓/✗、耗时、长度、采样数与不平衡数。最后按“全部通过则退出码 0否则退出码 1”结束——这让它可以直接接进 CI。CASE_FILTER环境变量支持按用例名子串过滤例如CASE_FILTERB6 pnpm e2e调试单条用例时非常顺手。添加用例门槛很低目录本身就是测试面README 的指导原则很直白编辑cases.ts门槛很低——任何你想在 Slack 里“亲眼看它工作”的东西都值得进目录。不必担心与单元测试重复单元测试证明“代码内部正确”e2e 证明“Slack 真的那样渲染”。实践中新增一条用例通常只需起一个语义清晰的名字沿用A/B/C/D/E/F分组前缀写一个能稳定触发目标维度的 prompt注意以U0B45V75NNR开头触发 mention或用/agent前缀走斜杠命令路径按需设置sampleIntervalMs、maxWaitMs、screenshots用expectations声明最终断言复杂场景用perReplyChecks直接检查线程里 Bot 的全部回复与原始 blocks。一个值得留意的兼容点/agent斜杠命令的回复落在频道顶层平铺而非线程内run.ts通过检测prompt是否以/agent开头来选择watchForChannelReply而不是watchForReply。当前限制与演进方向README 明确列出了框架的两点限制且仓库现状与之部分呼应发送消息仍依赖 UI 自动化无用户 token早期设计需要一次性登录持久化 profile。当前的run.ts已通过SLACK_USER_TOKENxoxp-走纯 API 发送绕开了浏览器README 里“未来的增强一个长期有效的用户 OAuth token 可以完全跳过浏览器的发送步骤”已经部分落地为grab-user-token.ts与slack-api.ts的实现。截图仍需要浏览器README 记载的截屏路径依赖 Playwright当前run.ts的 API-first 实现主要记录文本采样快照cases.ts中的screenshots时间点字段被保留可视为框架迁移过程中的过渡状态。另外examples/slack/README.md的 Tests 章节注明该 Slack e2e 框架还在迁移到新版createChannelAPI 的途中当前仍针对旧的桥实现与旧的 button-value resume 路径而examples/slack/e2e/TELEGRAM-README.md展示的 Telegram 对照框架则是“手动触发冒烟 自动升级路径”的设计——由于 Telegram Bot API 不允许冒充人类用户发消息默认由操作者手动粘贴 prompt再通过getUpdates轮询校验设置TELEGRAM_SENDER_BOT_TOKEN后可用第二个“发送者”bot 实现全自动须在群组/超群组里运行。小结这套 Slack e2e 框架回答了一个实际工程问题当你的 Agent 输出要在 Slack 里逐 token 流式渲染时什么样的测试才能真正拦住“测试里对、真实环境炸”的回归。它以cases.ts作为功能完备性的事实来源以slack-api.ts的轮询 isBalanced平衡判定作为流式采样的核心引擎用run.ts统一驱动“发送 → 采样 → 断言 → 报告”的闭环再以restart-recovery.ts专门钉死跨重启的持久动作可靠性。对于任何想把 CopilotKit Agent 接入 Slack 并保证长期质量的团队这套框架的结构——技术轴向的用例目录、逐样本的括号平衡校验、perReplyChecks的跨分块断言、以及“Slack 即真相来源”的重启恢复测试——都值得直接参考或移植。【免费下载链接】CopilotKitThe Frontend Stack for Agents Generative UI. React, Angular, Mobile, Slack, and more. Makers of the AG-UI Protocol项目地址: https://gitcode.com/GitHub_Trending/co/CopilotKit创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考