Skip to content
Open
Show file tree
Hide file tree
Changes from 48 commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
96aabba
feat(skillmarket): add the Space review UI and gate publishing behind it
Sep 1, 2026
0030c75
feat(mcp): wire the review flow for connectors, experts and expert teams
Sep 1, 2026
5729205
feat(market): draft/publish flow, one 发布 button, and one plugin table
Sep 2, 2026
f7b93d3
fix(market): announce the row edit/delete buttons the same way in eve…
Sep 2, 2026
928c893
fix(market): drop the redundant 可见 from the visibility labels
Sep 2, 2026
1dbae73
fix(market): make the 全部 tab match the per-type tabs
Sep 2, 2026
aae8608
fix(market): give the 全部 tab the actions its 操作 header promises
Sep 2, 2026
84ce007
feat(market): let 编辑 change the visibility, as the spec always said
Sep 2, 2026
868e7b1
feat(market): 编辑 gets the same 保存草稿 / 发布 pair as 创建
Sep 2, 2026
c8a14c5
fix(market): confirm leaving in the footer, and stop edits demoting a…
Sep 2, 2026
935cf5d
feat(market): connectors get the draft/publish flow
Sep 2, 2026
04c6d9d
feat(market): experts and expert teams get a visibility control
Sep 2, 2026
8f91b91
fix(market): retries stop discarding edits, and two unstyled surfaces
Sep 2, 2026
c8ea5c3
fix(market): make the table's columns resolve identically everywhere
Sep 2, 2026
6d05d3b
feat(market): validate the version label in the form, not only on the…
Sep 2, 2026
194d99b
fix(market): a review record states its decision, not the plugin's st…
Sep 2, 2026
172f251
refactor(market): rename the 组织审核 menu to 组织发布管理
Sep 2, 2026
c2e3a2c
fix(market): put the plugin table's 操作 header above its own buttons
Sep 2, 2026
bf0c635
fix(market): update the 组织发布管理 badge without a page reload
Sep 2, 2026
c76f425
fix(market): keep 我的发布's 技能 table aligned with its own tab strip
Sep 2, 2026
7bd1047
refactor(market): drop the review refreshes the endpoints now do them…
Sep 2, 2026
45926e4
fix(market): accept a plugin's own legacy version label when saving
Sep 2, 2026
e43b2b8
fix(market): stop a skill review carrying a version the server refuses
Sep 2, 2026
bdc6bbc
fix(market): deleting a connector or expert clears the review badge
Sep 2, 2026
dc2dd5e
fix(market): re-read the 组织发布管理 badge after a Space switch
Sep 2, 2026
19b0cb7
refactor(market): drop ReviewQueue's onAction, which no mount supplied
Sep 2, 2026
1200191
fix(market): stop the plugin table's header collapsing when the list …
Sep 2, 2026
24c8c6f
style(market): render 组织发布管理's active tab in the selected-state token
Sep 2, 2026
cf6423e
fix(market): stop the e2e proxy erroring on the review-badge probe
Sep 2, 2026
e99b02f
fix(market): close connector modals and refresh assets on a Space switch
Sep 2, 2026
bba8a3b
fix(market): route detail-modal skill edits through the review gate
Sep 2, 2026
ae3c779
fix(market): freeze a 专家团's member wiring across review submission
Sep 2, 2026
a5b4f44
fix(market): reset reason modals on close and scope the acting-row clear
Sep 2, 2026
c28ed48
style(market): tokenize the review upgrade badge and callout colors
Sep 2, 2026
4a2eec2
chore(market): drop seven unused review i18n keys
Sep 2, 2026
7d478ba
fix(market): thread review content/changelog, type all-tab rows, refe…
Sep 2, 2026
971d89d
feat: add space auto review setting
Sep 3, 2026
e950eaf
fix: replace native review policy confirmation
Sep 3, 2026
d37e5f6
fix: align space review role handling
Sep 3, 2026
54eed06
feat: show review policy to space admins
Sep 4, 2026
ad78c3c
fix: align space review role handling
Sep 4, 2026
8f3f4e2
fix: isolate cross-space review state
Sep 4, 2026
3f2244f
fix: guard connector details across spaces
Sep 4, 2026
666c20b
chore: retrigger pull request checks
Sep 4, 2026
bc5de91
test: cover connector space-switch races
Sep 4, 2026
6783714
test: stabilize settings and summary e2e
Sep 4, 2026
491b7ee
fix: address review policy feedback
Sep 4, 2026
92ee699
Merge remote-tracking branch 'upstream/main' into codex/space-review-…
Sep 4, 2026
7d8e480
fix: isolate review detail actions
Sep 4, 2026
e202a33
test: cover relative summary attention poll
Sep 4, 2026
2d2945c
test: guard summary poll across navigations
Sep 4, 2026
9271d7a
test: guard expert search bootstrap
Sep 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# RB2 删除有待审申请的连接器后组织发布管理徽标立即归零

## Metadata

- Case 类型: 回归守护
- 目标模式: real-page seed
- 登录状态: authed fixture
- 优先级: P1 (回归守护)
- Tags: `@RB2 @p1 @mcp @market @review`

## 目标

验证删除一个「有待审申请」的连接器之后,侧边栏「组织发布管理」上的待审计数徽标在同一页面实例内立即归零,不需要刷新页面。

RB1 守护的是「做出审核决策」这条路径;本 case 守护另一条让申请离开队列的路径——**承载它的插件被删掉了**。这条路径此前没有被覆盖,因为连接器 / 专家的删除走 dmworkmcp 自己的 api 模块(`mcpService` / `expertService`),而不是 `@dmwork/skillmarket` 的 `deleteSkill`,于是它可以在无人察觉的情况下与后者失去对称。

## 后端前提(已在 octo-marketplace 侧核实)

删除插件会在**同一事务**内把它上面 pending 的审核申请 cancel 掉
(`internal/repository/plugin/review.go` 的 `cancelPendingReviewFor`,reason
`plugin deleted`;由 `write.go:476/665/732` 调用,`internal/db/plugin_delete_cascade_test.go`
的 `TestDeleteCancelsThePendingReviewRequest` 钉住)。

原因是删除之后那条申请**两个方向都不可达**:所有读取(`ListReviewRequests` /
`GetReviewRequest` / `LoadReviewSnapshot`)都带 `p.deleted_at IS NULL`,申请人和
审核人都看不见它;而所有决策路径都经 `getReviewedPluginForUpdate` 加载插件,会拒绝
已删除的插件。所以只有删除所在的那个事务能了结它。

也正因为服务端计数**确实**会掉,客户端不重新读取才会被用户看见。

## 前置条件

- fixture: `fixtures-authed`;mock 用户在 `e2e-space-001` 中 `role: 2`(owner),可见 reviewer 专属入口。
- Per-case MSW handler: `e2e-kit/msw-handlers/rb2-connector-delete-review.ts`,
用 C40 的运行时 `window.__msw.worker.use` 方式安装。
- `GET /market/api/v1/plugins`(`mode=mine&plugin_type=connector`)— 未删除时返回该连接器。
- `GET /market/api/v1/plugins/review_requests` — 未删除时返回 1 条 pending。
- `POST /market/api/v1/plugins/delete` — 置 `deleted=true`,**同时**影响上面两个读。
- handler 必须是有状态的:如果 `total` 恒为 1,无论徽标是否重新拉取断言都会通过;
如果恒为 0,修复前也会通过。这一个 `deleted` 布尔量同时驱动列表和队列,正是模拟
后端的级联。

## 用户操作步骤

1. 打开 `/mcp-market/mine?type=mcp`(我的发布 → 连接器)。
2. 确认侧边栏「组织发布管理」行上的徽标显示 `1`。
3. 点击该行的「删除「待审连接器」」,在确认弹窗里点「删除」。
4. 不刷新、不跳转,直接观察侧边栏。

## 预期结果

- 删除前,侧边栏「组织发布管理」行的 `.wk-mcp-sidebar__badge` 文本为 `1`。
- 删除后,表格里该行消失。
- 同一页面实例内,侧边栏徽标消失(计数归零时整个徽标不渲染)。

## 反例

- 修复前实测:行消失了,徽标停在 `1` 十秒不动 —— 侧边栏在为一个**已经不存在的连接器**
显示待审申请。
- 如果把失效逻辑挂在调用点而不是接口上,`McpDeleteConfirmModal` 和 `McpDetailModal`
两处都要各自记得刷新,下一个删除入口(批量操作、快捷键)还会再漏一次。
- 如果只包 `deleteMcp` 而不包 `deleteExpert` / `deleteSquad`,专家侧同样的缺陷会留下;
三者由 `packages/dmworkmcp/src/api/pluginDelete.reviewInvalidation.test.ts` 一起钉住。

## 视觉基准

不建 pixel baseline;用 `getByRole` 定位侧边栏行与表格行,再用 `.wk-mcp-sidebar__badge`
类选择器断言计数存在与消失。

## 摸清依据

- `packages/dmworkmcp/src/api/mcpService.ts`: `deleteMcp` 的 `withReviewInvalidation` 包裹。
- `packages/dmworkmcp/src/api/expertService.ts`: `deleteExpert` / `deleteSquad` 同上。
- `packages/dmworkskillmarket/src/api/skillApi.ts`: `deleteSkill` 早已如此包裹——本 case 修的就是这个不对称。
- `packages/dmworkskillmarket/src/api/reviewSignal.ts`: 为什么失效挂在接口而不是调用点上。
- `packages/dmworkmcp/src/components/MarketSidebar.tsx`: `<ReviewGateProbe />` 的徽标读取。
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# RB3 切换组织后组织发布管理徽标读取新组织的待审数

## Metadata

- Case 类型: 回归守护
- 目标模式: real-page seed
- 登录状态: authed fixture
- 优先级: P1 (回归守护)
- Tags: `@RB3 @p1 @mcp @market @review`

## 目标

验证切换 Space 之后,侧边栏「组织发布管理」的待审计数徽标读取的是**新 Space** 的数字,
而不是把上一个 Space 的计数留在屏幕上。

## 为什么会错

审核计数是 Space 维度的(每次读取都带 `X-Space-Id` 请求头,见
`packages/dmworkskillmarket/src/api/skillApiReal.ts` 的 `getAuthHeaders`),但:

- `useReviewRequests` 的 fetch 只以 `[enabled, mode, status, pageSize]` 为 key,
切 Space 一个都不动;
- `enabled` 就是 `isReviewer`,owner 切到另一个自己也是 owner 的 Space 时它仍是 `true`,
reviewer 门禁救不了这个 case;
- 右侧市场页会被 `MarketSidebar.handleSpaceChanged` 整个替换重挂,所以它们各自的
`space-changed` handler 里那句显式刷新看起来「多余」;而**侧边栏自己从不重挂**,
所以它是唯一一个没人替它重读的读取点。

## 前置条件

- fixture: `fixtures-authed`。
- Per-case MSW handler: `e2e-kit/msw-handlers/rb3-space-switch-review-badge.ts`
- `GET /space/my` — 返回两个 Space:甲组织 `e2e-space-001`、乙组织 `e2e-space-002`,
用户在两边都是 `role: 2`(owner)。
- `GET /market/api/v1/plugins/review_requests` — **按请求头 `X-Space-Id` 分流**:
甲组织 1 条 pending,乙组织 3 条。
- 两边都是**非零**且**不相等**,这是本 case 的关键:`1 → 0` 是有歧义的,因为探针被
禁用或状态被清空时徽标同样会消失,"徽标不见了" 只有在你另外知道没有东西清空它时
才算重读的证据;`1 → 3` 只可能由一件事产生 —— 一次带着新 Space 请求头的新读取。

## 用户操作步骤

1. 打开 `/mcp-market/skills`(技能市场)。**站在技能页而不是审核页**:徽标必须在
`ReviewQueue` 未挂载时也正确,这正是它是一个独立读取的原因。
2. 确认侧边栏「组织发布管理」徽标为 `1`。
3. 点击 NavRail 底部的「切换组织」,选择「乙组织」。
4. 不刷新、不跳转,直接观察侧边栏。

## 预期结果

- 切换前徽标为 `1`(甲组织)。
- 切换后徽标为 `3`(乙组织)。

## 反例

- 修复前实测:切换后 `window.fetch` 上**没有任何** `review_requests` 请求发出,徽标
十秒不动地停在 `1`。连跑三次结果一致。
- 如果改用 `notifyReviewsChanged()` 来实现:那是在宣称「审核队列发生了写操作」,
而切 Space 并不是审核类写操作。这个说法对其余订阅者是错的,而且它们各自已经处理了
切 Space。所以这里用的是 `refresh()`,和三个市场页 `space-changed` handler 的做法一致。
- 如果把重读放进 `useReviewRequests` 内部:能修,但那会把三个市场页现有的显式刷新变成
隐式行为,属于对一个四处共用的 hook 的更大改动;本 case 只钉住可观察行为,不钉住这个选择。

## 视觉基准

不建 pixel baseline;用 `getByRole` 定位侧边栏行和「切换组织」按钮,再用
`.wk-mcp-sidebar__badge` 类选择器断言计数文本。

## 摸清依据

- `packages/dmworkmcp/src/components/MarketSidebar.tsx`: `<ReviewGateProbe />` 的
`space-changed` 重读。
- `packages/dmworkskillmarket/src/hooks/useReviewRequests.ts`: fetch 的 key 集合。
- `packages/dmworkskillmarket/src/hooks/useSpaceRole.ts`: 切 Space 时 role 的更新路径。
- `apps/web/src/Pages/Main/index.tsx`: `applySpaceSelection` 先写
`WKApp.shared.currentSpaceId` 再 emit `space-changed`,所以重读带的是新 Space 的头。
- `apps/web/src/Pages/Main/index.tsx` `MainContentLeft`: 已访问路由靠 `display` 切换常驻
DOM,侧边栏因此不会随切 Space 重挂。
- `packages/dmworkmcp/src/components/__tests__/MarketSidebar.test.tsx`: 同一行为的单测。
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# RB1 组织发布管理徽标在审核决策后立即更新

## Metadata

- Case 类型: 回归守护
- 目标模式: real-page seed
- 登录状态: authed fixture
- 优先级: P1 (回归守护)
- Tags: `@RB1 @p1 @skills @market @review`

## 目标

验证侧边栏「组织发布管理」入口上的待审核计数徽标,在同一页面实例内做出审核决策后立即更新,不需要刷新页面。

徽标和「待审核」列表是同一个队列的两次独立读取——徽标必须在 `ReviewQueue` 未挂载时(用户停留在技能 / 连接器 / 我的发布)也能显示计数,所以它无法从列表状态派生。二者共享的只能是「何时同时失效」这一时刻,而这个时刻是审核类写操作本身。

## 前置条件

- fixture: `fixtures-authed`,使用本地 mock 登录和 mock IM runtime;mock 用户在 `e2e-space-001` 中 `role: 2`(owner),因此可见 reviewer 专属入口。
- 页面初始化前设置 `sessionStorage.__e2e_scenario = "skill-market-review-badge"`,并清除 `__e2e_rb1_loaded`,启用本 case 的 MSW handler 并重置其队列状态。
- Per-case MSW handler: `e2e-kit/msw-handlers/skill-market-review-badge.ts`
- `GET /market/api/v1/plugins/review_requests` — 有状态:初始返回 1 条 pending 记录。
- `POST /market/api/v1/plugins/review_requests/:id/approve` — 把该记录从 pending 移到 approved;重复调用返回 409。
- handler 必须是有状态的:如果 `total` 恒为 1,无论徽标是否重新拉取断言都会通过,用例就失去意义。

## 用户操作步骤

1. 打开 `/mcp-market/review`(组织发布管理)。
2. 确认侧边栏「组织发布管理」行上的徽标显示 `1`。
3. 在待审核列表里点击该行的「通过」。
4. 不刷新、不跳转,直接观察侧边栏。

## 预期结果

- 决策前,侧边栏「组织发布管理」行的 `.wk-mcp-sidebar__badge` 文本为 `1`。
- 点击「通过」后,待审核列表显示空态「暂无待审核申请」。
- 同一页面实例内,侧边栏徽标消失(计数归零时整个徽标不渲染)。

## 反例

- 如果徽标只在挂载时拉取一次,点击「通过」后列表会清空而徽标仍停在 `1`,只有整页刷新才纠正——这正是本用例守护的缺陷。
- 如果把失效逻辑挂在调用点而不是接口上,从抽屉里做出的决策、申请人的「取消审核」、发布触发的送审、下架等路径中总会有一条漏掉,徽标只在部分操作后更新。
- 如果改用轮询「修」这个问题,徽标会在一个固定延迟后才追上,而不是立即更新;用一个足够短的超时断言仍会失败。

## 视觉基准

不建 pixel baseline; 用 `getByRole` 定位侧边栏行,再用 `.wk-mcp-sidebar__badge` 类选择器断言计数存在与消失。

## 摸清依据

- `packages/dmworkmcp/src/components/MarketSidebar.tsx:108-121`: 侧边栏 review 行的徽标渲染与计数为 0 时隐藏。
- `packages/dmworkmcp/src/components/MarketSidebar.tsx:155-171`: `<ReviewGateProbe />` 用一个独立的 `useReviewRequests`(`pageSize: 1`)拉徽标计数。
- `packages/dmworkskillmarket/src/components/ReviewQueue.tsx:322-364`: 队列自己的决策处理与 `refreshAllAsync`,只刷新列表。
- `packages/dmworkskillmarket/src/api/reviewSignal.ts`: 审核写操作失效信号,以及为什么它挂在接口而不是调用点上。
- `packages/dmworkskillmarket/src/api/skillApi.ts`: 被 `withReviewInvalidation` 包裹的写接口清单。
- `packages/dmworkskillmarket/src/hooks/useReviewRequests.ts`: 订阅失效信号后重新读取。
14 changes: 12 additions & 2 deletions apps/web/e2e-kit/msw-handlers/chat-baseline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ const MOCK_SPACE = {
space_no: "e2e-space",
owner: MOCK_UID,
status: 1,
role: 1,
role: 2,
};

const SP1_CREATED_SPACE = {
Expand All @@ -67,7 +67,7 @@ const SP1_CREATED_SPACE = {
space_no: "sp1-created-space",
owner: MOCK_UID,
status: 1,
role: 1,
role: 2,
};

function chatFollowScenario(request?: Request): string {
Expand Down Expand Up @@ -305,6 +305,16 @@ export const chatBaselineHandlers = [
http.get("*/conversations/:channelId/:channelType/extra", () =>
HttpResponse.json({})
),
// apiClient can retain the `/api/v1` prefix in browser requests. Keep explicit
// variants because MSW's leading wildcard does not reliably consume multiple
// path segments in every runtime, which previously leaked this call to Vite's
// dead CI proxy.
http.post("*/api/v1/conversations/:channelId/:channelType/extra", () =>
HttpResponse.json({})
),
http.get("*/api/v1/conversations/:channelId/:channelType/extra", () =>
HttpResponse.json({})
),
http.get("*/groups/:groupNo/threads", () => HttpResponse.json([])),
http.post("*/messages/_search_all", () =>
HttpResponse.json({ items: [], data: [], pagination: {} })
Expand Down
137 changes: 137 additions & 0 deletions apps/web/e2e-kit/msw-handlers/rb2-connector-delete-review.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import type { Page } from "@playwright/test";

// 连接器删除后的审核失效 (RB2).
//
// A connector with an OPEN review request is deleted. The marketplace backend
// cancels that request in the same transaction as the delete
// (`cancelPendingReviewFor` / `reasonCanceledOnDelete`, pinned by
// octo-marketplace's TestDeleteCancelsThePendingReviewRequest), so the Space's
// pending count genuinely drops the moment the delete returns — which is
// exactly why a client that does not re-read shows a badge for a plugin that no
// longer exists.
//
// Deliberately STATEFUL, for the same reason RB1's handler is: `POST
// /plugins/delete` removes the connector from BOTH the 我的发布 list and the
// pending queue. A stub that always answered total=1 would pass whether or not
// the sidebar badge re-read at all, and a stub that always answered total=0
// would pass before the fix.
export async function registerRb2ConnectorDeleteReview(page: Page): Promise<void> {
function install() {
type Msw = {
worker: { use: (...handlers: unknown[]) => void };
http: {
get: (path: string, resolver: (info: { request: { url: string } }) => unknown) => unknown;
post: (path: string, resolver: () => unknown) => unknown;
};
HttpResponse: { json: (body: unknown, init?: unknown) => unknown };
};
const win = globalThis as unknown as {
__msw?: Msw;
__rb2Installed?: boolean;
__rb2Timer?: number;
};
if (!win.__msw) {
if (!win.__rb2Timer) {
win.__rb2Timer = window.setInterval(() => {
if (install()) window.clearInterval(win.__rb2Timer);
}, 10);
}
return false;
}
if (win.__rb2Installed) return true;

const PLUGIN_ID = "rb2-connector";
const PLUGIN_NAME = "待审连接器";
// The one piece of mutable state: flipped by the delete, read by BOTH the
// list and the review queue.
let deleted = false;

const connector = {
plugin_id: PLUGIN_ID,
plugin_name: PLUGIN_NAME,
plugin_type: "connector",
category_id: "dev-cat",
tags: ["e2e"],
publisher: "E2E Tester",
owner_id: "e2e-space-001",
visibility: "space",
creator_name: "E2E Tester",
created_by_type: "human",
icon_url: "🔌",
tool_count: 1,
view_count: 0,
install_count: 0,
download_count: 0,
current_version: "1.0.0",
listing_state: "draft",
manifest_json: {
name: PLUGIN_ID,
description: "A connector with an open review request.",
labels: ["e2e"],
},
created_at: "2026-08-26T00:00:00Z",
updated_at: "2026-08-26T00:00:00Z",
};

const reviewRequest = {
review_id: "rb2-review-1",
plugin_id: PLUGIN_ID,
plugin_name: PLUGIN_NAME,
plugin_type: "connector",
space_id: "e2e-space-001",
target_scope: "space",
status: "pending",
kind: "first",
version: "1.0.0",
changelog: "首次提交组织审核",
applicant_id: "e2e-user-1",
applicant_name: "E2E Tester",
submitted_at: "2026-08-31T10:00:00.000Z",
plugin_listing_state: "draft",
};

function page(items: unknown[], url: URL) {
const p = Number.parseInt(url.searchParams.get("page") ?? "1", 10) || 1;
const size = Number.parseInt(url.searchParams.get("page_size") ?? "20", 10) || 20;
const start = (p - 1) * size;
return {
data: items.slice(start, start + size),
pagination: { total: items.length, page: p, page_size: size },
};
}

win.__msw.worker.use(
win.__msw.http.get("*/market/api/v1/plugins/review_requests", ({ request }) => {
const url = new URL(request.url);
const status = url.searchParams.get("status");
const open = deleted || (status !== null && status !== "pending") ? [] : [reviewRequest];
return win.__msw!.HttpResponse.json(page(open, url));
}),
win.__msw.http.post("*/market/api/v1/plugins/delete", () => {
deleted = true;
return win.__msw!.HttpResponse.json({ data: {} });
}),
win.__msw.http.get("*/market/api/v1/plugins", ({ request }) => {
const url = new URL(request.url);
const mine =
url.searchParams.get("mode") === "mine" &&
url.searchParams.get("plugin_type") === "connector";
const items = mine && !deleted ? [connector] : [];
return win.__msw!.HttpResponse.json(page(items, url));
}),
win.__msw.http.get("*/market/api/v1/plugin_categories", () =>
win.__msw!.HttpResponse.json({
data: [{ category_id: "dev-cat", name: "dev", sort_order: 0, plugin_count: 1 }],
}),
),
win.__msw.http.post("*/market/api/v1/metrics/track", () =>
win.__msw!.HttpResponse.json({ data: {} }),
),
);
win.__rb2Installed = true;
return true;
}

await page.addInitScript(install);
await page.evaluate(install);
}
Loading
Loading