feat(release): prepare 1.1.0 candidate
This commit is contained in:
parent
5a34f66981
commit
5205dcdfa9
736 changed files with 115667 additions and 12203 deletions
151
apps/mobile-rn/__tests__/content-report-service.test.ts
Normal file
151
apps/mobile-rn/__tests__/content-report-service.test.ts
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
import {
|
||||
ContentReportError,
|
||||
submitAiContentReport,
|
||||
} from '../src/features/reporting/content-report-service'
|
||||
|
||||
const originalFetch = global.fetch
|
||||
const GENERATION_ID = '11111111-1111-4111-8111-111111111111'
|
||||
const IDEMPOTENCY_KEY = '22222222-2222-4222-8222-222222222222'
|
||||
const REPORT_ID = '33333333-3333-4333-8333-333333333333'
|
||||
|
||||
function response(status: number, body: unknown): Response {
|
||||
return {
|
||||
ok: status >= 200 && status < 300,
|
||||
status,
|
||||
json: async () => body,
|
||||
} as unknown as Response
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
global.fetch = originalFetch
|
||||
jest.restoreAllMocks()
|
||||
})
|
||||
|
||||
describe('content report service', () => {
|
||||
it('sends the exact authenticated Talk report contract and validates the receipt', async () => {
|
||||
global.fetch = jest.fn().mockResolvedValue(response(201, {
|
||||
reportId: REPORT_ID,
|
||||
status: 'submitted',
|
||||
idempotent: false,
|
||||
createdAt: '2026-08-24T08:00:00.000Z',
|
||||
}))
|
||||
|
||||
await expect(submitAiContentReport({
|
||||
accessToken: ' user-token ',
|
||||
idempotencyKey: IDEMPOTENCY_KEY,
|
||||
generationId: GENERATION_ID,
|
||||
sourceType: 'talk_response',
|
||||
reason: 'privacy',
|
||||
comment: ' exposed a name ',
|
||||
snapshot: ' AI output ',
|
||||
})).resolves.toEqual({
|
||||
reportId: REPORT_ID,
|
||||
status: 'submitted',
|
||||
idempotent: false,
|
||||
createdAt: '2026-08-24T08:00:00.000Z',
|
||||
})
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledTimes(1)
|
||||
const [url, request] = (global.fetch as jest.Mock).mock.calls[0]
|
||||
expect(url).toMatch(/\/functions\/v1\/content-report$/)
|
||||
expect(request.headers).toMatchObject({
|
||||
Authorization: 'Bearer user-token',
|
||||
'Content-Type': 'application/json',
|
||||
'Idempotency-Key': IDEMPOTENCY_KEY,
|
||||
})
|
||||
expect(JSON.parse(request.body)).toEqual({
|
||||
kind: 'ai_output',
|
||||
source: { type: 'talk_response', generationId: GENERATION_ID },
|
||||
reason: 'privacy',
|
||||
comment: 'exposed a name',
|
||||
snapshot: 'AI output',
|
||||
})
|
||||
})
|
||||
|
||||
it('sends an owned generated meeting document through the same report boundary', async () => {
|
||||
global.fetch = jest.fn().mockResolvedValue(response(201, {
|
||||
reportId: REPORT_ID,
|
||||
status: 'submitted',
|
||||
idempotent: false,
|
||||
createdAt: '2026-08-29T08:00:00.000Z',
|
||||
}))
|
||||
|
||||
await submitAiContentReport({
|
||||
accessToken: 'user-token',
|
||||
idempotencyKey: IDEMPOTENCY_KEY,
|
||||
generationId: GENERATION_ID,
|
||||
sourceType: 'meeting_document',
|
||||
reason: 'misinformation',
|
||||
snapshot: 'Generated meeting document excerpt',
|
||||
})
|
||||
|
||||
const [, request] = (global.fetch as jest.Mock).mock.calls[0]
|
||||
expect(JSON.parse(request.body)).toEqual({
|
||||
kind: 'ai_output',
|
||||
source: { type: 'meeting_document', generationId: GENERATION_ID },
|
||||
reason: 'misinformation',
|
||||
snapshot: 'Generated meeting document excerpt',
|
||||
})
|
||||
})
|
||||
|
||||
it('rejects invalid or oversized input before any network request', async () => {
|
||||
global.fetch = jest.fn()
|
||||
|
||||
await expect(submitAiContentReport({
|
||||
accessToken: 'token',
|
||||
idempotencyKey: 'not-a-uuid',
|
||||
generationId: GENERATION_ID,
|
||||
sourceType: 'talk_response',
|
||||
reason: 'other',
|
||||
snapshot: 'output',
|
||||
})).rejects.toBeInstanceOf(ContentReportError)
|
||||
await expect(submitAiContentReport({
|
||||
accessToken: 'token',
|
||||
idempotencyKey: IDEMPOTENCY_KEY,
|
||||
generationId: GENERATION_ID,
|
||||
sourceType: 'talk_response',
|
||||
reason: 'other',
|
||||
snapshot: 'x'.repeat(4_001),
|
||||
})).rejects.toMatchObject({ code: 'INVALID_REQUEST', retryable: false })
|
||||
|
||||
expect(global.fetch).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it.each([
|
||||
[401, { error: 'unauthorized' }, 'AUTH_REQUIRED', false],
|
||||
[404, { error: 'source_not_found' }, 'SOURCE_NOT_FOUND', false],
|
||||
[409, { error: 'source_already_reported' }, 'ALREADY_REPORTED', false],
|
||||
[409, { error: 'idempotency_conflict' }, 'IDEMPOTENCY_CONFLICT', false],
|
||||
[429, { error: 'report_rate_limited' }, 'RATE_LIMITED', true],
|
||||
[503, { error: 'server_error' }, 'SERVER_ERROR', true],
|
||||
] as const)('maps HTTP %s without leaking raw server text', async (status, body, code, retryable) => {
|
||||
global.fetch = jest.fn().mockResolvedValue(response(status, body))
|
||||
|
||||
await expect(submitAiContentReport({
|
||||
accessToken: 'token',
|
||||
idempotencyKey: IDEMPOTENCY_KEY,
|
||||
generationId: GENERATION_ID,
|
||||
sourceType: 'talk_response',
|
||||
reason: 'harmful',
|
||||
snapshot: 'output',
|
||||
})).rejects.toMatchObject({ code, retryable, status })
|
||||
})
|
||||
|
||||
it('fails closed when the server returns an unverified success shape', async () => {
|
||||
global.fetch = jest.fn().mockResolvedValue(response(201, {
|
||||
reportId: 'raw-secret-instead-of-uuid',
|
||||
status: 'submitted',
|
||||
idempotent: false,
|
||||
createdAt: 'not-a-date',
|
||||
}))
|
||||
|
||||
await expect(submitAiContentReport({
|
||||
accessToken: 'token',
|
||||
idempotencyKey: IDEMPOTENCY_KEY,
|
||||
generationId: GENERATION_ID,
|
||||
sourceType: 'talk_response',
|
||||
reason: 'spam',
|
||||
snapshot: 'output',
|
||||
})).rejects.toMatchObject({ code: 'INVALID_RESPONSE', retryable: true })
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue