73 lines
3.1 KiB
TypeScript
73 lines
3.1 KiB
TypeScript
import { describe, expect, it, vi, afterEach } from 'vitest'
|
|
import { parseDirectorySync, startDirectorySync, fetchDirectorySync, completedSyncAction } from './qaOrgSync'
|
|
afterEach(() => vi.unstubAllGlobals())
|
|
const idle = { configured: true, worker_ready: true, job: null }
|
|
describe('directory synchronization contract', () => {
|
|
it('reloads a completed job even on first observation, without replacing an unsaved draft', () => {
|
|
const status = parseDirectorySync({
|
|
...idle,
|
|
job: {
|
|
id: 'new-job',
|
|
state: 'completed',
|
|
attempt: 1,
|
|
max_attempts: 3,
|
|
updated_at: '2026-10-17T08:00:00Z',
|
|
error_code: '',
|
|
result: { directory_id: 'd', departments: 1, people: 1, synced_at: '2026-10-17T08:00:00Z' },
|
|
},
|
|
})
|
|
expect(completedSyncAction(status, '', false)).toBe('reload')
|
|
expect(completedSyncAction(status, '', true)).toBe('preserve')
|
|
expect(completedSyncAction(status, 'new-job', false)).toBe('none')
|
|
expect(completedSyncAction({ ...status, job: { ...status.job!, state: 'running' } }, '', false)).toBe('none')
|
|
})
|
|
it('keeps unknown/failed state distinct from empty data', () => {
|
|
expect(parseDirectorySync(idle).job).toBeNull()
|
|
expect(() => parseDirectorySync({ ...idle, worker_ready: 'yes' })).toThrow()
|
|
expect(() => parseDirectorySync({ ...idle, job: { id: 'x', state: 'mystery' } })).toThrow()
|
|
const result = { directory_id: 'd', departments: 327, people: 1534, synced_at: '2026-10-17T07:00:00Z' }
|
|
const job = {
|
|
id: 'j',
|
|
state: 'completed',
|
|
attempt: 1,
|
|
max_attempts: 3,
|
|
updated_at: '2026-10-17T07:01:00Z',
|
|
error_code: '',
|
|
result,
|
|
}
|
|
expect(parseDirectorySync({ ...idle, job }).job?.result?.people).toBe(1534)
|
|
expect(() => parseDirectorySync({ ...idle, job: { ...job, result: { ...result, people: -1 } } })).toThrow()
|
|
})
|
|
it('sends a stable idempotency header and no tenant, credential or executable path', async () => {
|
|
const fetcher = vi.fn().mockResolvedValue({
|
|
ok: true,
|
|
json: async () => ({ data: { job_id: 'j', deduplicated: false }, operation: 'replayed' }),
|
|
})
|
|
vi.stubGlobal('fetch', fetcher)
|
|
expect((await startDirectorySync('same-action')).operation).toBe('replayed')
|
|
expect(fetcher).toHaveBeenCalledWith(
|
|
'/api/v1/qa-ops/organization/sync',
|
|
expect.objectContaining({
|
|
method: 'POST',
|
|
credentials: 'same-origin',
|
|
body: '{}',
|
|
headers: expect.objectContaining({
|
|
'Idempotency-Key': 'same-action',
|
|
'X-Governance-Action': 'qa-organization',
|
|
}),
|
|
})
|
|
)
|
|
})
|
|
it('reads live job state and surfaces server failure instead of a successful empty result', async () => {
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi
|
|
.fn()
|
|
.mockResolvedValueOnce({ ok: true, json: async () => ({ data: idle }) })
|
|
.mockResolvedValueOnce({ ok: false, status: 503, json: async () => ({ error: '执行器未在线' }) })
|
|
)
|
|
expect((await fetchDirectorySync()).worker_ready).toBe(true)
|
|
await expect(startDirectorySync('same-action')).rejects.toThrow('执行器未在线')
|
|
})
|
|
})
|