118 lines
6.0 KiB
TypeScript
118 lines
6.0 KiB
TypeScript
/** @jest-environment node */
|
||
import type { readArtPointConfiguration } from '../art-workflow-point-configuration'
|
||
|
||
jest.mock('node:fs/promises', () => ({ readFile: jest.fn() }))
|
||
jest.mock('../prisma', () => ({ prisma: {} }))
|
||
|
||
const originalFetch = global.fetch
|
||
const url = 'https://dashboard.example.test/api/art-design-dashboard/full'
|
||
const contribution = { workflowKey: 'flow', name: '策划甲', role: 'planner', overtimeExempted: true }
|
||
const payload = (contributions = [contribution]) => ({
|
||
generatedAt: '2026-10-28 12:00:06',
|
||
workflows: [{ businessId: 'flow', processInstanceId: 'instance', scoreContributions: contributions }],
|
||
})
|
||
const response = (body: unknown) => ({ ok: true, json: async () => body })
|
||
let readConfiguration: typeof readArtPointConfiguration
|
||
let request: jest.Mock
|
||
let readFile: jest.Mock
|
||
|
||
beforeEach(() => {
|
||
jest.resetModules()
|
||
jest.resetAllMocks()
|
||
jest.useFakeTimers({ now: new Date('2026-10-28T04:00:00Z') })
|
||
delete process.env.ART_WORKFLOW_POINTS_CONFIG_DIR
|
||
process.env.ART_WORKFLOW_POINTS_DASHBOARD_URL = url
|
||
request = jest.fn().mockResolvedValue(response(payload()))
|
||
global.fetch = request
|
||
readFile = require('node:fs/promises').readFile
|
||
readConfiguration = require('../art-workflow-point-configuration').readArtPointConfiguration
|
||
})
|
||
afterEach(() => {
|
||
jest.useRealTimers()
|
||
global.fetch = originalFetch
|
||
delete process.env.ART_WORKFLOW_POINTS_DASHBOARD_URL
|
||
delete process.env.ART_WORKFLOW_POINTS_CONFIG_DIR
|
||
})
|
||
|
||
test('远程免扣按流程、人员、角色去重,不叠加本地文件,人工调整来源不变', async () => {
|
||
process.env.ART_WORKFLOW_POINTS_CONFIG_DIR = '/private-config'
|
||
const deductions = [{ workflowKey: 'flow', name: '策划甲', type: 'add', points: 2 }]
|
||
;(readFile as jest.Mock).mockResolvedValue(JSON.stringify(deductions))
|
||
request.mockResolvedValue(response(payload([contribution, contribution, { ...contribution, name: '策划乙', overtimeExempted: false }])))
|
||
expect(await readConfiguration()).toEqual({ deductions, overtimeExemptions: [{ workflowKey: 'flow', name: '策划甲', role: 'planner' }] })
|
||
expect(readFile).toHaveBeenCalledTimes(1)
|
||
expect(readFile).toHaveBeenCalledWith('/private-config/deductions.json', 'utf8')
|
||
expect(request).toHaveBeenCalledWith(url, expect.objectContaining({ cache: 'no-store', signal: expect.any(AbortSignal) }))
|
||
})
|
||
|
||
test('未设置远程来源时保留原本的私有文件配置和空配置行为', async () => {
|
||
delete process.env.ART_WORKFLOW_POINTS_DASHBOARD_URL
|
||
expect(await readConfiguration()).toEqual({ deductions: [], overtimeExemptions: [] })
|
||
process.env.ART_WORKFLOW_POINTS_CONFIG_DIR = '/private-config'
|
||
;(readFile as jest.Mock).mockImplementation(async file => file.endsWith('deductions.json') ? '[]' : JSON.stringify([contribution]))
|
||
expect((await readConfiguration()).overtimeExemptions).toEqual([{ workflowKey: 'flow', name: '策划甲', role: 'planner' }])
|
||
expect(request).not.toHaveBeenCalled()
|
||
})
|
||
|
||
test('并发共享一次读取,成功结果缓存60秒,过期后撤销免扣生效', async () => {
|
||
let resolve!: (value: unknown) => void
|
||
request.mockImplementationOnce(() => new Promise(done => { resolve = done }))
|
||
const first = readConfiguration(), second = readConfiguration()
|
||
expect(request).toHaveBeenCalledTimes(1)
|
||
resolve(response(payload()))
|
||
expect(await first).toEqual(await second)
|
||
jest.advanceTimersByTime(59_999)
|
||
expect((await readConfiguration()).overtimeExemptions).toHaveLength(1)
|
||
expect(request).toHaveBeenCalledTimes(1)
|
||
request.mockResolvedValueOnce(response(payload([{ ...contribution, overtimeExempted: false }])))
|
||
jest.advanceTimersByTime(1)
|
||
expect((await readConfiguration()).overtimeExemptions).toEqual([])
|
||
expect(request).toHaveBeenCalledTimes(2)
|
||
})
|
||
|
||
test('缓存过期读取失败返回503,不使用旧免扣或空配置;后续请求可以恢复', async () => {
|
||
await readConfiguration()
|
||
jest.advanceTimersByTime(60_000)
|
||
request.mockRejectedValueOnce(new Error('offline'))
|
||
await expect(readConfiguration()).rejects.toMatchObject({ status: 503, code: 'WORKFLOW_POINTS_CONFIG_UNAVAILABLE' })
|
||
request.mockResolvedValueOnce(response(payload([])))
|
||
expect((await readConfiguration()).overtimeExemptions).toEqual([])
|
||
})
|
||
|
||
test('读取超过10秒被中止,并发调用均收到可重试错误且可再次读取', async () => {
|
||
request.mockImplementationOnce((_url, { signal }) => new Promise((_resolve, reject) => {
|
||
signal.addEventListener('abort', () => reject(new Error('aborted')))
|
||
}))
|
||
const results = Promise.allSettled([readConfiguration(), readConfiguration()])
|
||
await jest.advanceTimersByTimeAsync(10_000)
|
||
for (const result of await results) {
|
||
expect(result.status).toBe('rejected')
|
||
if (result.status === 'rejected') expect(result.reason).toMatchObject({ status: 503, code: 'WORKFLOW_POINTS_CONFIG_UNAVAILABLE' })
|
||
}
|
||
expect(request.mock.calls[0][1].signal.aborted).toBe(true)
|
||
expect((await readConfiguration()).overtimeExemptions).toHaveLength(1)
|
||
})
|
||
|
||
test.each([
|
||
{ ok: false, json: async () => payload() },
|
||
{ ok: true, json: async () => { throw new Error('Invalid JSON') } },
|
||
response({ ok: false, error: 'unavailable' }),
|
||
response({ ...payload(), generatedAt: '' }),
|
||
response({ generatedAt: 'now', workflows: [{}] }),
|
||
response(payload([{ ...contribution, workflowKey: 'different-flow' }])),
|
||
response(payload([{ ...contribution, name: '' }])),
|
||
response(payload([{ ...contribution, role: 'manual' }])),
|
||
response(payload([{ ...contribution, overtimeExempted: 'true' as unknown as boolean }])),
|
||
])('HTTP错误或无效响应均拒绝计算积分(%#)', async value => {
|
||
request.mockResolvedValueOnce(value)
|
||
await expect(readConfiguration()).rejects.toMatchObject({ status: 503, code: 'WORKFLOW_POINTS_CONFIG_UNAVAILABLE' })
|
||
})
|
||
|
||
test('更换来源时不使用原来源缓存', async () => {
|
||
await readConfiguration()
|
||
process.env.ART_WORKFLOW_POINTS_DASHBOARD_URL = 'https://other.example.test/full'
|
||
request.mockResolvedValueOnce(response(payload([])))
|
||
expect((await readConfiguration()).overtimeExemptions).toEqual([])
|
||
expect(request).toHaveBeenCalledTimes(2)
|
||
})
|