diff --git a/docs/PMFLOW_PRODUCT_DELIVERY_STANDARD.md b/docs/PMFLOW_PRODUCT_DELIVERY_STANDARD.md index 289070f9..7d0827b2 100644 --- a/docs/PMFLOW_PRODUCT_DELIVERY_STANDARD.md +++ b/docs/PMFLOW_PRODUCT_DELIVERY_STANDARD.md @@ -807,6 +807,32 @@ P0 公共壳冻结已完成,下一批可以进入 `Batch P1` 三模块并行 ## 8. 进度台账 +### 2026-04-26 / account-p7-review-notification-intensity / implemented + +完成内容: + +- 将 Account Settings 通知偏好从简单开关扩展为本地 UI 提醒表达合同:每类提醒继续保留 `enabled / desktop / emailDigest` 兼容字段,同时新增 `reminderIntensity=silent|normal|prominent` 与 `evidenceDisplay=compact|expanded`。 +- 新增 `getAccountReminderConsumption(settings, event)`,覆盖 `change_review_high_risk / iteration_draft_ready / release_blocked / execution_stalled`,稳定输出 `surface / notificationKey / reminderIntensity / evidenceDisplay / showInAppReminder / showProminentReminder / evidencePolicy / auditPolicy / notificationTransport=in_app_only`。 +- 默认值向后兼容 P1-P6:旧数据缺少新字段时,旧开启等价 `normal + compact`,旧关闭等价 `silent + compact`;偏好关闭只静默页面内提醒,不隐藏 ChangeSet、recommendation、release evidence、execution 状态或审计状态。 +- `AccountSettingsPanel` 通知偏好区新增提醒强度和证据展示配置;桌面提醒、邮件摘要仍只记录偏好,不代表真实邮件、桌面推送或系统通知中心已接入。 +- 本轮只扩展前端本地 helper 和页面设置,不改后端 schema、migration、router、PMFlow OS store、Space 或 Main Flow 页面。 + +涉及文件: + +- `frontend/src/api/accountSettings.ts` +- `frontend/src/features/account-settings/AccountSettingsPanel.tsx` +- `frontend/src/features/account-settings/accountSettings.test.ts` +- `CLAUDE.md` +- `docs/architecture/CODE_REFERENCE_INDEX.md` +- `docs/architecture/ARCHITECTURE_INVENTORY.md` +- `docs/PMFLOW_PRODUCT_DELIVERY_STANDARD.md` + +验证: + +- `cd frontend && npm run test -- src/features/account-settings/accountSettings.test.ts` 通过:`11 passed`。 +- `cd frontend && npm run typecheck` 通过。 +- `cd frontend && npm run build` 通过;Vite 仍提示既有 manual chunk circular warning(`ai-steward / sstc / charts / governance` chunk 环),构建成功。 + ### 2026-04-26 / big-module-integration-p6 / done 完成内容: diff --git a/frontend/src/api/accountSettings.ts b/frontend/src/api/accountSettings.ts index eed4ef2d..129bf825 100644 --- a/frontend/src/api/accountSettings.ts +++ b/frontend/src/api/accountSettings.ts @@ -4,8 +4,15 @@ import { getAuthToken } from "@/lib/api"; export type AccountLanguage = "zh-CN" | "en-US"; export type AccountAnswerDepth = "concise" | "balanced" | "deep"; export type AccountEvidenceDisplay = "summary" | "inline" | "full"; +export type AccountReminderIntensity = "silent" | "normal" | "prominent"; +export type AccountNotificationEvidenceDisplay = "compact" | "expanded"; export type AccountNotificationKey = "pendingConfirmation" | "executionWriteback" | "releaseGate" | "failureAlert"; export type AccountSettingsSaveScope = "local" | "account"; +export type AccountReminderConsumptionEvent = + | "change_review_high_risk" + | "iteration_draft_ready" + | "release_blocked" + | "execution_stalled"; export type AccountPostReleaseNotificationEvent = | "feedback_received" | "revenue_signal" @@ -35,6 +42,8 @@ export interface AccountNotificationPreference { enabled: boolean; desktop: boolean; emailDigest: boolean; + reminderIntensity?: AccountReminderIntensity; + evidenceDisplay?: AccountNotificationEvidenceDisplay; } export type AccountNotificationSettings = Record; @@ -47,20 +56,46 @@ export interface AccountNotificationConsumption { productArea: string; trigger: string; enabled: boolean; + reminderIntensity: AccountReminderIntensity; + evidenceDisplay: AccountNotificationEvidenceDisplay; showInAppReminder: boolean; + showProminentReminder: boolean; desktopPreference: boolean; emailDigestPreference: boolean; statusLabel: string; detail: string; } +export interface AccountReminderConsumptionContract { + event: AccountReminderConsumptionEvent; + notificationKey: AccountNotificationKey; + surface: Exclude; + productArea: string; + title: string; + reason: string; + reminderIntensity: AccountReminderIntensity; + evidenceDisplay: AccountNotificationEvidenceDisplay; + showInAppReminder: boolean; + showProminentReminder: boolean; + desktopPreference: boolean; + emailDigestPreference: boolean; + notificationTransport: "in_app_only"; + evidencePolicy: "preserve_business_evidence"; + auditPolicy: "preserve_audit_state"; + mutedDetail: string; +} + export interface AccountReleaseGateNotificationPreference { event: AccountReleaseNotificationEvent; notificationKey: AccountNotificationKey; surface: "release_growth" | "system_failure"; inAppEnabled: boolean; + reminderIntensity: AccountReminderIntensity; + evidenceDisplay: AccountNotificationEvidenceDisplay; + showProminentReminder: boolean; desktopPreference: boolean; emailDigestPreference: boolean; + notificationTransport: "in_app_only"; title: string; reason: string; } @@ -72,8 +107,12 @@ export interface AccountPostReleaseNotificationPreference { spaceReadable: boolean; inAppEnabled: boolean; showInAppReminder: boolean; + reminderIntensity: AccountReminderIntensity; + evidenceDisplay: AccountNotificationEvidenceDisplay; + showProminentReminder: boolean; desktopPreference: boolean; emailDigestPreference: boolean; + notificationTransport: "in_app_only"; title: string; reason: string; evidencePolicy: "preserve_business_evidence"; @@ -156,6 +195,36 @@ const accountNotificationConsumptionMeta: Record< }, }; +const reminderConsumptionEventMeta: Record< + AccountReminderConsumptionEvent, + { notificationKey: AccountNotificationKey; surface: Exclude; productArea: string; reason: string } +> = { + change_review_high_risk: { + notificationKey: "pendingConfirmation", + surface: "change_review", + productArea: "Change Review", + reason: "高风险 ChangeSet 或 ChangeItem 进入待审时使用待确认偏好控制页面内提醒表达。", + }, + iteration_draft_ready: { + notificationKey: "pendingConfirmation", + surface: "change_review", + productArea: "Change Review", + reason: "下一轮迭代建议草案准备好、需要人工拍板时使用待确认偏好控制页面内提醒表达。", + }, + release_blocked: { + notificationKey: "releaseGate", + surface: "release_growth", + productArea: "Release / Growth", + reason: "发布、合规或增长验证受阻时使用发布门禁偏好控制页面内提醒表达。", + }, + execution_stalled: { + notificationKey: "executionWriteback", + surface: "execution_bridge", + productArea: "Execution Bridge", + reason: "执行链路停滞、长时间无进展或等待运行端处理时使用执行回写偏好控制页面内提醒表达。", + }, +}; + const releaseNotificationEventMeta: Record< AccountReleaseNotificationEvent, { notificationKey: AccountNotificationKey; surface: "release_growth" | "system_failure"; reason: string } @@ -231,10 +300,34 @@ export function createDefaultAccountSettings(seed: AccountSettingsSeed = {}): Ac evidenceDisplay: "inline", }, notifications: { - pendingConfirmation: { enabled: true, desktop: true, emailDigest: false }, - executionWriteback: { enabled: true, desktop: true, emailDigest: false }, - releaseGate: { enabled: true, desktop: true, emailDigest: true }, - failureAlert: { enabled: true, desktop: true, emailDigest: true }, + pendingConfirmation: { + enabled: true, + desktop: true, + emailDigest: false, + reminderIntensity: "normal", + evidenceDisplay: "compact", + }, + executionWriteback: { + enabled: true, + desktop: true, + emailDigest: false, + reminderIntensity: "normal", + evidenceDisplay: "compact", + }, + releaseGate: { + enabled: true, + desktop: true, + emailDigest: true, + reminderIntensity: "normal", + evidenceDisplay: "compact", + }, + failureAlert: { + enabled: true, + desktop: true, + emailDigest: true, + reminderIntensity: "normal", + evidenceDisplay: "compact", + }, }, saveScope: "local", updatedAt: new Date().toISOString(), @@ -280,10 +373,7 @@ function cacheAccountSettingsSnapshot(settings: AccountSettings): void { function mergeAccountSettings(base: AccountSettings, stored: Partial | null): AccountSettings { if (!stored) return base; const mergedNotifications = accountNotificationKeys.reduce((result, key) => { - result[key] = { - ...base.notifications[key], - ...(stored.notifications?.[key] || {}), - }; + result[key] = normalizeNotificationPreference(base.notifications[key], stored.notifications?.[key]); return result; }, {} as AccountNotificationSettings); return { @@ -301,6 +391,49 @@ function mergeAccountSettings(base: AccountSettings, stored: Partial +): AccountNotificationPreference { + const raw = { + ...base, + ...(stored || {}), + }; + const storedIntensity = stored?.reminderIntensity; + const reminderIntensity: AccountReminderIntensity = + storedIntensity === "silent" || storedIntensity === "normal" || storedIntensity === "prominent" + ? storedIntensity + : raw.enabled + ? "normal" + : "silent"; + const evidenceDisplay: AccountNotificationEvidenceDisplay = + raw.evidenceDisplay === "expanded" ? "expanded" : "compact"; + return { + ...raw, + enabled: reminderIntensity !== "silent", + desktop: reminderIntensity !== "silent" && Boolean(raw.desktop), + emailDigest: reminderIntensity !== "silent" && Boolean(raw.emailDigest), + reminderIntensity, + evidenceDisplay, + }; +} + +function getNotificationReminderIntensity(preference: AccountNotificationPreference): AccountReminderIntensity { + return preference.reminderIntensity === "silent" || + preference.reminderIntensity === "normal" || + preference.reminderIntensity === "prominent" + ? preference.reminderIntensity + : preference.enabled + ? "normal" + : "silent"; +} + +function getNotificationEvidenceDisplay( + preference: AccountNotificationPreference +): AccountNotificationEvidenceDisplay { + return preference.evidenceDisplay === "expanded" ? "expanded" : "compact"; +} + export function shouldShowAccountNotification( settings: AccountSettings | Partial | null | undefined, key: AccountNotificationKey @@ -317,7 +450,9 @@ export function getAccountNotificationConsumption( const preference = normalized.notifications[key]; const label = accountNotificationLabels[key]; const meta = accountNotificationConsumptionMeta[key]; - const enabled = Boolean(preference.enabled); + const reminderIntensity = getNotificationReminderIntensity(preference); + const evidenceDisplay = getNotificationEvidenceDisplay(preference); + const enabled = reminderIntensity !== "silent" && Boolean(preference.enabled); return { key, surface: meta.surface, @@ -325,17 +460,56 @@ export function getAccountNotificationConsumption( productArea: meta.productArea, trigger: meta.trigger, enabled, + reminderIntensity, + evidenceDisplay, showInAppReminder: enabled, + showProminentReminder: enabled && reminderIntensity === "prominent", desktopPreference: enabled && Boolean(preference.desktop), emailDigestPreference: enabled && Boolean(preference.emailDigest), - statusLabel: enabled ? "页面内提醒显示" : "页面内提醒静默", + statusLabel: enabled + ? reminderIntensity === "prominent" + ? "页面内强提醒" + : "页面内提醒显示" + : "页面内提醒静默", detail: enabled - ? `${meta.productArea} 会显示该类页面内提醒;桌面/邮件只记录偏好,不代表系统级推送已接入。` + ? `${meta.productArea} 会按 ${reminderIntensity === "prominent" ? "强提醒" : "普通提醒"} 展示页面内提醒,证据以 ${evidenceDisplay === "expanded" ? "展开" : "紧凑"} 模式呈现;桌面/邮件只记录偏好,不代表系统级推送已接入。` : `${meta.productArea} 不显示该类页面内提醒;底层对象、状态和审计记录仍保留。`, }; }); } +export function getAccountReminderConsumption( + settings: AccountSettings | Partial | null | undefined, + event: AccountReminderConsumptionEvent +): AccountReminderConsumptionContract { + const normalized = mergeAccountSettings(createDefaultAccountSettings(), settings || null); + const meta = reminderConsumptionEventMeta[event]; + const preference = normalized.notifications[meta.notificationKey]; + const reminderIntensity = getNotificationReminderIntensity(preference); + const evidenceDisplay = getNotificationEvidenceDisplay(preference); + const enabled = reminderIntensity !== "silent" && Boolean(preference.enabled); + return { + event, + notificationKey: meta.notificationKey, + surface: meta.surface, + productArea: meta.productArea, + title: accountNotificationLabels[meta.notificationKey].title, + reason: meta.reason, + reminderIntensity, + evidenceDisplay, + showInAppReminder: enabled, + showProminentReminder: enabled && reminderIntensity === "prominent", + desktopPreference: enabled && Boolean(preference.desktop), + emailDigestPreference: enabled && Boolean(preference.emailDigest), + notificationTransport: "in_app_only", + evidencePolicy: "preserve_business_evidence", + auditPolicy: "preserve_audit_state", + mutedDetail: enabled + ? "仅影响页面内提醒强度和证据展开方式;不接真实邮件、桌面推送或系统通知中心。" + : "仅静默页面内提醒;真实证据、ChangeSet、recommendation、执行状态和审计状态仍必须按业务页面展示。", + }; +} + export function getReleaseGateNotificationPreference( settings: AccountSettings | Partial | null | undefined, event: AccountReleaseNotificationEvent = "release_gate_blocked" @@ -343,14 +517,20 @@ export function getReleaseGateNotificationPreference( const normalized = mergeAccountSettings(createDefaultAccountSettings(), settings || null); const meta = releaseNotificationEventMeta[event]; const preference = normalized.notifications[meta.notificationKey]; - const enabled = Boolean(preference.enabled); + const reminderIntensity = getNotificationReminderIntensity(preference); + const evidenceDisplay = getNotificationEvidenceDisplay(preference); + const enabled = reminderIntensity !== "silent" && Boolean(preference.enabled); return { event, notificationKey: meta.notificationKey, surface: meta.surface, inAppEnabled: enabled, + reminderIntensity, + evidenceDisplay, + showProminentReminder: enabled && reminderIntensity === "prominent", desktopPreference: enabled && Boolean(preference.desktop), emailDigestPreference: enabled && Boolean(preference.emailDigest), + notificationTransport: "in_app_only", title: accountNotificationLabels[meta.notificationKey].title, reason: meta.reason, }; @@ -363,7 +543,9 @@ export function getPostReleaseNotificationPreference( const normalized = mergeAccountSettings(createDefaultAccountSettings(), settings || null); const meta = postReleaseNotificationEventMeta[event]; const preference = normalized.notifications[meta.notificationKey]; - const enabled = Boolean(preference.enabled); + const reminderIntensity = getNotificationReminderIntensity(preference); + const evidenceDisplay = getNotificationEvidenceDisplay(preference); + const enabled = reminderIntensity !== "silent" && Boolean(preference.enabled); return { event, notificationKey: meta.notificationKey, @@ -371,8 +553,12 @@ export function getPostReleaseNotificationPreference( spaceReadable: true, inAppEnabled: enabled, showInAppReminder: enabled, + reminderIntensity, + evidenceDisplay, + showProminentReminder: enabled && reminderIntensity === "prominent", desktopPreference: enabled && Boolean(preference.desktop), emailDigestPreference: enabled && Boolean(preference.emailDigest), + notificationTransport: "in_app_only", title: accountNotificationLabels[meta.notificationKey].title, reason: meta.reason, evidencePolicy: "preserve_business_evidence", diff --git a/frontend/src/features/account-settings/AccountSettingsPanel.tsx b/frontend/src/features/account-settings/AccountSettingsPanel.tsx index a41f44c3..67527a2e 100644 --- a/frontend/src/features/account-settings/AccountSettingsPanel.tsx +++ b/frontend/src/features/account-settings/AccountSettingsPanel.tsx @@ -25,8 +25,10 @@ import { type AccountAnswerDepth, type AccountEvidenceDisplay, type AccountLanguage, + type AccountNotificationEvidenceDisplay, type AccountNotificationKey, type AccountProfileSettings, + type AccountReminderIntensity, type AccountSettings, type AccountSettingsSaveScope, } from "@/api/accountSettings"; @@ -58,6 +60,17 @@ const languageOptions: Array<{ value: AccountLanguage; title: string; detail: st { value: "en-US", title: "English", detail: "English replies when product context allows it" }, ]; +const reminderIntensityOptions: Array<{ value: AccountReminderIntensity; title: string; detail: string }> = [ + { value: "silent", title: "静默", detail: "不显示页面内提醒,业务对象和证据仍保留" }, + { value: "normal", title: "普通", detail: "在对应页面显示标准提醒" }, + { value: "prominent", title: "强提醒", detail: "用于高风险、阻塞或长时间停滞场景" }, +]; + +const notificationEvidenceOptions: Array<{ value: AccountNotificationEvidenceDisplay; title: string; detail: string }> = [ + { value: "compact", title: "紧凑", detail: "提醒旁只显示关键证据摘要" }, + { value: "expanded", title: "展开", detail: "提醒旁展开来源、状态和审计线索" }, +]; + function getDisplayName(user: ReturnType["user"]) { return user?.name || user?.username || "张子墨"; } @@ -312,7 +325,7 @@ function MobileAccountSettings({ } title="个人资料" detail={`${draft.profile.displayName} · ${draft.profile.roleTitle}`} onClick={() => onOpenSheet("profile")} /> } title="AI 偏好" detail={`${getAnswerDepthTitle(draft.aiPreferences.answerDepth)} · ${getEvidenceTitle(draft.aiPreferences.evidenceDisplay)}`} onClick={() => onOpenSheet("ai")} /> - } title="通知偏好" detail={`${getEnabledNotificationCount(draft)} 类提醒已开启`} onClick={() => onOpenSheet("notifications")} /> + } title="通知偏好" detail={`${getEnabledNotificationCount(draft)} 类提醒已开启 · ${getProminentNotificationCount(draft)} 类强提醒`} onClick={() => onOpenSheet("notifications")} /> @@ -570,6 +583,8 @@ function NotificationFields({ {accountNotificationKeys.map((key) => { const item = notifications[key]; const label = accountNotificationLabels[key]; + const reminderIntensity = item.reminderIntensity || (item.enabled ? "normal" : "silent"); + const evidenceDisplay = item.evidenceDisplay || "compact"; return (
@@ -578,22 +593,48 @@ function NotificationFields({
{label.detail}
onPatchNotification(key, { enabled })} + checked={reminderIntensity !== "silent" && item.enabled} + onCheckedChange={(enabled) => + onPatchNotification(key, { + enabled, + reminderIntensity: enabled ? (reminderIntensity === "silent" ? "normal" : reminderIntensity) : "silent", + }) + } className="mt-1 data-[state=checked]:bg-[#2463EB]" />
+
+ + + onPatchNotification(key, { + reminderIntensity, + enabled: reminderIntensity !== "silent", + }) + } + /> + + + onPatchNotification(key, { evidenceDisplay })} + /> + +
onPatchNotification(key, { desktop })} /> onPatchNotification(key, { emailDigest })} />
@@ -620,7 +661,7 @@ function NotificationConsumptionPreview({
页面内提醒消费说明

- 这些开关只控制 PMFlow 页面里的提醒展示:待确认、执行回写、发布门禁和失败提醒。当前不代表系统级桌面推送、邮件推送或真实通知中心已接入。 + 这些偏好只控制 PMFlow 页面里的提醒强度和证据展开方式:待确认、执行回写、发布门禁和失败提醒。当前不代表系统级桌面推送、邮件推送或真实通知中心已接入。

@@ -631,14 +672,21 @@ function NotificationConsumptionPreview({
{item.productArea}
{item.trigger}
+
+ {getReminderIntensityTitle(item.reminderIntensity || (item.enabled ? "normal" : "silent"))} · {getNotificationEvidenceTitle(item.evidenceDisplay || "compact")} +
- {item.showInAppReminder ? "显示" : "静默"} + {item.showProminentReminder ? "强提醒" : item.showInAppReminder ? "显示" : "静默"}
{item.detail}
@@ -810,6 +858,10 @@ function getEnabledNotificationCount(settings: AccountSettings) { return accountNotificationKeys.filter((key) => settings.notifications[key].enabled).length; } +function getProminentNotificationCount(settings: AccountSettings) { + return accountNotificationKeys.filter((key) => settings.notifications[key].reminderIntensity === "prominent").length; +} + function getAnswerDepthTitle(value: AccountAnswerDepth) { return answerDepthOptions.find((option) => option.value === value)?.title || "标准"; } @@ -817,3 +869,11 @@ function getAnswerDepthTitle(value: AccountAnswerDepth) { function getEvidenceTitle(value: AccountEvidenceDisplay) { return evidenceOptions.find((option) => option.value === value)?.title || "内联"; } + +function getReminderIntensityTitle(value: AccountReminderIntensity) { + return reminderIntensityOptions.find((option) => option.value === value)?.title || "普通"; +} + +function getNotificationEvidenceTitle(value: AccountNotificationEvidenceDisplay) { + return notificationEvidenceOptions.find((option) => option.value === value)?.title || "紧凑"; +} diff --git a/frontend/src/features/account-settings/accountSettings.test.ts b/frontend/src/features/account-settings/accountSettings.test.ts index e2de7d1e..e79b3f2f 100644 --- a/frontend/src/features/account-settings/accountSettings.test.ts +++ b/frontend/src/features/account-settings/accountSettings.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it, beforeEach, afterEach, vi } from "vitest"; import { createDefaultAccountSettings, getAccountNotificationConsumption, + getAccountReminderConsumption, getAccountSettings, getPostReleaseNotificationPreference, getReleaseGateNotificationPreference, @@ -41,6 +42,8 @@ describe("account settings api", () => { expect(settings.notifications.executionWriteback.enabled).toBe(true); expect(settings.notifications.releaseGate.enabled).toBe(true); expect(settings.notifications.failureAlert.enabled).toBe(true); + expect(settings.notifications.pendingConfirmation.reminderIntensity).toBe("normal"); + expect(settings.notifications.pendingConfirmation.evidenceDisplay).toBe("compact"); }); it("persists updated preferences through the api layer", async () => { @@ -51,7 +54,13 @@ describe("account settings api", () => { aiPreferences: { answerDepth: "deep", evidenceDisplay: "full" }, notifications: { ...defaults.notifications, - failureAlert: { enabled: false, desktop: false, emailDigest: false }, + failureAlert: { + ...defaults.notifications.failureAlert, + enabled: false, + desktop: false, + emailDigest: false, + reminderIntensity: "silent" as const, + }, }, }); const restored = await getAccountSettings({ displayName: "陈雨" }); @@ -70,8 +79,21 @@ describe("account settings api", () => { ...defaults, notifications: { ...defaults.notifications, - executionWriteback: { enabled: false, desktop: false, emailDigest: false }, - releaseGate: { enabled: true, desktop: false, emailDigest: true }, + executionWriteback: { + ...defaults.notifications.executionWriteback, + enabled: false, + desktop: false, + emailDigest: false, + reminderIntensity: "silent" as const, + }, + releaseGate: { + ...defaults.notifications.releaseGate, + enabled: true, + desktop: false, + emailDigest: true, + reminderIntensity: "prominent" as const, + evidenceDisplay: "expanded" as const, + }, }, }; @@ -86,16 +108,73 @@ describe("account settings api", () => { expect(execution?.statusLabel).toBe("页面内提醒静默"); expect(release?.productArea).toBe("Release / Growth"); expect(release?.showInAppReminder).toBe(true); + expect(release?.showProminentReminder).toBe(true); + expect(release?.evidenceDisplay).toBe("expanded"); expect(release?.emailDigestPreference).toBe(true); }); + it("returns stable reminder intensity contracts for review, release, and execution events", () => { + const defaults = createDefaultAccountSettings(); + const settings = { + ...defaults, + notifications: { + ...defaults.notifications, + pendingConfirmation: { + ...defaults.notifications.pendingConfirmation, + reminderIntensity: "prominent" as const, + evidenceDisplay: "expanded" as const, + }, + releaseGate: { + ...defaults.notifications.releaseGate, + reminderIntensity: "silent" as const, + enabled: false, + }, + executionWriteback: { + ...defaults.notifications.executionWriteback, + reminderIntensity: "normal" as const, + evidenceDisplay: "compact" as const, + }, + }, + }; + + const highRiskReview = getAccountReminderConsumption(settings, "change_review_high_risk"); + const iterationDraft = getAccountReminderConsumption(settings, "iteration_draft_ready"); + const releaseBlocked = getAccountReminderConsumption(settings, "release_blocked"); + const executionStalled = getAccountReminderConsumption(settings, "execution_stalled"); + + expect(highRiskReview.notificationKey).toBe("pendingConfirmation"); + expect(highRiskReview.surface).toBe("change_review"); + expect(highRiskReview.reminderIntensity).toBe("prominent"); + expect(highRiskReview.evidenceDisplay).toBe("expanded"); + expect(highRiskReview.showProminentReminder).toBe(true); + expect(highRiskReview.notificationTransport).toBe("in_app_only"); + expect(highRiskReview.evidencePolicy).toBe("preserve_business_evidence"); + expect(highRiskReview.auditPolicy).toBe("preserve_audit_state"); + expect(iterationDraft.notificationKey).toBe("pendingConfirmation"); + expect(iterationDraft.showInAppReminder).toBe(true); + expect(releaseBlocked.notificationKey).toBe("releaseGate"); + expect(releaseBlocked.surface).toBe("release_growth"); + expect(releaseBlocked.showInAppReminder).toBe(false); + expect(releaseBlocked.mutedDetail).toContain("真实证据"); + expect(executionStalled.notificationKey).toBe("executionWriteback"); + expect(executionStalled.surface).toBe("execution_bridge"); + expect(executionStalled.showInAppReminder).toBe(true); + expect(executionStalled.showProminentReminder).toBe(false); + }); + it("maps release gate and release validation events to the releaseGate preference", () => { const defaults = createDefaultAccountSettings(); const settings = { ...defaults, notifications: { ...defaults.notifications, - releaseGate: { enabled: true, desktop: false, emailDigest: true }, + releaseGate: { + ...defaults.notifications.releaseGate, + enabled: true, + desktop: false, + emailDigest: true, + reminderIntensity: "normal" as const, + }, }, }; @@ -119,7 +198,13 @@ describe("account settings api", () => { ...defaults, notifications: { ...defaults.notifications, - failureAlert: { enabled: false, desktop: true, emailDigest: true }, + failureAlert: { + ...defaults.notifications.failureAlert, + enabled: false, + desktop: true, + emailDigest: true, + reminderIntensity: "silent" as const, + }, }, }; @@ -139,8 +224,20 @@ describe("account settings api", () => { ...defaults, notifications: { ...defaults.notifications, - releaseGate: { enabled: true, desktop: false, emailDigest: true }, - pendingConfirmation: { enabled: false, desktop: true, emailDigest: true }, + releaseGate: { + ...defaults.notifications.releaseGate, + enabled: true, + desktop: false, + emailDigest: true, + reminderIntensity: "normal" as const, + }, + pendingConfirmation: { + ...defaults.notifications.pendingConfirmation, + enabled: false, + desktop: true, + emailDigest: true, + reminderIntensity: "silent" as const, + }, }, }; @@ -191,7 +288,9 @@ describe("account settings api", () => { expect(restored.notifications.pendingConfirmation.desktop).toBe(true); expect(restored.notifications.failureAlert.enabled).toBe(false); - expect(restored.notifications.failureAlert.desktop).toBe(true); + expect(restored.notifications.failureAlert.desktop).toBe(false); + expect(restored.notifications.failureAlert.reminderIntensity).toBe("silent"); + expect(restored.notifications.failureAlert.evidenceDisplay).toBe("compact"); expect(shouldShowAccountNotification(restored, "failureAlert")).toBe(false); expect(consumption.find((item) => item.key === "failureAlert")?.detail).toContain("仍保留"); });