merge release growth p2 notification preference
# Conflicts: # docs/architecture/CODE_REFERENCE_INDEX.md
This commit is contained in:
commit
e1ecba1f39
|
|
@ -807,6 +807,35 @@ P0 公共壳冻结已完成,下一批可以进入 `Batch P1` 三模块并行
|
||||||
|
|
||||||
## 8. 进度台账
|
## 8. 进度台账
|
||||||
|
|
||||||
|
### 2026-04-26 / release-growth-p2-notification-preference-consume / implemented
|
||||||
|
|
||||||
|
完成内容:
|
||||||
|
|
||||||
|
- `ReleaseGrowthPage` 真实模式读取 Account Settings 通知偏好,通过 `shouldShowAccountNotification(settings, "releaseGate")` 控制发布门禁强调提醒,通过 `shouldShowAccountNotification(settings, "failureAlert")` 控制失败强调提醒。
|
||||||
|
- `releaseGate` 偏好关闭时,页面不展示“发布门禁提醒”强调卡,也降低缺证据 / 阻塞门禁视觉强调;底层 `releaseGate` 状态、执行证据、阻塞原因、下一步动作和发布验证记录仍完整展示。
|
||||||
|
- `failureAlert` 偏好关闭时,失败 evidence 与阻塞原因仍可见,但不展示额外醒目的“失败提醒”卡,也不把阻塞原因渲染成额外告警态。
|
||||||
|
- 本阶段只消费本地页面内提醒偏好,不接邮件、桌面推送或通知中心;不修改 Account Settings 合同、PMFlow OS store、后端发布验证 API、router、Space 或 Account 页面。
|
||||||
|
- `store.api.test.tsx` 补充 ReleaseGrowth 偏好开启 / 关闭两条路径,覆盖提醒展示、静默展示和 release evidence 仍可见。
|
||||||
|
|
||||||
|
涉及文件:
|
||||||
|
|
||||||
|
- `frontend/src/features/pmflow-os/pages/ReleaseGrowthPage.tsx`
|
||||||
|
- `frontend/src/features/pmflow-os/store.api.test.tsx`
|
||||||
|
- `docs/PMFLOW_PRODUCT_DELIVERY_STANDARD.md`
|
||||||
|
- `docs/architecture/CODE_REFERENCE_INDEX.md`
|
||||||
|
- `docs/architecture/ARCHITECTURE_INVENTORY.md`
|
||||||
|
|
||||||
|
验证:
|
||||||
|
|
||||||
|
- `cd frontend && npm run test -- src/features/pmflow-os` 通过:`2 passed / 6 passed`。
|
||||||
|
- `cd frontend && npm run typecheck` 未通过:当前前端树缺失整组共享 `src/lib/*` 模块(例如 `@/lib/api`、`@/lib/utils`、`@/lib/datetime`、`@/lib/display`、`@/lib/rbac`),影响全仓大量既有文件和 forbidden `router/index.tsx`;本 Packet 未越界修复。
|
||||||
|
- `cd frontend && npm run build` 同样停在上述共享 `src/lib/*` 缺失的 TypeScript 解析错误,未进入 Vite build。
|
||||||
|
|
||||||
|
未完成或风险:
|
||||||
|
|
||||||
|
- 本阶段不隐藏业务真相,只控制提醒表达强度;发布证据、审计状态和 validation run 仍以 PMFlow OS 真实读侧为准。
|
||||||
|
- 全量前端 typecheck/build 需由 Shell / Integration owner 或共享前端基础模块 owner 先恢复 `frontend/src/lib/*` 后再重新验证。
|
||||||
|
|
||||||
### 2026-04-26 / release-validation-p1-real-run-records / implemented
|
### 2026-04-26 / release-validation-p1-real-run-records / implemented
|
||||||
|
|
||||||
完成内容:
|
完成内容:
|
||||||
|
|
|
||||||
|
|
@ -198,7 +198,7 @@
|
||||||
- `frontend/src/features/project-launch/types.ts`、`mockData.ts`、`useProjectLaunchState.ts` 是新项目启动台状态层;默认真实模式通过 `frontend/src/api/pmflowLaunch.ts` 生成草案并确认创建真实项目,API 失败进入明确失败态,不静默 fallback 成 mock 成功;`mockData.ts` 仅在显式 `?demo=1` 或 `?dataSource=demo` 下作为演示状态机使用。`components/LaunchShell.tsx` 负责桌面/移动分流,桌面端复用 `frontend/src/features/personal-space/components/SpaceChrome.tsx` 的空间左侧栏和顶部条,`LaunchCanvases.tsx` 和 `MobileLaunchLayout.tsx` 承接高保真图中的意图输入、AI 工作、人类确认、草案修改、生成失败、创建成功过渡与移动端 bottom sheet 交互。
|
- `frontend/src/features/project-launch/types.ts`、`mockData.ts`、`useProjectLaunchState.ts` 是新项目启动台状态层;默认真实模式通过 `frontend/src/api/pmflowLaunch.ts` 生成草案并确认创建真实项目,API 失败进入明确失败态,不静默 fallback 成 mock 成功;`mockData.ts` 仅在显式 `?demo=1` 或 `?dataSource=demo` 下作为演示状态机使用。`components/LaunchShell.tsx` 负责桌面/移动分流,桌面端复用 `frontend/src/features/personal-space/components/SpaceChrome.tsx` 的空间左侧栏和顶部条,`LaunchCanvases.tsx` 和 `MobileLaunchLayout.tsx` 承接高保真图中的意图输入、AI 工作、人类确认、草案修改、生成失败、创建成功过渡与移动端 bottom sheet 交互。
|
||||||
- `frontend/src/features/account-settings/AccountSettingsPanel.tsx` 是 `/me` 设置页实现:桌面端为个人资料摘要 + 分区设置面板,移动端为分组列表 + `MobileBottomSheet` 编辑;当前覆盖个人资料、默认语言、AI 回答深度、证据展示偏好、待确认、执行回写、发布门禁和失败提醒通知偏好,并在通知偏好区展示页面内提醒消费说明。`frontend/src/api/accountSettings.ts` 是前端合同层;有 token 时优先调用 `backend/app/api/account_settings_api/`,失败时回落浏览器本机暂存;P2 新增 `shouldShowAccountNotification` 与 `getAccountNotificationConsumption`,将四类通知偏好映射为本地 UI 的待确认、Execution Bridge、Release/Growth 和失败提醒显示/静默状态;P3 新增 `getReleaseGateNotificationPreference(settings, event)`,把 Release/Growth 的 `release_gate_* / release_validation_*` 事件映射到 `releaseGate` 偏好,把 `failed_event` 映射到 `failureAlert` 偏好,并返回 `inAppEnabled / desktopPreference / emailDigestPreference` 供后续 Main Flow 页面消费。该合同不代表邮件、桌面推送或系统通知中心已接入。`backend/app/schemas/account_settings.py` 定义账号偏好合同,`backend/app/services/account_settings_service.py` 当前复用既有 `integrations` 表按 `tenant_id + user_id` 保存 `account_settings_preferences` 配置包,`backend/app/api/router.py` 已挂载该 API;未改用户表、OAuth/JWT/auth core 或 PMFlow OS truth store。
|
- `frontend/src/features/account-settings/AccountSettingsPanel.tsx` 是 `/me` 设置页实现:桌面端为个人资料摘要 + 分区设置面板,移动端为分组列表 + `MobileBottomSheet` 编辑;当前覆盖个人资料、默认语言、AI 回答深度、证据展示偏好、待确认、执行回写、发布门禁和失败提醒通知偏好,并在通知偏好区展示页面内提醒消费说明。`frontend/src/api/accountSettings.ts` 是前端合同层;有 token 时优先调用 `backend/app/api/account_settings_api/`,失败时回落浏览器本机暂存;P2 新增 `shouldShowAccountNotification` 与 `getAccountNotificationConsumption`,将四类通知偏好映射为本地 UI 的待确认、Execution Bridge、Release/Growth 和失败提醒显示/静默状态;P3 新增 `getReleaseGateNotificationPreference(settings, event)`,把 Release/Growth 的 `release_gate_* / release_validation_*` 事件映射到 `releaseGate` 偏好,把 `failed_event` 映射到 `failureAlert` 偏好,并返回 `inAppEnabled / desktopPreference / emailDigestPreference` 供后续 Main Flow 页面消费。该合同不代表邮件、桌面推送或系统通知中心已接入。`backend/app/schemas/account_settings.py` 定义账号偏好合同,`backend/app/services/account_settings_service.py` 当前复用既有 `integrations` 表按 `tenant_id + user_id` 保存 `account_settings_preferences` 配置包,`backend/app/api/router.py` 已挂载该 API;未改用户表、OAuth/JWT/auth core 或 PMFlow OS truth store。
|
||||||
- `frontend/src/features/pmflow-os/store.tsx` 当前是 PMFlow OS 共享 truth store:`?projectId=<id>` 时通过 React Query 调用 `frontend/src/api/pmflowOs.ts` 读取真实 `GET /pmflow-os/projects/{project_id}/truth-state` 与 `GET /pmflow-os/projects/{project_id}/execution-summary`,真实模式下项目内 message、`proposed_items` extraction、ChangeItem `accept/edit/defer`、ChangeSet commit、Execution Handoff create/consume、execution progress writeback 与 release validation run create 均调用 `/pmflow-os` 后端并 refetch;只有显式 `?demo=1` 或 `?dataSource=demo` 时才启用本地 mock 演示状态机。真实读取/动作失败会进入错误/空态,不静默 fallback 成 mock 成功。
|
- `frontend/src/features/pmflow-os/store.tsx` 当前是 PMFlow OS 共享 truth store:`?projectId=<id>` 时通过 React Query 调用 `frontend/src/api/pmflowOs.ts` 读取真实 `GET /pmflow-os/projects/{project_id}/truth-state` 与 `GET /pmflow-os/projects/{project_id}/execution-summary`,真实模式下项目内 message、`proposed_items` extraction、ChangeItem `accept/edit/defer`、ChangeSet commit、Execution Handoff create/consume、execution progress writeback 与 release validation run create 均调用 `/pmflow-os` 后端并 refetch;只有显式 `?demo=1` 或 `?dataSource=demo` 时才启用本地 mock 演示状态机。真实读取/动作失败会进入错误/空态,不静默 fallback 成 mock 成功。
|
||||||
- `frontend/src/features/pmflow-os/store.tsx` 仍保留演示用项目内 AI 结构化闭环状态机:`command_idle / ai_extracting / extraction_ready / review_open / writeback_success`。`frontend/src/features/pmflow-os/pages/StructuredExtractionPage.tsx` 是本轮结构化提取页,`ChangeReviewPage.tsx` 是决策与待确认中心,`MemoryWritebackResultPage.tsx` 是项目记忆写入结果页,`WorkGraphPage.tsx` 是项目工作台关系总览与对象投影视图,`ExecutionBridgePage.tsx` 是执行桥上下文包、交接按钮、运行端领取确认、PR/测试与回写状态页;`ReleaseGrowthPage.tsx` 真实模式读取 `execution-summary.releaseGate` 与 `latestValidationRun`,从 branch、commit、PR、test、failed evidence 展示 `可发布 / 阻塞 / 需要人工确认 / 缺少证据` 判断、阻塞原因、下一步动作和发布验证记录创建动作,不执行发布命令、不展示 mock 发布结果;静态发布/增长原型只保留在显式 demo 模式。A3 已接入后端 truth-state,C1 已接入真实 message/extraction/review 状态机,C2 已接入真实 ChangeSet commit,Phase D 已接入真实 execution-summary/dev-sync,D.2 已接入 `POST /execution-handoffs` 创建 PromptPackage + handoff_created progress event,P1 已接入 `POST /execution-handoffs/{prompt_package_id}/consume` 创建 handoff_consumed progress event 并把 execution status 投影为 coding,Release Growth P1 已接入 execution evidence gate,Release Validation P1 已接入 `POST /release-validation-runs` 创建 `ReleaseValidationRun` 快照;当前仍不调用真实 AI 提取,不把 mock 当真实失败 fallback。`ProjectMemoryPage.tsx` 在真实模式下展示后端 `truth_nodes + semantic_facts` 的 `projectMemory` 投影,accepted/edited ChangeItem 只有 commit 成功后才进入 Project Memory。`frontend/src/features/pmflow-os/components/ProjectStructuredLoop.tsx` 提供 `StatusLabel / EvidenceSnippet / ObjectChangeCard / ProjectConversationPanel / StructuredExtractionPanel / ProjectAICopilotPanel` 等闭环组件,并复用契约化 `InlineNotice` 表达“未确认不得写入”的局部提示。
|
- `frontend/src/features/pmflow-os/store.tsx` 仍保留演示用项目内 AI 结构化闭环状态机:`command_idle / ai_extracting / extraction_ready / review_open / writeback_success`。`frontend/src/features/pmflow-os/pages/StructuredExtractionPage.tsx` 是本轮结构化提取页,`ChangeReviewPage.tsx` 是决策与待确认中心,`MemoryWritebackResultPage.tsx` 是项目记忆写入结果页,`WorkGraphPage.tsx` 是项目工作台关系总览与对象投影视图,`ExecutionBridgePage.tsx` 是执行桥上下文包、交接按钮、运行端领取确认、PR/测试与回写状态页;`ReleaseGrowthPage.tsx` 真实模式读取 `execution-summary.releaseGate` 与 `latestValidationRun`,从 branch、commit、PR、test、failed evidence 展示 `可发布 / 阻塞 / 需要人工确认 / 缺少证据` 判断、阻塞原因、下一步动作和发布验证记录创建动作,不执行发布命令、不展示 mock 发布结果;同时读取 Account notification preference helper,用 `releaseGate` 偏好控制发布门禁强调提醒,用 `failureAlert` 偏好控制失败强调提醒,偏好关闭时只静默提醒表达强度,不隐藏 release evidence、阻塞原因、审计状态或 validation run;静态发布/增长原型只保留在显式 demo 模式。A3 已接入后端 truth-state,C1 已接入真实 message/extraction/review 状态机,C2 已接入真实 ChangeSet commit,Phase D 已接入真实 execution-summary/dev-sync,D.2 已接入 `POST /execution-handoffs` 创建 PromptPackage + handoff_created progress event,P1 已接入 `POST /execution-handoffs/{prompt_package_id}/consume` 创建 handoff_consumed progress event 并把 execution status 投影为 coding,Release Growth P1 已接入 execution evidence gate,Release Validation P1 已接入 `POST /release-validation-runs` 创建 `ReleaseValidationRun` 快照;当前仍不调用真实 AI 提取,不把 mock 当真实失败 fallback。`ProjectMemoryPage.tsx` 在真实模式下展示后端 `truth_nodes + semantic_facts` 的 `projectMemory` 投影,accepted/edited ChangeItem 只有 commit 成功后才进入 Project Memory。`frontend/src/features/pmflow-os/components/ProjectStructuredLoop.tsx` 提供 `StatusLabel / EvidenceSnippet / ObjectChangeCard / ProjectConversationPanel / StructuredExtractionPanel / ProjectAICopilotPanel` 等闭环组件,并复用契约化 `InlineNotice` 表达“未确认不得写入”的局部提示。
|
||||||
- PMFlow OS 后端入口位于 `backend/app/api/pmflow_os_api/`,projection service 位于 `backend/app/services/pmflow_os_projection_service.py`,Change Review / commit 服务位于 `backend/app/services/pmflow_change_review_service.py`,Pydantic 合同位于 `backend/app/schemas/pmflow_os.py`;当前聚合 `truth_nodes / tasks / semantic_facts / ai_project_control / dev_sync`,`Thread / Message` 来自 `pmflow_threads / pmflow_thread_messages`,`ChangeSet / ChangeItem / Approval / EvidenceLink` 来自 `backend/app/models/pmflow_change_review.py` 对应真实表。C1 已实现 message、`proposed_items` extraction、ChangeSet detail、ChangeItem `accept/edit/defer`;C2 backend/frontend 已实现 `POST /pmflow-os/change-sets/{change_set_id}/commit` 与真实模式前端接入,默认 atomic,将 `Requirement / Decision / Risk` 写入 `truth_nodes`,将 `Task` 写入 `truth_nodes(node_type='task') + tasks`,高风险项强制 approval gate;D.2 已实现 `POST /pmflow-os/projects/{project_id}/execution-handoffs`,校验 task/repo project 边界,生成/复用 `PromptPackage(package_kind='execution_handoff')` 并写 `ProgressEvent(event_type='handoff_created')` 起点;P1 已实现 `POST /pmflow-os/projects/{project_id}/execution-handoffs/{prompt_package_id}/consume`,校验 execution handoff package 后更新 `PromptPackage.package_payload.latest_handoff.handoff_state=consumed` 并写 `ProgressEvent(event_type='handoff_consumed', derived_task_status='coding')`;Release Growth P1 在 `ExecutionSummaryProjection.releaseGate` 中从 `ProgressEvent` 推导发布门禁状态,失败事件 fail-closed 阻塞发布,无证据返回 empty/warning;Release Validation P1 新增 `POST /pmflow-os/projects/{project_id}/release-validation-runs`,复用现有 `ReleaseValidationRun` 模型固化当前 `releaseGate` 快照、人工决策、证据、阻塞原因和下一步动作,并在 `releaseGate.latestValidationRun` 返回最新记录;这些步骤都不直接执行本机命令或真实发布命令;`ModelDecision / MetricSignal` 在 C2 V1 仍 fail-closed。
|
- PMFlow OS 后端入口位于 `backend/app/api/pmflow_os_api/`,projection service 位于 `backend/app/services/pmflow_os_projection_service.py`,Change Review / commit 服务位于 `backend/app/services/pmflow_change_review_service.py`,Pydantic 合同位于 `backend/app/schemas/pmflow_os.py`;当前聚合 `truth_nodes / tasks / semantic_facts / ai_project_control / dev_sync`,`Thread / Message` 来自 `pmflow_threads / pmflow_thread_messages`,`ChangeSet / ChangeItem / Approval / EvidenceLink` 来自 `backend/app/models/pmflow_change_review.py` 对应真实表。C1 已实现 message、`proposed_items` extraction、ChangeSet detail、ChangeItem `accept/edit/defer`;C2 backend/frontend 已实现 `POST /pmflow-os/change-sets/{change_set_id}/commit` 与真实模式前端接入,默认 atomic,将 `Requirement / Decision / Risk` 写入 `truth_nodes`,将 `Task` 写入 `truth_nodes(node_type='task') + tasks`,高风险项强制 approval gate;D.2 已实现 `POST /pmflow-os/projects/{project_id}/execution-handoffs`,校验 task/repo project 边界,生成/复用 `PromptPackage(package_kind='execution_handoff')` 并写 `ProgressEvent(event_type='handoff_created')` 起点;P1 已实现 `POST /pmflow-os/projects/{project_id}/execution-handoffs/{prompt_package_id}/consume`,校验 execution handoff package 后更新 `PromptPackage.package_payload.latest_handoff.handoff_state=consumed` 并写 `ProgressEvent(event_type='handoff_consumed', derived_task_status='coding')`;Release Growth P1 在 `ExecutionSummaryProjection.releaseGate` 中从 `ProgressEvent` 推导发布门禁状态,失败事件 fail-closed 阻塞发布,无证据返回 empty/warning;Release Validation P1 新增 `POST /pmflow-os/projects/{project_id}/release-validation-runs`,复用现有 `ReleaseValidationRun` 模型固化当前 `releaseGate` 快照、人工决策、证据、阻塞原因和下一步动作,并在 `releaseGate.latestValidationRun` 返回最新记录;这些步骤都不直接执行本机命令或真实发布命令;`ModelDecision / MetricSignal` 在 C2 V1 仍 fail-closed。
|
||||||
- PMFlow Launch 后端 Phase B 创建链位于 `backend/app/api/pmflow_launch_api/`、`backend/app/services/pmflow_launch_service.py`、`backend/app/schemas/pmflow_launch.py` 与 `backend/app/models/pmflow_launch.py`;`POST /pmflow-launch/drafts` 只写 `pmflow_launch_drafts`,`POST /pmflow-launch/drafts/{draft_id}/confirm` 在同一事务内写 `projects / user_projects / pmflow_threads / pmflow_thread_messages / truth_nodes / tasks / semantic_facts seed / operation_logs`,并把 `launch_draft_id / thread_id / thread_message_id` 写入正式对象 metadata。
|
- PMFlow Launch 后端 Phase B 创建链位于 `backend/app/api/pmflow_launch_api/`、`backend/app/services/pmflow_launch_service.py`、`backend/app/schemas/pmflow_launch.py` 与 `backend/app/models/pmflow_launch.py`;`POST /pmflow-launch/drafts` 只写 `pmflow_launch_drafts`,`POST /pmflow-launch/drafts/{draft_id}/confirm` 在同一事务内写 `projects / user_projects / pmflow_threads / pmflow_thread_messages / truth_nodes / tasks / semantic_facts seed / operation_logs`,并把 `launch_draft_id / thread_id / thread_message_id` 写入正式对象 metadata。
|
||||||
- `frontend/src/features/pmflow-os/components/ChangeReviewGroup.tsx`、`WorkbenchPanel.tsx`、`ExecutionBridgeCards.tsx`、`PMFlowOsFrame.tsx`、`AccountControls.tsx`、`FeedbackPrimitives.tsx` 是高保真原型新增的行为组件层:分别承载变更 diff 审查块、工作台对象卡组、执行桥上下文/日志卡片、全局上下文条与 AI 副驾、账号菜单/退出登录确认交互,以及 `PageBanner / InlineNotice / HumanDecisionBar / ConfirmDialog / MobileBottomSheet` 这组提示、提醒、确认与移动端 bottom sheet 基础组件,避免再把这些业务片段内联塞回 `pages/`。
|
- `frontend/src/features/pmflow-os/components/ChangeReviewGroup.tsx`、`WorkbenchPanel.tsx`、`ExecutionBridgeCards.tsx`、`PMFlowOsFrame.tsx`、`AccountControls.tsx`、`FeedbackPrimitives.tsx` 是高保真原型新增的行为组件层:分别承载变更 diff 审查块、工作台对象卡组、执行桥上下文/日志卡片、全局上下文条与 AI 副驾、账号菜单/退出登录确认交互,以及 `PageBanner / InlineNotice / HumanDecisionBar / ConfirmDialog / MobileBottomSheet` 这组提示、提醒、确认与移动端 bottom sheet 基础组件,避免再把这些业务片段内联塞回 `pages/`。
|
||||||
|
|
|
||||||
|
|
@ -106,9 +106,9 @@
|
||||||
- PMFlow OS 前端 MVP 产品域:参考 `frontend/src/features/pmflow-os/`
|
- PMFlow OS 前端 MVP 产品域:参考 `frontend/src/features/pmflow-os/`
|
||||||
- 个人空间首页 / 空间层入口前端产品域:参考 `frontend/src/features/personal-space/`、`frontend/src/api/space.ts` 与薄入口 `frontend/src/pages/PersonalSpacePage.tsx`;本地预览路由入口为 `/space`,覆盖继续推进、待我确认、发布关注、执行中 / AI 正在推进、暂存想法、草案箱与最近项目,点击项目主动作进入 `/pmflow-os?projectId=<id>`,待确认项进入 `/change-review?projectId=<id>&changeSetId=<id>`,执行中项进入 `/execution-bridge?projectId=<id>`,发布关注项进入 `/release-growth?projectId=<id>`;Space 只读聚合项目摘要、PMFlow OS `truth-state.releases / execution-summary.releaseGate`,发布验证缺失时显示明确空态,不虚构验证结果,不直接写 Project Memory、ChangeSet 状态、execution event 或 ReleaseValidationRun;暂存想法后端只读聚合尚未接入时必须显示 empty/warning,不得用 mock 数据冒充真实内容;空间层共享左侧栏、顶部条与空间用量组件在 `frontend/src/features/personal-space/components/SpaceChrome.tsx`,`/project-launch` 桌面首次新建壳也复用该组件
|
- 个人空间首页 / 空间层入口前端产品域:参考 `frontend/src/features/personal-space/`、`frontend/src/api/space.ts` 与薄入口 `frontend/src/pages/PersonalSpacePage.tsx`;本地预览路由入口为 `/space`,覆盖继续推进、待我确认、发布关注、执行中 / AI 正在推进、暂存想法、草案箱与最近项目,点击项目主动作进入 `/pmflow-os?projectId=<id>`,待确认项进入 `/change-review?projectId=<id>&changeSetId=<id>`,执行中项进入 `/execution-bridge?projectId=<id>`,发布关注项进入 `/release-growth?projectId=<id>`;Space 只读聚合项目摘要、PMFlow OS `truth-state.releases / execution-summary.releaseGate`,发布验证缺失时显示明确空态,不虚构验证结果,不直接写 Project Memory、ChangeSet 状态、execution event 或 ReleaseValidationRun;暂存想法后端只读聚合尚未接入时必须显示 empty/warning,不得用 mock 数据冒充真实内容;空间层共享左侧栏、顶部条与空间用量组件在 `frontend/src/features/personal-space/components/SpaceChrome.tsx`,`/project-launch` 桌面首次新建壳也复用该组件
|
||||||
- 新项目启动台 / 首次进入页前端产品域:参考 `frontend/src/features/project-launch/` 与薄入口 `frontend/src/pages/ProjectLaunchPage.tsx`;真实模式默认通过 `frontend/src/api/pmflowLaunch.ts` 调用后端 draft/confirm API,只有显式 `?demo=1` 或 `?dataSource=demo` 才启用 `mockData.ts` 本地演示状态机;本地预览路由入口为 `/project-launch`
|
- 新项目启动台 / 首次进入页前端产品域:参考 `frontend/src/features/project-launch/` 与薄入口 `frontend/src/pages/ProjectLaunchPage.tsx`;真实模式默认通过 `frontend/src/api/pmflowLaunch.ts` 调用后端 draft/confirm API,只有显式 `?demo=1` 或 `?dataSource=demo` 才启用 `mockData.ts` 本地演示状态机;本地预览路由入口为 `/project-launch`
|
||||||
- Account Settings / `/me` 前端产品域与 P1/P2/P3 偏好合同:参考 `frontend/src/features/account-settings/AccountSettingsPanel.tsx`、`frontend/src/features/pmflow-os/pages/AccountPage.tsx`、`frontend/src/api/accountSettings.ts`、`backend/app/api/account_settings_api/`、`backend/app/schemas/account_settings.py` 与 `backend/app/services/account_settings_service.py`;当前覆盖个人资料、默认语言、AI 回答深度、证据展示偏好、待确认、执行回写、发布门禁和失败提醒通知偏好,移动端使用分组列表与 bottom sheet,账户退出仍复用 `AccountControls.tsx`,不改 OAuth/JWT/auth core;P1 后端合同提供 `GET /account-settings/me` 与 `PUT /account-settings/me`,已由 `backend/app/api/router.py` 挂入核心路由组,按当前 `tenant_id + user_id` 保存账号偏好,前端有 token 时优先账号同步,后端不可用时明确回落 `仅本机暂存 / 保存失败`;P2 在 `frontend/src/api/accountSettings.ts` 提供 `shouldShowAccountNotification` 与 `getAccountNotificationConsumption`,把通知偏好映射为页面内提醒消费模型;P3 新增 `getReleaseGateNotificationPreference(settings, event)` 供 Release/Growth 后续消费:`release_gate_*` 与 `release_validation_*` 使用 `releaseGate` 偏好,`failed_event` 使用 `failureAlert` 偏好,返回 `inAppEnabled / desktopPreference / emailDigestPreference`;这些 helper 只控制本地 UI 提醒消费合同,不代表真实邮件、桌面推送或系统通知中心已接入
|
- Account Settings / `/me` 前端产品域与 P1/P2/P3 偏好合同:参考 `frontend/src/features/account-settings/AccountSettingsPanel.tsx`、`frontend/src/features/pmflow-os/pages/AccountPage.tsx`、`frontend/src/api/accountSettings.ts`、`backend/app/api/account_settings_api/`、`backend/app/schemas/account_settings.py` 与 `backend/app/services/account_settings_service.py`;当前覆盖个人资料、默认语言、AI 回答深度、证据展示偏好、待确认、执行回写、发布门禁和失败提醒通知偏好,移动端使用分组列表与 bottom sheet,账户退出仍复用 `AccountControls.tsx`,不改 OAuth/JWT/auth core;P1 后端合同提供 `GET /account-settings/me` 与 `PUT /account-settings/me`,已由 `backend/app/api/router.py` 挂入核心路由组,按当前 `tenant_id + user_id` 保存账号偏好,前端有 token 时优先账号同步,后端不可用时明确回落 `仅本机暂存 / 保存失败`;P2 在 `frontend/src/api/accountSettings.ts` 提供 `shouldShowAccountNotification` 与 `getAccountNotificationConsumption`,把通知偏好映射为页面内提醒消费模型;P3 新增 `getReleaseGateNotificationPreference(settings, event)` 供 Release/Growth 后续消费:`release_gate_*` 与 `release_validation_*` 使用 `releaseGate` 偏好,`failed_event` 使用 `failureAlert` 偏好,返回 `inAppEnabled / desktopPreference / emailDigestPreference`;ReleaseGrowthPage 当前消费 `releaseGate` 与 `failureAlert` 两个偏好,只静默强调型提醒,不隐藏 release evidence、阻塞原因、审计状态或 validation run;这些 helper 只控制本地 UI 提醒消费合同,不代表真实邮件、桌面推送或系统通知中心已接入
|
||||||
- PMFlow OS 共享 truth store 与主链状态动作:参考 `frontend/src/features/pmflow-os/store.tsx`;当前支持 `?projectId=<id>` 走 React Query 读取真实 `truth-state` 与 `execution-summary`,真实模式下 thread message、`proposed_items` extraction、ChangeItem `accept/edit/defer`、ChangeSet commit、Execution Handoff create/consume、execution progress writeback、release validation run create 均调后端并 refetch,不本地 promote 入 Project Memory、不伪造 execution event 或发布验证记录;`?demo=1` 才启用本地 mock 演示状态机和本地动作;项目内 AI 结构化闭环状态机为 `command_idle / ai_extracting / extraction_ready / review_open / writeback_success`
|
- PMFlow OS 共享 truth store 与主链状态动作:参考 `frontend/src/features/pmflow-os/store.tsx`;当前支持 `?projectId=<id>` 走 React Query 读取真实 `truth-state` 与 `execution-summary`,真实模式下 thread message、`proposed_items` extraction、ChangeItem `accept/edit/defer`、ChangeSet commit、Execution Handoff create/consume、execution progress writeback、release validation run create 均调后端并 refetch,不本地 promote 入 Project Memory、不伪造 execution event 或发布验证记录;`?demo=1` 才启用本地 mock 演示状态机和本地动作;项目内 AI 结构化闭环状态机为 `command_idle / ai_extracting / extraction_ready / review_open / writeback_success`
|
||||||
- PMFlow OS 产品页入口:参考 `frontend/src/features/pmflow-os/pages/CommandCenterPage.tsx`、`StructuredExtractionPage.tsx`、`ChangeReviewPage.tsx`、`MemoryWritebackResultPage.tsx`、`WorkGraphPage.tsx`、`ExecutionBridgePage.tsx`、`ReleaseGrowthPage.tsx`、`TeamLayerPage.tsx`、`ProjectMemoryPage.tsx`、`AccountPage.tsx`;`ReleaseGrowthPage.tsx` 真实模式只展示 `execution-summary.releaseGate`、execution evidence、阻塞原因、下一步动作和 `latestValidationRun`,缺证据时显示 empty/warning,不展示 mock 发布结果;页面可调用真实 API 创建 release validation run,但仍不执行部署命令;静态高保真发布/增长数据仅保留在显式 demo 模式
|
- PMFlow OS 产品页入口:参考 `frontend/src/features/pmflow-os/pages/CommandCenterPage.tsx`、`StructuredExtractionPage.tsx`、`ChangeReviewPage.tsx`、`MemoryWritebackResultPage.tsx`、`WorkGraphPage.tsx`、`ExecutionBridgePage.tsx`、`ReleaseGrowthPage.tsx`、`TeamLayerPage.tsx`、`ProjectMemoryPage.tsx`、`AccountPage.tsx`;`ReleaseGrowthPage.tsx` 真实模式只展示 `execution-summary.releaseGate`、execution evidence、阻塞原因、下一步动作和 `latestValidationRun`,缺证据时显示 empty/warning,不展示 mock 发布结果;页面可调用真实 API 创建 release validation run,但仍不执行部署命令;同时消费 Account notification preference helper,`releaseGate` 关闭时静默发布门禁强调提醒,`failureAlert` 关闭时静默失败强调提醒,所有真实证据和审计状态仍可见;静态高保真发布/增长数据仅保留在显式 demo 模式
|
||||||
- PMFlow OS 行为组件:参考 `frontend/src/features/pmflow-os/components/ProjectStructuredLoop.tsx`、`ProjectStageRail.tsx`、`ThreadComposer.tsx`、`ExtractionRail.tsx`、`ChangeGroupPanel.tsx`、`ObjectGraphSection.tsx`、`ContextPacketPanel.tsx`、`ExecutionTimeline.tsx`、`ReleaseGatePanel.tsx`、`TeamFeedPanel.tsx`、`ChangeReviewGroup.tsx`、`WorkbenchPanel.tsx`、`ExecutionBridgeCards.tsx`、`PMFlowOsFrame.tsx`、`AccountControls.tsx`、`FeedbackPrimitives.tsx`、`MobileBottomNav.tsx`;其中 `ProjectStructuredLoop.tsx` 承接 `StatusLabel / EvidenceSnippet / ObjectChangeCard / ProjectConversationPanel / StructuredExtractionPanel / ProjectAICopilotPanel`,`PMFlowOsFrame.tsx` 承接项目内面包屑和项目切换器,`AccountControls.tsx` 承接桌面头像菜单、退出登录确认与移动端 bottom sheet,`FeedbackPrimitives.tsx` 承接 `PageBanner / InlineNotice / HumanDecisionBar / ConfirmDialog / MobileBottomSheet`,`MobileBottomNav.tsx` 承接正式应用壳移动端固定底部导航
|
- PMFlow OS 行为组件:参考 `frontend/src/features/pmflow-os/components/ProjectStructuredLoop.tsx`、`ProjectStageRail.tsx`、`ThreadComposer.tsx`、`ExtractionRail.tsx`、`ChangeGroupPanel.tsx`、`ObjectGraphSection.tsx`、`ContextPacketPanel.tsx`、`ExecutionTimeline.tsx`、`ReleaseGatePanel.tsx`、`TeamFeedPanel.tsx`、`ChangeReviewGroup.tsx`、`WorkbenchPanel.tsx`、`ExecutionBridgeCards.tsx`、`PMFlowOsFrame.tsx`、`AccountControls.tsx`、`FeedbackPrimitives.tsx`、`MobileBottomNav.tsx`;其中 `ProjectStructuredLoop.tsx` 承接 `StatusLabel / EvidenceSnippet / ObjectChangeCard / ProjectConversationPanel / StructuredExtractionPanel / ProjectAICopilotPanel`,`PMFlowOsFrame.tsx` 承接项目内面包屑和项目切换器,`AccountControls.tsx` 承接桌面头像菜单、退出登录确认与移动端 bottom sheet,`FeedbackPrimitives.tsx` 承接 `PageBanner / InlineNotice / HumanDecisionBar / ConfirmDialog / MobileBottomSheet`,`MobileBottomNav.tsx` 承接正式应用壳移动端固定底部导航
|
||||||
- PMFlow OS 原型状态回归测试:参考 `frontend/src/features/pmflow-os/store.test.ts`;真实 API 动作回归参考 `frontend/src/features/pmflow-os/store.api.test.tsx`
|
- PMFlow OS 原型状态回归测试:参考 `frontend/src/features/pmflow-os/store.test.ts`;真实 API 动作回归参考 `frontend/src/features/pmflow-os/store.api.test.tsx`
|
||||||
- Project Launch 真实/演示分支回归测试:参考 `frontend/src/features/project-launch/useProjectLaunchState.test.ts`
|
- Project Launch 真实/演示分支回归测试:参考 `frontend/src/features/project-launch/useProjectLaunchState.test.ts`
|
||||||
|
|
|
||||||
|
|
@ -18,18 +18,41 @@ import {
|
||||||
TestTube2,
|
TestTube2,
|
||||||
X,
|
X,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import type { ReactNode } from "react";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { Button } from "@/components/ui/button";
|
import type { ButtonHTMLAttributes, ReactNode } from "react";
|
||||||
|
import { getAccountSettings, shouldShowAccountNotification } from "@/api/accountSettings";
|
||||||
import type {
|
import type {
|
||||||
PMFlowReleaseGateEvidence,
|
PMFlowReleaseGateEvidence,
|
||||||
PMFlowReleaseValidationDecision,
|
PMFlowReleaseValidationDecision,
|
||||||
PMFlowReleaseValidationRun,
|
PMFlowReleaseValidationRun,
|
||||||
} from "@/api/pmflowOs";
|
} from "@/api/pmflowOs";
|
||||||
import { usePMFlowOsStore } from "@/features/pmflow-os/store";
|
import { usePMFlowOsStore } from "@/features/pmflow-os/store";
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
type StatusTone = "success" | "warning" | "danger" | "neutral" | "info";
|
type StatusTone = "success" | "warning" | "danger" | "neutral" | "info";
|
||||||
|
|
||||||
|
function cn(...classes: Array<string | false | null | undefined>) {
|
||||||
|
return classes.filter(Boolean).join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
function Button({
|
||||||
|
variant,
|
||||||
|
className,
|
||||||
|
type = "button",
|
||||||
|
...props
|
||||||
|
}: ButtonHTMLAttributes<HTMLButtonElement> & { variant?: "outline" }) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type={type}
|
||||||
|
className={cn(
|
||||||
|
"inline-flex items-center justify-center whitespace-nowrap text-sm font-medium transition-colors disabled:pointer-events-none disabled:opacity-50",
|
||||||
|
variant === "outline" ? "border bg-white hover:bg-slate-50" : "",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const gateItems = [
|
const gateItems = [
|
||||||
{ label: "功能完整性", status: "通过", detail: "核心流程与功能验证完成", tone: "success" as const },
|
{ label: "功能完整性", status: "通过", detail: "核心流程与功能验证完成", tone: "success" as const },
|
||||||
{ label: "性能基线", status: "通过", detail: "P95 响应时间 < 800ms", tone: "success" as const },
|
{ label: "性能基线", status: "通过", detail: "P95 响应时间 < 800ms", tone: "success" as const },
|
||||||
|
|
@ -370,6 +393,15 @@ function evidenceIcon(type: string) {
|
||||||
return CheckCircle2;
|
return CheckCircle2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasFailureSignal(gate?: { status?: string; evidence?: PMFlowReleaseGateEvidence[] }) {
|
||||||
|
if (!gate) return false;
|
||||||
|
if (gate.status === "blocked") return true;
|
||||||
|
return (gate.evidence || []).some((item) => {
|
||||||
|
const type = String(item.type || "").toLowerCase();
|
||||||
|
return type.includes("fail") || type.includes("block");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function EvidenceRow({ item }: { item: PMFlowReleaseGateEvidence }) {
|
function EvidenceRow({ item }: { item: PMFlowReleaseGateEvidence }) {
|
||||||
const Icon = evidenceIcon(item.type);
|
const Icon = evidenceIcon(item.type);
|
||||||
return (
|
return (
|
||||||
|
|
@ -410,9 +442,19 @@ function RealReleaseGrowthPage({ store }: { store: PMFlowOsStoreSnapshot }) {
|
||||||
} = store;
|
} = store;
|
||||||
const gate = executionSummary?.releaseGate;
|
const gate = executionSummary?.releaseGate;
|
||||||
const latestValidationRun = gate?.latestValidationRun;
|
const latestValidationRun = gate?.latestValidationRun;
|
||||||
|
const accountSettingsQuery = useQuery({
|
||||||
|
queryKey: ["account-settings", "release-growth-notifications"],
|
||||||
|
queryFn: () => getAccountSettings(),
|
||||||
|
staleTime: 60_000,
|
||||||
|
});
|
||||||
const isLoading = dataSourceStatus === "loading" || executionSummaryStatus === "loading";
|
const isLoading = dataSourceStatus === "loading" || executionSummaryStatus === "loading";
|
||||||
const warnings = [...projectionWarnings, ...(executionSummary?.warnings || [])];
|
const warnings = [...projectionWarnings, ...(executionSummary?.warnings || [])];
|
||||||
const emptyMode = dataSourceMode === "empty" || gate?.status === "empty" || !gate;
|
const emptyMode = dataSourceMode === "empty" || gate?.status === "empty" || !gate;
|
||||||
|
const releaseGateReminderEnabled = shouldShowAccountNotification(accountSettingsQuery.data, "releaseGate");
|
||||||
|
const failureAlertReminderEnabled = shouldShowAccountNotification(accountSettingsQuery.data, "failureAlert");
|
||||||
|
const failureSignal = hasFailureSignal(gate);
|
||||||
|
const showReleaseGateReminder = releaseGateReminderEnabled && !emptyMode;
|
||||||
|
const showFailureAlertReminder = failureAlertReminderEnabled && failureSignal;
|
||||||
const canCreateValidationRun = Boolean(!emptyMode && gate?.evidence?.length);
|
const canCreateValidationRun = Boolean(!emptyMode && gate?.evidence?.length);
|
||||||
const handleCreateValidationRun = (decision: PMFlowReleaseValidationDecision) => {
|
const handleCreateValidationRun = (decision: PMFlowReleaseValidationDecision) => {
|
||||||
const decisionLabel = decision === "enter_release_candidate" ? "进入发布候选" : "保持阻塞";
|
const decisionLabel = decision === "enter_release_candidate" ? "进入发布候选" : "保持阻塞";
|
||||||
|
|
@ -450,9 +492,39 @@ function RealReleaseGrowthPage({ store }: { store: PMFlowOsStoreSnapshot }) {
|
||||||
</Card>
|
</Card>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
{showReleaseGateReminder ? (
|
||||||
|
<Card className="border-blue-100 bg-blue-50 p-4">
|
||||||
|
<div className="flex gap-3 text-sm text-blue-700">
|
||||||
|
<ShieldAlert className="mt-0.5 h-4 w-4 shrink-0" />
|
||||||
|
<div>
|
||||||
|
<div className="font-semibold">发布门禁提醒</div>
|
||||||
|
<div className="mt-1 leading-6">{gate?.summary || "发布门禁状态已更新,底层证据仍以 execution-summary 为准。"}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{showFailureAlertReminder ? (
|
||||||
|
<Card className="border-rose-100 bg-rose-50 p-4">
|
||||||
|
<div className="flex gap-3 text-sm text-rose-700">
|
||||||
|
<AlertTriangle className="mt-0.5 h-4 w-4 shrink-0" />
|
||||||
|
<div>
|
||||||
|
<div className="font-semibold">失败提醒</div>
|
||||||
|
<div className="mt-1 leading-6">检测到失败或阻塞 evidence,发布验证保持 fail-closed;证据与审计状态仍保留在下方。</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<div className="grid gap-5 lg:grid-cols-[minmax(0,1fr)_360px]">
|
<div className="grid gap-5 lg:grid-cols-[minmax(0,1fr)_360px]">
|
||||||
<main className="space-y-5">
|
<main className="space-y-5">
|
||||||
<Card className={cn("p-5", gate?.status === "blocked" && "border-rose-200 bg-rose-50", gate?.status === "ready" && "border-emerald-200 bg-emerald-50")}>
|
<Card
|
||||||
|
className={cn(
|
||||||
|
"p-5",
|
||||||
|
gate?.status === "blocked" && showFailureAlertReminder && "border-rose-200 bg-rose-50",
|
||||||
|
gate?.status === "ready" && showReleaseGateReminder && "border-emerald-200 bg-emerald-50"
|
||||||
|
)}
|
||||||
|
>
|
||||||
<div className="flex flex-col gap-4 md:flex-row md:items-start md:justify-between">
|
<div className="flex flex-col gap-4 md:flex-row md:items-start md:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<StatusChip label={gateLabel(gate?.status)} tone={gateTone(gate?.status)} />
|
<StatusChip label={gateLabel(gate?.status)} tone={gateTone(gate?.status)} />
|
||||||
|
|
@ -471,9 +543,9 @@ function RealReleaseGrowthPage({ store }: { store: PMFlowOsStoreSnapshot }) {
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{emptyMode ? (
|
{emptyMode ? (
|
||||||
<Card className="border-orange-100 bg-orange-50 p-5">
|
<Card className={cn("p-5", releaseGateReminderEnabled ? "border-orange-100 bg-orange-50" : "border-[#e5edf8] bg-white")}>
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
<ShieldAlert className="mt-1 h-5 w-5 shrink-0 text-orange-500" />
|
<ShieldAlert className={cn("mt-1 h-5 w-5 shrink-0", releaseGateReminderEnabled ? "text-orange-500" : "text-slate-400")} />
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-lg font-semibold text-slate-950">没有可用于发布判断的执行证据</h2>
|
<h2 className="text-lg font-semibold text-slate-950">没有可用于发布判断的执行证据</h2>
|
||||||
<p className="mt-2 text-sm leading-6 text-slate-600">
|
<p className="mt-2 text-sm leading-6 text-slate-600">
|
||||||
|
|
@ -516,7 +588,15 @@ function RealReleaseGrowthPage({ store }: { store: PMFlowOsStoreSnapshot }) {
|
||||||
<SectionTitle title="阻塞原因" icon={AlertTriangle} />
|
<SectionTitle title="阻塞原因" icon={AlertTriangle} />
|
||||||
<div className="mt-4 space-y-2">
|
<div className="mt-4 space-y-2">
|
||||||
{(gate?.blockingReasons || []).map((item) => (
|
{(gate?.blockingReasons || []).map((item) => (
|
||||||
<div key={item} className="rounded-lg border border-rose-100 bg-rose-50 px-3 py-2 text-sm leading-6 text-rose-700">
|
<div
|
||||||
|
key={item}
|
||||||
|
className={cn(
|
||||||
|
"rounded-lg border px-3 py-2 text-sm leading-6",
|
||||||
|
showFailureAlertReminder
|
||||||
|
? "border-rose-100 bg-rose-50 text-rose-700"
|
||||||
|
: "border-[#e6edf7] bg-[#fbfdff] text-slate-600"
|
||||||
|
)}
|
||||||
|
>
|
||||||
{item}
|
{item}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
import { act, renderHook, waitFor } from "@testing-library/react";
|
import { act, cleanup, render, renderHook, screen, waitFor } from "@testing-library/react";
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
import { getAccountSettings } from "@/api/accountSettings";
|
||||||
import {
|
import {
|
||||||
acceptPMFlowChangeItem,
|
acceptPMFlowChangeItem,
|
||||||
commitPMFlowChangeSet,
|
commitPMFlowChangeSet,
|
||||||
|
|
@ -15,12 +16,25 @@ import {
|
||||||
editPMFlowChangeItem,
|
editPMFlowChangeItem,
|
||||||
getPMFlowExecutionSummary,
|
getPMFlowExecutionSummary,
|
||||||
getPMFlowTruthState,
|
getPMFlowTruthState,
|
||||||
|
type PMFlowExecutionSummary,
|
||||||
type PMFlowReleaseGate,
|
type PMFlowReleaseGate,
|
||||||
} from "@/api/pmflowOs";
|
} from "@/api/pmflowOs";
|
||||||
import { createInitialPMFlowState } from "@/features/pmflow-os/mockData";
|
import { createInitialPMFlowState } from "@/features/pmflow-os/mockData";
|
||||||
import { PMFlowOsProvider, usePMFlowOsStore } from "@/features/pmflow-os/store";
|
import { PMFlowOsProvider, usePMFlowOsStore } from "@/features/pmflow-os/store";
|
||||||
import type { ProjectTruthState } from "@/features/pmflow-os/types";
|
import type { ProjectTruthState } from "@/features/pmflow-os/types";
|
||||||
|
|
||||||
|
vi.mock("@/lib/utils", () => ({
|
||||||
|
cn: (...inputs: Array<string | false | null | undefined>) => inputs.filter(Boolean).join(" "),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/api/accountSettings", () => ({
|
||||||
|
getAccountSettings: vi.fn(),
|
||||||
|
shouldShowAccountNotification: vi.fn(
|
||||||
|
(settings: { notifications?: Record<string, { enabled?: boolean }> } | null | undefined, key: string) =>
|
||||||
|
settings?.notifications?.[key]?.enabled ?? true
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
vi.mock("@/api/pmflowOs", () => ({
|
vi.mock("@/api/pmflowOs", () => ({
|
||||||
getPMFlowTruthState: vi.fn(),
|
getPMFlowTruthState: vi.fn(),
|
||||||
getPMFlowExecutionSummary: vi.fn(),
|
getPMFlowExecutionSummary: vi.fn(),
|
||||||
|
|
@ -39,6 +53,7 @@ vi.mock("@/api/pmflowOs", () => ({
|
||||||
|
|
||||||
const getTruthStateMock = vi.mocked(getPMFlowTruthState);
|
const getTruthStateMock = vi.mocked(getPMFlowTruthState);
|
||||||
const getExecutionSummaryMock = vi.mocked(getPMFlowExecutionSummary);
|
const getExecutionSummaryMock = vi.mocked(getPMFlowExecutionSummary);
|
||||||
|
const getAccountSettingsMock = vi.mocked(getAccountSettings);
|
||||||
const createThreadMessageMock = vi.mocked(createPMFlowThreadMessage);
|
const createThreadMessageMock = vi.mocked(createPMFlowThreadMessage);
|
||||||
const createExecutionHandoffMock = vi.mocked(createPMFlowExecutionHandoff);
|
const createExecutionHandoffMock = vi.mocked(createPMFlowExecutionHandoff);
|
||||||
const consumeExecutionHandoffMock = vi.mocked(consumePMFlowExecutionHandoff);
|
const consumeExecutionHandoffMock = vi.mocked(consumePMFlowExecutionHandoff);
|
||||||
|
|
@ -105,12 +120,56 @@ function releaseGate(status: "ready" | "blocked" | "needs_confirmation" | "empty
|
||||||
{ key: "test", label: "测试证据", status: status === "blocked" ? "failed" : "passed", detail: "真实测试回写" },
|
{ key: "test", label: "测试证据", status: status === "blocked" ? "failed" : "passed", detail: "真实测试回写" },
|
||||||
],
|
],
|
||||||
evidence: [
|
evidence: [
|
||||||
{ id: "703", type: "test_passed", summary: "真实测试回写", prNumber: 88 },
|
{
|
||||||
|
id: "703",
|
||||||
|
type: status === "blocked" ? "test_failed" : "test_passed",
|
||||||
|
summary: status === "blocked" ? "测试失败" : "真实测试回写",
|
||||||
|
prNumber: 88,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
updatedAt: "2026-04-26T00:00:00",
|
updatedAt: "2026-04-26T00:00:00",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function accountSettingsPreference({ releaseGate = true, failureAlert = true }: { releaseGate?: boolean; failureAlert?: boolean } = {}) {
|
||||||
|
return {
|
||||||
|
profile: { displayName: "Owner", roleTitle: "产品负责人", email: "", defaultLanguage: "zh-CN" },
|
||||||
|
aiPreferences: { answerDepth: "balanced", evidenceDisplay: "inline" },
|
||||||
|
notifications: {
|
||||||
|
pendingConfirmation: { enabled: true, desktop: true, emailDigest: false },
|
||||||
|
executionWriteback: { enabled: true, desktop: true, emailDigest: false },
|
||||||
|
releaseGate: { enabled: releaseGate, desktop: false, emailDigest: false },
|
||||||
|
failureAlert: { enabled: failureAlert, desktop: false, emailDigest: false },
|
||||||
|
},
|
||||||
|
saveScope: "local",
|
||||||
|
updatedAt: "2026-04-26T00:00:00",
|
||||||
|
} as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
function executionSummaryWithGate(gate: PMFlowReleaseGate): PMFlowExecutionSummary {
|
||||||
|
const executionStatus: PMFlowExecutionSummary["execution"]["status"] = gate.status === "blocked" ? "failed" : "testing";
|
||||||
|
return {
|
||||||
|
projectId: "123",
|
||||||
|
execution: {
|
||||||
|
taskId: "task-99",
|
||||||
|
status: executionStatus,
|
||||||
|
prompt: "真实 prompt package",
|
||||||
|
commitLog: ["真实进度事件"],
|
||||||
|
prStatus: "PR #88 · linked",
|
||||||
|
testSummary: gate.status === "blocked" ? "测试失败" : "真实测试回写",
|
||||||
|
writebackStatus: gate.status === "blocked" ? "failed" : "testing",
|
||||||
|
},
|
||||||
|
releaseGate: gate,
|
||||||
|
devSync: { active_task_count: 1 },
|
||||||
|
promptPackage: null,
|
||||||
|
repoBindings: [],
|
||||||
|
activeDevTasks: [],
|
||||||
|
recentProgressEvents: [],
|
||||||
|
recentDeliveries: [],
|
||||||
|
warnings: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function wrapper() {
|
function wrapper() {
|
||||||
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
||||||
return function Wrapper({ children }: { children: ReactNode }) {
|
return function Wrapper({ children }: { children: ReactNode }) {
|
||||||
|
|
@ -126,6 +185,7 @@ describe("PMFlowOsProvider real change review actions", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
window.history.pushState({}, "", "/pmflow-os?projectId=123");
|
window.history.pushState({}, "", "/pmflow-os?projectId=123");
|
||||||
|
getAccountSettingsMock.mockResolvedValue(accountSettingsPreference());
|
||||||
getTruthStateMock.mockResolvedValue(realState());
|
getTruthStateMock.mockResolvedValue(realState());
|
||||||
getExecutionSummaryMock.mockResolvedValue({
|
getExecutionSummaryMock.mockResolvedValue({
|
||||||
projectId: "123",
|
projectId: "123",
|
||||||
|
|
@ -392,6 +452,7 @@ describe("PMFlowOsProvider real change review actions", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
cleanup();
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -492,6 +553,32 @@ describe("PMFlowOsProvider real change review actions", () => {
|
||||||
expect(result.current.state.changeItems[0]?.status).toBe("pending_review");
|
expect(result.current.state.changeItems[0]?.status).toBe("pending_review");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("shows release gate and failure reminders when account notification preferences are enabled", async () => {
|
||||||
|
getAccountSettingsMock.mockResolvedValue(accountSettingsPreference({ releaseGate: true, failureAlert: true }));
|
||||||
|
getExecutionSummaryMock.mockResolvedValue(executionSummaryWithGate(releaseGate("blocked")));
|
||||||
|
const { default: ReleaseGrowthPage } = await import("@/features/pmflow-os/pages/ReleaseGrowthPage");
|
||||||
|
|
||||||
|
render(<ReleaseGrowthPage />, { wrapper: wrapper() });
|
||||||
|
|
||||||
|
expect(await screen.findByText("发布门禁提醒")).toBeTruthy();
|
||||||
|
expect(await screen.findByText("失败提醒")).toBeTruthy();
|
||||||
|
expect(screen.getByText("执行证据")).toBeTruthy();
|
||||||
|
expect(screen.getAllByText("测试失败").length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps release evidence visible while silencing release and failure reminders when preferences are disabled", async () => {
|
||||||
|
getAccountSettingsMock.mockResolvedValue(accountSettingsPreference({ releaseGate: false, failureAlert: false }));
|
||||||
|
getExecutionSummaryMock.mockResolvedValue(executionSummaryWithGate(releaseGate("blocked")));
|
||||||
|
const { default: ReleaseGrowthPage } = await import("@/features/pmflow-os/pages/ReleaseGrowthPage");
|
||||||
|
|
||||||
|
render(<ReleaseGrowthPage />, { wrapper: wrapper() });
|
||||||
|
|
||||||
|
expect((await screen.findAllByText("执行证据")).length).toBeGreaterThan(0);
|
||||||
|
expect(screen.queryByText("发布门禁提醒")).toBeNull();
|
||||||
|
expect(screen.queryByText("失败提醒")).toBeNull();
|
||||||
|
expect((await screen.findAllByText("测试失败")).length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
it("still uses local demo mutation when demo mode is explicit", () => {
|
it("still uses local demo mutation when demo mode is explicit", () => {
|
||||||
window.history.pushState({}, "", "/pmflow-os?demo=1");
|
window.history.pushState({}, "", "/pmflow-os?demo=1");
|
||||||
const { result } = renderHook(() => usePMFlowOsStore(), { wrapper: wrapper() });
|
const { result } = renderHook(() => usePMFlowOsStore(), { wrapper: wrapper() });
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue