101 lines
3.8 KiB
TypeScript
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')
|
|
})
|