fix(meeting-document): hold quota for in-flight generation claims
This commit is contained in:
parent
f4724ddf53
commit
b35676c75c
5 changed files with 994 additions and 179 deletions
|
|
@ -0,0 +1,219 @@
|
|||
import {
|
||||
assertEquals,
|
||||
assertRejects,
|
||||
} from 'https://deno.land/std@0.224.0/assert/mod.ts'
|
||||
import type { GenerateMeetingDocumentRequest } from '../_shared/meeting-document-contract.ts'
|
||||
import {
|
||||
buildProviderRequest,
|
||||
type GenerationFailureCode,
|
||||
generateMeetingDocument,
|
||||
mapDatabaseError,
|
||||
type MeetingDocumentStore,
|
||||
parseClaim,
|
||||
PROVIDER_MAX_TOKENS,
|
||||
type ProviderOutcome,
|
||||
type ProviderRequest,
|
||||
type RpcResult,
|
||||
} from './generation.ts'
|
||||
|
||||
const actorId = '44444444-4444-4444-8444-444444444444'
|
||||
const request: GenerateMeetingDocumentRequest = {
|
||||
meetingId: '11111111-1111-4111-8111-111111111111',
|
||||
templateId: '22222222-2222-4222-8222-222222222222',
|
||||
idempotencyKey: '33333333-3333-4333-8333-333333333333',
|
||||
title: 'Weekly summary',
|
||||
model: 'claude-haiku-4-5-20251001',
|
||||
}
|
||||
|
||||
const claimedPayload = {
|
||||
claimed: true,
|
||||
status: 'processing',
|
||||
documentId: null,
|
||||
meetingTitle: 'Weekly sync',
|
||||
documentTitle: 'Weekly summary',
|
||||
templateType: 'custom',
|
||||
systemPrompt: 'Summarize.',
|
||||
transcript: 'A: hello',
|
||||
model: 'claude-haiku-4-5-20251001',
|
||||
}
|
||||
|
||||
const providerBody = {
|
||||
content: [{ type: 'text', text: 'Generated body' }],
|
||||
usage: { input_tokens: 10, output_tokens: 20 },
|
||||
}
|
||||
|
||||
interface Harness {
|
||||
store: MeetingDocumentStore
|
||||
providerCalls: ProviderRequest[]
|
||||
failures: GenerationFailureCode[]
|
||||
commits: number
|
||||
logs: string[]
|
||||
}
|
||||
|
||||
function harness(options: {
|
||||
claim?: RpcResult
|
||||
provider?: () => Promise<ProviderOutcome>
|
||||
commit?: () => Promise<RpcResult>
|
||||
findDocument?: RpcResult
|
||||
} = {}): Harness & { run: () => ReturnType<typeof generateMeetingDocument> } {
|
||||
const state: Harness = {
|
||||
store: undefined as unknown as MeetingDocumentStore,
|
||||
providerCalls: [],
|
||||
failures: [],
|
||||
commits: 0,
|
||||
logs: [],
|
||||
}
|
||||
state.store = {
|
||||
claim: () => Promise.resolve(options.claim ?? { data: claimedPayload, error: null }),
|
||||
markFailed: (_actor, _key, code) => {
|
||||
state.failures.push(code)
|
||||
return Promise.resolve(null)
|
||||
},
|
||||
commit: () => {
|
||||
state.commits += 1
|
||||
return options.commit?.() ?? Promise.resolve({
|
||||
data: { idempotent: false, document: { id: 'doc-1' } },
|
||||
error: null,
|
||||
})
|
||||
},
|
||||
findDocument: () => Promise.resolve(options.findDocument ?? { data: { id: 'doc-1' }, error: null }),
|
||||
}
|
||||
let clock = 1_000
|
||||
return Object.assign(state, {
|
||||
run: () => generateMeetingDocument(actorId, request, {
|
||||
store: state.store,
|
||||
provider: {
|
||||
generate: (payload: ProviderRequest) => {
|
||||
state.providerCalls.push(payload)
|
||||
return options.provider?.() ?? Promise.resolve<ProviderOutcome>({ kind: 'ok', body: providerBody })
|
||||
},
|
||||
},
|
||||
now: () => (clock += 250),
|
||||
log: (message: string) => { state.logs.push(message) },
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
Deno.test('a claim rejected for quota never reaches the provider', async () => {
|
||||
const h = harness({
|
||||
claim: { data: null, error: { code: 'P0001', message: 'generation_quota_exceeded' } },
|
||||
})
|
||||
assertEquals(await h.run(), { status: 429, body: { error: 'quota_exceeded' } })
|
||||
assertEquals(h.providerCalls.length, 0)
|
||||
assertEquals(h.commits, 0)
|
||||
})
|
||||
|
||||
Deno.test('regression r1-17: an unexpected error after claim releases the in-flight quota unit', async () => {
|
||||
const h = harness({ commit: () => Promise.reject(new Error('socket hang up')) })
|
||||
await assertRejects(() => h.run(), Error, 'socket hang up')
|
||||
assertEquals(h.failures, ['commit_failed'])
|
||||
})
|
||||
|
||||
Deno.test('an unexpected provider adapter error also releases the claim', async () => {
|
||||
const h = harness({ provider: () => Promise.reject(new TypeError('boom')) })
|
||||
await assertRejects(() => h.run(), TypeError)
|
||||
assertEquals(h.failures, ['commit_failed'])
|
||||
assertEquals(h.commits, 0)
|
||||
})
|
||||
|
||||
Deno.test('successful generation commits once and returns the document', async () => {
|
||||
const h = harness()
|
||||
assertEquals(await h.run(), {
|
||||
status: 200,
|
||||
body: { document: { id: 'doc-1' }, idempotent: false },
|
||||
})
|
||||
assertEquals(h.providerCalls.length, 1)
|
||||
assertEquals(h.failures, [])
|
||||
})
|
||||
|
||||
Deno.test('provider failures mark the claim failed with the matching code', async () => {
|
||||
const cases: Array<[ProviderOutcome, number, string, GenerationFailureCode]> = [
|
||||
[{ kind: 'timeout' }, 504, 'provider_timeout', 'provider_timeout'],
|
||||
[{ kind: 'network_error' }, 502, 'provider_request_failed', 'provider_request_failed'],
|
||||
[{ kind: 'http_error', status: 529 }, 502, 'provider_request_failed', 'provider_request_failed'],
|
||||
[{ kind: 'ok', body: null }, 502, 'provider_invalid_response', 'provider_invalid_response'],
|
||||
]
|
||||
for (const [outcome, status, error, failure] of cases) {
|
||||
const h = harness({ provider: () => Promise.resolve(outcome) })
|
||||
assertEquals(await h.run(), { status, body: { error } })
|
||||
assertEquals(h.failures, [failure])
|
||||
assertEquals(h.commits, 0)
|
||||
}
|
||||
})
|
||||
|
||||
Deno.test('commit quota rejection marks quota_exceeded and returns 429', async () => {
|
||||
const h = harness({
|
||||
commit: () => Promise.resolve({ data: null, error: { code: 'P0001', message: 'generation_quota_exceeded' } }),
|
||||
})
|
||||
assertEquals(await h.run(), { status: 429, body: { error: 'quota_exceeded' } })
|
||||
assertEquals(h.failures, ['quota_exceeded'])
|
||||
})
|
||||
|
||||
Deno.test('other commit errors mark commit_failed and return 500', async () => {
|
||||
const h = harness({
|
||||
commit: () => Promise.resolve({ data: null, error: { code: '55000', message: 'not committable' } }),
|
||||
})
|
||||
assertEquals(await h.run(), { status: 500, body: { error: 'commit_failed' } })
|
||||
assertEquals(h.failures, ['commit_failed'])
|
||||
})
|
||||
|
||||
Deno.test('claim without prompt or transcript is released', async () => {
|
||||
const h = harness({ claim: { data: { ...claimedPayload, transcript: null }, error: null } })
|
||||
assertEquals(await h.run(), { status: 500, body: { error: 'internal_error' } })
|
||||
assertEquals(h.failures, ['commit_failed'])
|
||||
assertEquals(h.providerCalls.length, 0)
|
||||
})
|
||||
|
||||
Deno.test('replays return the stored document or a conflict without provider work', async () => {
|
||||
const succeeded = harness({
|
||||
claim: {
|
||||
data: { ...claimedPayload, claimed: false, status: 'succeeded', documentId: 'doc-1', systemPrompt: null, transcript: null },
|
||||
error: null,
|
||||
},
|
||||
})
|
||||
assertEquals(await succeeded.run(), {
|
||||
status: 200,
|
||||
body: { document: { id: 'doc-1' }, idempotent: true },
|
||||
})
|
||||
|
||||
const inProgress = harness({
|
||||
claim: { data: { ...claimedPayload, claimed: false, systemPrompt: null, transcript: null }, error: null },
|
||||
})
|
||||
assertEquals(await inProgress.run(), { status: 409, body: { error: 'generation_in_progress' } })
|
||||
|
||||
const failed = harness({
|
||||
claim: { data: { ...claimedPayload, claimed: false, status: 'failed', systemPrompt: null, transcript: null }, error: null },
|
||||
})
|
||||
assertEquals(await failed.run(), { status: 409, body: { error: 'generation_failed' } })
|
||||
|
||||
for (const h of [succeeded, inProgress, failed]) {
|
||||
assertEquals(h.providerCalls.length, 0)
|
||||
assertEquals(h.failures, [])
|
||||
}
|
||||
})
|
||||
|
||||
Deno.test('parseClaim rejects a null claimed flag', () => {
|
||||
assertEquals(parseClaim({ ...claimedPayload, claimed: null }), null)
|
||||
assertEquals(parseClaim(claimedPayload)?.claimed, true)
|
||||
})
|
||||
|
||||
Deno.test('mapDatabaseError keeps the HTTP contract', () => {
|
||||
const log = () => {}
|
||||
assertEquals(mapDatabaseError({ code: '42501' }, log).status, 403)
|
||||
assertEquals(mapDatabaseError({ code: 'P0002' }, log).status, 404)
|
||||
assertEquals(mapDatabaseError({ code: '22023' }, log).status, 400)
|
||||
assertEquals(mapDatabaseError({ code: 'P0001', message: 'generation_quota_exceeded' }, log), {
|
||||
status: 429,
|
||||
body: { error: 'quota_exceeded' },
|
||||
})
|
||||
assertEquals(mapDatabaseError(null, log), { status: 500, body: { error: 'internal_error' } })
|
||||
})
|
||||
|
||||
Deno.test('buildProviderRequest bounds output and wraps the template prompt', () => {
|
||||
const payload = buildProviderRequest({ ...claimedPayload, status: 'processing' } as Parameters<typeof buildProviderRequest>[0])
|
||||
assertEquals(payload.max_tokens, PROVIDER_MAX_TOKENS)
|
||||
assertEquals(payload.stream, false)
|
||||
assertEquals(payload.model, 'claude-haiku-4-5-20251001')
|
||||
assertEquals(payload.messages.length, 1)
|
||||
assertEquals(payload.system.includes('Summarize.'), true)
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue