mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 15:13:07 +00:00
A catalogação do C232 nunca rodou em produção: catalog.* zerado nas 6.492 fotos publicadas e o engine de visão fora do ar. O pnpm archive:catalog ganha --metadata-only: deriva município (gazetteer) e temas do texto já ingerido, sem engine e sem mídia, grava catalog.metadataCheckedAt (migration aditiva) e não marca catalogedAt — a fila da IA segue aberta. Nunca escreve catalog.people (C244 assume a faceta por facial). A passada de IA passa a carimbar os dois markers; a fila de metadados exclui linha já catalogada pela IA (não rebaixa o source); o --verify relata as duas camadas com exit pela selecionada; o --apply de metadados dispensa S3_* e engine. Runbook §C245: metadados → revalidate archivePhotos → IA. Closes #1417
201 lines
6.8 KiB
TypeScript
201 lines
6.8 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
|
|
import {
|
|
ARCHIVE_CATALOG_DEFAULT_OUT_DIR,
|
|
archiveCatalogLayerLabel,
|
|
archiveCatalogPendingForLayer,
|
|
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. C245 adds the layer selector and the two-layer
|
|
// inventory.
|
|
|
|
describe('parseArchiveCatalogCliArgs (C232)', () => {
|
|
it('defaults to the plan mode, the AI layer and the canonical receipt dir', () => {
|
|
expect(parseArchiveCatalogCliArgs([])).toEqual({
|
|
apply: false,
|
|
verify: false,
|
|
metadataOnly: 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('parses the metadata-only layer selector in every mode', () => {
|
|
expect(parseArchiveCatalogCliArgs(['--metadata-only'])).toMatchObject({
|
|
metadataOnly: true,
|
|
apply: false,
|
|
verify: false,
|
|
})
|
|
expect(parseArchiveCatalogCliArgs(['--metadata-only', '--apply'])).toMatchObject({
|
|
metadataOnly: true,
|
|
apply: true,
|
|
})
|
|
expect(parseArchiveCatalogCliArgs(['--metadata-only', '--verify'])).toMatchObject({
|
|
metadataOnly: true,
|
|
verify: 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/C245)', () => {
|
|
it('separates both layers, counts the provenance 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' } },
|
|
{
|
|
flickrId: '5',
|
|
filename: 'e.jpg',
|
|
catalog: { metadataCheckedAt: 'x', source: 'metadata', municipality: 9 },
|
|
},
|
|
])
|
|
|
|
expect(inventory).toEqual({
|
|
total: 5,
|
|
ai: { cataloged: 3, pending: 2 },
|
|
metadata: { checked: 4, pending: 1 },
|
|
bySource: { ai: 1, metadata: 2, none: 1 },
|
|
withoutScene: 2,
|
|
withoutMunicipality: 2,
|
|
missingFilename: ['4'],
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('archiveCatalogLayerLabel (C245)', () => {
|
|
it('labels the two layers in pt-BR', () => {
|
|
expect(archiveCatalogLayerLabel('ai')).toBe('IA')
|
|
expect(archiveCatalogLayerLabel('metadata')).toBe('metadados')
|
|
})
|
|
|
|
it('reads the pending gap of the selected layer only', () => {
|
|
const inventory = {
|
|
ai: { cataloged: 3, pending: 7 },
|
|
metadata: { checked: 6, pending: 4 },
|
|
}
|
|
expect(archiveCatalogPendingForLayer(inventory, 'metadata')).toBe(4)
|
|
expect(archiveCatalogPendingForLayer(inventory, 'ai')).toBe(7)
|
|
})
|
|
})
|
|
|
|
describe('report lines (C232/C245)', () => {
|
|
it('renders the plan line with the layer and the pending count', () => {
|
|
const lines = formatArchiveCatalogReport({
|
|
mode: 'plan',
|
|
layer: 'ai',
|
|
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)')
|
|
const text = lines.join('\n')
|
|
expect(text).toContain('camada: IA')
|
|
expect(text).toContain('pendentes na fila: 12')
|
|
})
|
|
|
|
it('renders the metadata-only plan without an engine line', () => {
|
|
const lines = formatArchiveCatalogReport({
|
|
mode: 'plan',
|
|
layer: 'metadata',
|
|
engine: null,
|
|
pending: 3,
|
|
durationMs: 10,
|
|
})
|
|
|
|
const text = lines.join('\n')
|
|
expect(text).toContain('camada: metadados')
|
|
expect(text).not.toContain('engine:')
|
|
})
|
|
|
|
it('renders the apply summary and every failure', () => {
|
|
const lines = formatArchiveCatalogReport({
|
|
mode: 'apply',
|
|
layer: 'metadata',
|
|
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('camada: metadados')
|
|
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 of each layer', () => {
|
|
const lines = formatArchiveCatalogInventory({
|
|
total: 5,
|
|
ai: { cataloged: 3, pending: 2 },
|
|
metadata: { checked: 4, pending: 1 },
|
|
bySource: { ai: 1, metadata: 2, none: 1 },
|
|
withoutScene: 2,
|
|
withoutMunicipality: 2,
|
|
missingFilename: [],
|
|
})
|
|
|
|
const text = lines.join('\n')
|
|
expect(text).toContain('camada IA: catalogadas 3 · pendentes 2')
|
|
expect(text).toContain('camada metadados: verificadas 4 · pendentes 1')
|
|
expect(text).toContain('pendentes de metadados: 1')
|
|
expect(text).toContain('pendentes de IA: 2')
|
|
})
|
|
})
|