import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { act, cleanup, fireEvent, render, renderHook, screen, waitFor } from "@testing-library/react"; import type { ReactNode } from "react"; import { MemoryRouter, useLocation } from "react-router-dom"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { getAccountSettings } from "@/api/accountSettings"; import { acceptPMFlowChangeItem, commitPMFlowChangeSet, consumePMFlowExecutionHandoff, createPMFlowExecutionHandoff, createPMFlowExecutionProgressEvent, createPMFlowIterationRecommendationGroupChangeSetDraft, createPMFlowIterationRecommendationChangeSetDraft, createPMFlowReleaseValidationRun, createPMFlowReleaseFeedbackIngest, createPMFlowReviewDecision, createPMFlowThreadMessage, getPMFlowExecutionCapabilities, deferPMFlowChangeItem, editPMFlowChangeItem, getPMFlowExecutionSummary, getPMFlowTruthState, type PMFlowExecutionSummary, type PMFlowReleaseGate, } from "@/api/pmflowOs"; import { createInitialPMFlowState } from "@/features/pmflow-os/mockData"; import TeamLayerPage from "@/features/pmflow-os/pages/TeamLayerPage"; import { PMFlowOsProvider, usePMFlowOsStore } from "@/features/pmflow-os/store"; import type { PMFlowReviewerRouting, ProjectTruthState } from "@/features/pmflow-os/types"; vi.mock("@/lib/utils", () => ({ cn: (...inputs: Array) => inputs.filter(Boolean).join(" "), })); vi.mock("@/api/accountSettings", () => ({ getAccountSettings: vi.fn(), shouldShowAccountNotification: vi.fn( (settings: { notifications?: Record } | null | undefined, key: string) => settings?.notifications?.[key]?.enabled ?? true ), })); vi.mock("@/api/pmflowOs", () => ({ getPMFlowTruthState: vi.fn(), getPMFlowExecutionSummary: vi.fn(), getPMFlowExecutionCapabilities: vi.fn(), createPMFlowThreadMessage: vi.fn(), createPMFlowExtraction: vi.fn(), getPMFlowChangeSet: vi.fn(), acceptPMFlowChangeItem: vi.fn(), commitPMFlowChangeSet: vi.fn(), consumePMFlowExecutionHandoff: vi.fn(), createPMFlowExecutionHandoff: vi.fn(), createPMFlowExecutionProgressEvent: vi.fn(), createPMFlowIterationRecommendationGroupChangeSetDraft: vi.fn(), createPMFlowIterationRecommendationChangeSetDraft: vi.fn(), createPMFlowReleaseValidationRun: vi.fn(), createPMFlowReleaseFeedbackIngest: vi.fn(), createPMFlowReviewDecision: vi.fn(), editPMFlowChangeItem: vi.fn(), deferPMFlowChangeItem: vi.fn(), })); const getTruthStateMock = vi.mocked(getPMFlowTruthState); const getExecutionSummaryMock = vi.mocked(getPMFlowExecutionSummary); const getExecutionCapabilitiesMock = vi.mocked(getPMFlowExecutionCapabilities); const getAccountSettingsMock = vi.mocked(getAccountSettings); const createThreadMessageMock = vi.mocked(createPMFlowThreadMessage); const createExecutionHandoffMock = vi.mocked(createPMFlowExecutionHandoff); const consumeExecutionHandoffMock = vi.mocked(consumePMFlowExecutionHandoff); const createExecutionProgressEventMock = vi.mocked(createPMFlowExecutionProgressEvent); const createIterationRecommendationGroupChangeSetDraftMock = vi.mocked(createPMFlowIterationRecommendationGroupChangeSetDraft); const createIterationRecommendationChangeSetDraftMock = vi.mocked(createPMFlowIterationRecommendationChangeSetDraft); const createReleaseValidationRunMock = vi.mocked(createPMFlowReleaseValidationRun); const createReleaseFeedbackIngestMock = vi.mocked(createPMFlowReleaseFeedbackIngest); const createReviewDecisionMock = vi.mocked(createPMFlowReviewDecision); const acceptItemMock = vi.mocked(acceptPMFlowChangeItem); const commitChangeSetMock = vi.mocked(commitPMFlowChangeSet); const editItemMock = vi.mocked(editPMFlowChangeItem); const deferItemMock = vi.mocked(deferPMFlowChangeItem); function realState(): ProjectTruthState { const state = createInitialPMFlowState(); state.project.id = "123"; state.thread.id = "7"; state.requirements = []; state.activeChangeSetId = "11"; state.changeSets = [ { id: "11", threadId: "7", summary: "真实待审变更", headline: "真实变更集", createdAt: "2026-04-26T00:00:00", status: "open", itemIds: ["21"], }, ]; state.changeItems = [ { id: "21", changeSetId: "11", group: "new_object", objectType: "Requirement", objectId: "pending-21", title: "新增需求:真实审查", summary: "来自后端 projection", diff: "待确认", confidence: 0.8, riskLevel: "medium", status: "pending_review", evidenceIds: ["31"], }, ]; state.evidence = [ { id: "31", kind: "conversation", title: "项目线程消息", excerpt: "用户输入", sourceLabel: "项目线程", }, ]; state.feed = [ { id: "review-request-11", kind: "review", title: "已请求 ChangeSet 审查", summary: "真实变更集 已请求 审查负责人 审查;包含 high risk 变更。", timestamp: "2026-04-26T10:00:00", }, { id: "release-gate-blocked", kind: "release", title: "发布门禁已阻塞", summary: "执行回写中存在失败或阻塞事件,发布门禁已关闭。", timestamp: "2026-04-26T11:00:00", }, ]; state.reviewerRouting = reviewerRoutingFixture(); return state; } function reviewerRoutingFixture(): PMFlowReviewerRouting { return { status: "ready", warningCode: null, summary: { totalPendingReview: 1, highRiskCount: 1, missingReviewerCount: 0, iterationDraftCount: 1, }, items: [ { projectId: "123", changeSetId: "11", changeSetTitle: "真实变更集", changeSetStatus: "open", sourceType: "iteration_recommendation", reviewReason: "包含 high risk 变更,需要 REVIEWER 或 OWNER 复核。", riskLevel: "high", priority: "urgent", changeTypes: ["Risk", "Task"], itemCount: 2, pendingItemCount: 1, evidenceCount: 1, reviewDecision: null, approvalState: "not_requested", suggestedReviewer: { userId: "9", displayName: "审查负责人", role: "REVIEWER", reason: "REVIEWER 角色,优先复核 high risk 变更。", confidence: "high", }, fallbackReviewerRole: null, targetUrl: "/change-review?projectId=123&changeSetId=11", createdAt: "2026-04-26T00:00:00", updatedAt: "2026-04-26T00:00:00", }, ], }; } function LocationProbe() { const location = useLocation(); return
{`${location.pathname}${location.search}`}
; } function releaseGate(status: "ready" | "blocked" | "needs_confirmation" | "empty" = "needs_confirmation"): PMFlowReleaseGate { return { status, title: status === "blocked" ? "发布阻塞" : "发布候选,需人工确认", summary: "来自真实 execution-summary 的发布门禁判断", blockingReasons: status === "blocked" ? ["测试失败"] : [], nextActions: ["负责人确认后再发布"], checks: [ { key: "test", label: "测试证据", status: status === "blocked" ? "failed" : "passed", detail: "真实测试回写" }, ], evidence: [ { id: "703", type: status === "blocked" ? "test_failed" : "test_passed", summary: status === "blocked" ? "测试失败" : "真实测试回写", prNumber: 88, }, ], 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() { const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); return function Wrapper({ children }: { children: ReactNode }) { return ( {children} ); }; } describe("PMFlowOsProvider real change review actions", () => { beforeEach(() => { vi.clearAllMocks(); window.history.pushState({}, "", "/pmflow-os?projectId=123"); getAccountSettingsMock.mockResolvedValue(accountSettingsPreference()); getTruthStateMock.mockResolvedValue(realState()); getExecutionSummaryMock.mockResolvedValue({ projectId: "123", execution: { taskId: "task-99", status: "coding", prompt: "真实 prompt package", commitLog: ["真实进度事件"], prStatus: "pending", testSummary: "pending", writebackStatus: "coding", }, releaseGate: releaseGate(), devSync: { active_task_count: 1 }, promptPackage: { id: "501", project_id: "123", charter_id: "301", task_id: "task-99", status: "active", package_kind: "development", package_text: "真实 prompt package", package_payload: {}, created_at: "2026-04-26T00:00:00", }, repoBindings: [ { id: "601", project_id: "123", provider: "github", owner: "pmflow", repo_name: "app", full_name: "pmflow/app", default_branch: "main", status: "active", }, ], activeDevTasks: [ { id: "task-99", title: "真实任务", description: "真实任务描述", status: "confirmed", priority: "high", project_id: "123", charter_id: "301", charter_title: "真实宪章", assignee: { id: "1", name: "Owner", role: "MEMBER" }, prompt_package_count: 1, code_link: null, }, ], recentProgressEvents: [], recentDeliveries: [], warnings: [], }); getExecutionCapabilitiesMock.mockResolvedValue({ projectId: "123", executionModes: [ { mode: "ai_with_user_acceptance", label: "AI 执行 + 用户验收", status: "partial", reason: "等待 repo 与执行证据补齐。", targetHref: "/execution-bridge?projectId=123", }, ], personalReadiness: { status: "partial", toolReadiness: [ { key: "repo", label: "Repo 连接", status: "blocked", summary: "未接入 repo,AI 执行包缺少真实代码落点。", href: "/execution-bridge?projectId=123", }, ], waitingForUser: { pendingConfirmations: 1, pendingReviews: 1, waitingAcceptance: 0, }, blockers: ["未接入 repo,AI 执行包缺少真实代码落点。"], recommendedNextStep: "先处理分配给你的审查项,再继续推进 AI 执行。", targetHref: "/change-review?projectId=123", }, teamSignals: [ { userId: "9", displayName: "审查负责人", role: "REVIEWER", judgementResponsibilities: ["技术/产品审查"], reviewResponsibilities: ["实现风险"], capacity: { status: "ready", label: "可承接", workloadRatio: 32, pendingReviewCount: 1, activeExecutionCount: 0, }, context: { status: "partial", label: "有项目证据或能力参考", evidenceCount: 1, recentContributionCount: 1, }, toolStatus: { status: "unknown", label: "未接入", connectedTools: [], }, referenceSkills: ["审查责任"], recommendedModes: ["ai_with_technical_review"], warnings: ["未发现活跃 IDE / Agent 工具连接。"], }, ], warnings: ["repo_missing: 未接入 repo,AI 执行包无法落到真实代码上下文。"], }); createThreadMessageMock.mockResolvedValue({ id: "41", threadId: "7", projectId: "123", role: "user", author: "你", content: "继续补充", createdAt: "2026-04-26T00:00:00", status: "sent", source: "web", metadata: {}, }); acceptItemMock.mockResolvedValue({ changeSet: {}, items: [], evidence: [], approvals: [] }); commitChangeSetMock.mockResolvedValue({ changeSet: { id: "11", status: "committed" }, committedItems: [{ id: "21", status: "committed" }], skippedItems: [], failedItems: [], }); editItemMock.mockResolvedValue({ changeSet: {}, items: [], evidence: [], approvals: [] }); deferItemMock.mockResolvedValue({ changeSet: {}, items: [], evidence: [], approvals: [] }); createExecutionHandoffMock.mockResolvedValue({ projectId: "123", execution: { taskId: "task-99", status: "pushed", prompt: "真实 prompt package", commitLog: ["执行交接已创建"], prStatus: "pending", testSummary: "pending", writebackStatus: "pushed", }, releaseGate: releaseGate(), devSync: { active_task_count: 1, recent_event_count: 1 }, promptPackage: { id: "501", project_id: "123", charter_id: "301", task_id: "task-99", status: "active", package_kind: "execution_handoff", package_text: "真实 prompt package", package_payload: {}, created_at: "2026-04-26T00:00:00", }, repoBindings: [ { id: "601", project_id: "123", provider: "github", owner: "pmflow", repo_name: "app", full_name: "pmflow/app", default_branch: "main", status: "active", }, ], activeDevTasks: [], recentProgressEvents: [ { id: "701", event_type: "handoff_created", source: "pmflow_os", summary: "执行交接已创建", workflow_state: "pushed", derived_task_status: "pushed", }, ], recentDeliveries: [], warnings: [], }); consumeExecutionHandoffMock.mockResolvedValue({ projectId: "123", execution: { taskId: "task-99", status: "coding", prompt: "真实 prompt package", commitLog: ["执行交接已领取"], prStatus: "pending", testSummary: "pending", writebackStatus: "coding", }, releaseGate: releaseGate(), devSync: { active_task_count: 1, recent_event_count: 2 }, promptPackage: { id: "501", project_id: "123", charter_id: "301", task_id: "task-99", status: "active", package_kind: "execution_handoff", package_text: "真实 prompt package", package_payload: { latest_handoff: { handoff_state: "consumed" } }, created_at: "2026-04-26T00:00:00", }, repoBindings: [], activeDevTasks: [], recentProgressEvents: [ { id: "702", event_type: "handoff_consumed", source: "pmflow_os", summary: "执行交接已领取", workflow_state: "coding", derived_task_status: "coding", }, ], recentDeliveries: [], warnings: [], }); createExecutionProgressEventMock.mockResolvedValue({ projectId: "123", execution: { taskId: "task-99", status: "pr_open", prompt: "真实 prompt package", commitLog: ["执行结果回写"], prStatus: "PR #88 · linked", testSummary: "pending", writebackStatus: "in_review", }, releaseGate: releaseGate(), devSync: { active_task_count: 1, recent_event_count: 3 }, promptPackage: { id: "501", project_id: "123", charter_id: "301", task_id: "task-99", status: "active", package_kind: "execution_handoff", package_text: "真实 prompt package", package_payload: {}, created_at: "2026-04-26T00:00:00", }, repoBindings: [], activeDevTasks: [], recentProgressEvents: [ { id: "703", event_type: "pr_opened", source: "github_agent", summary: "执行结果回写", workflow_state: "in_review", derived_task_status: "review", pr_number: 88, }, ], recentDeliveries: [], warnings: [], }); createReleaseValidationRunMock.mockResolvedValue({ releaseValidationRun: { id: "901", projectId: "123", status: "needs_confirmation", score: 0.6, keyMetrics: { evidenceCount: 1 }, riskItems: [], rollbackPlan: { deploymentExecuted: false }, validationSummary: { source: "pmflow_os_release_gate", releaseGateStatus: "needs_confirmation", deploymentExecuted: false, }, createdBy: "1", createdAt: "2026-04-26T00:00:00", }, executionSummary: { projectId: "123", execution: { taskId: "task-99", status: "testing", prompt: "真实 prompt package", commitLog: ["发布验证记录已创建"], prStatus: "PR #88 · linked", testSummary: "真实测试回写", writebackStatus: "testing", }, releaseGate: { ...releaseGate(), latestValidationRun: { id: "901", projectId: "123", status: "needs_confirmation", score: 0.6, keyMetrics: { evidenceCount: 1 }, riskItems: [], rollbackPlan: { deploymentExecuted: false }, validationSummary: { source: "pmflow_os_release_gate", releaseGateStatus: "needs_confirmation", deploymentExecuted: false, }, createdBy: "1", createdAt: "2026-04-26T00:00:00", }, }, devSync: { active_task_count: 1 }, promptPackage: null, repoBindings: [], activeDevTasks: [], recentProgressEvents: [], recentDeliveries: [], warnings: [], }, warnings: [], }); createReviewDecisionMock.mockResolvedValue({ changeSetDetail: { changeSet: { id: "11", status: "reviewed" }, items: [{ id: "21", status: "accepted" }], evidence: [], approvals: [{ id: "91", action: "approve", decision: "approved", changeSetId: "11", changeItemId: "21" }], }, truthState: realState(), warnings: [], }); }); afterEach(() => { cleanup(); vi.clearAllMocks(); }); it("renders reviewer routing on team layer without assigning reviewers", async () => { render( , { wrapper: wrapper() } ); expect(await screen.findByText("待审 ChangeSet 审查路由")).toBeInTheDocument(); expect(await screen.findByText("执行能力与容量信号")).toBeInTheDocument(); expect(await screen.findByText("真实变更集")).toBeInTheDocument(); expect(screen.getAllByText("审查负责人").length).toBeGreaterThan(0); expect(screen.getAllByText("REVIEWER").length).toBeGreaterThan(0); expect(screen.getByText("下一轮迭代")).toBeInTheDocument(); expect(screen.getByText("证据 1")).toBeInTheDocument(); expect(screen.getByText("已请求 ChangeSet 审查")).toBeInTheDocument(); expect(screen.getByText("发布门禁已阻塞")).toBeInTheDocument(); expect(screen.getAllByText("待我审").length).toBeGreaterThan(0); fireEvent.click(screen.getByRole("button", { name: /去审查/ })); await waitFor(() => expect(screen.getByTestId("location").textContent).toBe("/change-review?projectId=123&changeSetId=11")); expect(acceptItemMock).not.toHaveBeenCalled(); expect(commitChangeSetMock).not.toHaveBeenCalled(); }); it("records reviewer decisions through the PMFlow OS api in real mode", async () => { const { result } = renderHook(() => usePMFlowOsStore(), { wrapper: wrapper() }); await waitFor(() => expect(result.current.dataSourceStatus).toBe("success")); act(() => result.current.createReviewDecision("11", { action: "approve", reviewer_id: 9, reason: "reviewer 通过,不自动 commit。", item_ids: ["21"], }) ); await waitFor(() => expect(createReviewDecisionMock).toHaveBeenCalledWith( "11", expect.objectContaining({ action: "approve", reviewer_id: 9, item_ids: ["21"], }) ) ); expect(commitChangeSetMock).not.toHaveBeenCalled(); }); it("keeps reviewer routing empty when payload omits it", async () => { const state = realState(); delete state.reviewerRouting; getTruthStateMock.mockResolvedValueOnce(state); const { result } = renderHook(() => usePMFlowOsStore(), { wrapper: wrapper() }); await waitFor(() => expect(result.current.dataSourceStatus).toBe("success")); expect(result.current.state.reviewerRouting).toBeUndefined(); }); it("calls real message and review APIs in api mode and does not locally promote to project memory", async () => { const { result } = renderHook(() => usePMFlowOsStore(), { wrapper: wrapper() }); await waitFor(() => expect(result.current.dataSourceStatus).toBe("success")); await waitFor(() => expect(result.current.executionSummaryStatus).toBe("success")); expect(getExecutionSummaryMock).toHaveBeenCalledWith("123"); expect(result.current.executionSummary?.promptPackage?.package_text).toBe("真实 prompt package"); expect(result.current.executionSummary?.releaseGate.status).toBe("needs_confirmation"); act(() => result.current.sendMessage("继续补充")); await waitFor(() => expect(createThreadMessageMock).toHaveBeenCalledWith("123", "7", expect.objectContaining({ content: "继续补充" }))); act(() => result.current.acceptChange("21")); await waitFor(() => expect(acceptItemMock).toHaveBeenCalledWith("21")); act(() => result.current.editChange("21", "人工修订")); await waitFor(() => expect(editItemMock).toHaveBeenCalledWith("21", expect.objectContaining({ diff: "人工修订" }))); act(() => result.current.deferChange("21")); await waitFor(() => expect(deferItemMock).toHaveBeenCalledWith("21", "用户暂缓")); act(() => result.current.promoteAcceptedToGraph("11")); await waitFor(() => expect(commitChangeSetMock).toHaveBeenCalledWith( "11", expect.objectContaining({ mode: "atomic", note: "用户确认写入项目记忆" }) ) ); act(() => result.current.createExecutionHandoff({ task_id: "task-99", repo_binding_id: "601", runtime_target: "local_bridge", note: "测试执行交接", }) ); await waitFor(() => expect(createExecutionHandoffMock).toHaveBeenCalledWith( "123", expect.objectContaining({ task_id: "task-99", repo_binding_id: "601", runtime_target: "local_bridge" }) ) ); act(() => result.current.consumeExecutionHandoff("501")); await waitFor(() => expect(consumeExecutionHandoffMock).toHaveBeenCalledWith( "123", "501", expect.objectContaining({ runtime_target: "codex", consumer_label: "Codex" }) ) ); act(() => result.current.recordExecutionProgress({ task_id: "task-99", repo_binding_id: "601", event_type: "pr_opened", source: "github_agent", branch_name: "codex/main-flow-d3", pr_number: 88, }) ); await waitFor(() => expect(createExecutionProgressEventMock).toHaveBeenCalledWith( "123", expect.objectContaining({ task_id: "task-99", event_type: "pr_opened", source: "github_agent", pr_number: 88 }) ) ); act(() => result.current.createReleaseValidationRun({ decision: "enter_release_candidate", note: "人工确认进入发布候选", }) ); await waitFor(() => expect(createReleaseValidationRunMock).toHaveBeenCalledWith( "123", expect.objectContaining({ decision: "enter_release_candidate", note: "人工确认进入发布候选" }) ) ); expect(result.current.state.requirements).toEqual([]); }); it("keeps api failures explicit instead of mutating local success state", async () => { acceptItemMock.mockRejectedValueOnce(new Error("item_already_reviewed")); const { result } = renderHook(() => usePMFlowOsStore(), { wrapper: wrapper() }); await waitFor(() => expect(result.current.dataSourceStatus).toBe("success")); act(() => result.current.acceptChange("21")); await waitFor(() => expect(result.current.dataSourceError).toBe("item_already_reviewed")); 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(, { 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(, { 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("shows real release feedback signals and keeps empty state explicit", async () => { getExecutionSummaryMock.mockResolvedValue({ ...executionSummaryWithGate(releaseGate("ready")), releaseSignals: { status: "active", relationship: { releaseGateStatus: "ready", latestValidationRunId: "900", latestValidationRunStatus: "ready", source: "pmflow_os_release_growth", changeSetCreated: false, }, summary: { feedbackCount: 1, revenueEventCount: 0, iterationRecommendationCount: 1, latestSignalAt: "2026-04-26T00:00:00" }, feedback: [ { id: "feedback-1", projectId: "123", sourceType: "manual", title: "首批试用反馈", content: "用户反馈上传入口难找,希望下一轮优化 onboarding。", metrics: { negative_feedback_count: 3 }, samples: [], status: "active", createdAt: "2026-04-26T00:00:00", updatedAt: "2026-04-26T00:00:00", }, ], revenueEvents: [], recommendations: [ { id: "rec-1", projectId: "123", feedbackIngestId: "feedback-1", status: "draft", priority: "high", recommendationSummary: { summary: "优先优化 onboarding 上传入口。" }, proposedCharterPatch: {}, proposedTasks: [], changeSetCreated: false, createdAt: "2026-04-26T00:00:00", updatedAt: "2026-04-26T00:00:00", }, ], nextActions: ["基于真实反馈查看 iteration recommendation,但本阶段不自动创建 ChangeSet。"], warnings: [], }, }); const { default: ReleaseGrowthPage } = await import("@/features/pmflow-os/pages/ReleaseGrowthPage"); render(, { wrapper: wrapper() }); expect(await screen.findByText("发布后反馈信号")).toBeTruthy(); expect(await screen.findByText("首批试用反馈")).toBeTruthy(); expect(screen.getByText("优先优化 onboarding 上传入口。")).toBeTruthy(); expect(screen.getByText("生成下一轮待确认变更")).toBeTruthy(); expect(screen.queryByText(/页面不会用 mock 成功数据/)).toBeNull(); }); it("creates a reviewable ChangeSet draft from a release recommendation", async () => { getExecutionSummaryMock.mockResolvedValue({ ...executionSummaryWithGate(releaseGate("ready")), releaseSignals: { status: "active", relationship: { releaseGateStatus: "ready", latestValidationRunId: "900", latestValidationRunStatus: "ready", source: "pmflow_os_release_growth", changeSetCreated: false, }, summary: { feedbackCount: 1, revenueEventCount: 0, iterationRecommendationCount: 1, latestSignalAt: "2026-04-26T00:00:00" }, feedback: [], revenueEvents: [], recommendations: [ { id: "rec-1", projectId: "123", feedbackIngestId: "feedback-1", status: "draft", priority: "high", recommendationSummary: { summary: "优先优化 onboarding 上传入口。" }, proposedCharterPatch: {}, proposedTasks: [], changeSetCreated: false, createdAt: "2026-04-26T00:00:00", updatedAt: "2026-04-26T00:00:00", }, ], nextActions: ["基于真实反馈选择 iteration recommendation,生成下一轮待确认 ChangeSet draft。"], warnings: [], }, }); createIterationRecommendationChangeSetDraftMock.mockResolvedValue({ changeSetDetail: { changeSet: { id: "77", status: "open" }, items: [{ id: "78", objectType: "Requirement", status: "pending_review" }], evidence: [{ id: "79", sourceType: "iteration_recommendation" }], approvals: [], }, executionSummary: executionSummaryWithGate(releaseGate("ready")), warnings: [], }); const { default: ReleaseGrowthPage } = await import("@/features/pmflow-os/pages/ReleaseGrowthPage"); render(, { wrapper: wrapper() }); const action = await screen.findByText("生成下一轮待确认变更"); fireEvent.click(action); await waitFor(() => expect(createIterationRecommendationChangeSetDraftMock).toHaveBeenCalledWith( "123", "rec-1", expect.objectContaining({ note: expect.stringContaining("ReleaseGrowthPage") }) ) ); }); it("creates a grouped ChangeSet draft from recommendation quality groups", async () => { getExecutionSummaryMock.mockResolvedValue({ ...executionSummaryWithGate(releaseGate("ready")), releaseSignals: { status: "active", relationship: { releaseGateStatus: "ready", latestValidationRunId: "900", latestValidationRunStatus: "ready", source: "pmflow_os_release_growth", changeSetCreated: false, }, summary: { feedbackCount: 2, revenueEventCount: 0, iterationRecommendationCount: 2, latestSignalAt: "2026-04-26T00:00:00" }, feedback: [], revenueEvents: [], recommendations: [ { id: "rec-1", projectId: "123", feedbackIngestId: "feedback-1", status: "draft", priority: "high", recommendationSummary: { summary: "补齐失败样本。" }, proposedCharterPatch: {}, proposedTasks: [], changeSetCreated: false, groupKey: "quality", mergedCount: 2, impactLevel: "high", riskLevel: "high", suggestedChangeTypes: ["Requirement", "Decision", "Task", "Risk"], reviewPriority: "p0", }, { id: "rec-2", projectId: "123", feedbackIngestId: "feedback-2", status: "draft", priority: "high", recommendationSummary: { summary: "修复幻觉回答。" }, proposedCharterPatch: {}, proposedTasks: [], changeSetCreated: false, groupKey: "quality", duplicateOf: "rec-1", mergedCount: 2, impactLevel: "high", riskLevel: "high", suggestedChangeTypes: ["Requirement", "Decision", "Task", "Risk"], reviewPriority: "p0", }, ], recommendationGroups: [ { groupKey: "quality", title: "quality 反馈组", recommendationIds: ["rec-1", "rec-2"], mergedCount: 2, impactLevel: "high", riskLevel: "high", suggestedChangeTypes: ["Requirement", "Decision", "Task", "Risk"], reviewPriority: "p0", changeSetCreated: false, rationale: "聚焦 quality;负向反馈 4 条;建议任务 2 个。", }, ], nextActions: ["同组 recommendation 可批量生成下一轮待确认 ChangeSet draft。"], warnings: [], }, }); createIterationRecommendationGroupChangeSetDraftMock.mockResolvedValue({ changeSetDetail: { changeSet: { id: "88", status: "open" }, items: [{ id: "89", objectType: "Requirement", status: "pending_review" }], evidence: [{ id: "90", sourceType: "iteration_recommendation_group", metadata: { groupKey: "quality" } }], approvals: [], }, executionSummary: executionSummaryWithGate(releaseGate("ready")), warnings: [], }); const { default: ReleaseGrowthPage } = await import("@/features/pmflow-os/pages/ReleaseGrowthPage"); render(, { wrapper: wrapper() }); expect(await screen.findByText("quality 反馈组")).toBeTruthy(); expect(screen.getByText("聚焦 quality;负向反馈 4 条;建议任务 2 个。")).toBeTruthy(); const action = await screen.findByText("批量生成这一组待确认变更"); fireEvent.click(action); await waitFor(() => expect(createIterationRecommendationGroupChangeSetDraftMock).toHaveBeenCalledWith( "123", "quality", expect.objectContaining({ recommendation_ids: ["rec-1", "rec-2"], note: expect.stringContaining("quality"), }) ) ); }); it("writes release feedback through the PMFlow OS api in real mode", async () => { createReleaseFeedbackIngestMock.mockResolvedValue({ ingest: { id: "feedback-1", projectId: "123", sourceType: "manual", title: "Release/Growth 人工反馈", content: "用户反馈上传入口难找", metrics: {}, samples: [], status: "active", }, recommendation: { id: "rec-1", projectId: "123", feedbackIngestId: "feedback-1", status: "draft", priority: "high", recommendationSummary: {}, proposedCharterPatch: {}, proposedTasks: [], changeSetCreated: false, }, releaseSignals: { status: "active", relationship: { releaseGateStatus: "ready", changeSetCreated: false }, summary: { feedbackCount: 1, revenueEventCount: 0, iterationRecommendationCount: 1 }, feedback: [], revenueEvents: [], recommendations: [], nextActions: [], warnings: [], }, executionSummary: executionSummaryWithGate(releaseGate("ready")), warnings: [], }); const { result } = renderHook(() => usePMFlowOsStore(), { wrapper: wrapper() }); await waitFor(() => expect(result.current.dataSourceStatus).toBe("success")); act(() => result.current.createReleaseFeedbackIngest({ source_type: "manual", title: "Release/Growth 人工反馈", content: "用户反馈上传入口难找", }) ); await waitFor(() => expect(createReleaseFeedbackIngestMock).toHaveBeenCalledWith( "123", expect.objectContaining({ source_type: "manual", content: "用户反馈上传入口难找" }) ) ); }); it("still uses local demo mutation when demo mode is explicit", () => { window.history.pushState({}, "", "/pmflow-os?demo=1"); const { result } = renderHook(() => usePMFlowOsStore(), { wrapper: wrapper() }); const pending = result.current.state.changeItems.find((item) => item.status === "pending_review"); act(() => result.current.acceptChange(pending!.id)); expect(acceptItemMock).not.toHaveBeenCalled(); expect(createExecutionHandoffMock).not.toHaveBeenCalled(); expect(consumeExecutionHandoffMock).not.toHaveBeenCalled(); expect(createExecutionProgressEventMock).not.toHaveBeenCalled(); expect(createReleaseValidationRunMock).not.toHaveBeenCalled(); expect(createReleaseFeedbackIngestMock).not.toHaveBeenCalled(); expect(createIterationRecommendationGroupChangeSetDraftMock).not.toHaveBeenCalled(); expect(createIterationRecommendationChangeSetDraftMock).not.toHaveBeenCalled(); expect(getExecutionSummaryMock).not.toHaveBeenCalled(); expect(result.current.state.changeItems.find((item) => item.id === pending!.id)?.status).toBe("accepted"); }); });