feat:Strengthen the testing framework
This commit is contained in:
29
tests/unit/guards/changed-file-test-impact-guard.test.ts
Normal file
29
tests/unit/guards/changed-file-test-impact-guard.test.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { inspectChangedFiles } from '../../../scripts/guards/changed-file-test-impact-guard.mjs'
|
||||
|
||||
describe('changed-file-test-impact-guard', () => {
|
||||
it('requires api changes to be paired with contract, system, or regression tests', () => {
|
||||
const violations = inspectChangedFiles([
|
||||
'src/app/api/novel-promotion/[projectId]/generate-image/route.ts',
|
||||
])
|
||||
expect(violations).toEqual([
|
||||
'api: changing src/app/api/** requires a matching contract, system, or regression test change; sources=src/app/api/novel-promotion/[projectId]/generate-image/route.ts',
|
||||
])
|
||||
})
|
||||
|
||||
it('accepts worker changes when system tests are updated together', () => {
|
||||
const violations = inspectChangedFiles([
|
||||
'src/lib/workers/image.worker.ts',
|
||||
'tests/system/generate-image.system.test.ts',
|
||||
])
|
||||
expect(violations).toEqual([])
|
||||
})
|
||||
|
||||
it('accepts provider changes when provider contract coverage is updated', () => {
|
||||
const violations = inspectChangedFiles([
|
||||
'src/lib/model-gateway/openai-compat/image.ts',
|
||||
'tests/unit/model-gateway/openai-compat-template-image-output-urls.test.ts',
|
||||
])
|
||||
expect(violations).toEqual([])
|
||||
})
|
||||
})
|
||||
27
tests/unit/task/error-catalog.contract.test.ts
Normal file
27
tests/unit/task/error-catalog.contract.test.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { ERROR_CATALOG, ERROR_CATEGORY, getErrorSpec } from '@/lib/errors/codes'
|
||||
|
||||
describe('error catalog contract', () => {
|
||||
it('keeps every catalog entry self-consistent and reachable through getErrorSpec', () => {
|
||||
const seenMessageKeys = new Set<string>()
|
||||
|
||||
for (const [code, spec] of Object.entries(ERROR_CATALOG)) {
|
||||
expect(getErrorSpec(code as keyof typeof ERROR_CATALOG)).toEqual(spec)
|
||||
expect(spec.defaultMessage.trim().length).toBeGreaterThan(0)
|
||||
expect(spec.userMessageKey.trim().length).toBeGreaterThan(0)
|
||||
expect(spec.httpStatus).toBeGreaterThanOrEqual(200)
|
||||
expect(spec.httpStatus).toBeLessThan(600)
|
||||
expect(Object.values(ERROR_CATEGORY)).toContain(spec.category)
|
||||
expect(seenMessageKeys.has(spec.userMessageKey)).toBe(false)
|
||||
seenMessageKeys.add(spec.userMessageKey)
|
||||
}
|
||||
})
|
||||
|
||||
it('keeps retryable provider/system errors out of 4xx except 429 and 202', () => {
|
||||
for (const spec of Object.values(ERROR_CATALOG)) {
|
||||
if (!spec.retryable) continue
|
||||
if (spec.httpStatus >= 500) continue
|
||||
expect([202, 429]).toContain(spec.httpStatus)
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -1,78 +1,51 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { withUserConcurrencyGate } from '@/lib/workers/user-concurrency-gate'
|
||||
|
||||
function wait(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(resolve, ms)
|
||||
function deferred<T>() {
|
||||
let resolve!: (value: T | PromiseLike<T>) => void
|
||||
const promise = new Promise<T>((nextResolve) => {
|
||||
resolve = nextResolve
|
||||
})
|
||||
return { promise, resolve }
|
||||
}
|
||||
|
||||
describe('user concurrency gate', () => {
|
||||
it('enforces max concurrent runs for same user and scope', async () => {
|
||||
let activeCount = 0
|
||||
let maxActiveCount = 0
|
||||
it('serializes same-scope work for the same user when limit is 1', async () => {
|
||||
const firstDone = deferred<void>()
|
||||
const events: string[] = []
|
||||
|
||||
const runTask = async (taskId: number) => await withUserConcurrencyGate({
|
||||
scope: 'video',
|
||||
userId: 'user-gate-video',
|
||||
limit: 2,
|
||||
const first = withUserConcurrencyGate({
|
||||
scope: 'image',
|
||||
userId: 'user-1',
|
||||
limit: 1,
|
||||
run: async () => {
|
||||
void taskId
|
||||
activeCount += 1
|
||||
maxActiveCount = Math.max(maxActiveCount, activeCount)
|
||||
await wait(20)
|
||||
activeCount -= 1
|
||||
events.push('first:start')
|
||||
await firstDone.promise
|
||||
events.push('first:end')
|
||||
},
|
||||
})
|
||||
|
||||
await Promise.all([
|
||||
runTask(1),
|
||||
runTask(2),
|
||||
runTask(3),
|
||||
runTask(4),
|
||||
const second = withUserConcurrencyGate({
|
||||
scope: 'image',
|
||||
userId: 'user-1',
|
||||
limit: 1,
|
||||
run: async () => {
|
||||
events.push('second:start')
|
||||
events.push('second:end')
|
||||
},
|
||||
})
|
||||
|
||||
await Promise.resolve()
|
||||
expect(events).toEqual(['first:start'])
|
||||
|
||||
firstDone.resolve()
|
||||
await Promise.all([first, second])
|
||||
|
||||
expect(events).toEqual([
|
||||
'first:start',
|
||||
'first:end',
|
||||
'second:start',
|
||||
'second:end',
|
||||
])
|
||||
|
||||
expect(maxActiveCount).toBe(2)
|
||||
})
|
||||
|
||||
it('does not share slots between different users', async () => {
|
||||
let activeCount = 0
|
||||
let maxActiveCount = 0
|
||||
|
||||
await Promise.all([
|
||||
withUserConcurrencyGate({
|
||||
scope: 'image',
|
||||
userId: 'user-gate-image-a',
|
||||
limit: 1,
|
||||
run: async () => {
|
||||
activeCount += 1
|
||||
maxActiveCount = Math.max(maxActiveCount, activeCount)
|
||||
await wait(20)
|
||||
activeCount -= 1
|
||||
},
|
||||
}),
|
||||
withUserConcurrencyGate({
|
||||
scope: 'image',
|
||||
userId: 'user-gate-image-b',
|
||||
limit: 1,
|
||||
run: async () => {
|
||||
activeCount += 1
|
||||
maxActiveCount = Math.max(maxActiveCount, activeCount)
|
||||
await wait(20)
|
||||
activeCount -= 1
|
||||
},
|
||||
}),
|
||||
])
|
||||
|
||||
expect(maxActiveCount).toBe(2)
|
||||
})
|
||||
|
||||
it('throws when concurrency limit is invalid', async () => {
|
||||
await expect(withUserConcurrencyGate({
|
||||
scope: 'video',
|
||||
userId: 'user-gate-invalid',
|
||||
limit: 0,
|
||||
run: async () => undefined,
|
||||
})).rejects.toThrow('WORKFLOW_CONCURRENCY_INVALID')
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user