mirror of
https://github.com/whekin/household-bot.git
synced 2026-03-31 10:24:02 +00:00
fix(bot): refine reminder scheduling and topic routing
This commit is contained in:
@@ -11,6 +11,8 @@ describe('parseAdHocNotificationSchedule', () => {
|
|||||||
resolvedLocalDate: '2026-03-24',
|
resolvedLocalDate: '2026-03-24',
|
||||||
resolvedHour: 15,
|
resolvedHour: 15,
|
||||||
resolvedMinute: 30,
|
resolvedMinute: 30,
|
||||||
|
relativeOffsetMinutes: null,
|
||||||
|
dateReferenceMode: 'relative',
|
||||||
resolutionMode: 'exact',
|
resolutionMode: 'exact',
|
||||||
now: Temporal.Instant.from('2026-03-23T09:00:00Z')
|
now: Temporal.Instant.from('2026-03-23T09:00:00Z')
|
||||||
})
|
})
|
||||||
@@ -26,6 +28,8 @@ describe('parseAdHocNotificationSchedule', () => {
|
|||||||
resolvedLocalDate: '2026-03-24',
|
resolvedLocalDate: '2026-03-24',
|
||||||
resolvedHour: 12,
|
resolvedHour: 12,
|
||||||
resolvedMinute: 0,
|
resolvedMinute: 0,
|
||||||
|
relativeOffsetMinutes: null,
|
||||||
|
dateReferenceMode: 'calendar',
|
||||||
resolutionMode: 'date_only',
|
resolutionMode: 'date_only',
|
||||||
now: Temporal.Instant.from('2026-03-23T09:00:00Z')
|
now: Temporal.Instant.from('2026-03-23T09:00:00Z')
|
||||||
})
|
})
|
||||||
@@ -41,6 +45,8 @@ describe('parseAdHocNotificationSchedule', () => {
|
|||||||
resolvedLocalDate: '2026-03-24',
|
resolvedLocalDate: '2026-03-24',
|
||||||
resolvedHour: 9,
|
resolvedHour: 9,
|
||||||
resolvedMinute: 0,
|
resolvedMinute: 0,
|
||||||
|
relativeOffsetMinutes: null,
|
||||||
|
dateReferenceMode: 'relative',
|
||||||
resolutionMode: 'fuzzy_window',
|
resolutionMode: 'fuzzy_window',
|
||||||
now: Temporal.Instant.from('2026-03-23T09:00:00Z')
|
now: Temporal.Instant.from('2026-03-23T09:00:00Z')
|
||||||
})
|
})
|
||||||
@@ -56,6 +62,8 @@ describe('parseAdHocNotificationSchedule', () => {
|
|||||||
resolvedLocalDate: null,
|
resolvedLocalDate: null,
|
||||||
resolvedHour: null,
|
resolvedHour: null,
|
||||||
resolvedMinute: null,
|
resolvedMinute: null,
|
||||||
|
relativeOffsetMinutes: null,
|
||||||
|
dateReferenceMode: null,
|
||||||
resolutionMode: 'ambiguous',
|
resolutionMode: 'ambiguous',
|
||||||
now: Temporal.Instant.from('2026-03-23T09:00:00Z')
|
now: Temporal.Instant.from('2026-03-23T09:00:00Z')
|
||||||
})
|
})
|
||||||
@@ -69,10 +77,45 @@ describe('parseAdHocNotificationSchedule', () => {
|
|||||||
resolvedLocalDate: '2026-03-23',
|
resolvedLocalDate: '2026-03-23',
|
||||||
resolvedHour: 10,
|
resolvedHour: 10,
|
||||||
resolvedMinute: 0,
|
resolvedMinute: 0,
|
||||||
|
relativeOffsetMinutes: null,
|
||||||
|
dateReferenceMode: 'calendar',
|
||||||
resolutionMode: 'exact',
|
resolutionMode: 'exact',
|
||||||
now: Temporal.Instant.from('2026-03-23T09:00:00Z')
|
now: Temporal.Instant.from('2026-03-23T09:00:00Z')
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(parsed.kind).toBe('invalid_past')
|
expect(parsed.kind).toBe('invalid_past')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('supports relative offsets like in 30 minutes', () => {
|
||||||
|
const parsed = parseAdHocNotificationSchedule({
|
||||||
|
timezone: 'Asia/Tbilisi',
|
||||||
|
resolvedLocalDate: null,
|
||||||
|
resolvedHour: null,
|
||||||
|
resolvedMinute: null,
|
||||||
|
relativeOffsetMinutes: 30,
|
||||||
|
dateReferenceMode: null,
|
||||||
|
resolutionMode: 'exact',
|
||||||
|
now: Temporal.Instant.from('2026-03-24T08:00:00Z')
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(parsed.kind).toBe('parsed')
|
||||||
|
expect(parsed.timePrecision).toBe('exact')
|
||||||
|
expect(parsed.scheduledFor?.toString()).toBe('2026-03-24T08:30:00Z')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('reinterprets pre-dawn relative tomorrow as the upcoming same-calendar day', () => {
|
||||||
|
const parsed = parseAdHocNotificationSchedule({
|
||||||
|
timezone: 'Asia/Tbilisi',
|
||||||
|
resolvedLocalDate: '2026-03-25',
|
||||||
|
resolvedHour: 9,
|
||||||
|
resolvedMinute: 0,
|
||||||
|
relativeOffsetMinutes: null,
|
||||||
|
dateReferenceMode: 'relative',
|
||||||
|
resolutionMode: 'fuzzy_window',
|
||||||
|
now: Temporal.Instant.from('2026-03-24T00:39:00Z')
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(parsed.kind).toBe('parsed')
|
||||||
|
expect(parsed.scheduledFor?.toString()).toBe('2026-03-24T05:00:00Z')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -27,12 +27,14 @@ export function parseAdHocNotificationSchedule(input: {
|
|||||||
resolvedLocalDate: string | null
|
resolvedLocalDate: string | null
|
||||||
resolvedHour: number | null
|
resolvedHour: number | null
|
||||||
resolvedMinute: number | null
|
resolvedMinute: number | null
|
||||||
|
relativeOffsetMinutes?: number | null
|
||||||
|
dateReferenceMode?: 'relative' | 'calendar' | null
|
||||||
resolutionMode: AdHocNotificationResolutionMode | null
|
resolutionMode: AdHocNotificationResolutionMode | null
|
||||||
now?: Instant
|
now?: Instant
|
||||||
}): ParsedAdHocNotificationSchedule {
|
}): ParsedAdHocNotificationSchedule {
|
||||||
|
const effectiveNow = input.now ?? nowInstant()
|
||||||
const timePrecision = precisionFromResolutionMode(input.resolutionMode)
|
const timePrecision = precisionFromResolutionMode(input.resolutionMode)
|
||||||
if (
|
if (
|
||||||
!input.resolvedLocalDate ||
|
|
||||||
input.resolutionMode === null ||
|
input.resolutionMode === null ||
|
||||||
input.resolutionMode === 'ambiguous' ||
|
input.resolutionMode === 'ambiguous' ||
|
||||||
timePrecision === null
|
timePrecision === null
|
||||||
@@ -44,6 +46,31 @@ export function parseAdHocNotificationSchedule(input: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (input.relativeOffsetMinutes !== null && input.relativeOffsetMinutes !== undefined) {
|
||||||
|
const scheduled = effectiveNow.add({ minutes: input.relativeOffsetMinutes })
|
||||||
|
if (scheduled.epochMilliseconds <= effectiveNow.epochMilliseconds) {
|
||||||
|
return {
|
||||||
|
kind: 'invalid_past',
|
||||||
|
scheduledFor: null,
|
||||||
|
timePrecision: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
kind: 'parsed',
|
||||||
|
scheduledFor: scheduled,
|
||||||
|
timePrecision
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!input.resolvedLocalDate) {
|
||||||
|
return {
|
||||||
|
kind: 'missing_schedule',
|
||||||
|
scheduledFor: null,
|
||||||
|
timePrecision: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const hour =
|
const hour =
|
||||||
input.resolutionMode === 'date_only' ? (input.resolvedHour ?? 12) : input.resolvedHour
|
input.resolutionMode === 'date_only' ? (input.resolvedHour ?? 12) : input.resolvedHour
|
||||||
const minute =
|
const minute =
|
||||||
@@ -58,7 +85,17 @@ export function parseAdHocNotificationSchedule(input: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const date = Temporal.PlainDate.from(input.resolvedLocalDate)
|
const nowZdt = effectiveNow.toZonedDateTimeISO(input.timezone)
|
||||||
|
let date = Temporal.PlainDate.from(input.resolvedLocalDate)
|
||||||
|
|
||||||
|
if (
|
||||||
|
input.dateReferenceMode === 'relative' &&
|
||||||
|
nowZdt.hour <= 4 &&
|
||||||
|
Temporal.PlainDate.compare(date, nowZdt.toPlainDate().add({ days: 1 })) === 0
|
||||||
|
) {
|
||||||
|
date = nowZdt.toPlainDate()
|
||||||
|
}
|
||||||
|
|
||||||
const scheduled = Temporal.ZonedDateTime.from({
|
const scheduled = Temporal.ZonedDateTime.from({
|
||||||
timeZone: input.timezone,
|
timeZone: input.timezone,
|
||||||
year: date.year,
|
year: date.year,
|
||||||
@@ -70,7 +107,6 @@ export function parseAdHocNotificationSchedule(input: {
|
|||||||
millisecond: 0
|
millisecond: 0
|
||||||
}).toInstant()
|
}).toInstant()
|
||||||
|
|
||||||
const effectiveNow = input.now ?? nowInstant()
|
|
||||||
if (scheduled.epochMilliseconds <= effectiveNow.epochMilliseconds) {
|
if (scheduled.epochMilliseconds <= effectiveNow.epochMilliseconds) {
|
||||||
return {
|
return {
|
||||||
kind: 'invalid_past',
|
kind: 'invalid_past',
|
||||||
|
|||||||
@@ -16,34 +16,38 @@ import { formatReminderWhen, registerAdHocNotifications } from './ad-hoc-notific
|
|||||||
import type { AdHocNotificationInterpreter } from './openai-ad-hoc-notification-interpreter'
|
import type { AdHocNotificationInterpreter } from './openai-ad-hoc-notification-interpreter'
|
||||||
|
|
||||||
function createPromptRepository(): TelegramPendingActionRepository {
|
function createPromptRepository(): TelegramPendingActionRepository {
|
||||||
let pending: TelegramPendingActionRecord | null = null
|
const pending = new Map<string, TelegramPendingActionRecord>()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
async upsertPendingAction(input) {
|
async upsertPendingAction(input) {
|
||||||
pending = input
|
pending.set(`${input.telegramChatId}:${input.telegramUserId}`, input)
|
||||||
return input
|
return input
|
||||||
},
|
},
|
||||||
async getPendingAction() {
|
async getPendingAction(telegramChatId, telegramUserId) {
|
||||||
return pending
|
return pending.get(`${telegramChatId}:${telegramUserId}`) ?? null
|
||||||
},
|
},
|
||||||
async clearPendingAction() {
|
async clearPendingAction(telegramChatId, telegramUserId) {
|
||||||
pending = null
|
pending.delete(`${telegramChatId}:${telegramUserId}`)
|
||||||
},
|
},
|
||||||
async clearPendingActionsForChat(telegramChatId, action) {
|
async clearPendingActionsForChat(telegramChatId, action) {
|
||||||
if (!pending || pending.telegramChatId !== telegramChatId) {
|
for (const [key, value] of pending.entries()) {
|
||||||
return
|
if (value.telegramChatId !== telegramChatId) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (action && value.action !== action) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
pending.delete(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (action && pending.action !== action) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
pending = null
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function reminderMessageUpdate(text: string, threadId = 777) {
|
function reminderMessageUpdate(
|
||||||
|
text: string,
|
||||||
|
threadId = 777,
|
||||||
|
from: { id: number; firstName: string } = { id: 10002, firstName: 'Dima' }
|
||||||
|
) {
|
||||||
return {
|
return {
|
||||||
update_id: 4001,
|
update_id: 4001,
|
||||||
message: {
|
message: {
|
||||||
@@ -56,9 +60,9 @@ function reminderMessageUpdate(text: string, threadId = 777) {
|
|||||||
type: 'supergroup'
|
type: 'supergroup'
|
||||||
},
|
},
|
||||||
from: {
|
from: {
|
||||||
id: 10002,
|
id: from.id,
|
||||||
is_bot: false,
|
is_bot: false,
|
||||||
first_name: 'Dima'
|
first_name: from.firstName
|
||||||
},
|
},
|
||||||
text
|
text
|
||||||
}
|
}
|
||||||
@@ -110,6 +114,7 @@ function member(
|
|||||||
function createHouseholdRepository() {
|
function createHouseholdRepository() {
|
||||||
const members = [
|
const members = [
|
||||||
member({ id: 'dima', telegramUserId: '10002', displayName: 'Дима' }),
|
member({ id: 'dima', telegramUserId: '10002', displayName: 'Дима' }),
|
||||||
|
member({ id: 'stas', telegramUserId: '10003', displayName: 'Стас' }),
|
||||||
member({ id: 'georgiy', displayName: 'Георгий' })
|
member({ id: 'georgiy', displayName: 'Георгий' })
|
||||||
]
|
]
|
||||||
const settings: HouseholdBillingSettingsRecord = {
|
const settings: HouseholdBillingSettingsRecord = {
|
||||||
@@ -238,6 +243,8 @@ describe('registerAdHocNotifications', () => {
|
|||||||
resolvedLocalDate: tomorrow,
|
resolvedLocalDate: tomorrow,
|
||||||
resolvedHour: 9,
|
resolvedHour: 9,
|
||||||
resolvedMinute: 0,
|
resolvedMinute: 0,
|
||||||
|
relativeOffsetMinutes: null,
|
||||||
|
dateReferenceMode: 'relative',
|
||||||
resolutionMode: 'fuzzy_window',
|
resolutionMode: 'fuzzy_window',
|
||||||
clarificationQuestion: null,
|
clarificationQuestion: null,
|
||||||
confidence: 90,
|
confidence: 90,
|
||||||
@@ -250,6 +257,8 @@ describe('registerAdHocNotifications', () => {
|
|||||||
resolvedLocalDate: tomorrow,
|
resolvedLocalDate: tomorrow,
|
||||||
resolvedHour: 9,
|
resolvedHour: 9,
|
||||||
resolvedMinute: 0,
|
resolvedMinute: 0,
|
||||||
|
relativeOffsetMinutes: null,
|
||||||
|
dateReferenceMode: 'relative',
|
||||||
resolutionMode: 'fuzzy_window',
|
resolutionMode: 'fuzzy_window',
|
||||||
clarificationQuestion: null,
|
clarificationQuestion: null,
|
||||||
confidence: 90,
|
confidence: 90,
|
||||||
@@ -267,6 +276,8 @@ describe('registerAdHocNotifications', () => {
|
|||||||
resolvedLocalDate: null,
|
resolvedLocalDate: null,
|
||||||
resolvedHour: null,
|
resolvedHour: null,
|
||||||
resolvedMinute: null,
|
resolvedMinute: null,
|
||||||
|
relativeOffsetMinutes: null,
|
||||||
|
dateReferenceMode: null,
|
||||||
resolutionMode: null,
|
resolutionMode: null,
|
||||||
deliveryMode: null,
|
deliveryMode: null,
|
||||||
dmRecipientMemberIds: null,
|
dmRecipientMemberIds: null,
|
||||||
@@ -284,6 +295,8 @@ describe('registerAdHocNotifications', () => {
|
|||||||
resolvedLocalDate: tomorrow,
|
resolvedLocalDate: tomorrow,
|
||||||
resolvedHour: 10,
|
resolvedHour: 10,
|
||||||
resolvedMinute: 0,
|
resolvedMinute: 0,
|
||||||
|
relativeOffsetMinutes: null,
|
||||||
|
dateReferenceMode: 'relative',
|
||||||
resolutionMode: 'exact',
|
resolutionMode: 'exact',
|
||||||
deliveryMode: null,
|
deliveryMode: null,
|
||||||
dmRecipientMemberIds: null,
|
dmRecipientMemberIds: null,
|
||||||
@@ -573,6 +586,8 @@ describe('registerAdHocNotifications', () => {
|
|||||||
resolvedLocalDate: tomorrow,
|
resolvedLocalDate: tomorrow,
|
||||||
resolvedHour: 9,
|
resolvedHour: 9,
|
||||||
resolvedMinute: 0,
|
resolvedMinute: 0,
|
||||||
|
relativeOffsetMinutes: null,
|
||||||
|
dateReferenceMode: 'relative',
|
||||||
resolutionMode: 'fuzzy_window',
|
resolutionMode: 'fuzzy_window',
|
||||||
clarificationQuestion: null,
|
clarificationQuestion: null,
|
||||||
confidence: 90,
|
confidence: 90,
|
||||||
@@ -610,6 +625,148 @@ describe('registerAdHocNotifications', () => {
|
|||||||
expect(expandedPayload.reply_markup?.inline_keyboard[0]?.[2]?.text).toBe('Скрыть')
|
expect(expandedPayload.reply_markup?.inline_keyboard[0]?.[2]?.text).toBe('Скрыть')
|
||||||
expect(expandedPayload.reply_markup?.inline_keyboard[1]?.[0]?.text).toContain('В топик')
|
expect(expandedPayload.reply_markup?.inline_keyboard[1]?.[0]?.text).toContain('В топик')
|
||||||
})
|
})
|
||||||
|
test('supports relative duration reminders like in 30 minutes', async () => {
|
||||||
|
const bot = createTelegramBot('000000:test-token')
|
||||||
|
const calls: Array<{ method: string; payload: unknown }> = []
|
||||||
|
const scheduledRequests: Array<{ scheduledFor: string }> = []
|
||||||
|
const now = Temporal.Instant.from('2026-03-24T08:00:00Z')
|
||||||
|
const promptRepository = createPromptRepository()
|
||||||
|
|
||||||
|
bot.botInfo = {
|
||||||
|
id: 999000,
|
||||||
|
is_bot: true,
|
||||||
|
first_name: 'Household Test Bot',
|
||||||
|
username: 'household_test_bot',
|
||||||
|
can_join_groups: true,
|
||||||
|
can_read_all_group_messages: false,
|
||||||
|
supports_inline_queries: false,
|
||||||
|
can_connect_to_business: false,
|
||||||
|
has_main_web_app: false,
|
||||||
|
has_topics_enabled: true,
|
||||||
|
allows_users_to_create_topics: false
|
||||||
|
}
|
||||||
|
|
||||||
|
bot.api.config.use(async (_prev, method, payload) => {
|
||||||
|
calls.push({ method, payload })
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
result: {
|
||||||
|
message_id: calls.length,
|
||||||
|
date: Math.floor(Date.now() / 1000),
|
||||||
|
chat: {
|
||||||
|
id: -10012345,
|
||||||
|
type: 'supergroup'
|
||||||
|
},
|
||||||
|
text: 'ok'
|
||||||
|
}
|
||||||
|
} as never
|
||||||
|
})
|
||||||
|
|
||||||
|
const originalNow = Temporal.Now.instant
|
||||||
|
Temporal.Now.instant = () => now
|
||||||
|
|
||||||
|
try {
|
||||||
|
registerAdHocNotifications({
|
||||||
|
bot,
|
||||||
|
householdConfigurationRepository: createHouseholdRepository() as never,
|
||||||
|
promptRepository,
|
||||||
|
notificationService: {
|
||||||
|
async scheduleNotification(input) {
|
||||||
|
scheduledRequests.push({
|
||||||
|
scheduledFor: input.scheduledFor.toString()
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
status: 'scheduled',
|
||||||
|
notification: {
|
||||||
|
id: 'notif-1',
|
||||||
|
householdId: input.householdId,
|
||||||
|
creatorMemberId: input.creatorMemberId,
|
||||||
|
assigneeMemberId: input.assigneeMemberId ?? null,
|
||||||
|
originalRequestText: input.originalRequestText,
|
||||||
|
notificationText: input.notificationText,
|
||||||
|
timezone: input.timezone,
|
||||||
|
scheduledFor: input.scheduledFor,
|
||||||
|
timePrecision: input.timePrecision,
|
||||||
|
deliveryMode: input.deliveryMode,
|
||||||
|
dmRecipientMemberIds: input.dmRecipientMemberIds ?? [],
|
||||||
|
friendlyTagAssignee: false,
|
||||||
|
status: 'scheduled',
|
||||||
|
sourceTelegramChatId: input.sourceTelegramChatId ?? null,
|
||||||
|
sourceTelegramThreadId: input.sourceTelegramThreadId ?? null,
|
||||||
|
sentAt: null,
|
||||||
|
cancelledAt: null,
|
||||||
|
cancelledByMemberId: null,
|
||||||
|
createdAt: Temporal.Instant.from('2026-03-23T09:00:00Z'),
|
||||||
|
updatedAt: Temporal.Instant.from('2026-03-23T09:00:00Z')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async listUpcomingNotifications() {
|
||||||
|
return []
|
||||||
|
},
|
||||||
|
async cancelNotification() {
|
||||||
|
return { status: 'not_found' }
|
||||||
|
},
|
||||||
|
async updateNotification() {
|
||||||
|
return { status: 'not_found' }
|
||||||
|
},
|
||||||
|
async listDueNotifications() {
|
||||||
|
return []
|
||||||
|
},
|
||||||
|
async claimDueNotification() {
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
async releaseDueNotification() {},
|
||||||
|
async markNotificationSent() {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
reminderInterpreter: {
|
||||||
|
async interpretRequest() {
|
||||||
|
return {
|
||||||
|
decision: 'notification',
|
||||||
|
notificationText: 'починить тебя',
|
||||||
|
assigneeMemberId: null,
|
||||||
|
resolvedLocalDate: null,
|
||||||
|
resolvedHour: null,
|
||||||
|
resolvedMinute: null,
|
||||||
|
relativeOffsetMinutes: 30,
|
||||||
|
dateReferenceMode: null,
|
||||||
|
resolutionMode: 'exact',
|
||||||
|
clarificationQuestion: null,
|
||||||
|
confidence: 94,
|
||||||
|
parserMode: 'llm'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async interpretSchedule() {
|
||||||
|
throw new Error('not used')
|
||||||
|
},
|
||||||
|
async interpretDraftEdit() {
|
||||||
|
throw new Error('not used')
|
||||||
|
},
|
||||||
|
async renderDeliveryText() {
|
||||||
|
return 'Пора чинить бота.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
await bot.handleUpdate(reminderMessageUpdate('Напомни починить тебя через 30 минут') as never)
|
||||||
|
const pending = await promptRepository.getPendingAction('-10012345', '10002')
|
||||||
|
const proposalId = (pending?.payload as { proposalId?: string } | null)?.proposalId
|
||||||
|
expect(proposalId).toBeTruthy()
|
||||||
|
|
||||||
|
await bot.handleUpdate(reminderCallbackUpdate(`adhocnotif:confirm:${proposalId}`) as never)
|
||||||
|
} finally {
|
||||||
|
Temporal.Now.instant = originalNow
|
||||||
|
}
|
||||||
|
|
||||||
|
expect((calls[0]?.payload as { text?: string })?.text).toContain('сегодня в 12:30')
|
||||||
|
expect(scheduledRequests).toEqual([
|
||||||
|
{
|
||||||
|
scheduledFor: '2026-03-24T08:30:00Z'
|
||||||
|
}
|
||||||
|
])
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('formatReminderWhen', () => {
|
describe('formatReminderWhen', () => {
|
||||||
|
|||||||
@@ -775,6 +775,8 @@ export function registerAdHocNotifications(options: {
|
|||||||
resolvedLocalDate: interpretedSchedule.resolvedLocalDate,
|
resolvedLocalDate: interpretedSchedule.resolvedLocalDate,
|
||||||
resolvedHour: interpretedSchedule.resolvedHour,
|
resolvedHour: interpretedSchedule.resolvedHour,
|
||||||
resolvedMinute: interpretedSchedule.resolvedMinute,
|
resolvedMinute: interpretedSchedule.resolvedMinute,
|
||||||
|
relativeOffsetMinutes: interpretedSchedule.relativeOffsetMinutes,
|
||||||
|
dateReferenceMode: interpretedSchedule.dateReferenceMode,
|
||||||
resolutionMode: interpretedSchedule.resolutionMode
|
resolutionMode: interpretedSchedule.resolutionMode
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -822,139 +824,148 @@ export function registerAdHocNotifications(options: {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!options.reminderInterpreter) {
|
if (existingDraft.stage === 'confirm') {
|
||||||
await replyInTopic(ctx, unavailableReply(reminderContext.locale))
|
if (!options.reminderInterpreter) {
|
||||||
return
|
await replyInTopic(ctx, unavailableReply(reminderContext.locale))
|
||||||
}
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const currentSchedule = draftLocalSchedule(existingDraft)
|
const currentSchedule = draftLocalSchedule(existingDraft)
|
||||||
const interpretedEdit = await options.reminderInterpreter.interpretDraftEdit({
|
const interpretedEdit = await options.reminderInterpreter.interpretDraftEdit({
|
||||||
locale: reminderContext.locale,
|
locale: reminderContext.locale,
|
||||||
timezone: existingDraft.timezone,
|
|
||||||
localNow: localNowText(existingDraft.timezone),
|
|
||||||
text: messageText,
|
|
||||||
members: interpreterMembers(reminderContext.members),
|
|
||||||
senderMemberId: reminderContext.member.id,
|
|
||||||
currentNotificationText: existingDraft.normalizedNotificationText,
|
|
||||||
currentAssigneeMemberId: existingDraft.assigneeMemberId,
|
|
||||||
currentScheduledLocalDate: currentSchedule.date,
|
|
||||||
currentScheduledHour: currentSchedule.hour,
|
|
||||||
currentScheduledMinute: currentSchedule.minute,
|
|
||||||
currentDeliveryMode: existingDraft.deliveryMode,
|
|
||||||
currentDmRecipientMemberIds: existingDraft.dmRecipientMemberIds,
|
|
||||||
assistantContext: reminderContext.assistantContext,
|
|
||||||
assistantTone: reminderContext.assistantTone
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!interpretedEdit) {
|
|
||||||
await replyInTopic(ctx, unavailableReply(reminderContext.locale))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (interpretedEdit.decision === 'clarification') {
|
|
||||||
await replyInTopic(
|
|
||||||
ctx,
|
|
||||||
interpretedEdit.clarificationQuestion ??
|
|
||||||
(reminderContext.locale === 'ru'
|
|
||||||
? 'Что именно поправить в напоминании?'
|
|
||||||
: 'What should I adjust in the reminder?')
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (interpretedEdit.decision === 'cancel') {
|
|
||||||
await options.promptRepository.clearPendingAction(
|
|
||||||
ctx.chat!.id.toString(),
|
|
||||||
ctx.from!.id.toString()
|
|
||||||
)
|
|
||||||
await replyInTopic(ctx, cancelledDraftReply(reminderContext.locale))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const scheduleChanged =
|
|
||||||
interpretedEdit.resolvedLocalDate !== null ||
|
|
||||||
interpretedEdit.resolvedHour !== null ||
|
|
||||||
interpretedEdit.resolvedMinute !== null ||
|
|
||||||
interpretedEdit.resolutionMode !== null
|
|
||||||
|
|
||||||
let nextSchedule = {
|
|
||||||
scheduledForIso: existingDraft.scheduledForIso,
|
|
||||||
timePrecision: existingDraft.timePrecision
|
|
||||||
}
|
|
||||||
|
|
||||||
if (scheduleChanged) {
|
|
||||||
const parsedSchedule = parseAdHocNotificationSchedule({
|
|
||||||
timezone: existingDraft.timezone,
|
timezone: existingDraft.timezone,
|
||||||
resolvedLocalDate: interpretedEdit.resolvedLocalDate ?? currentSchedule.date,
|
localNow: localNowText(existingDraft.timezone),
|
||||||
resolvedHour: interpretedEdit.resolvedHour ?? currentSchedule.hour,
|
text: messageText,
|
||||||
resolvedMinute: interpretedEdit.resolvedMinute ?? currentSchedule.minute,
|
members: interpreterMembers(reminderContext.members),
|
||||||
resolutionMode: interpretedEdit.resolutionMode ?? 'exact'
|
senderMemberId: reminderContext.member.id,
|
||||||
|
currentNotificationText: existingDraft.normalizedNotificationText,
|
||||||
|
currentAssigneeMemberId: existingDraft.assigneeMemberId,
|
||||||
|
currentScheduledLocalDate: currentSchedule.date,
|
||||||
|
currentScheduledHour: currentSchedule.hour,
|
||||||
|
currentScheduledMinute: currentSchedule.minute,
|
||||||
|
currentDeliveryMode: existingDraft.deliveryMode,
|
||||||
|
currentDmRecipientMemberIds: existingDraft.dmRecipientMemberIds,
|
||||||
|
assistantContext: reminderContext.assistantContext,
|
||||||
|
assistantTone: reminderContext.assistantTone
|
||||||
})
|
})
|
||||||
|
|
||||||
if (parsedSchedule.kind === 'missing_schedule') {
|
if (!interpretedEdit) {
|
||||||
|
await replyInTopic(ctx, unavailableReply(reminderContext.locale))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (interpretedEdit.decision === 'clarification') {
|
||||||
await replyInTopic(
|
await replyInTopic(
|
||||||
ctx,
|
ctx,
|
||||||
reminderContext.locale === 'ru'
|
interpretedEdit.clarificationQuestion ??
|
||||||
? 'Нужны понятные дата или время, чтобы обновить напоминание.'
|
(reminderContext.locale === 'ru'
|
||||||
: 'I need a clear date or time to update the reminder.'
|
? 'Что именно поправить в напоминании?'
|
||||||
|
: 'What should I adjust in the reminder?')
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parsedSchedule.kind === 'invalid_past') {
|
if (interpretedEdit.decision === 'cancel') {
|
||||||
await replyInTopic(
|
await options.promptRepository.clearPendingAction(
|
||||||
ctx,
|
ctx.chat!.id.toString(),
|
||||||
reminderContext.locale === 'ru'
|
ctx.from!.id.toString()
|
||||||
? 'Это время уже в прошлом. Пришлите будущую дату или время.'
|
|
||||||
: 'That time is already in the past. Send a future date or time.'
|
|
||||||
)
|
)
|
||||||
|
await replyInTopic(ctx, cancelledDraftReply(reminderContext.locale))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
nextSchedule = {
|
const scheduleChanged =
|
||||||
scheduledForIso: parsedSchedule.scheduledFor!.toString(),
|
interpretedEdit.resolvedLocalDate !== null ||
|
||||||
timePrecision: parsedSchedule.timePrecision!
|
interpretedEdit.resolvedHour !== null ||
|
||||||
|
interpretedEdit.resolvedMinute !== null ||
|
||||||
|
interpretedEdit.relativeOffsetMinutes !== null ||
|
||||||
|
interpretedEdit.resolutionMode !== null
|
||||||
|
|
||||||
|
let nextSchedule = {
|
||||||
|
scheduledForIso: existingDraft.scheduledForIso,
|
||||||
|
timePrecision: existingDraft.timePrecision
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const nextNormalizedNotificationText =
|
if (scheduleChanged) {
|
||||||
interpretedEdit.notificationText ?? existingDraft.normalizedNotificationText
|
const parsedSchedule = parseAdHocNotificationSchedule({
|
||||||
const nextOriginalRequestText =
|
timezone: existingDraft.timezone,
|
||||||
interpretedEdit.notificationText !== null ? messageText : existingDraft.originalRequestText
|
resolvedLocalDate: interpretedEdit.resolvedLocalDate ?? currentSchedule.date,
|
||||||
const nextAssigneeMemberId = interpretedEdit.assigneeChanged
|
resolvedHour: interpretedEdit.resolvedHour ?? currentSchedule.hour,
|
||||||
? interpretedEdit.assigneeMemberId
|
resolvedMinute: interpretedEdit.resolvedMinute ?? currentSchedule.minute,
|
||||||
: existingDraft.assigneeMemberId
|
relativeOffsetMinutes: interpretedEdit.relativeOffsetMinutes,
|
||||||
const nextDeliveryMode = interpretedEdit.deliveryMode ?? existingDraft.deliveryMode
|
dateReferenceMode: interpretedEdit.dateReferenceMode,
|
||||||
const nextDmRecipientMemberIds =
|
resolutionMode: interpretedEdit.resolutionMode ?? 'exact'
|
||||||
interpretedEdit.dmRecipientMemberIds ??
|
})
|
||||||
(nextDeliveryMode === existingDraft.deliveryMode ? existingDraft.dmRecipientMemberIds : [])
|
|
||||||
|
|
||||||
const renderedNotificationText = await renderNotificationText({
|
if (parsedSchedule.kind === 'missing_schedule') {
|
||||||
reminderContext,
|
await replyInTopic(
|
||||||
originalRequestText: nextOriginalRequestText,
|
ctx,
|
||||||
normalizedNotificationText: nextNormalizedNotificationText,
|
reminderContext.locale === 'ru'
|
||||||
assigneeMemberId: nextAssigneeMemberId
|
? 'Нужны понятные дата или время, чтобы обновить напоминание.'
|
||||||
})
|
: 'I need a clear date or time to update the reminder.'
|
||||||
if (!renderedNotificationText) {
|
)
|
||||||
await replyInTopic(ctx, unavailableReply(reminderContext.locale))
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsedSchedule.kind === 'invalid_past') {
|
||||||
|
await replyInTopic(
|
||||||
|
ctx,
|
||||||
|
reminderContext.locale === 'ru'
|
||||||
|
? 'Это время уже в прошлом. Пришлите будущую дату или время.'
|
||||||
|
: 'That time is already in the past. Send a future date or time.'
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
nextSchedule = {
|
||||||
|
scheduledForIso: parsedSchedule.scheduledFor!.toString(),
|
||||||
|
timePrecision: parsedSchedule.timePrecision!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextNormalizedNotificationText =
|
||||||
|
interpretedEdit.notificationText ?? existingDraft.normalizedNotificationText
|
||||||
|
const nextOriginalRequestText =
|
||||||
|
interpretedEdit.notificationText !== null
|
||||||
|
? messageText
|
||||||
|
: existingDraft.originalRequestText
|
||||||
|
const nextAssigneeMemberId = interpretedEdit.assigneeChanged
|
||||||
|
? interpretedEdit.assigneeMemberId
|
||||||
|
: existingDraft.assigneeMemberId
|
||||||
|
const nextDeliveryMode = interpretedEdit.deliveryMode ?? existingDraft.deliveryMode
|
||||||
|
const nextDmRecipientMemberIds =
|
||||||
|
interpretedEdit.dmRecipientMemberIds ??
|
||||||
|
(nextDeliveryMode === existingDraft.deliveryMode
|
||||||
|
? existingDraft.dmRecipientMemberIds
|
||||||
|
: [])
|
||||||
|
|
||||||
|
const renderedNotificationText = await renderNotificationText({
|
||||||
|
reminderContext,
|
||||||
|
originalRequestText: nextOriginalRequestText,
|
||||||
|
normalizedNotificationText: nextNormalizedNotificationText,
|
||||||
|
assigneeMemberId: nextAssigneeMemberId
|
||||||
|
})
|
||||||
|
if (!renderedNotificationText) {
|
||||||
|
await replyInTopic(ctx, unavailableReply(reminderContext.locale))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextPayload: Extract<NotificationDraftPayload, { stage: 'confirm' }> = {
|
||||||
|
...existingDraft,
|
||||||
|
originalRequestText: nextOriginalRequestText,
|
||||||
|
normalizedNotificationText: nextNormalizedNotificationText,
|
||||||
|
renderedNotificationText,
|
||||||
|
assigneeMemberId: nextAssigneeMemberId,
|
||||||
|
scheduledForIso: nextSchedule.scheduledForIso,
|
||||||
|
timePrecision: nextSchedule.timePrecision,
|
||||||
|
deliveryMode: nextDeliveryMode,
|
||||||
|
dmRecipientMemberIds: nextDmRecipientMemberIds,
|
||||||
|
viewMode: 'compact'
|
||||||
|
}
|
||||||
|
|
||||||
|
await showDraftConfirmation(ctx, nextPayload, existingDraft.confirmationMessageId)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextPayload: Extract<NotificationDraftPayload, { stage: 'confirm' }> = {
|
|
||||||
...existingDraft,
|
|
||||||
originalRequestText: nextOriginalRequestText,
|
|
||||||
normalizedNotificationText: nextNormalizedNotificationText,
|
|
||||||
renderedNotificationText,
|
|
||||||
assigneeMemberId: nextAssigneeMemberId,
|
|
||||||
scheduledForIso: nextSchedule.scheduledForIso,
|
|
||||||
timePrecision: nextSchedule.timePrecision,
|
|
||||||
deliveryMode: nextDeliveryMode,
|
|
||||||
dmRecipientMemberIds: nextDmRecipientMemberIds,
|
|
||||||
viewMode: 'compact'
|
|
||||||
}
|
|
||||||
|
|
||||||
await showDraftConfirmation(ctx, nextPayload, existingDraft.confirmationMessageId)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!options.reminderInterpreter) {
|
if (!options.reminderInterpreter) {
|
||||||
@@ -1025,6 +1036,8 @@ export function registerAdHocNotifications(options: {
|
|||||||
resolvedLocalDate: interpretedRequest.resolvedLocalDate,
|
resolvedLocalDate: interpretedRequest.resolvedLocalDate,
|
||||||
resolvedHour: interpretedRequest.resolvedHour,
|
resolvedHour: interpretedRequest.resolvedHour,
|
||||||
resolvedMinute: interpretedRequest.resolvedMinute,
|
resolvedMinute: interpretedRequest.resolvedMinute,
|
||||||
|
relativeOffsetMinutes: interpretedRequest.relativeOffsetMinutes,
|
||||||
|
dateReferenceMode: interpretedRequest.dateReferenceMode,
|
||||||
resolutionMode: interpretedRequest.resolutionMode
|
resolutionMode: interpretedRequest.resolutionMode
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -1167,6 +1180,7 @@ export function registerAdHocNotifications(options: {
|
|||||||
const payload = await loadDraft(options.promptRepository, ctx)
|
const payload = await loadDraft(options.promptRepository, ctx)
|
||||||
if (
|
if (
|
||||||
!payload ||
|
!payload ||
|
||||||
|
payload.stage !== 'confirm' ||
|
||||||
payload.proposalId !== proposalId ||
|
payload.proposalId !== proposalId ||
|
||||||
!ctx.chat ||
|
!ctx.chat ||
|
||||||
!ctx.from ||
|
!ctx.from ||
|
||||||
|
|||||||
@@ -47,6 +47,8 @@ describe('createOpenAiAdHocNotificationInterpreter', () => {
|
|||||||
resolvedLocalDate: '2026-03-24',
|
resolvedLocalDate: '2026-03-24',
|
||||||
resolvedHour: 15,
|
resolvedHour: 15,
|
||||||
resolvedMinute: 30,
|
resolvedMinute: 30,
|
||||||
|
relativeOffsetMinutes: null,
|
||||||
|
dateReferenceMode: 'relative',
|
||||||
resolutionMode: 'exact',
|
resolutionMode: 'exact',
|
||||||
confidence: 93,
|
confidence: 93,
|
||||||
clarificationQuestion: null
|
clarificationQuestion: null
|
||||||
@@ -72,6 +74,8 @@ describe('createOpenAiAdHocNotificationInterpreter', () => {
|
|||||||
resolvedLocalDate: '2026-03-24',
|
resolvedLocalDate: '2026-03-24',
|
||||||
resolvedHour: 15,
|
resolvedHour: 15,
|
||||||
resolvedMinute: 30,
|
resolvedMinute: 30,
|
||||||
|
relativeOffsetMinutes: null,
|
||||||
|
dateReferenceMode: 'relative',
|
||||||
resolutionMode: 'exact',
|
resolutionMode: 'exact',
|
||||||
clarificationQuestion: null,
|
clarificationQuestion: null,
|
||||||
confidence: 93,
|
confidence: 93,
|
||||||
@@ -100,6 +104,8 @@ describe('createOpenAiAdHocNotificationInterpreter', () => {
|
|||||||
resolvedLocalDate: '2026-03-24',
|
resolvedLocalDate: '2026-03-24',
|
||||||
resolvedHour: 9,
|
resolvedHour: 9,
|
||||||
resolvedMinute: 0,
|
resolvedMinute: 0,
|
||||||
|
relativeOffsetMinutes: null,
|
||||||
|
dateReferenceMode: 'relative',
|
||||||
resolutionMode: 'fuzzy_window',
|
resolutionMode: 'fuzzy_window',
|
||||||
confidence: 90,
|
confidence: 90,
|
||||||
clarificationQuestion: null
|
clarificationQuestion: null
|
||||||
@@ -141,6 +147,8 @@ describe('createOpenAiAdHocNotificationInterpreter', () => {
|
|||||||
resolvedLocalDate: null,
|
resolvedLocalDate: null,
|
||||||
resolvedHour: null,
|
resolvedHour: null,
|
||||||
resolvedMinute: null,
|
resolvedMinute: null,
|
||||||
|
relativeOffsetMinutes: null,
|
||||||
|
dateReferenceMode: null,
|
||||||
resolutionMode: 'ambiguous',
|
resolutionMode: 'ambiguous',
|
||||||
confidence: 82,
|
confidence: 82,
|
||||||
clarificationQuestion: 'Когда напомнить: завтра утром, днем или вечером?'
|
clarificationQuestion: 'Когда напомнить: завтра утром, днем или вечером?'
|
||||||
@@ -185,6 +193,8 @@ describe('createOpenAiAdHocNotificationInterpreter', () => {
|
|||||||
resolvedLocalDate: null,
|
resolvedLocalDate: null,
|
||||||
resolvedHour: null,
|
resolvedHour: null,
|
||||||
resolvedMinute: null,
|
resolvedMinute: null,
|
||||||
|
relativeOffsetMinutes: null,
|
||||||
|
dateReferenceMode: null,
|
||||||
resolutionMode: null,
|
resolutionMode: null,
|
||||||
confidence: 96,
|
confidence: 96,
|
||||||
clarificationQuestion: null
|
clarificationQuestion: null
|
||||||
@@ -225,6 +235,8 @@ describe('createOpenAiAdHocNotificationInterpreter', () => {
|
|||||||
resolvedLocalDate: '2026-03-24',
|
resolvedLocalDate: '2026-03-24',
|
||||||
resolvedHour: 10,
|
resolvedHour: 10,
|
||||||
resolvedMinute: 0,
|
resolvedMinute: 0,
|
||||||
|
relativeOffsetMinutes: null,
|
||||||
|
dateReferenceMode: 'relative',
|
||||||
resolutionMode: 'exact',
|
resolutionMode: 'exact',
|
||||||
deliveryMode: null,
|
deliveryMode: null,
|
||||||
dmRecipientMemberIds: null,
|
dmRecipientMemberIds: null,
|
||||||
@@ -256,6 +268,8 @@ describe('createOpenAiAdHocNotificationInterpreter', () => {
|
|||||||
decision: 'updated',
|
decision: 'updated',
|
||||||
resolvedHour: 10,
|
resolvedHour: 10,
|
||||||
resolvedMinute: 0,
|
resolvedMinute: 0,
|
||||||
|
relativeOffsetMinutes: null,
|
||||||
|
dateReferenceMode: 'relative',
|
||||||
resolutionMode: 'exact',
|
resolutionMode: 'exact',
|
||||||
notificationText: null,
|
notificationText: null,
|
||||||
deliveryMode: null
|
deliveryMode: null
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ export interface AdHocNotificationInterpretation {
|
|||||||
resolvedLocalDate: string | null
|
resolvedLocalDate: string | null
|
||||||
resolvedHour: number | null
|
resolvedHour: number | null
|
||||||
resolvedMinute: number | null
|
resolvedMinute: number | null
|
||||||
|
relativeOffsetMinutes: number | null
|
||||||
|
dateReferenceMode: 'relative' | 'calendar' | null
|
||||||
resolutionMode: AdHocNotificationResolutionMode | null
|
resolutionMode: AdHocNotificationResolutionMode | null
|
||||||
clarificationQuestion: string | null
|
clarificationQuestion: string | null
|
||||||
confidence: number
|
confidence: number
|
||||||
@@ -28,6 +30,8 @@ export interface AdHocNotificationScheduleInterpretation {
|
|||||||
resolvedLocalDate: string | null
|
resolvedLocalDate: string | null
|
||||||
resolvedHour: number | null
|
resolvedHour: number | null
|
||||||
resolvedMinute: number | null
|
resolvedMinute: number | null
|
||||||
|
relativeOffsetMinutes: number | null
|
||||||
|
dateReferenceMode: 'relative' | 'calendar' | null
|
||||||
resolutionMode: AdHocNotificationResolutionMode | null
|
resolutionMode: AdHocNotificationResolutionMode | null
|
||||||
clarificationQuestion: string | null
|
clarificationQuestion: string | null
|
||||||
confidence: number
|
confidence: number
|
||||||
@@ -42,6 +46,8 @@ export interface AdHocNotificationDraftEditInterpretation {
|
|||||||
resolvedLocalDate: string | null
|
resolvedLocalDate: string | null
|
||||||
resolvedHour: number | null
|
resolvedHour: number | null
|
||||||
resolvedMinute: number | null
|
resolvedMinute: number | null
|
||||||
|
relativeOffsetMinutes: number | null
|
||||||
|
dateReferenceMode: 'relative' | 'calendar' | null
|
||||||
resolutionMode: AdHocNotificationResolutionMode | null
|
resolutionMode: AdHocNotificationResolutionMode | null
|
||||||
deliveryMode: AdHocNotificationDeliveryMode | null
|
deliveryMode: AdHocNotificationDeliveryMode | null
|
||||||
dmRecipientMemberIds: readonly string[] | null
|
dmRecipientMemberIds: readonly string[] | null
|
||||||
@@ -57,6 +63,8 @@ interface ReminderInterpretationResult {
|
|||||||
resolvedLocalDate: string | null
|
resolvedLocalDate: string | null
|
||||||
resolvedHour: number | null
|
resolvedHour: number | null
|
||||||
resolvedMinute: number | null
|
resolvedMinute: number | null
|
||||||
|
relativeOffsetMinutes: number | null
|
||||||
|
dateReferenceMode: 'relative' | 'calendar' | null
|
||||||
resolutionMode: AdHocNotificationResolutionMode | null
|
resolutionMode: AdHocNotificationResolutionMode | null
|
||||||
confidence: number
|
confidence: number
|
||||||
clarificationQuestion: string | null
|
clarificationQuestion: string | null
|
||||||
@@ -67,6 +75,8 @@ interface ReminderScheduleResult {
|
|||||||
resolvedLocalDate: string | null
|
resolvedLocalDate: string | null
|
||||||
resolvedHour: number | null
|
resolvedHour: number | null
|
||||||
resolvedMinute: number | null
|
resolvedMinute: number | null
|
||||||
|
relativeOffsetMinutes: number | null
|
||||||
|
dateReferenceMode: 'relative' | 'calendar' | null
|
||||||
resolutionMode: AdHocNotificationResolutionMode | null
|
resolutionMode: AdHocNotificationResolutionMode | null
|
||||||
confidence: number
|
confidence: number
|
||||||
clarificationQuestion: string | null
|
clarificationQuestion: string | null
|
||||||
@@ -84,6 +94,8 @@ interface ReminderDraftEditResult {
|
|||||||
resolvedLocalDate: string | null
|
resolvedLocalDate: string | null
|
||||||
resolvedHour: number | null
|
resolvedHour: number | null
|
||||||
resolvedMinute: number | null
|
resolvedMinute: number | null
|
||||||
|
relativeOffsetMinutes: number | null
|
||||||
|
dateReferenceMode: 'relative' | 'calendar' | null
|
||||||
resolutionMode: AdHocNotificationResolutionMode | null
|
resolutionMode: AdHocNotificationResolutionMode | null
|
||||||
deliveryMode: AdHocNotificationDeliveryMode | null
|
deliveryMode: AdHocNotificationDeliveryMode | null
|
||||||
dmRecipientMemberIds: string[] | null
|
dmRecipientMemberIds: string[] | null
|
||||||
@@ -204,6 +216,26 @@ function normalizeMinute(value: number | null | undefined): number | null {
|
|||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeRelativeOffsetMinutes(value: number | null | undefined): number | null {
|
||||||
|
if (
|
||||||
|
value === null ||
|
||||||
|
value === undefined ||
|
||||||
|
!Number.isInteger(value) ||
|
||||||
|
value <= 0 ||
|
||||||
|
value > 7 * 24 * 60
|
||||||
|
) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeDateReferenceMode(
|
||||||
|
value: string | null | undefined
|
||||||
|
): 'relative' | 'calendar' | null {
|
||||||
|
return value === 'relative' || value === 'calendar' ? value : null
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeMemberId(
|
function normalizeMemberId(
|
||||||
value: string | null | undefined,
|
value: string | null | undefined,
|
||||||
members: readonly AdHocNotificationInterpreterMember[]
|
members: readonly AdHocNotificationInterpreterMember[]
|
||||||
@@ -348,6 +380,18 @@ export function createOpenAiAdHocNotificationInterpreter(input: {
|
|||||||
resolvedMinute: {
|
resolvedMinute: {
|
||||||
anyOf: [{ type: 'integer' }, { type: 'null' }]
|
anyOf: [{ type: 'integer' }, { type: 'null' }]
|
||||||
},
|
},
|
||||||
|
relativeOffsetMinutes: {
|
||||||
|
anyOf: [{ type: 'integer' }, { type: 'null' }]
|
||||||
|
},
|
||||||
|
dateReferenceMode: {
|
||||||
|
anyOf: [
|
||||||
|
{
|
||||||
|
type: 'string',
|
||||||
|
enum: ['relative', 'calendar']
|
||||||
|
},
|
||||||
|
{ type: 'null' }
|
||||||
|
]
|
||||||
|
},
|
||||||
resolutionMode: {
|
resolutionMode: {
|
||||||
anyOf: [
|
anyOf: [
|
||||||
{
|
{
|
||||||
@@ -373,6 +417,8 @@ export function createOpenAiAdHocNotificationInterpreter(input: {
|
|||||||
'resolvedLocalDate',
|
'resolvedLocalDate',
|
||||||
'resolvedHour',
|
'resolvedHour',
|
||||||
'resolvedMinute',
|
'resolvedMinute',
|
||||||
|
'relativeOffsetMinutes',
|
||||||
|
'dateReferenceMode',
|
||||||
'resolutionMode',
|
'resolutionMode',
|
||||||
'confidence',
|
'confidence',
|
||||||
'clarificationQuestion'
|
'clarificationQuestion'
|
||||||
@@ -385,9 +431,12 @@ export function createOpenAiAdHocNotificationInterpreter(input: {
|
|||||||
'Use the provided member ids when a reminder is clearly aimed at a specific household member.',
|
'Use the provided member ids when a reminder is clearly aimed at a specific household member.',
|
||||||
'resolvedLocalDate must be YYYY-MM-DD in the provided household timezone.',
|
'resolvedLocalDate must be YYYY-MM-DD in the provided household timezone.',
|
||||||
'resolvedHour and resolvedMinute must be in 24-hour local time when a reminder can be scheduled.',
|
'resolvedHour and resolvedMinute must be in 24-hour local time when a reminder can be scheduled.',
|
||||||
|
'If the user uses a relative duration like "через 30 минут" or "in 2 hours", prefer relativeOffsetMinutes over absolute date/time fields.',
|
||||||
|
'dateReferenceMode must be relative for words like today, tomorrow, the day after tomorrow, today evening, tomorrow morning; use calendar only for explicit calendar dates.',
|
||||||
'Use resolutionMode exact for explicit clock time, fuzzy_window for phrases like morning/evening, date_only for plain day/date without an explicit time, ambiguous when the request is still too vague to schedule.',
|
'Use resolutionMode exact for explicit clock time, fuzzy_window for phrases like morning/evening, date_only for plain day/date without an explicit time, ambiguous when the request is still too vague to schedule.',
|
||||||
'If schedule information is missing or ambiguous, return decision clarification and a short clarificationQuestion in the user language.',
|
'If schedule information is missing or ambiguous, return decision clarification and a short clarificationQuestion in the user language.',
|
||||||
'If the message is not a reminder request, return decision not_notification.',
|
'If the message is not a reminder request, return decision not_notification.',
|
||||||
|
'During local 00:00-04:59, vague tomorrow phrases like "завтра", "завтра утром", "tomorrow", "tomorrow morning" refer to the upcoming same-calendar-day reminder window unless the user gave an explicit calendar date.',
|
||||||
promptWindowRules(),
|
promptWindowRules(),
|
||||||
options.assistantContext ? `Household context: ${options.assistantContext}` : null,
|
options.assistantContext ? `Household context: ${options.assistantContext}` : null,
|
||||||
options.assistantTone ? `Preferred tone: ${options.assistantTone}` : null,
|
options.assistantTone ? `Preferred tone: ${options.assistantTone}` : null,
|
||||||
@@ -419,6 +468,8 @@ export function createOpenAiAdHocNotificationInterpreter(input: {
|
|||||||
resolvedLocalDate: normalizeOptionalText(parsed.resolvedLocalDate),
|
resolvedLocalDate: normalizeOptionalText(parsed.resolvedLocalDate),
|
||||||
resolvedHour: normalizeHour(parsed.resolvedHour),
|
resolvedHour: normalizeHour(parsed.resolvedHour),
|
||||||
resolvedMinute: normalizeMinute(parsed.resolvedMinute),
|
resolvedMinute: normalizeMinute(parsed.resolvedMinute),
|
||||||
|
relativeOffsetMinutes: normalizeRelativeOffsetMinutes(parsed.relativeOffsetMinutes),
|
||||||
|
dateReferenceMode: normalizeDateReferenceMode(parsed.dateReferenceMode),
|
||||||
resolutionMode: normalizeResolutionMode(parsed.resolutionMode),
|
resolutionMode: normalizeResolutionMode(parsed.resolutionMode),
|
||||||
clarificationQuestion: normalizeOptionalText(parsed.clarificationQuestion),
|
clarificationQuestion: normalizeOptionalText(parsed.clarificationQuestion),
|
||||||
confidence: normalizeConfidence(parsed.confidence),
|
confidence: normalizeConfidence(parsed.confidence),
|
||||||
@@ -448,6 +499,18 @@ export function createOpenAiAdHocNotificationInterpreter(input: {
|
|||||||
resolvedMinute: {
|
resolvedMinute: {
|
||||||
anyOf: [{ type: 'integer' }, { type: 'null' }]
|
anyOf: [{ type: 'integer' }, { type: 'null' }]
|
||||||
},
|
},
|
||||||
|
relativeOffsetMinutes: {
|
||||||
|
anyOf: [{ type: 'integer' }, { type: 'null' }]
|
||||||
|
},
|
||||||
|
dateReferenceMode: {
|
||||||
|
anyOf: [
|
||||||
|
{
|
||||||
|
type: 'string',
|
||||||
|
enum: ['relative', 'calendar']
|
||||||
|
},
|
||||||
|
{ type: 'null' }
|
||||||
|
]
|
||||||
|
},
|
||||||
resolutionMode: {
|
resolutionMode: {
|
||||||
anyOf: [
|
anyOf: [
|
||||||
{
|
{
|
||||||
@@ -471,6 +534,8 @@ export function createOpenAiAdHocNotificationInterpreter(input: {
|
|||||||
'resolvedLocalDate',
|
'resolvedLocalDate',
|
||||||
'resolvedHour',
|
'resolvedHour',
|
||||||
'resolvedMinute',
|
'resolvedMinute',
|
||||||
|
'relativeOffsetMinutes',
|
||||||
|
'dateReferenceMode',
|
||||||
'resolutionMode',
|
'resolutionMode',
|
||||||
'confidence',
|
'confidence',
|
||||||
'clarificationQuestion'
|
'clarificationQuestion'
|
||||||
@@ -481,8 +546,11 @@ export function createOpenAiAdHocNotificationInterpreter(input: {
|
|||||||
'Decide whether the message contains enough schedule information to produce a local date/time or whether you need clarification.',
|
'Decide whether the message contains enough schedule information to produce a local date/time or whether you need clarification.',
|
||||||
'resolvedLocalDate must be YYYY-MM-DD in the provided household timezone.',
|
'resolvedLocalDate must be YYYY-MM-DD in the provided household timezone.',
|
||||||
'resolvedHour and resolvedMinute must be local 24-hour time when parsed.',
|
'resolvedHour and resolvedMinute must be local 24-hour time when parsed.',
|
||||||
|
'If the message uses a relative duration like "через 30 минут" or "in 2 hours", prefer relativeOffsetMinutes over absolute date/time fields.',
|
||||||
|
'dateReferenceMode must be relative for words like today, tomorrow, and the day after tomorrow; use calendar only for explicit calendar dates.',
|
||||||
'Use resolutionMode exact for explicit clock time, fuzzy_window for phrases like morning/evening, date_only for plain day/date without explicit time, ambiguous when still unclear.',
|
'Use resolutionMode exact for explicit clock time, fuzzy_window for phrases like morning/evening, date_only for plain day/date without explicit time, ambiguous when still unclear.',
|
||||||
'If the schedule is missing or ambiguous, return clarification and ask a short question in the user language.',
|
'If the schedule is missing or ambiguous, return clarification and ask a short question in the user language.',
|
||||||
|
'During local 00:00-04:59, vague tomorrow phrases like "завтра", "завтра утром", "tomorrow", "tomorrow morning" refer to the upcoming same-calendar-day reminder window unless the user gave an explicit calendar date.',
|
||||||
promptWindowRules(),
|
promptWindowRules(),
|
||||||
`Household timezone: ${options.timezone}`,
|
`Household timezone: ${options.timezone}`,
|
||||||
`Current local date/time in that timezone: ${options.localNow}`,
|
`Current local date/time in that timezone: ${options.localNow}`,
|
||||||
@@ -502,6 +570,8 @@ export function createOpenAiAdHocNotificationInterpreter(input: {
|
|||||||
resolvedLocalDate: normalizeOptionalText(parsed.resolvedLocalDate),
|
resolvedLocalDate: normalizeOptionalText(parsed.resolvedLocalDate),
|
||||||
resolvedHour: normalizeHour(parsed.resolvedHour),
|
resolvedHour: normalizeHour(parsed.resolvedHour),
|
||||||
resolvedMinute: normalizeMinute(parsed.resolvedMinute),
|
resolvedMinute: normalizeMinute(parsed.resolvedMinute),
|
||||||
|
relativeOffsetMinutes: normalizeRelativeOffsetMinutes(parsed.relativeOffsetMinutes),
|
||||||
|
dateReferenceMode: normalizeDateReferenceMode(parsed.dateReferenceMode),
|
||||||
resolutionMode: normalizeResolutionMode(parsed.resolutionMode),
|
resolutionMode: normalizeResolutionMode(parsed.resolutionMode),
|
||||||
clarificationQuestion: normalizeOptionalText(parsed.clarificationQuestion),
|
clarificationQuestion: normalizeOptionalText(parsed.clarificationQuestion),
|
||||||
confidence: normalizeConfidence(parsed.confidence),
|
confidence: normalizeConfidence(parsed.confidence),
|
||||||
@@ -540,6 +610,18 @@ export function createOpenAiAdHocNotificationInterpreter(input: {
|
|||||||
resolvedMinute: {
|
resolvedMinute: {
|
||||||
anyOf: [{ type: 'integer' }, { type: 'null' }]
|
anyOf: [{ type: 'integer' }, { type: 'null' }]
|
||||||
},
|
},
|
||||||
|
relativeOffsetMinutes: {
|
||||||
|
anyOf: [{ type: 'integer' }, { type: 'null' }]
|
||||||
|
},
|
||||||
|
dateReferenceMode: {
|
||||||
|
anyOf: [
|
||||||
|
{
|
||||||
|
type: 'string',
|
||||||
|
enum: ['relative', 'calendar']
|
||||||
|
},
|
||||||
|
{ type: 'null' }
|
||||||
|
]
|
||||||
|
},
|
||||||
resolutionMode: {
|
resolutionMode: {
|
||||||
anyOf: [
|
anyOf: [
|
||||||
{
|
{
|
||||||
@@ -586,6 +668,8 @@ export function createOpenAiAdHocNotificationInterpreter(input: {
|
|||||||
'resolvedLocalDate',
|
'resolvedLocalDate',
|
||||||
'resolvedHour',
|
'resolvedHour',
|
||||||
'resolvedMinute',
|
'resolvedMinute',
|
||||||
|
'relativeOffsetMinutes',
|
||||||
|
'dateReferenceMode',
|
||||||
'resolutionMode',
|
'resolutionMode',
|
||||||
'deliveryMode',
|
'deliveryMode',
|
||||||
'dmRecipientMemberIds',
|
'dmRecipientMemberIds',
|
||||||
@@ -601,8 +685,11 @@ export function createOpenAiAdHocNotificationInterpreter(input: {
|
|||||||
'Use notificationText only when the user changes what should be reminded.',
|
'Use notificationText only when the user changes what should be reminded.',
|
||||||
'Use deliveryMode when the user changes where the reminder should be sent.',
|
'Use deliveryMode when the user changes where the reminder should be sent.',
|
||||||
'Use dmRecipientMemberIds only when the user clearly changes selected DM recipients.',
|
'Use dmRecipientMemberIds only when the user clearly changes selected DM recipients.',
|
||||||
|
'If the user uses a relative duration like "через 30 минут" or "in 2 hours", prefer relativeOffsetMinutes over absolute date/time fields.',
|
||||||
|
'dateReferenceMode must be relative for words like today, tomorrow, and the day after tomorrow; use calendar only for explicit calendar dates.',
|
||||||
'Use resolutionMode exact for explicit clock time, fuzzy_window for phrases like morning/evening, date_only for plain day/date without explicit time, ambiguous when still unclear.',
|
'Use resolutionMode exact for explicit clock time, fuzzy_window for phrases like morning/evening, date_only for plain day/date without explicit time, ambiguous when still unclear.',
|
||||||
'If the latest message is too ambiguous, return clarification and a short clarificationQuestion in the user language.',
|
'If the latest message is too ambiguous, return clarification and a short clarificationQuestion in the user language.',
|
||||||
|
'During local 00:00-04:59, vague tomorrow phrases like "завтра", "завтра утром", "tomorrow", "tomorrow morning" refer to the upcoming same-calendar-day reminder window unless the user gave an explicit calendar date.',
|
||||||
promptWindowRules(),
|
promptWindowRules(),
|
||||||
options.assistantContext ? `Household context: ${options.assistantContext}` : null,
|
options.assistantContext ? `Household context: ${options.assistantContext}` : null,
|
||||||
options.assistantTone ? `Preferred tone: ${options.assistantTone}` : null,
|
options.assistantTone ? `Preferred tone: ${options.assistantTone}` : null,
|
||||||
@@ -643,6 +730,8 @@ export function createOpenAiAdHocNotificationInterpreter(input: {
|
|||||||
resolvedLocalDate: normalizeOptionalText(parsed.resolvedLocalDate),
|
resolvedLocalDate: normalizeOptionalText(parsed.resolvedLocalDate),
|
||||||
resolvedHour: normalizeHour(parsed.resolvedHour),
|
resolvedHour: normalizeHour(parsed.resolvedHour),
|
||||||
resolvedMinute: normalizeMinute(parsed.resolvedMinute),
|
resolvedMinute: normalizeMinute(parsed.resolvedMinute),
|
||||||
|
relativeOffsetMinutes: normalizeRelativeOffsetMinutes(parsed.relativeOffsetMinutes),
|
||||||
|
dateReferenceMode: normalizeDateReferenceMode(parsed.dateReferenceMode),
|
||||||
resolutionMode: normalizeResolutionMode(parsed.resolutionMode),
|
resolutionMode: normalizeResolutionMode(parsed.resolutionMode),
|
||||||
deliveryMode: normalizeDeliveryMode(parsed.deliveryMode),
|
deliveryMode: normalizeDeliveryMode(parsed.deliveryMode),
|
||||||
dmRecipientMemberIds: normalizeMemberIds(parsed.dmRecipientMemberIds, options.members),
|
dmRecipientMemberIds: normalizeMemberIds(parsed.dmRecipientMemberIds, options.members),
|
||||||
|
|||||||
@@ -68,6 +68,9 @@ describe('createOpenAiChatAssistant', () => {
|
|||||||
expect(capturedBody!.input[1]?.content).toContain(
|
expect(capturedBody!.input[1]?.content).toContain(
|
||||||
'Members can ask the bot to schedule a future notification in this topic.'
|
'Members can ask the bot to schedule a future notification in this topic.'
|
||||||
)
|
)
|
||||||
|
expect(capturedBody!.input[1]?.content).toContain(
|
||||||
|
'Never tell the user to set a reminder on their own device in this topic.'
|
||||||
|
)
|
||||||
} finally {
|
} finally {
|
||||||
globalThis.fetch = originalFetch
|
globalThis.fetch = originalFetch
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,7 +64,8 @@ function topicCapabilityNotes(topicRole: TopicMessageRole): string {
|
|||||||
'- You can discuss existing household rent/utilities reminder timing, the supported utility-bill collection flow, and ad hoc household notifications.',
|
'- You can discuss existing household rent/utilities reminder timing, the supported utility-bill collection flow, and ad hoc household notifications.',
|
||||||
'- Members can ask the bot to schedule a future notification in this topic.',
|
'- Members can ask the bot to schedule a future notification in this topic.',
|
||||||
'- If the date or time is missing, ask a concise follow-up instead of pretending it was scheduled.',
|
'- If the date or time is missing, ask a concise follow-up instead of pretending it was scheduled.',
|
||||||
'- Do not claim a notification was saved unless the system explicitly confirmed it.'
|
'- Do not claim a notification was saved unless the system explicitly confirmed it.',
|
||||||
|
'- Never tell the user to set a reminder on their own device in this topic.'
|
||||||
].join('\n')
|
].join('\n')
|
||||||
case 'feedback':
|
case 'feedback':
|
||||||
return [
|
return [
|
||||||
@@ -111,7 +112,7 @@ const ASSISTANT_SYSTEM_PROMPT = [
|
|||||||
'Do not repeat the same clarification after the user declines, backs off, or says they are only thinking.',
|
'Do not repeat the same clarification after the user declines, backs off, or says they are only thinking.',
|
||||||
'Do not restate the full household context unless the user explicitly asks for details.',
|
'Do not restate the full household context unless the user explicitly asks for details.',
|
||||||
'Do not imply capabilities that are not explicitly provided in the system context.',
|
'Do not imply capabilities that are not explicitly provided in the system context.',
|
||||||
'There is no general feature for creating or scheduling arbitrary personal reminders unless the system explicitly says so.',
|
'There is no general feature for creating or scheduling arbitrary personal reminders unless the system explicitly says so in the current topic capability notes.',
|
||||||
'Avoid bullet lists unless the user asked for a list or several distinct items.',
|
'Avoid bullet lists unless the user asked for a list or several distinct items.',
|
||||||
'Reply in the user language inferred from the latest user message and locale context.'
|
'Reply in the user language inferred from the latest user message and locale context.'
|
||||||
].join(' ')
|
].join(' ')
|
||||||
|
|||||||
@@ -191,7 +191,7 @@ describe('createAdHocNotificationService', () => {
|
|||||||
originalRequestText: 'Напомни Георгию завтра',
|
originalRequestText: 'Напомни Георгию завтра',
|
||||||
notificationText: 'пошпынять Георгия о том, позвонил ли он',
|
notificationText: 'пошпынять Георгия о том, позвонил ли он',
|
||||||
timezone: 'Asia/Tbilisi',
|
timezone: 'Asia/Tbilisi',
|
||||||
scheduledFor: Temporal.Instant.from('2026-03-24T08:00:00Z'),
|
scheduledFor: Temporal.Instant.from('2026-03-25T08:00:00Z'),
|
||||||
timePrecision: 'date_only_defaulted',
|
timePrecision: 'date_only_defaulted',
|
||||||
deliveryMode: 'topic'
|
deliveryMode: 'topic'
|
||||||
})
|
})
|
||||||
@@ -222,7 +222,7 @@ describe('createAdHocNotificationService', () => {
|
|||||||
originalRequestText: 'remind everyone tomorrow',
|
originalRequestText: 'remind everyone tomorrow',
|
||||||
notificationText: 'pay rent',
|
notificationText: 'pay rent',
|
||||||
timezone: 'Asia/Tbilisi',
|
timezone: 'Asia/Tbilisi',
|
||||||
scheduledFor: Temporal.Instant.from('2026-03-24T08:00:00Z'),
|
scheduledFor: Temporal.Instant.from('2026-03-25T08:00:00Z'),
|
||||||
timePrecision: 'date_only_defaulted',
|
timePrecision: 'date_only_defaulted',
|
||||||
deliveryMode: 'dm_all'
|
deliveryMode: 'dm_all'
|
||||||
})
|
})
|
||||||
@@ -246,7 +246,7 @@ describe('createAdHocNotificationService', () => {
|
|||||||
originalRequestText: 'remind tomorrow',
|
originalRequestText: 'remind tomorrow',
|
||||||
notificationText: 'check rent',
|
notificationText: 'check rent',
|
||||||
timezone: 'Asia/Tbilisi',
|
timezone: 'Asia/Tbilisi',
|
||||||
scheduledFor: Temporal.Instant.from('2026-03-24T08:00:00Z'),
|
scheduledFor: Temporal.Instant.from('2026-03-25T08:00:00Z'),
|
||||||
timePrecision: 'date_only_defaulted',
|
timePrecision: 'date_only_defaulted',
|
||||||
deliveryMode: 'topic',
|
deliveryMode: 'topic',
|
||||||
friendlyTagAssignee: true
|
friendlyTagAssignee: true
|
||||||
@@ -273,7 +273,7 @@ describe('createAdHocNotificationService', () => {
|
|||||||
originalRequestText: 'remind tomorrow',
|
originalRequestText: 'remind tomorrow',
|
||||||
notificationText: 'call landlord',
|
notificationText: 'call landlord',
|
||||||
timezone: 'Asia/Tbilisi',
|
timezone: 'Asia/Tbilisi',
|
||||||
scheduledFor: Temporal.Instant.from('2026-03-24T08:00:00Z'),
|
scheduledFor: Temporal.Instant.from('2026-03-25T08:00:00Z'),
|
||||||
timePrecision: 'date_only_defaulted',
|
timePrecision: 'date_only_defaulted',
|
||||||
deliveryMode: 'topic',
|
deliveryMode: 'topic',
|
||||||
friendlyTagAssignee: false
|
friendlyTagAssignee: false
|
||||||
@@ -306,7 +306,7 @@ describe('createAdHocNotificationService', () => {
|
|||||||
originalRequestText: 'remind tomorrow',
|
originalRequestText: 'remind tomorrow',
|
||||||
notificationText: 'call landlord',
|
notificationText: 'call landlord',
|
||||||
timezone: 'Asia/Tbilisi',
|
timezone: 'Asia/Tbilisi',
|
||||||
scheduledFor: Temporal.Instant.from('2026-03-24T08:00:00Z'),
|
scheduledFor: Temporal.Instant.from('2026-03-25T08:00:00Z'),
|
||||||
timePrecision: 'date_only_defaulted',
|
timePrecision: 'date_only_defaulted',
|
||||||
deliveryMode: 'topic',
|
deliveryMode: 'topic',
|
||||||
friendlyTagAssignee: false
|
friendlyTagAssignee: false
|
||||||
@@ -342,7 +342,7 @@ describe('createAdHocNotificationService', () => {
|
|||||||
originalRequestText: 'remind tomorrow',
|
originalRequestText: 'remind tomorrow',
|
||||||
notificationText: 'call landlord',
|
notificationText: 'call landlord',
|
||||||
timezone: 'Asia/Tbilisi',
|
timezone: 'Asia/Tbilisi',
|
||||||
scheduledFor: Temporal.Instant.from('2026-03-24T08:00:00Z'),
|
scheduledFor: Temporal.Instant.from('2026-03-25T08:00:00Z'),
|
||||||
timePrecision: 'date_only_defaulted',
|
timePrecision: 'date_only_defaulted',
|
||||||
deliveryMode: 'topic',
|
deliveryMode: 'topic',
|
||||||
friendlyTagAssignee: false
|
friendlyTagAssignee: false
|
||||||
@@ -351,7 +351,7 @@ describe('createAdHocNotificationService', () => {
|
|||||||
const result = await service.updateNotification({
|
const result = await service.updateNotification({
|
||||||
notificationId: created.id,
|
notificationId: created.id,
|
||||||
viewerMemberId: 'creator',
|
viewerMemberId: 'creator',
|
||||||
scheduledFor: Temporal.Instant.from('2026-03-24T09:00:00Z'),
|
scheduledFor: Temporal.Instant.from('2026-03-25T09:00:00Z'),
|
||||||
timePrecision: 'exact',
|
timePrecision: 'exact',
|
||||||
deliveryMode: 'dm_selected',
|
deliveryMode: 'dm_selected',
|
||||||
dmRecipientMemberIds: ['alice', 'bob'],
|
dmRecipientMemberIds: ['alice', 'bob'],
|
||||||
@@ -360,7 +360,7 @@ describe('createAdHocNotificationService', () => {
|
|||||||
|
|
||||||
expect(result.status).toBe('updated')
|
expect(result.status).toBe('updated')
|
||||||
if (result.status === 'updated') {
|
if (result.status === 'updated') {
|
||||||
expect(result.notification.scheduledFor.toString()).toBe('2026-03-24T09:00:00Z')
|
expect(result.notification.scheduledFor.toString()).toBe('2026-03-25T09:00:00Z')
|
||||||
expect(result.notification.deliveryMode).toBe('dm_selected')
|
expect(result.notification.deliveryMode).toBe('dm_selected')
|
||||||
expect(result.notification.dmRecipientMemberIds).toEqual(['alice', 'bob'])
|
expect(result.notification.dmRecipientMemberIds).toEqual(['alice', 'bob'])
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user