d3ro-voice/server/supabase/functions/_shared/content-report-contract.test.ts
2026-08-29 18:33:45 +09:00

101 lines
3.8 KiB
TypeScript

import {
ContentReportRequestError,
mapContentReportRpcError,
parseContentReportReceipt,
parseContentReportRequest,
parseIdempotencyKey,
} from './content-report-contract.ts'
function assert(condition: boolean, message: string): void {
if (!condition) throw new Error(message)
}
function rejects(action: () => unknown): boolean {
try {
action()
return false
} catch (error) {
return error instanceof ContentReportRequestError
}
}
const generationId = '29100000-0000-4000-8000-000000000001'
Deno.test('content report contract normalizes a bounded Talk AI report', () => {
const report = parseContentReportRequest({
kind: 'ai_output',
source: { type: 'talk_response', generationId: generationId.toUpperCase() },
reason: 'harmful',
comment: ' review this ',
snapshot: ' generated answer ',
})
assert(report.source.generationId === generationId, 'generation id should be normalized')
assert(report.comment === 'review this', 'comment should be trimmed')
assert(report.snapshot === 'generated answer', 'snapshot should be trimmed')
})
Deno.test('content report contract admits only implemented AI response surfaces', () => {
for (const sourceType of [
'talk_response',
'command_response',
'action_response',
'meeting_document',
]) {
const report = parseContentReportRequest({
kind: 'ai_output',
source: { type: sourceType, generationId },
reason: 'other',
snapshot: 'answer',
})
assert(report.source.type === sourceType, `${sourceType} should be supported`)
}
})
Deno.test('content report contract rejects unknown fields, sources, reasons and bad lengths', () => {
const valid = {
kind: 'ai_output',
source: { type: 'talk_response', generationId },
reason: 'other',
snapshot: 'answer',
}
const invalid: unknown[] = [
{ ...valid, extra: true },
{ ...valid, source: { ...valid.source, extra: true } },
{ ...valid, kind: 'user_content' },
{ ...valid, source: { type: 'chat_message', generationId } },
{ ...valid, reason: 'dislike' },
{ ...valid, snapshot: ' ' },
{ ...valid, snapshot: 'x'.repeat(4_001) },
{ ...valid, comment: '' },
{ ...valid, comment: 'x'.repeat(501) },
]
for (const value of invalid) {
assert(rejects(() => parseContentReportRequest(value)), 'invalid report was accepted')
}
})
Deno.test('content report idempotency and RPC receipts require strict UUIDs and shape', () => {
assert(parseIdempotencyKey(generationId.toUpperCase()) === generationId, 'header UUID not normalized')
assert(rejects(() => parseIdempotencyKey(null)), 'missing idempotency header was accepted')
assert(rejects(() => parseIdempotencyKey('not-a-uuid')), 'bad idempotency header was accepted')
const receipt = parseContentReportReceipt({
reportId: generationId,
status: 'submitted',
idempotent: false,
createdAt: '2026-08-24T00:00:00.000Z',
})
assert(receipt.reportId === generationId && !receipt.idempotent, 'valid receipt was not parsed')
assert(rejects(() => parseContentReportReceipt({ reportId: generationId })), 'bad RPC result was accepted')
})
Deno.test('content report RPC errors expose stable non-sensitive API codes', () => {
assert(mapContentReportRpcError({ message: 'content_report_source_not_found' }).status === 404,
'source error should be hidden behind 404')
assert(mapContentReportRpcError({ message: 'content_report_rate_limited' }).status === 429,
'rate error should map to 429')
assert(mapContentReportRpcError({ message: 'content_report_idempotency_conflict' }).status === 409,
'idempotency error should map to 409')
assert(mapContentReportRpcError({ message: 'database detail that must not escape' }).code === 'internal_error',
'unknown database details should not be exposed')
})