gongxiangp2/lib/__tests__/art-workflow-point-configuration.test.ts

118 lines
6.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/** @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)
})