From 22c5f6bbdba24ddc30112ea23b4d26be719453dc Mon Sep 17 00:00:00 2001 From: geasd <15225349+gexs04812@user.noreply.gitee.com> Date: Mon, 10 Aug 2026 11:08:36 +0800 Subject: [PATCH 1/4] feat(reminder): add local mock adapters Add deterministic mock adapters for reminder infrastructure and local data. Related to #177 --- .../data/local/MockLocalScheduleReader.ts | 22 +++++ .../data/local/MockReminderApplication.ts | 98 +++++++++++++++++++ .../data/local/MockReminderDispositionSync.ts | 15 +++ .../data/local/MockReminderStateStore.ts | 27 +++++ .../src/features/reminder/data/local/index.ts | 5 + .../data/local/mockReminderSchedules.ts | 73 ++++++++++++++ frontend/src/features/reminder/index.ts | 71 ++++++++++++++ .../presentation/MockReminderPresenter.ts | 26 +++++ .../presentation/ReminderViewModel.ts | 14 +++ .../features/reminder/presentation/index.ts | 2 + .../infrastructure/audio/MockAudioPlayback.ts | 32 ++++++ frontend/src/infrastructure/audio/index.ts | 1 + .../location/LocationProvider.ts | 6 ++ .../location/MockLocationMonitor.ts | 57 +++++++++++ frontend/src/infrastructure/location/index.ts | 2 + .../notifications/MockAlarmScheduler.ts | 28 ++++++ .../notifications/MockDeviceCapability.ts | 39 ++++++++ .../notifications/MockNotificationChannels.ts | 39 ++++++++ .../notifications/MockReminderDelivery.ts | 26 +++++ .../notifications/MockReminderRecovery.ts | 15 +++ .../src/infrastructure/notifications/index.ts | 5 + frontend/src/shared/time/MockClock.ts | 12 +++ frontend/src/shared/time/MockTimeListener.ts | 20 ++++ frontend/src/shared/time/index.ts | 3 + 24 files changed, 638 insertions(+) create mode 100644 frontend/src/features/reminder/data/local/MockLocalScheduleReader.ts create mode 100644 frontend/src/features/reminder/data/local/MockReminderApplication.ts create mode 100644 frontend/src/features/reminder/data/local/MockReminderDispositionSync.ts create mode 100644 frontend/src/features/reminder/data/local/MockReminderStateStore.ts create mode 100644 frontend/src/features/reminder/data/local/index.ts create mode 100644 frontend/src/features/reminder/data/local/mockReminderSchedules.ts create mode 100644 frontend/src/features/reminder/index.ts create mode 100644 frontend/src/features/reminder/presentation/MockReminderPresenter.ts create mode 100644 frontend/src/features/reminder/presentation/ReminderViewModel.ts create mode 100644 frontend/src/features/reminder/presentation/index.ts create mode 100644 frontend/src/infrastructure/audio/MockAudioPlayback.ts create mode 100644 frontend/src/infrastructure/audio/index.ts create mode 100644 frontend/src/infrastructure/location/LocationProvider.ts create mode 100644 frontend/src/infrastructure/location/MockLocationMonitor.ts create mode 100644 frontend/src/infrastructure/location/index.ts create mode 100644 frontend/src/infrastructure/notifications/MockAlarmScheduler.ts create mode 100644 frontend/src/infrastructure/notifications/MockDeviceCapability.ts create mode 100644 frontend/src/infrastructure/notifications/MockNotificationChannels.ts create mode 100644 frontend/src/infrastructure/notifications/MockReminderDelivery.ts create mode 100644 frontend/src/infrastructure/notifications/MockReminderRecovery.ts create mode 100644 frontend/src/infrastructure/notifications/index.ts create mode 100644 frontend/src/shared/time/MockClock.ts create mode 100644 frontend/src/shared/time/MockTimeListener.ts create mode 100644 frontend/src/shared/time/index.ts diff --git a/frontend/src/features/reminder/data/local/MockLocalScheduleReader.ts b/frontend/src/features/reminder/data/local/MockLocalScheduleReader.ts new file mode 100644 index 0000000..3826d52 --- /dev/null +++ b/frontend/src/features/reminder/data/local/MockLocalScheduleReader.ts @@ -0,0 +1,22 @@ +import type { LocalScheduleReader } from '../../application/interfaces'; +import type { LocalReminderSchedule } from '../../domain'; + +import { MOCK_REMINDER_SCHEDULES } from './mockReminderSchedules'; + +/** 代替本地数据库端口的固定只读适配器。 */ +export class MockLocalScheduleReader implements LocalScheduleReader { + readonly schedules = MOCK_REMINDER_SCHEDULES; + + async listReminderSchedules(): Promise { + return this.schedules; + } + + async getReminderSchedule(scheduleId: string): Promise { + return this.schedules.find((schedule) => schedule.id === scheduleId) ?? null; + } + + subscribe(listener: (schedules: readonly LocalReminderSchedule[]) => void): () => void { + listener(this.schedules); + return () => undefined; + } +} diff --git a/frontend/src/features/reminder/data/local/MockReminderApplication.ts b/frontend/src/features/reminder/data/local/MockReminderApplication.ts new file mode 100644 index 0000000..dbc05c6 --- /dev/null +++ b/frontend/src/features/reminder/data/local/MockReminderApplication.ts @@ -0,0 +1,98 @@ +import type { + ReminderApplicationDependencies, + ReminderApplicationPort, + ReminderApplicationResult, + ReminderSnoozeRequest, +} from '../../application/interfaces'; +import type { + LocalReminderSchedule, + LocationSample, + ReminderDeliveryReceipt, + ReminderRegistration, + ReminderTrigger, +} from '../../domain'; + +const MOCK_REGISTRATION: ReminderRegistration = { + schedule_id: 'mock-schedule-time-001', + time_listener_id: 'mock-time-listener-001', + location_listener_id: null, + alarm_id: 'mock-alarm-001', +}; + +const MOCK_LOCATION_REGISTRATION: ReminderRegistration = { + schedule_id: 'mock-schedule-location-001', + time_listener_id: null, + location_listener_id: 'mock-location-listener-001', + alarm_id: null, +}; + +const MOCK_DELIVERY: ReminderDeliveryReceipt = { + delivery_id: 'mock-delivery-001', + schedule_id: 'mock-schedule-time-001', + delivered_at: '2026-08-07T01:00:00.000Z', + channels: ['system_notification', 'popup', 'vibration'], + used_fallback_audio: false, +}; + +/** 真实协调器完成前供应用外壳使用的应用门面。 */ +export class MockReminderApplication implements ReminderApplicationPort { + constructor(readonly dependencies: ReminderApplicationDependencies) {} + + async start(): Promise { + return Promise.resolve(); + } + + async stop(): Promise { + return Promise.resolve(); + } + + async register(schedule: LocalReminderSchedule): Promise { + const fixture = + schedule.schedule_type === 'location' ? MOCK_LOCATION_REGISTRATION : MOCK_REGISTRATION; + return { ...fixture, schedule_id: schedule.id }; + } + + async rebuild(): Promise { + return [MOCK_REGISTRATION, MOCK_LOCATION_REGISTRATION]; + } + + async handleTime(_tick: { observed_at: string }): Promise { + return Promise.resolve(); + } + + async handleLocation(_sample: LocationSample): Promise { + return Promise.resolve(); + } + + async deliver(trigger: ReminderTrigger): Promise { + return { ...MOCK_DELIVERY, schedule_id: trigger.schedule_id }; + } + + async confirm(scheduleId: string, confirmedAt: string): Promise { + return { + accepted: true, + schedule_id: scheduleId, + disposition: { + schedule_id: scheduleId, + state: 'confirmed', + updated_at: confirmedAt, + snoozed_until: null, + sync_status: 'pending', + }, + }; + } + + async snooze(request: ReminderSnoozeRequest): Promise { + return { + accepted: true, + schedule_id: request.schedule_id, + disposition: { + schedule_id: request.schedule_id, + state: 'snoozed', + updated_at: '2026-08-07T01:00:00.000Z', + snoozed_until: request.snooze_until, + sync_status: 'pending', + }, + }; + } +} diff --git a/frontend/src/features/reminder/data/local/MockReminderDispositionSync.ts b/frontend/src/features/reminder/data/local/MockReminderDispositionSync.ts new file mode 100644 index 0000000..f9bd999 --- /dev/null +++ b/frontend/src/features/reminder/data/local/MockReminderDispositionSync.ts @@ -0,0 +1,15 @@ +import type { + ReminderDispositionSyncPort, + ReminderDispositionSyncReceipt, +} from '../../application/interfaces'; +import type { ReminderDisposition } from '../../domain'; + +/** 最终确认状态网络同步回调的模拟实现。 */ +export class MockReminderDispositionSync implements ReminderDispositionSyncPort { + async submitConfirmed(disposition: ReminderDisposition): Promise { + return { + schedule_id: disposition.schedule_id, + accepted: true, + }; + } +} diff --git a/frontend/src/features/reminder/data/local/MockReminderStateStore.ts b/frontend/src/features/reminder/data/local/MockReminderStateStore.ts new file mode 100644 index 0000000..187d26c --- /dev/null +++ b/frontend/src/features/reminder/data/local/MockReminderStateStore.ts @@ -0,0 +1,27 @@ +import type { ReminderStateStore } from '../../application/interfaces'; +import type { ReminderDisposition, ReminderRuntimeState } from '../../domain'; + +const MOCK_STATE: ReminderRuntimeState = { + reminder_disposition_state: null, + next_trigger_at: '2026-08-07T01:00:00.000Z', + snoozed_until: null, + geofence_armed: false, + disposition_updated_at: null, + sync_status: 'synced', + recorded_location: null, +}; + +/** 读取结果固定的本地状态空操作适配器,不打开本地数据库。 */ +export class MockReminderStateStore implements ReminderStateStore { + async read(_scheduleId: string): Promise { + return { ...MOCK_STATE }; + } + + async write(_scheduleId: string, _state: ReminderRuntimeState): Promise { + return Promise.resolve(); + } + + async setDisposition(_scheduleId: string, _disposition: ReminderDisposition): Promise { + return Promise.resolve(); + } +} diff --git a/frontend/src/features/reminder/data/local/index.ts b/frontend/src/features/reminder/data/local/index.ts new file mode 100644 index 0000000..f18ece4 --- /dev/null +++ b/frontend/src/features/reminder/data/local/index.ts @@ -0,0 +1,5 @@ +export { MockLocalScheduleReader } from './MockLocalScheduleReader'; +export { MockReminderApplication } from './MockReminderApplication'; +export { MockReminderDispositionSync } from './MockReminderDispositionSync'; +export { MockReminderStateStore } from './MockReminderStateStore'; +export { MOCK_REMINDER_SCHEDULES } from './mockReminderSchedules'; diff --git a/frontend/src/features/reminder/data/local/mockReminderSchedules.ts b/frontend/src/features/reminder/data/local/mockReminderSchedules.ts new file mode 100644 index 0000000..cc5de05 --- /dev/null +++ b/frontend/src/features/reminder/data/local/mockReminderSchedules.ts @@ -0,0 +1,73 @@ +import type { LocalReminderSchedule } from '../../domain'; + +const MOCK_RUNTIME = { + reminder_disposition_state: null, + next_trigger_at: '2026-08-07T01:00:00.000Z', + snoozed_until: null, + geofence_armed: false, + disposition_updated_at: null, + sync_status: 'synced' as const, + recorded_location: null, +}; + +/** 接入本地数据库读取器前使用的固定快照。 */ +export const MOCK_REMINDER_SCHEDULES: readonly LocalReminderSchedule[] = [ + { + id: 'mock-schedule-time-001', + account_id: 'mock-account-001', + title: '项目例会', + schedule_type: 'time', + schedule_kind: 'once', + is_all_day: false, + start_time: '2026-08-07T01:15:00.000Z', + end_time: null, + timezone: 'Asia/Shanghai', + recurrence_rule: null, + location_name: '203 会议室', + latitude: null, + longitude: null, + geofence_radius_meters: 100, + reminder: { + reminder_type: 'before_start', + reminder_trigger_at: null, + reminder_offset_minutes: 15, + reminder_strength: 'medium', + }, + runtime: { ...MOCK_RUNTIME }, + status: 'active', + revision: 1, + cloud_revision: 1, + updated_at: '2026-08-06T09:00:00.000Z', + }, + { + id: 'mock-schedule-location-001', + account_id: 'mock-account-001', + title: '取车提醒', + schedule_type: 'location', + schedule_kind: 'once', + is_all_day: false, + start_time: null, + end_time: null, + timezone: 'Asia/Shanghai', + recurrence_rule: null, + location_name: '停车场', + latitude: 31.2304, + longitude: 121.4737, + geofence_radius_meters: 100, + reminder: { + reminder_type: 'return_to_recorded_location', + reminder_trigger_at: null, + reminder_offset_minutes: null, + reminder_strength: 'high', + }, + runtime: { + ...MOCK_RUNTIME, + next_trigger_at: null, + recorded_location: { latitude: 31.2304, longitude: 121.4737 }, + }, + status: 'active', + revision: 1, + cloud_revision: 1, + updated_at: '2026-08-06T09:00:00.000Z', + }, +]; diff --git a/frontend/src/features/reminder/index.ts b/frontend/src/features/reminder/index.ts new file mode 100644 index 0000000..95b4ea1 --- /dev/null +++ b/frontend/src/features/reminder/index.ts @@ -0,0 +1,71 @@ +export type { + DeliveryChannel, + GeoPoint, + LocalReminderSchedule, + LocalScheduleKind, + LocalScheduleStatus, + LocalScheduleType, + LocationSample, + ReminderConfiguration, + ReminderDeliveryReceipt, + ReminderDeliveryRequest, + ReminderDisposition, + ReminderDispositionState, + ReminderRegistration, + ReminderRuntimeState, + ReminderStrength, + ReminderSyncStatus, + ReminderTrigger, + ReminderTriggerReason, + ReminderType, +} from './domain'; +export { DEFAULT_SNOOZE_MINUTES } from './domain'; +export type { + AlarmScheduleReceipt, + AlarmScheduleRequest, + AlarmSchedulerPort, + AudioPlaybackPort, + AudioPlaybackReceipt, + AudioPlaybackRequest, + DeviceCapabilityPort, + DeviceCapabilityStatus, + DevicePermission, + LocalScheduleReader, + LocalTimeTick, + LocationMonitorEvent, + LocationMonitorPort, + LocationWatchHandle, + LocationWatchMode, + LocationWatchRequest, + PopupPort, + PopupReceipt, + PopupRequest, + ReminderApplicationDependencies, + ReminderApplicationPort, + ReminderApplicationResult, + ReminderDeliveryPort, + ReminderDispositionSyncPort, + ReminderDispositionSyncReceipt, + ReminderPresentationAction, + ReminderPresentationReceipt, + ReminderPresenterPort, + ReminderRecoveryPort, + ReminderRecoveryReceipt, + ReminderSnoozeRequest, + ReminderStateStore, + SystemNotificationPort, + SystemNotificationReceipt, + SystemNotificationRequest, + TimeListenerHandle, + TimeListenerOptions, + TimeListenerPort, + VibrationPort, +} from './application'; +export { + MockLocalScheduleReader, + MockReminderApplication, + MockReminderDispositionSync, + MockReminderStateStore, + MOCK_REMINDER_SCHEDULES, +} from './data/local'; +export { MockReminderPresenter } from './presentation'; diff --git a/frontend/src/features/reminder/presentation/MockReminderPresenter.ts b/frontend/src/features/reminder/presentation/MockReminderPresenter.ts new file mode 100644 index 0000000..34bc0fc --- /dev/null +++ b/frontend/src/features/reminder/presentation/MockReminderPresenter.ts @@ -0,0 +1,26 @@ +import type { + ReminderPresenterPort, + ReminderPresentationAction, + ReminderPresentationReceipt, +} from '../application/interfaces'; +import type { ReminderDeliveryRequest } from '../domain'; + +/** 应用级弹窗/悬浮层完成前使用的固定展示器。 */ +export class MockReminderPresenter implements ReminderPresenterPort { + async show(_request: ReminderDeliveryRequest): Promise { + return { + presentation_id: 'mock-presentation-001', + visible: true, + }; + } + + async hide(_scheduleId: string): Promise { + return Promise.resolve(); + } + + onAction( + _listener: (event: { schedule_id: string; action: ReminderPresentationAction }) => void, + ): () => void { + return () => undefined; + } +} diff --git a/frontend/src/features/reminder/presentation/ReminderViewModel.ts b/frontend/src/features/reminder/presentation/ReminderViewModel.ts new file mode 100644 index 0000000..c634d78 --- /dev/null +++ b/frontend/src/features/reminder/presentation/ReminderViewModel.ts @@ -0,0 +1,14 @@ +import type { ReminderPresentationAction } from '../application/interfaces'; +import type { ReminderDeliveryRequest } from '../domain'; + +/** 仅供视图使用的模型;界面悬浮层无需了解端口即可消费。 */ +export type ReminderViewModel = { + schedule_id: string; + title: string; + strength: ReminderDeliveryRequest['strength']; + trigger_reason: ReminderDeliveryRequest['trigger']['reason']; + visible: boolean; + actions: readonly ReminderPresentationAction[]; +}; + +export type ReminderActionHandler = (action: ReminderPresentationAction) => void; diff --git a/frontend/src/features/reminder/presentation/index.ts b/frontend/src/features/reminder/presentation/index.ts new file mode 100644 index 0000000..721b892 --- /dev/null +++ b/frontend/src/features/reminder/presentation/index.ts @@ -0,0 +1,2 @@ +export { MockReminderPresenter } from './MockReminderPresenter'; +export type { ReminderActionHandler, ReminderViewModel } from './ReminderViewModel'; diff --git a/frontend/src/infrastructure/audio/MockAudioPlayback.ts b/frontend/src/infrastructure/audio/MockAudioPlayback.ts new file mode 100644 index 0000000..f59131b --- /dev/null +++ b/frontend/src/infrastructure/audio/MockAudioPlayback.ts @@ -0,0 +1,32 @@ +import type { + AudioPlaybackPort, + AudioPlaybackReceipt, + AudioPlaybackRequest, +} from '../../features/reminder/application/interfaces'; + +/** 固定音频能力:远程语音合成不可用,本地兜底音效可用。 */ +export class MockAudioPlayback implements AudioPlaybackPort { + async isTtsAvailable(): Promise { + return false; + } + + async playTts(_request: AudioPlaybackRequest): Promise { + return { + playback_id: 'mock-tts-playback-001', + played: false, + used_local_fallback: false, + }; + } + + async playLocalFallback(_request: AudioPlaybackRequest): Promise { + return { + playback_id: 'mock-local-sound-001', + played: true, + used_local_fallback: true, + }; + } + + async stop(_scheduleId: string): Promise { + return Promise.resolve(); + } +} diff --git a/frontend/src/infrastructure/audio/index.ts b/frontend/src/infrastructure/audio/index.ts new file mode 100644 index 0000000..8cc9460 --- /dev/null +++ b/frontend/src/infrastructure/audio/index.ts @@ -0,0 +1 @@ +export { MockAudioPlayback } from './MockAudioPlayback'; diff --git a/frontend/src/infrastructure/location/LocationProvider.ts b/frontend/src/infrastructure/location/LocationProvider.ts new file mode 100644 index 0000000..8493370 --- /dev/null +++ b/frontend/src/infrastructure/location/LocationProvider.ts @@ -0,0 +1,6 @@ +import type { LocationSample } from '../../features/reminder/domain'; + +/** 获取一次当前位置样本的平台适配器。 */ +export interface LocationProvider { + getCurrentSample(): Promise; +} diff --git a/frontend/src/infrastructure/location/MockLocationMonitor.ts b/frontend/src/infrastructure/location/MockLocationMonitor.ts new file mode 100644 index 0000000..b5fb4e6 --- /dev/null +++ b/frontend/src/infrastructure/location/MockLocationMonitor.ts @@ -0,0 +1,57 @@ +import type { + LocationMonitorEvent, + LocationMonitorPort, + LocationWatchHandle, + LocationWatchRequest, +} from '../../features/reminder/application/interfaces'; +import type { LocalReminderSchedule, LocationSample } from '../../features/reminder/domain'; + +import type { LocationProvider } from './LocationProvider'; + +const MOCK_SAMPLE: LocationSample = { + latitude: 31.2304, + longitude: 121.4737, + accuracy_meters: 12, + observed_at: '2026-08-07T01:00:00.000Z', +}; + +function isLocationSchedule(schedule: LocalReminderSchedule): boolean { + return schedule.schedule_type === 'location' && schedule.status === 'active'; +} + +/** 固定定位能力适配器,不访问平台定位接口。 */ +export class MockLocationMonitor implements LocationMonitorPort, LocationProvider { + async watch( + request: LocationWatchRequest, + _listener: (event: LocationMonitorEvent) => void, + ): Promise { + return { + listener_id: `mock-location-listener-${request.schedule_id}`, + schedule_id: request.schedule_id, + }; + } + + async unwatch(_listenerId: string): Promise { + return Promise.resolve(); + } + + async rebuild( + schedules: readonly LocalReminderSchedule[], + _listener: (event: LocationMonitorEvent) => void, + ): Promise { + return schedules.filter(isLocationSchedule).map((schedule) => ({ + listener_id: `mock-location-listener-${schedule.id}`, + schedule_id: schedule.id, + })); + } + + async getLastSample(): Promise { + return { ...MOCK_SAMPLE }; + } + + async getCurrentSample(): Promise { + return { ...MOCK_SAMPLE }; + } +} + +export { MOCK_SAMPLE as MOCK_LOCATION_SAMPLE }; diff --git a/frontend/src/infrastructure/location/index.ts b/frontend/src/infrastructure/location/index.ts new file mode 100644 index 0000000..ef2d2b9 --- /dev/null +++ b/frontend/src/infrastructure/location/index.ts @@ -0,0 +1,2 @@ +export { MockLocationMonitor, MOCK_LOCATION_SAMPLE } from './MockLocationMonitor'; +export type { LocationProvider } from './LocationProvider'; diff --git a/frontend/src/infrastructure/notifications/MockAlarmScheduler.ts b/frontend/src/infrastructure/notifications/MockAlarmScheduler.ts new file mode 100644 index 0000000..847b38e --- /dev/null +++ b/frontend/src/infrastructure/notifications/MockAlarmScheduler.ts @@ -0,0 +1,28 @@ +import type { + AlarmScheduleReceipt, + AlarmScheduleRequest, + AlarmSchedulerPort, +} from '../../features/reminder/application/interfaces'; + +/** 固定闹钟适配器,后续可替换为原生精确闹钟接口。 */ +export class MockAlarmScheduler implements AlarmSchedulerPort { + async schedule(request: AlarmScheduleRequest): Promise { + return { + alarm_id: `mock-alarm-${request.schedule_id}`, + schedule_id: request.schedule_id, + }; + } + + async cancel(_alarmId: string | null): Promise<{ cancelled: boolean }> { + return { cancelled: true }; + } + + async rebuild( + requests: readonly AlarmScheduleRequest[], + ): Promise { + return requests.map((request) => ({ + alarm_id: `mock-alarm-${request.schedule_id}`, + schedule_id: request.schedule_id, + })); + } +} diff --git a/frontend/src/infrastructure/notifications/MockDeviceCapability.ts b/frontend/src/infrastructure/notifications/MockDeviceCapability.ts new file mode 100644 index 0000000..42ea2f3 --- /dev/null +++ b/frontend/src/infrastructure/notifications/MockDeviceCapability.ts @@ -0,0 +1,39 @@ +import type { + DeviceCapabilityPort, + DeviceCapabilityStatus, + DevicePermission, +} from '../../features/reminder/application/interfaces'; + +const MOCK_PERMISSIONS: Readonly> = { + notifications: true, + exact_alarm: true, + overlay: false, + full_screen: true, + battery_optimization: false, + location_foreground: true, + location_background: false, +}; + +const MOCK_STATUS: DeviceCapabilityStatus = { + platform: 'android', + supported: true, + permissions: MOCK_PERMISSIONS, + background_execution: false, +}; + +/** 原生模块接入前使用的固定设备能力状态。 */ +export class MockDeviceCapability implements DeviceCapabilityPort { + async getStatus(): Promise { + return { ...MOCK_STATUS, permissions: { ...MOCK_STATUS.permissions } }; + } + + async requestPermission(_permission: DevicePermission): Promise { + return true; + } + + async openSettings(_permission: DevicePermission): Promise { + return true; + } +} + +export { MOCK_STATUS as MOCK_DEVICE_CAPABILITY_STATUS }; diff --git a/frontend/src/infrastructure/notifications/MockNotificationChannels.ts b/frontend/src/infrastructure/notifications/MockNotificationChannels.ts new file mode 100644 index 0000000..d42c7a9 --- /dev/null +++ b/frontend/src/infrastructure/notifications/MockNotificationChannels.ts @@ -0,0 +1,39 @@ +import type { + PopupPort, + PopupReceipt, + PopupRequest, + SystemNotificationPort, + SystemNotificationReceipt, + SystemNotificationRequest, + VibrationPort, +} from '../../features/reminder/application/interfaces'; + +export class MockSystemNotification implements SystemNotificationPort { + async show(request: SystemNotificationRequest): Promise { + return { notification_id: request.notification_id, shown: true }; + } + + async cancel(_notificationId: string): Promise { + return Promise.resolve(); + } +} + +export class MockPopup implements PopupPort { + async show(request: PopupRequest): Promise { + return { popup_id: request.popup_id, visible: true }; + } + + async dismiss(_popupId: string): Promise { + return Promise.resolve(); + } +} + +export class MockVibration implements VibrationPort { + async vibrate(): Promise { + return Promise.resolve(); + } + + async stop(): Promise { + return Promise.resolve(); + } +} diff --git a/frontend/src/infrastructure/notifications/MockReminderDelivery.ts b/frontend/src/infrastructure/notifications/MockReminderDelivery.ts new file mode 100644 index 0000000..f811570 --- /dev/null +++ b/frontend/src/infrastructure/notifications/MockReminderDelivery.ts @@ -0,0 +1,26 @@ +import type { ReminderDeliveryPort } from '../../features/reminder/application/interfaces'; +import type { + ReminderDeliveryReceipt, + ReminderDeliveryRequest, +} from '../../features/reminder/domain'; + +const MOCK_RECEIPT: ReminderDeliveryReceipt = { + delivery_id: 'mock-delivery-001', + schedule_id: 'mock-schedule-time-001', + delivered_at: '2026-08-07T01:00:00.000Z', + channels: ['system_notification'], + used_fallback_audio: false, +}; + +/** 系统通知送达使用的固定通知边界。 */ +export class MockReminderDelivery implements ReminderDeliveryPort { + async deliver(request: ReminderDeliveryRequest): Promise { + return { ...MOCK_RECEIPT, schedule_id: request.schedule_id }; + } + + async dismiss(_scheduleId: string): Promise { + return Promise.resolve(); + } +} + +export { MOCK_RECEIPT as MOCK_REMINDER_DELIVERY_RECEIPT }; diff --git a/frontend/src/infrastructure/notifications/MockReminderRecovery.ts b/frontend/src/infrastructure/notifications/MockReminderRecovery.ts new file mode 100644 index 0000000..34be1b0 --- /dev/null +++ b/frontend/src/infrastructure/notifications/MockReminderRecovery.ts @@ -0,0 +1,15 @@ +import type { + ReminderRecoveryPort, + ReminderRecoveryReceipt, +} from '../../features/reminder/application/interfaces'; + +/** 固定重启恢复适配器,后续可替换为安卓启动接收器。 */ +export class MockReminderRecovery implements ReminderRecoveryPort { + async registerForRestart(): Promise { + return { registered: true, recovery_id: 'mock-recovery-001' }; + } + + async restoreAfterRestart(): Promise { + return { registered: true, recovery_id: 'mock-recovery-001' }; + } +} diff --git a/frontend/src/infrastructure/notifications/index.ts b/frontend/src/infrastructure/notifications/index.ts new file mode 100644 index 0000000..8eaf098 --- /dev/null +++ b/frontend/src/infrastructure/notifications/index.ts @@ -0,0 +1,5 @@ +export { MockAlarmScheduler } from './MockAlarmScheduler'; +export { MockPopup, MockSystemNotification, MockVibration } from './MockNotificationChannels'; +export { MockReminderRecovery } from './MockReminderRecovery'; +export { MockReminderDelivery, MOCK_REMINDER_DELIVERY_RECEIPT } from './MockReminderDelivery'; +export { MockDeviceCapability, MOCK_DEVICE_CAPABILITY_STATUS } from './MockDeviceCapability'; diff --git a/frontend/src/shared/time/MockClock.ts b/frontend/src/shared/time/MockClock.ts new file mode 100644 index 0000000..edc7ff1 --- /dev/null +++ b/frontend/src/shared/time/MockClock.ts @@ -0,0 +1,12 @@ +/** 用于确定性测试和模拟组合的最小时钟端口。 */ +export interface Clock { + nowIso(): string; +} + +export class MockClock implements Clock { + constructor(private readonly value = '2026-08-07T01:00:00.000Z') {} + + nowIso(): string { + return this.value; + } +} diff --git a/frontend/src/shared/time/MockTimeListener.ts b/frontend/src/shared/time/MockTimeListener.ts new file mode 100644 index 0000000..e77e36e --- /dev/null +++ b/frontend/src/shared/time/MockTimeListener.ts @@ -0,0 +1,20 @@ +import type { + LocalTimeTick, + TimeListenerHandle, + TimeListenerOptions, + TimeListenerPort, +} from '../../features/reminder/application/interfaces'; + +/** 固定时间监听边界,替换前不会发出平台事件。 */ +export class MockTimeListener implements TimeListenerPort { + async start( + _listener: (tick: LocalTimeTick) => void, + _options?: TimeListenerOptions, + ): Promise { + return { listener_id: 'mock-time-listener-001' }; + } + + async stop(_listenerId: string): Promise { + return Promise.resolve(); + } +} diff --git a/frontend/src/shared/time/index.ts b/frontend/src/shared/time/index.ts new file mode 100644 index 0000000..1924ce7 --- /dev/null +++ b/frontend/src/shared/time/index.ts @@ -0,0 +1,3 @@ +export { MockClock } from './MockClock'; +export type { Clock } from './MockClock'; +export { MockTimeListener } from './MockTimeListener'; From 13f27539a22ae5fa481df6234fe19fbcbda523f6 Mon Sep 17 00:00:00 2001 From: mac Date: Tue, 11 Aug 2026 16:13:08 +0800 Subject: [PATCH 2/4] fix(reminder): align local mocks with confirmed sync and snooze union Adapt mock adapters to ReminderSnoozeRequest exclusivity and ReminderConfirmedDisposition after merging application ports from main. --- .../features/reminder/data/local/MockReminderApplication.ts | 6 +++++- .../reminder/data/local/MockReminderDispositionSync.ts | 6 ++++-- frontend/src/features/reminder/index.ts | 1 + 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/frontend/src/features/reminder/data/local/MockReminderApplication.ts b/frontend/src/features/reminder/data/local/MockReminderApplication.ts index dbc05c6..6998784 100644 --- a/frontend/src/features/reminder/data/local/MockReminderApplication.ts +++ b/frontend/src/features/reminder/data/local/MockReminderApplication.ts @@ -83,6 +83,10 @@ export class MockReminderApplication implements ReminderApplicationPort { } async snooze(request: ReminderSnoozeRequest): Promise { + // 确定性 mock:绝对时间原样回传;相对分钟映射到固定 fixture 时刻。 + const snoozed_until = + 'snooze_minutes' in request ? '2026-08-07T01:10:00.000Z' : request.snooze_until; + return { accepted: true, schedule_id: request.schedule_id, @@ -90,7 +94,7 @@ export class MockReminderApplication implements ReminderApplicationPort { schedule_id: request.schedule_id, state: 'snoozed', updated_at: '2026-08-07T01:00:00.000Z', - snoozed_until: request.snooze_until, + snoozed_until, sync_status: 'pending', }, }; diff --git a/frontend/src/features/reminder/data/local/MockReminderDispositionSync.ts b/frontend/src/features/reminder/data/local/MockReminderDispositionSync.ts index f9bd999..5a7ef1c 100644 --- a/frontend/src/features/reminder/data/local/MockReminderDispositionSync.ts +++ b/frontend/src/features/reminder/data/local/MockReminderDispositionSync.ts @@ -1,12 +1,14 @@ import type { + ReminderConfirmedDisposition, ReminderDispositionSyncPort, ReminderDispositionSyncReceipt, } from '../../application/interfaces'; -import type { ReminderDisposition } from '../../domain'; /** 最终确认状态网络同步回调的模拟实现。 */ export class MockReminderDispositionSync implements ReminderDispositionSyncPort { - async submitConfirmed(disposition: ReminderDisposition): Promise { + async submitConfirmed( + disposition: ReminderConfirmedDisposition, + ): Promise { return { schedule_id: disposition.schedule_id, accepted: true, diff --git a/frontend/src/features/reminder/index.ts b/frontend/src/features/reminder/index.ts index 95b4ea1..60caa6c 100644 --- a/frontend/src/features/reminder/index.ts +++ b/frontend/src/features/reminder/index.ts @@ -43,6 +43,7 @@ export type { ReminderApplicationDependencies, ReminderApplicationPort, ReminderApplicationResult, + ReminderConfirmedDisposition, ReminderDeliveryPort, ReminderDispositionSyncPort, ReminderDispositionSyncReceipt, From 92f8f1f20eb09d13838aed01bf7cd510f4552787 Mon Sep 17 00:00:00 2001 From: mac Date: Tue, 11 Aug 2026 16:20:16 +0800 Subject: [PATCH 3/4] fix(reminder): derive mock registration IDs from schedule.id Avoid reusing fixed alarm/location listener handles so each registered schedule can be cancelled or unwatched independently. --- .../data/local/MockReminderApplication.ts | 39 ++++++++++--------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/frontend/src/features/reminder/data/local/MockReminderApplication.ts b/frontend/src/features/reminder/data/local/MockReminderApplication.ts index 6998784..bc717c7 100644 --- a/frontend/src/features/reminder/data/local/MockReminderApplication.ts +++ b/frontend/src/features/reminder/data/local/MockReminderApplication.ts @@ -12,20 +12,6 @@ import type { ReminderTrigger, } from '../../domain'; -const MOCK_REGISTRATION: ReminderRegistration = { - schedule_id: 'mock-schedule-time-001', - time_listener_id: 'mock-time-listener-001', - location_listener_id: null, - alarm_id: 'mock-alarm-001', -}; - -const MOCK_LOCATION_REGISTRATION: ReminderRegistration = { - schedule_id: 'mock-schedule-location-001', - time_listener_id: null, - location_listener_id: 'mock-location-listener-001', - alarm_id: null, -}; - const MOCK_DELIVERY: ReminderDeliveryReceipt = { delivery_id: 'mock-delivery-001', schedule_id: 'mock-schedule-time-001', @@ -34,6 +20,24 @@ const MOCK_DELIVERY: ReminderDeliveryReceipt = { used_fallback_audio: false, }; +function registrationFor(schedule: LocalReminderSchedule): ReminderRegistration { + if (schedule.schedule_type === 'location') { + return { + schedule_id: schedule.id, + time_listener_id: null, + location_listener_id: `mock-location-listener-${schedule.id}`, + alarm_id: null, + }; + } + + return { + schedule_id: schedule.id, + time_listener_id: `mock-time-listener-${schedule.id}`, + location_listener_id: null, + alarm_id: `mock-alarm-${schedule.id}`, + }; +} + /** 真实协调器完成前供应用外壳使用的应用门面。 */ export class MockReminderApplication implements ReminderApplicationPort { constructor(readonly dependencies: ReminderApplicationDependencies) {} @@ -47,13 +51,12 @@ export class MockReminderApplication implements ReminderApplicationPort { } async register(schedule: LocalReminderSchedule): Promise { - const fixture = - schedule.schedule_type === 'location' ? MOCK_LOCATION_REGISTRATION : MOCK_REGISTRATION; - return { ...fixture, schedule_id: schedule.id }; + return registrationFor(schedule); } async rebuild(): Promise { - return [MOCK_REGISTRATION, MOCK_LOCATION_REGISTRATION]; + const schedules = await this.dependencies.schedules.listReminderSchedules(); + return schedules.map(registrationFor); } async handleTime(_tick: { observed_at: string }): Promise { From 952b9ec3411a549f2a4971ed0e1e6e6f36d8447c Mon Sep 17 00:00:00 2001 From: mac Date: Tue, 11 Aug 2026 16:21:34 +0800 Subject: [PATCH 4/4] fix(reminder): align mock permission grant with fixed status Return each permission's fixed MockDeviceCapability value from requestPermission so request-then-refresh matches getStatus(). --- .../src/infrastructure/notifications/MockDeviceCapability.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend/src/infrastructure/notifications/MockDeviceCapability.ts b/frontend/src/infrastructure/notifications/MockDeviceCapability.ts index 42ea2f3..cda5590 100644 --- a/frontend/src/infrastructure/notifications/MockDeviceCapability.ts +++ b/frontend/src/infrastructure/notifications/MockDeviceCapability.ts @@ -27,8 +27,9 @@ export class MockDeviceCapability implements DeviceCapabilityPort { return { ...MOCK_STATUS, permissions: { ...MOCK_STATUS.permissions } }; } - async requestPermission(_permission: DevicePermission): Promise { - return true; + async requestPermission(permission: DevicePermission): Promise { + // 与 getStatus() 保持一致:返回该权限的固定值,不假装 grant 成功。 + return MOCK_PERMISSIONS[permission]; } async openSettings(_permission: DevicePermission): Promise {