mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 15:13:07 +00:00
- /fotos/encontre + POST /api/fotos/selfie: só o vetor 128-d, sem imagem, sem score e sem nomes de terceiros; fail-closed pela flag photoAlbum.selfieSearchEnabled (default false) e pelos Consents por chave estável (busca-selfie-fotos/busca-selfie-indice, não seedados — a superfície nasce fechada até o aval jurídico/DPIA). - Índice A/C do gate (PR #1370): só quem aderiu é indexado; descriptors do acervo são transitórios. Collection faceSubject (admin-only, sem vínculo com Contact), vínculos matchedPhotos e marker faces.checkedKey no archivePhoto (modelo + enrolledAt). - Engine @vladmandic/face-api nos dois lados: browser lazy com modelos same-origin (copy-face-vision-assets no prebuild/predev/Docker) e CLI node-wasm sem tfjs-node. CLIs ops-only faces:enroll / faces:index --apply|--verify com guardas C231/C232. - Opt-out self-service por reconhecimento (leave-index) + canal de remoção do C233; rate limit próprio (namespace selfie:<hash>) e bounded body extraído do beacon. - UI portada classe-a-classe do design estendido (paridade certificada, tier primário); e2e frontendFotosSelfie (manifest+curado+projeto, engine stubada) e canário do engine real; runbook §C234, contrato em AGENTS-public.md e changelog. - Migration aditiva add_face_subject (SQL revisado); hard-stops aprovados na sessão.
52 lines
1.9 KiB
TypeScript
52 lines
1.9 KiB
TypeScript
// @vitest-environment node
|
|
|
|
import { describe, expect, it } from 'vitest'
|
|
|
|
import { FACE_SEARCH_DESCRIPTOR_LENGTH } from '@/lib/faceSearch'
|
|
import { faceSearchRequestSchema } from '@/lib/schemas/faceSearch'
|
|
|
|
// C234 — the wire contract of the selfie endpoint: strict object, exact
|
|
// descriptor length, finite numbers only and the two intents. Everything else
|
|
// is a malformed body (never a silently ignored field).
|
|
|
|
const vector = (): number[] => Array.from({ length: FACE_SEARCH_DESCRIPTOR_LENGTH }, () => 0.5)
|
|
|
|
describe('faceSearchRequestSchema', () => {
|
|
it('accepts both intents with a full descriptor', () => {
|
|
expect(faceSearchRequestSchema.safeParse({ vector: vector(), intent: 'search' }).success).toBe(
|
|
true,
|
|
)
|
|
expect(
|
|
faceSearchRequestSchema.safeParse({ vector: vector(), intent: 'leave-index' }).success,
|
|
).toBe(true)
|
|
})
|
|
|
|
it('rejects unknown intents and extra keys', () => {
|
|
expect(faceSearchRequestSchema.safeParse({ vector: vector(), intent: 'enroll' }).success).toBe(
|
|
false,
|
|
)
|
|
expect(
|
|
faceSearchRequestSchema.safeParse({ vector: vector(), intent: 'search', image: 'x' }).success,
|
|
).toBe(false)
|
|
})
|
|
|
|
it('rejects descriptors of the wrong length or with non-finite values', () => {
|
|
expect(
|
|
faceSearchRequestSchema.safeParse({ vector: vector().slice(1), intent: 'search' }).success,
|
|
).toBe(false)
|
|
expect(
|
|
faceSearchRequestSchema.safeParse({
|
|
vector: [...vector().slice(1), Number.NaN],
|
|
intent: 'search',
|
|
}).success,
|
|
).toBe(false)
|
|
expect(
|
|
faceSearchRequestSchema.safeParse({
|
|
vector: [...vector().slice(1), Number.POSITIVE_INFINITY],
|
|
intent: 'search',
|
|
}).success,
|
|
).toBe(false)
|
|
expect(faceSearchRequestSchema.safeParse({ vector: {}, intent: 'search' }).success).toBe(false)
|
|
expect(faceSearchRequestSchema.safeParse({ intent: 'search' }).success).toBe(false)
|
|
})
|
|
})
|