1090 lines
39 KiB
TypeScript
1090 lines
39 KiB
TypeScript
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<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", () => ({
|
||
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 <div data-testid="location">{`${location.pathname}${location.search}`}</div>;
|
||
}
|
||
|
||
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 (
|
||
<QueryClientProvider client={queryClient}>
|
||
<PMFlowOsProvider>{children}</PMFlowOsProvider>
|
||
</QueryClientProvider>
|
||
);
|
||
};
|
||
}
|
||
|
||
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(
|
||
<MemoryRouter>
|
||
<TeamLayerPage />
|
||
<LocationProbe />
|
||
</MemoryRouter>,
|
||
{ 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(<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("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(<ReleaseGrowthPage />, { 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(<ReleaseGrowthPage />, { 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(<ReleaseGrowthPage />, { 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");
|
||
});
|
||
});
|