mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 06:43:06 +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.
128 lines
3.8 KiB
TypeScript
128 lines
3.8 KiB
TypeScript
// @vitest-environment node
|
|
|
|
import { describe, expect, it } from 'vitest'
|
|
|
|
import {
|
|
FACE_ENROLL_DEFAULT_OUT_DIR,
|
|
faceEnrollReportStamp,
|
|
formatFaceEnrollReport,
|
|
parseFaceEnrollCliArgs,
|
|
} from '../../scripts/lib/faceEnrollPlan.mjs'
|
|
|
|
// C234 — the pure parser/report of `pnpm faces:enroll`: every refusal happens
|
|
// before any engine call or DB connection, and the human lines never carry the
|
|
// descriptor.
|
|
|
|
describe('parseFaceEnrollCliArgs', () => {
|
|
it('defaults to the dry-run with the standard receipt dir', () => {
|
|
const options = parseFaceEnrollCliArgs(['--label', 'Pessoa', '--selfie', '/tmp/selfie.jpg'])
|
|
expect(options).toEqual({
|
|
apply: false,
|
|
label: 'Pessoa',
|
|
selfie: '/tmp/selfie.jpg',
|
|
subject: null,
|
|
out: FACE_ENROLL_DEFAULT_OUT_DIR,
|
|
help: false,
|
|
})
|
|
})
|
|
|
|
it('parses --apply, --subject, --out and --help', () => {
|
|
const options = parseFaceEnrollCliArgs([
|
|
'--apply',
|
|
'--label',
|
|
' Pessoa ',
|
|
'--selfie',
|
|
'/tmp/selfie.jpg',
|
|
'--subject',
|
|
'7',
|
|
'--out',
|
|
'data/outro',
|
|
])
|
|
expect(options.apply).toBe(true)
|
|
expect(options.label).toBe(' Pessoa ')
|
|
expect(options.subject).toBe(7)
|
|
expect(options.out).toBe('data/outro')
|
|
|
|
expect(parseFaceEnrollCliArgs(['--help']).help).toBe(true)
|
|
})
|
|
|
|
it('fails closed on missing label/selfie, bad subject and unknown args', () => {
|
|
expect(() => parseFaceEnrollCliArgs(['--selfie', '/tmp/a.jpg'])).toThrow(/--label/)
|
|
expect(() => parseFaceEnrollCliArgs(['--label', 'Pessoa'])).toThrow(/--selfie/)
|
|
expect(() => parseFaceEnrollCliArgs(['--label', ' ', '--selfie', '/tmp/a.jpg'])).toThrow(
|
|
/--label/,
|
|
)
|
|
expect(() =>
|
|
parseFaceEnrollCliArgs(['--label', 'P', '--selfie', '/tmp/a.jpg', '--subject', '0']),
|
|
).toThrow(/--subject/)
|
|
expect(() =>
|
|
parseFaceEnrollCliArgs(['--label', 'P', '--selfie', '/tmp/a.jpg', '--nope']),
|
|
).toThrow(/argumento desconhecido/)
|
|
expect(() => parseFaceEnrollCliArgs(['--label'])).toThrow(/faltou valor/)
|
|
expect(() =>
|
|
parseFaceEnrollCliArgs(['--label', 'P', '--selfie', '/tmp/a.jpg', '--out', '../etc']),
|
|
).toThrow(/--out/)
|
|
})
|
|
})
|
|
|
|
describe('faceEnrollReportStamp', () => {
|
|
it('is filesystem-safe', () => {
|
|
expect(faceEnrollReportStamp('2026-09-27T16:42:01.123Z')).toBe('2026-09-27T16-42-01-123Z')
|
|
})
|
|
})
|
|
|
|
describe('formatFaceEnrollReport', () => {
|
|
const base = {
|
|
runAt: '2026-09-27T16:42:01.123Z',
|
|
mode: 'apply',
|
|
target: '127.0.0.1/teqo_wt234',
|
|
model: 'face-api@1.7.15/faceRecognitionNet',
|
|
label: 'Pessoa',
|
|
subjectId: 3,
|
|
subjectAction: 'created' as const,
|
|
faceCount: 1,
|
|
allowed: true,
|
|
reason: null,
|
|
consentKey: 'busca-selfie-indice',
|
|
consentHash: 'abc',
|
|
durationMs: 10,
|
|
}
|
|
|
|
it('reports the enrollment without ever mentioning a descriptor', () => {
|
|
const lines = formatFaceEnrollReport(base)
|
|
const text = lines.join('\n')
|
|
|
|
expect(text).toContain('criada: #3 (Pessoa)')
|
|
expect(text).toContain('busca-selfie-indice')
|
|
expect(text).toContain('pnpm faces:index --apply')
|
|
expect(text).not.toMatch(/descriptor|vetor|vector/i)
|
|
})
|
|
|
|
it('shows the dry-run as a plan, never as a write', () => {
|
|
const lines = formatFaceEnrollReport({
|
|
...base,
|
|
mode: 'plan',
|
|
subjectAction: null,
|
|
subjectId: null,
|
|
})
|
|
const text = lines.join('\n')
|
|
|
|
expect(text).toContain('plano: Pessoa (nada gravado')
|
|
expect(text).not.toContain('criada')
|
|
})
|
|
|
|
it('shows the refusal reason and no subject line', () => {
|
|
const lines = formatFaceEnrollReport({
|
|
...base,
|
|
allowed: false,
|
|
subjectId: null,
|
|
subjectAction: null,
|
|
faceCount: 2,
|
|
reason: 'mais de um rosto na selfie',
|
|
})
|
|
const text = lines.join('\n')
|
|
|
|
expect(text).toContain('RECUSADO: mais de um rosto na selfie')
|
|
expect(text).not.toContain('criada')
|
|
})
|
|
})
|