OpenMetadata Conversation V2 端到端可追溯性矩阵:从旧版 Feed 行为迁移到新对话与活动体系
【免费下载链接】OpenMetadataThe Open Context Layer for Data and AI , OpenMetadata is the open platform for building trusted data context and business semantics for humans, AI assistants, and agents.项目地址: https://gitcode.com/GitHub_Trending/op/OpenMetadata
导读
本文以 OpenMetadata 仓库中的 CONVERSATION_V2_TRACEABILITY.md 为主线,系统梳理 Conversation V2 端点切换(endpoint cutover)后,旧的 legacy feed 行为如何在新的/conversations与/activity路由体系中找到等价替代测试。你将看到一张完整的"旧行为 → 新测试"映射表,理解 keyset 分页(cursor 分页)、有界回复水合(bounded hydration)、首条/后续回复去重(no synthetic root / no duplicate POST)等关键机制的落地方式,并学会如何阅读与扩展这套可追溯性矩阵,防止测试随套件改名而"悄悄消失"。
为什么需要一份 Conversation V2 可追溯性矩阵
在 OpenMetadata 的 UI 演进过程中,活动流(Activity Feed)长期依赖旧的 feed 端点,而 Conversation V2 将用户会话(user conversation)、活动(activity)与公告(announcement)拆分为独立的路由体系。端点切换后,一个最现实的风险是:旧的测试套件被重命名或删除,测试覆盖率在无声无息中丢失。为此,仓库在 CONVERSATION_V2_TRACEABILITY.md 中维护了一张逐行映射矩阵,明确记录:
- 每一个可观察的 legacy feed 行为由哪个新测试承担;
- 哪些旧场景被"移除",以及被哪些确定性场景"替代"。
这份矩阵的本质是一份行为契约:即使测试文件名变了,行为必须仍然存在且被某个明确的位置覆盖。它同时回答了三个问题:谁在测、测什么、旧行为去哪了。
从源码结构看,新体系分为两大路由族:
/conversations:用户会话的根(root)、回复(reply)、反应(reaction)、解决(resolve)、编辑(edit)、删除(delete)与 keyset 分页,见 ConversationResourceIT.java(服务端集成测试)与 conversationsAPI.test.ts(前端 REST 客户端单测);/activity:活动事件(activity event)的展示、首条/后续回复、编辑/反应/删除以及同about隔离,见 ActivityAPI.spec.ts(Playwright E2E)与 activityAPI.test.ts(前端 REST 客户端单测)。
公告(announcement)场景则继续沿用/announcements路由,不受本次切换影响。
完整映射矩阵:每个旧行为对应的 Conversation V2 替代测试
下表完整继承自原文档,逐行说明"旧行为/旧测试"与"Conversation V2 替代测试"的对应关系。阅读时建议配合后文的分层验证体系理解每个单元格的实际含义。
| Legacy behavior or test(旧行为或测试) | Conversation V2 replacement(Conversation V2 替代测试) |
|---|---|
| Selected root, bounded replies, and reply count(选中根节点、有界回复、回复计数) | ActivityThreadPanelBody.test.tsx:renders the selected conversation with its hydrated replies;ConversationResourceIT:testCompleteConversationCrudAndBoundedHydration |
| Empty/create/select conversation states(空态/创建/选中会话) | ActivityThreadPanelBody.test.tsx:lists and selects conversations using Conversation V2和creates a conversation and updates the bounded list |
| Conversation keyset pagination(会话 keyset 分页) | Features/ContextCenterArticles.spec.ts等待携带 cursor 的第二次请求及全部 11 条种子根;ActivityThreadPanelBody.test.tsx与ConversationResourceIT覆盖 cursor 转发与 root/reply cursor 正确性 |
| Landing-page widget rendering, navigation, filters, footer, and card structure(落地页组件渲染、导航、过滤器、页脚、卡片结构) | Features/ActivityFeed.spec.ts:Activity Feed Widget场景;Features/ActivityAPI.spec.ts:Homepage Widget场景 |
| User-conversation root reaction and tooltip identity(用户会话根反应与 tooltip 身份) | Features/ContextCenterArticles.spec.ts:Related assets, activity feed, user mentions, and article mentions work |
| User-conversation resolve, edit, and delete(用户会话解决/编辑/删除) | 同一个 Context Center 场景通过/conversations/{id}执行并逐一校验每次变更 |
| User-conversation drawer reply creation(抽屉回复创建) | Features/ActivityFeed.spec.ts:thread drawer opens from reply count and allows posting a reply |
| Mention notification identity and navigation(@提及通知身份与导航) | Features/ActivityFeed.spec.ts:Mention notification shows correct user details in Notification box |
| Chinese mention encoding(中文提及编码) | Features/ActivityFeed.spec.ts:Should encode the chinese character while mentioning api endpoint |
| Homepage and entity All/My Data/Following filters(首页与实体的全部/我的数据/关注过滤器) | Features/Tasks/ActivityFeed.spec.ts,使用 activity 专用路由 |
| Task filters, badge, drawer, and navigation(任务过滤器、徽标、抽屉、导航) | Features/Tasks/ActivityFeed.spec.ts,使用/tasks路由 |
| Context Center conversation entry point and permissions(Context Center 会话入口与权限) | Features/ContextCenterArticles.spec.ts与Features/ContextCenterPermission.spec.ts,使用/conversations路由 |
| Announcement scenarios(公告场景) | 现有公告套件继续通过/announcements |
| First and subsequent activity replies; no synthetic root or duplicate POST(首条与后续活动回复;无合成根、无重复 POST) | Features/ActivityAPI.spec.ts:creates exactly one reply and isolates activities with the same about |
| Activity reply edit, reaction tooltip identity, and delete(活动回复编辑、反应 tooltip 身份、删除) | 同一个确定性 Activity API 场景执行 PATCH、PUT reaction、DELETE 路由 |
Two activities with the sameabout(两个同about的活动) | 同一个 Activity API 场景验证以 ActivityEvent ID 为键的独立容器 |
Removed GlossaryAF-05reply smoke(已移除的术语表回复冒烟测试) | 由确定性的用户会话抽屉回复、活动首条/后续回复场景替代 |
Removed GlossaryAF-06/AF-07vacuous edit/delete checks(已移除的空转编辑/删除检查) | 由确定性的 Context Center 根变更与 Activity API 回复变更场景替代 |
原文档还特别指出:运行时 REST 客户端路由覆盖另外由 conversationsAPI.test.ts 与 activityAPI.test.ts 维护;而 author/non-author/admin 动作可见性、解析分派(resolution dispatch)、活动回复状态替换(activity-reply state replacement)与抽屉根动作组合(drawer root-action composition)则由对应的组件/Provider 单元测试覆盖。
分层验证体系:三层测试如何共同兜住行为
矩阵中的替代测试分布在三个层次,理解分层有助于按需定位与排查:
- 服务端集成测试(Java IT):
ConversationResourceIT.java以真实服务端为对象,验证/v1/conversations的 CRUD、有界回复水合与 keyset 分页语义,是行为契约的"源头事实"; - 前端 REST 客户端单测(Jest):
conversationsAPI.test.ts与activityAPI.test.tsmock 掉APIClient,断言前端每次调用命中正确的 HTTP 方法与路径(GET/POST/PATCH/PUT/DELETE),防止路由漂移; - 端到端测试(Playwright):
ActivityFeed.spec.ts、ActivityAPI.spec.ts、ContextCenterArticles.spec.ts、ContextCenterPermission.spec.ts、Features/Tasks/ActivityFeed.spec.ts等在真实浏览器中驱动 UI,验证可观察行为(渲染、导航、通知、编码、权限)。
当某个新需求改动路由时,正确做法是从第 1、2 层确认语义与路径,再到第 3 层确认 UI 行为,最后回到本矩阵更新映射行。
服务端语义:有界回复水合与回复计数
"有界回复水合(bounded hydration)"是 Conversation V2 最核心的语义之一。在 ConversationResourceIT.java 的testCompleteConversationCrudAndBoundedHydration中,测试按以下步骤钉死该语义:
// 创建根会话,断言初始状态 Conversation conversation = createConversation(about, "Root message"); assertEquals(0, conversation.getReplyCount()); // 初始 replyCount = 0 assertEquals(ConversationSource.User, conversation.getSource()); // 连续追加 4 条回复 for (int i = 0; i < 4; i++) { createdReplies.add(addReply(conversation.getId(), "Reply " + i)); } // 关键断言:hydrated 对象只内嵌最近的 3 条回复 Conversation hydrated = getConversation(conversation.getId()); assertEquals(4, hydrated.getReplyCount()); // 计数完整 assertEquals(3, hydrated.getReplies().size()); // 内嵌回复有界 // 独立分页接口可以取回全部 4 条 ConversationReplyList replies = listReplies(conversation.getId(), 100, null, null); assertEquals(4, replies.getPaging().getTotal());这段代码揭示了两个要点:
- 计数与内容分离:
replyCount是权威总数,而replies列表按设计只携带最近 N 条(此处为 3),避免一次请求把整棵回复树全部拉回; - 完整内容走独立分页:需要历史回复时,通过
/conversations/{id}/replies分页接口按需获取,这正是矩阵中 "root/reply cursor correctness" 的服务端依据。
同样的语义在前端 ActivityThreadPanelBody.test.tsx 的renders the selected conversation with its hydrated replies用例中得到 UI 级印证:选中会话后,FeedPanelBodyV1New与ActivityFeedcardNew.component以feed.id渲染水合后的回复。
Keyset 分页(cursor 分页)的端到端链路
Conversation V2 使用 keyset(cursor)分页而非 offset 分页,矩阵对此有三处覆盖点:
- 服务端:
ConversationResourceIT的testListingFiltersAndKeysetPagination先请求limit=2的第一页,断言paging.after非空,再用该 cursor 请求第二页,断言两页数据不重叠; - 前端组件:
ActivityThreadPanelBody.test.tsx的loads the next conversation page with the keyset cursor模拟paging.after: 'next-conversation-cursor',滚动触发observer-element后断言listConversations以after: 'next-conversation-cursor'再次调用,并把两页结果合并刷新进列表; - E2E:
ContextCenterArticles.spec.ts等待"携带 cursor 的第二次请求",并验证全部 11 条种子根均被渲染。
前端 REST 层对应的契约在 conversationsAPI.test.ts 中:
it('lists conversations with filters and cursors', async () => { const params = { entityLink: '<#E::table::service.table>', after: 'next' }; await listConversations(params); expect(APIClient.get).toHaveBeenCalledWith('/conversations', { params }); });从组件单测可见,listConversations的参数形态为{ after, entityLink },其中after即来自上一页paging的 cursor;回复分页则使用独立 cursor,见listConversationReplies单测中的{ before: 'previous', limit: 50 }。
首条/后续回复:无合成根、无重复 POST
旧 feed 体系下,回复行为容易出现"合成根"(synthetic root)或重复 POST 的隐患。新的活动路由用确定性断言锁死这一行为。在 ActivityAPI.spec.ts 的creates exactly one reply and isolates activities with the same about中,测试先为同一张表播种两条独立的 activity event(firstActivityText与secondActivityText),然后:
page.on('request', (request) => { if (request.method() !== 'POST') return; if (request.url().includes(`/api/v1/activity/${firstActivityId}/replies`)) { activityReplyRequests.push(request.url()); // 统计 activity 回复 POST } if (/\/api\/v1\/conversations(?:\?|$)/.test(request.url())) { conversationCreateRequests.push(request.url()); // 统计 conversation 创建 POST } }); // 发第一条回复 await postActivityComment(page, firstReply); expect(activityReplyRequests).toHaveLength(1); // 恰好一次回复 POST expect(conversationCreateRequests).toHaveLength(0); // 零次 conversation 创建 // 再发后续回复 await postActivityComment(page, subsequentReply); expect(activityReplyRequests).toHaveLength(2); expect(conversationCreateRequests).toHaveLength(0);两个断言分别回答:
- 无重复 POST:每次回复只触发一次
/activity/{id}/repliesPOST; - 无合成根:整个过程中
/conversations的 POST 请求数为 0,证明回复直接挂在 activity event 下,而不是偷偷创建一棵假会话根。
随后同一场景继续对首条回复执行编辑(PATCH)、反应(PUT reaction)、删除(DELETE),验证矩阵中 "Activity reply edit, reaction tooltip identity, and delete" 一行。
同about活动的隔离:以 ActivityEvent ID 为键
矩阵中"Two activities with the sameabout"一行指向同一 Activity API 场景。其原理是:即使两条 activity 拥有相同的about(同一实体链接),前端也以ActivityEvent ID作为独立容器的键,而不是把回复塞进同一个根。这从 activityAPI.test.ts 中可以看到,前端对活动数据的获取全部围绕活动 ID/实体 FQN 展开(如getEntityActivityById、getActivityByEntityLink),而组件渲染时按 event ID 定位卡片;E2E 中则用feed-reply-card过滤hasText来逐一断言两条活动各自的回复互不串扰。
用户会话的完整变更链路:解决/编辑/删除/反应
矩阵将 "User-conversation root reaction and tooltip identity" 与 "resolve, edit, and delete" 两行都映射到ContextCenterArticles.spec.ts的Related assets, activity feed, user mentions, and article mentions work场景——该场景通过/conversations/{id}执行并逐一校验每次变更。REST 层的完整路由在 conversationsAPI.test.ts 中被逐条钉死:
it('gets a conversation', async () => { await getConversation('conversation-1'); expect(APIClient.get).toHaveBeenCalledWith('/conversations/conversation-1'); }); it('patches a conversation', async () => { const patch: Operation[] = [{ op: 'replace', path: '/message', value: 'Updated' }]; await patchConversation('conversation-1', patch); expect(APIClient.patch).toHaveBeenCalledWith('/conversations/conversation-1', patch); }); it('deletes a conversation', async () => { await deleteConversation('conversation-1'); expect(APIClient.delete).toHaveBeenCalledWith('/conversations/conversation-1'); }); it('adds and removes a root reaction', async () => { await addConversationReaction('conversation-1', ReactionType.Heart); await removeConversationReaction('conversation-1', ReactionType.Heart); const path = '/conversations/conversation-1/reaction/heart'; expect(APIClient.put).toHaveBeenCalledWith(path); expect(APIClient.delete).toHaveBeenCalledWith(path); });与此对应,回复子资源的完整 REST 契约同样被覆盖:GET/POST /conversations/{id}/replies、PATCH/DELETE /conversations/{id}/replies/{replyId}、PUT/DELETE /conversations/{id}/replies/{replyId}/reaction/{type}(见 conversationsAPI.test.ts 中lists replies with an independent cursor与adds and removes a reply reaction用例)。注意 reaction 的增删分别使用PUT与DELETE命中同一路径,ReactionType以小写形式进入 URL(如heart、rocket)。
服务端对 resolve 语义的验证同样在ConversationResourceIT中:patchConversation(conversation.getId(), patch("/resolved", true))后断言patchedRoot.getResolved()为真;回复的 PATCH(/message)与 DELETE 也按序执行并断言replyCount随之从 4 变为 3,形成一条完整的服务端 CRUD 闭环。
通知、提及与中文编码
矩阵中有两行与 @提及(mention)直接相关,均在 ActivityFeed.spec.ts 中:
Mention notification shows correct user details in Notification box:验证被提及的用户在通知框中看到正确的用户详情(身份与导航正确);Should encode the chinese character while mentioning api endpoint:验证调用提及 API 时中文字符被正确编码。该用例在仓库的 Discovery.md 中同样被登记为重要场景:"Mentions: Chinese character encoding in activity feed"。
这两个用例保护了多语言环境下的通知链路与 URL 编码细节,是国际化场景中容易回归的部分。
被移除场景的替代逻辑:从冒烟/空转检查到确定性断言
矩阵的最后两行明确标注了"Removed(已移除)"场景,这是整张矩阵最具治理意义的部分:
- 旧术语表(Glossary)的
AF-05回复冒烟测试被移除,理由是它仅做"能发出一条回复"的冒烟验证,缺少行为断言;替代方案是确定性的用户会话抽屉回复(ActivityFeed.spec.ts的thread drawer opens from reply count and allows posting a reply)与活动首条/后续回复(Activity API 场景)——两者都有精确的请求计数与结果断言; - 旧的
AF-06/AF-07编辑/删除检查被移除,因为它们属于"空转检查"(vacuous checks,即断言恒为真或未触及真实路由);替代方案是确定性的Context Center 根变更(resolve/edit/delete 逐一走/conversations/{id})与Activity API 回复变更(PATCH/PUT/DELETE 逐条断言)。
矩阵的注释点明了设计意图:被移除的场景必须显式点名(called out explicitly),这样测试才不会因为套件改名而在统计中悄悄消失。这也是整份文档最有价值的工程实践——它把"删测试"变成了一次必须留下书面痕迹的决策。
如何阅读、维护与扩展这份矩阵
结合 CONVERSATION_V2_TRACEABILITY.md 与上述源码,推荐以下维护姿势:
- 变更路由时先改契约层:若改动
/conversations或/activity的路径、HTTP 方法或参数,先同步更新 conversationsAPI.test.ts / activityAPI.test.ts 与 ConversationResourceIT.java,它们锁死了"前端叫对了后端"的事实; - 变更行为时补 E2E:涉及用户可见行为(渲染、导航、通知、编码、权限)时,在
ActivityFeed.spec.ts、ActivityAPI.spec.ts、ContextCenterArticles.spec.ts、ContextCenterPermission.spec.ts、Features/Tasks/ActivityFeed.spec.ts中选择对应场景补充断言; - 删除或重命名测试时必须更新矩阵:在"Legacy behavior or test"列保留原行为描述,在"Replacement"列写明承接方;若是彻底移除,要像
AF-05/AF-06/AF-07那样说明被哪些确定性场景替代,杜绝无声消失; - 关注 keyset 分页的双 cursor:root 列表用
aftercursor,回复列表使用独立 cursor(before/limit),二者不要混用,组件单测与 E2E 都已对此做出约束。
小结
Conversation V2 可追溯性矩阵不是一张静态表格,而是一份"行为不灭"的治理契约:旧 feed 的每一项可观察行为,都能在新/conversations与/activity路由体系下找到明确的承接测试;被删除的冒烟/空转用例,也被更具断言力的确定性场景替换。配合 Java 集成测试、前端 REST 单测与 Playwright E2E 三层验证,OpenMetadata 在端点切换后依然能保证活动流、用户会话、任务、公告与提及通知等核心体验的可回归性。任何改动端点的开发者,都应以本矩阵为起点、以三层测试为护栏,让每一次路由变更都留下可追溯的覆盖记录。
【免费下载链接】OpenMetadataThe Open Context Layer for Data and AI , OpenMetadata is the open platform for building trusted data context and business semantics for humans, AI assistants, and agents.项目地址: https://gitcode.com/GitHub_Trending/op/OpenMetadata
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考