teqo-backup/tests/unit/archiveCatalogPlan.unit.spec.ts
Francisco Solla 671af1e21f
Some checks are pending
Deploy / preflight (push) Waiting to run
Deploy / verify (push) Blocked by required conditions
Deploy / deploy staging (teqo-staging) (push) Blocked by required conditions
Deploy / requeue when main advanced (push) Blocked by required conditions
Deploy / deploy production (teqo-1313) (push) Blocked by required conditions
feat(C232): pré-catalogar o acervo de fotos com IA local e curadoria que vence
Collection archivePhoto ganha o grupo catalog (legenda/descrição/cena/texto
visível/temas/pessoas/município/origem/catalogedAt), curatedFields (o que a
assessoria edita nunca é sobrescrito), takenOn e searchText com índice GIN trgm
(migration aditiva add_archive_photo_catalog); hook beforeValidate deriva o
índice e marca curadoria só em edição humana; pipeline transacional baixa os
bytes privados, prepara JPEG <=1024px com sharp e mescla o VLM local com o
gazetteer/catálogo curado (pessoas só do TEXTO, nunca de rosto); CLI
pnpm archive:catalog (plan/--apply/--verify, recibo honesto, idempotente por
catalogedAt) com engine OpenAI-compatible na rede privada atrás de guarda de
host (ARCHIVE_VISION_ALLOW_REMOTE é o escape); client HTTP com retry/timeout e
parser zod fail-closed; resolver de município extraído para owner compartilhado
com o catálogo de peças delegando; unit/int novos e runbook staging-primeiro.
2026-09-26 12:30:34 -03:00

141 lines
4.7 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import {
ARCHIVE_CATALOG_DEFAULT_OUT_DIR,
formatArchiveCatalogInventory,
formatArchiveCatalogReport,
parseArchiveCatalogCliArgs,
summarizeArchiveCatalogInventory,
summarizeArchiveCatalogResults,
} from '../../scripts/lib/archiveCatalogPlan.mjs'
// C232 — the pure CLI planning/reporting of the archive cataloguing: the argv
// contract, the run summaries and the `--verify` inventory the receipt and the
// stdout lines are built from.
describe('parseArchiveCatalogCliArgs (C232)', () => {
it('defaults to the plan mode and the canonical receipt dir', () => {
expect(parseArchiveCatalogCliArgs([])).toEqual({
apply: false,
verify: false,
limit: null,
out: ARCHIVE_CATALOG_DEFAULT_OUT_DIR,
help: false,
})
})
it('parses the modes and options', () => {
expect(
parseArchiveCatalogCliArgs(['--apply', '--limit', '5', '--out', 'data/x']),
).toMatchObject({
apply: true,
limit: 5,
out: 'data/x',
})
expect(parseArchiveCatalogCliArgs(['--verify'])).toMatchObject({ verify: true })
expect(parseArchiveCatalogCliArgs(['--help'])).toMatchObject({ help: true })
})
it('refuses conflicting, unknown and unsafe input', () => {
expect(() => parseArchiveCatalogCliArgs(['--apply', '--verify'])).toThrow(/mutuamente/)
expect(() => parseArchiveCatalogCliArgs(['--limit', '0'])).toThrow(/--limit/)
expect(() => parseArchiveCatalogCliArgs(['--limit'])).toThrow(/faltou valor/)
expect(() => parseArchiveCatalogCliArgs(['--force'])).toThrow(/desconhecido/)
expect(() => parseArchiveCatalogCliArgs(['--out', '../fora'])).toThrow(/\.\./)
})
})
describe('summarizeArchiveCatalogResults (C232)', () => {
it('counts the outcomes, the sources and names every failure', () => {
const summary = summarizeArchiveCatalogResults([
{ flickrId: '1', status: 'cataloged', source: 'ai' },
{ flickrId: '2', status: 'cataloged', source: 'none' },
{ flickrId: '3', status: 'skipped' },
{ flickrId: '4', status: 'failed', stage: 'analyze', error: 'engine fora do ar' },
])
expect(summary).toEqual({
cataloged: 2,
skipped: 1,
failed: 1,
bySource: { ai: 1, metadata: 0, none: 1 },
failures: [{ flickrId: '4', stage: 'analyze', error: 'engine fora do ar' }],
})
})
})
describe('summarizeArchiveCatalogInventory (C232)', () => {
it('separates catalogued from pending and flags the missing file', () => {
const inventory = summarizeArchiveCatalogInventory([
{
flickrId: '1',
filename: 'a.jpg',
catalog: { catalogedAt: 'x', source: 'ai', scene: 'evento', municipality: 1 },
},
{ flickrId: '2', filename: 'b.jpg', catalog: { catalogedAt: 'x', source: 'metadata' } },
{ flickrId: '3', filename: 'c.jpg', catalog: {} },
{ flickrId: '4', filename: null, catalog: { catalogedAt: 'x', source: 'none' } },
])
expect(inventory).toMatchObject({
total: 4,
cataloged: 3,
pending: 1,
bySource: { ai: 1, metadata: 1, none: 1 },
withoutScene: 2,
withoutMunicipality: 2,
missingFilename: ['4'],
})
})
})
describe('report lines (C232)', () => {
it('renders the plan line with the pending count', () => {
const lines = formatArchiveCatalogReport({
mode: 'plan',
target: '127.0.0.1/teqo_staging',
engine: { host: '100.94.122.26', model: 'qwen2.5vl:7b', scope: 'local' },
pending: 12,
durationMs: 1500,
})
expect(lines[0]).toContain('100.94.122.26 (qwen2.5vl:7b, local)')
expect(lines.join('\n')).toContain('pendentes na fila: 12')
})
it('renders the apply summary and every failure', () => {
const lines = formatArchiveCatalogReport({
mode: 'apply',
target: '127.0.0.1/teqo_staging',
durationMs: 2000,
summary: summarizeArchiveCatalogResults([
{ flickrId: '1', status: 'cataloged', source: 'ai' },
{ flickrId: '2', status: 'failed', stage: 'analyze', error: 'timeout' },
]),
})
const text = lines.join('\n')
expect(text).toContain('processadas: 1')
expect(text).toContain('falharam: 1')
expect(text).toContain('IA: 1')
expect(text).toContain('2 (analyze): timeout')
})
it('renders the verify inventory with the pending gap', () => {
const lines = formatArchiveCatalogInventory({
total: 3,
cataloged: 2,
pending: 1,
bySource: { ai: 1, metadata: 1, none: 0 },
withoutScene: 1,
withoutMunicipality: 1,
missingFilename: [],
})
const text = lines.join('\n')
expect(text).toContain('catalogadas: 2')
expect(text).toContain('pendentes: 1')
expect(text).toContain('IA: 1')
expect(text).toContain('ainda pendentes de catalogação: 1')
})
})