Files
Wyndham-RSVN/client/src/tests/reservationBookingProcessingRunView.spec.ts
T
2026-08-08 16:25:17 +08:00

198 lines
6.3 KiB
TypeScript

import { flushPromises, mount } from '@vue/test-utils'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { createI18n } from 'vue-i18n'
import { createMemoryHistory, createRouter } from 'vue-router'
import zhCN from '@/i18n/locales/zh-CN'
import type { BookingProcessingRunView } from '@/types/bookingContracts'
import ReservationBookingProcessingRunView from '@/views/reservation/ReservationBookingProcessingRunView.vue'
vi.mock('@/services/reservationService', async (importOriginal) => {
const actual = await importOriginal<typeof import('@/services/reservationService')>()
return {
...actual,
fetchBookingProcessingRun: vi.fn(),
confirmBookingProcessingRun: vi.fn(),
retryBookingProcessingRun: vi.fn(),
}
})
const service = await import('@/services/reservationService')
describe('ReservationBookingProcessingRunView', () => {
beforeEach(() => {
vi.mocked(service.fetchBookingProcessingRun).mockReset()
vi.mocked(service.confirmBookingProcessingRun).mockReset()
vi.mocked(service.retryBookingProcessingRun).mockReset()
})
it('shows the safe confirmation projection and freezes corrected values without a PMS action', async () => {
const initial = awaitingRun()
const confirmed = {
...initial,
status: 'CONFIRMED' as const,
confirmation_projection: {
...initial.confirmation_projection!,
projection_status: 'AWAITING_CONFIRMATION' as const,
processing_status: 'CONFIRMED' as const,
},
}
vi.mocked(service.fetchBookingProcessingRun).mockResolvedValue(initial)
vi.mocked(service.confirmBookingProcessingRun).mockResolvedValue({
confirmed: true,
processing_run: confirmed,
})
const wrapper = await mountView()
await flushPromises()
expect(wrapper.text()).toContain('等待确认')
expect(wrapper.text()).toContain('RATE_CODE_REQUIRED')
await wrapper.find('select').setValue('FIT')
await wrapper.find('input[type="text"]').setValue('QBD-850')
await wrapper.find('form').trigger('submit')
await flushPromises()
expect(service.confirmBookingProcessingRun).toHaveBeenCalledWith('booking-001', {
items: [
{
confirmation_item_id: 'confirmation-fact-1',
field_overrides: {
booking_type: 'FIT',
rate_code: 'QBD-850',
},
},
],
})
expect(wrapper.text()).toContain('参数已冻结')
expect(wrapper.text()).toContain('已确认')
})
it('retries only a failed processing run and resumes safe polling', async () => {
const failed = {
...awaitingRun(),
status: 'FAILED' as const,
result_disposition: 'RISK_NOTIFICATION' as const,
retry_count: 0,
error_code: 'BOOKING_ORCHESTRATION_FAILED',
error_summary: 'Booking processing failed before confirmation.',
confirmation_projection: null,
}
const retried = {
...failed,
status: 'CONTEXT_READY' as const,
retry_count: 1,
error_code: null,
error_summary: null,
}
vi.mocked(service.fetchBookingProcessingRun).mockResolvedValue(failed)
vi.mocked(service.retryBookingProcessingRun).mockResolvedValue(retried)
const wrapper = await mountView()
await flushPromises()
const retryButton = wrapper.findAll('button').find((button) => button.text().includes('重新处理'))
expect(retryButton).toBeDefined()
await retryButton!.trigger('click')
await flushPromises()
expect(service.retryBookingProcessingRun).toHaveBeenCalledWith('booking-001')
expect(wrapper.text()).toContain('上下文已组装')
expect(wrapper.text()).toContain('已重新进入处理队列')
})
})
async function mountView() {
const router = createRouter({
history: createMemoryHistory(),
routes: [
{
path: '/reservation/booking-processing-runs/:runId',
component: ReservationBookingProcessingRunView,
},
{ path: '/reservation/email-intake', component: { template: '<div>Intake</div>' } },
],
})
await router.push('/reservation/booking-processing-runs/booking-001')
await router.isReady()
return mount(ReservationBookingProcessingRunView, {
global: {
plugins: [
router,
createI18n({
legacy: false,
locale: 'zh-CN',
messages: { 'zh-CN': zhCN },
}),
],
},
})
}
function awaitingRun(): BookingProcessingRunView {
return {
processing_run_id: 'booking-001',
source_message_id: 'manual-eml-001',
revision_number: 1,
status: 'AWAITING_CONFIRMATION',
result_disposition: 'IN_SCOPE_TASKS',
duplicate: false,
retry_count: 0,
error_code: null,
error_summary: null,
started_at: '2026-08-08T00:00:00Z',
completed_at: null,
confirmation_projection: {
contract_meta: {
contract_version: 'booking-contracts-v1',
catalog_version: 'booking-catalog-v20260808',
parser_version: 'booking-email-parser-v0.1',
agent_profile_version: 'agent-not-invoked',
processing_run_id: 'booking-001',
source_message_id: 'manual-eml-001',
source_revision: 0,
},
evidence_refs: [],
projection_status: 'REVIEW_REQUIRED',
confirmation_items: [
{
confirmation_item_id: 'confirmation-fact-1',
target_decision_id: 'fact-1',
disposition: 'IN_SCOPE_TASKS',
display_fields: [
{
field_code: 'booking_type',
value: null,
editable: true,
allowed_values: ['FIT', 'GROUP'],
evidence_refs: [],
issues: [],
},
],
blocked_fields: [
{
field_code: 'rate_code',
value: null,
editable: true,
allowed_values: [],
evidence_refs: [],
issues: [
{
code: 'RATE_CODE_REQUIRED',
severity: 'BLOCKER',
scope: 'FIELD',
message_key: 'booking.validator.rate_code_required',
evidence_refs: [],
retryable: false,
},
],
},
],
linked_actions: [],
safe_evidence_refs: [],
},
],
issues: [],
processing_status: 'AWAITING_CONFIRMATION',
},
}
}