198 lines
6.3 KiB
TypeScript
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',
|
|
},
|
|
}
|
|
}
|