mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-15 10:43:07 +00:00
A importação do perfil oficial deixa a janela fixa de 12 mídias e ganha o recorte por período (desde; até opcional), com prévia honesta (novas · já estavam · total) e confirmação antes de criar; "Recentes" segue o default e o clique único. O planner puro de janela/dedupe passa ao dono em src/lib (CLI re-exporta) e a varredura usa o teto de páginas derivado de maxResults, com early-stop por data e corte denunciado (truncated). Sem migration/Consent; tudo Rascunho; fail-closed de credencial intacto. Design tier: DEGRADED (central-conteudos-importar-janela-ui-design) — port classe-a-classes das cenas 01–08 com sign-off humano explícito (2026-09-29); a crítica de paridade do designer no tier primário não pôde rodar por quota.
179 lines
5.8 KiB
TypeScript
179 lines
5.8 KiB
TypeScript
// @vitest-environment node
|
|
|
|
import { describe, expect, it } from 'vitest'
|
|
|
|
import {
|
|
contentPieceProfileFeedIdentityUrls,
|
|
planContentPieceProfileWindow,
|
|
} from '@/lib/contentPieceProfileWindow'
|
|
|
|
import {
|
|
formatInstagramContentReport,
|
|
instagramContentFeedIdentityUrls,
|
|
instagramContentReportStamp,
|
|
parseInstagramContentCliArgs,
|
|
planInstagramContentWindow,
|
|
shouldPublishImportedPiece,
|
|
summarizeInstagramContentResults,
|
|
} from '../../scripts/lib/instagramContentPlan.mjs'
|
|
|
|
// C230-followup — the import planner contract: the recency window filter, the
|
|
// identity dedupe (in both directions: the feed against the Central and the
|
|
// feed against itself), the link-only classification and the honest report.
|
|
// The feed is always a plain array; no network, no Payload.
|
|
|
|
const post = (
|
|
shortcode: string,
|
|
{
|
|
mediaType = 'REEL',
|
|
mediaUrl = `https://cdn.example/${shortcode}.mp4`,
|
|
timestamp = '2026-09-20T12:00:00.000Z',
|
|
permalink,
|
|
}: {
|
|
mediaType?: string
|
|
mediaUrl?: string | null
|
|
timestamp?: string
|
|
permalink?: string
|
|
} = {},
|
|
) => ({
|
|
id: shortcode,
|
|
permalink: permalink ?? `https://www.instagram.com/reel/${shortcode}/`,
|
|
mediaType,
|
|
mediaUrl,
|
|
timestamp,
|
|
})
|
|
|
|
describe('content:instagram:import arguments', () => {
|
|
it('defaults to the 60-day plan with no writes', () => {
|
|
expect(parseInstagramContentCliArgs([])).toEqual({
|
|
apply: false,
|
|
publish: false,
|
|
days: 60,
|
|
limit: null,
|
|
out: 'data/content-instagram',
|
|
help: false,
|
|
})
|
|
})
|
|
|
|
it('accepts the apply/publish pair and the window options', () => {
|
|
expect(
|
|
parseInstagramContentCliArgs([
|
|
'--apply',
|
|
'--publish',
|
|
'--days',
|
|
'45',
|
|
'--limit',
|
|
'3',
|
|
'--out',
|
|
'data/out',
|
|
]),
|
|
).toMatchObject({ apply: true, publish: true, days: 45, limit: 3, out: 'data/out' })
|
|
})
|
|
|
|
it('refuses --publish without --apply', () => {
|
|
expect(() => parseInstagramContentCliArgs(['--publish'])).toThrow(/exige --apply/)
|
|
})
|
|
|
|
it('refuses an unknown argument', () => {
|
|
expect(() => parseInstagramContentCliArgs(['--force'])).toThrow(/argumento desconhecido/)
|
|
})
|
|
|
|
it('refuses an out-of-range window, a zero limit and an escaping out dir', () => {
|
|
expect(() => parseInstagramContentCliArgs(['--days', '0'])).toThrow(/--days/)
|
|
expect(() => parseInstagramContentCliArgs(['--days', '366'])).toThrow(/--days/)
|
|
expect(() => parseInstagramContentCliArgs(['--limit', '0'])).toThrow(/--limit/)
|
|
expect(() => parseInstagramContentCliArgs(['--out', '../etc'])).toThrow(/--out/)
|
|
})
|
|
|
|
it('prints the help before any validation', () => {
|
|
expect(parseInstagramContentCliArgs(['--help', '--days', '0'])).toMatchObject({ help: true })
|
|
})
|
|
})
|
|
|
|
describe('planner re-exports (C235)', () => {
|
|
it('keeps the CLI names bound to the single owner in src/lib', () => {
|
|
// The window policy is pinned by its owner spec; here only the delegation
|
|
// contract of the CLI shell matters.
|
|
expect(planInstagramContentWindow).toBe(planContentPieceProfileWindow)
|
|
expect(instagramContentFeedIdentityUrls).toBe(contentPieceProfileFeedIdentityUrls)
|
|
})
|
|
})
|
|
|
|
describe('summaries and receipt', () => {
|
|
it('publishes only a ready piece when the run asked for it', () => {
|
|
expect(shouldPublishImportedPiece({ processingStatus: 'pronto', publish: true })).toBe(true)
|
|
expect(shouldPublishImportedPiece({ processingStatus: 'pronto', publish: false })).toBe(false)
|
|
expect(shouldPublishImportedPiece({ processingStatus: 'falhou', publish: true })).toBe(false)
|
|
})
|
|
|
|
it('aggregates the run with the provider cost', () => {
|
|
const summary = summarizeInstagramContentResults(
|
|
[
|
|
{
|
|
outcome: 'created',
|
|
processingStatus: 'pronto',
|
|
durationSeconds: 60,
|
|
published: true,
|
|
linkFailureReason: null,
|
|
},
|
|
{
|
|
outcome: 'created',
|
|
processingStatus: 'pronto',
|
|
durationSeconds: 30,
|
|
published: true,
|
|
linkFailureReason: 'carrossel',
|
|
},
|
|
{ outcome: 'created', processingStatus: 'falhou', error: 'ffmpeg' },
|
|
{ outcome: 'existing' },
|
|
],
|
|
{ costPerMinuteUsd: 0.00045 },
|
|
)
|
|
|
|
expect(summary).toMatchObject({
|
|
attempted: 4,
|
|
created: 3,
|
|
existing: 1,
|
|
ready: 2,
|
|
failed: 1,
|
|
published: 2,
|
|
linkOnly: 1,
|
|
asrSeconds: 90,
|
|
})
|
|
expect(summary.asrCostUsd).toBeCloseTo(0.0007, 4)
|
|
})
|
|
|
|
it('reports the window, the plan and the named failures', () => {
|
|
const plan = planInstagramContentWindow({
|
|
posts: [post('A'), post('B', { mediaType: 'CAROUSEL_ALBUM', mediaUrl: null })],
|
|
existingSourceUrls: [],
|
|
from: '2026-08-01T00:00:00.000Z',
|
|
to: '2026-09-30T00:00:00.000Z',
|
|
})
|
|
const lines = formatInstagramContentReport({
|
|
target: '127.0.0.1/teqo_1313',
|
|
mode: 'apply',
|
|
window: { days: 60, from: '2026-08-01T00:00:00.000Z', to: '2026-09-30T00:00:00.000Z' },
|
|
feedCount: 2,
|
|
plan,
|
|
summary: summarizeInstagramContentResults([
|
|
{ outcome: 'created', processingStatus: 'falhou', error: 'ffmpeg ausente' },
|
|
]),
|
|
failures: [{ shortcode: 'A', error: 'ffmpeg ausente' }],
|
|
candidatesRemaining: 1,
|
|
reportPath: 'data/content-instagram/x.json',
|
|
})
|
|
|
|
const text = lines.join('\n')
|
|
expect(text).toContain('alvo: 127.0.0.1/teqo_1313 | modo: apply')
|
|
expect(text).toContain('janela: 60 dias')
|
|
expect(text).toContain('candidatos: 2 (peça-link previstos: 1)')
|
|
expect(text).toContain('falha A: ffmpeg ausente')
|
|
expect(text).toContain('peça-link B: carrossel')
|
|
expect(text).toContain('restam 1 candidatos')
|
|
expect(text).toContain('recibo: data/content-instagram/x.json')
|
|
})
|
|
|
|
it('stamps the receipt with the run instant', () => {
|
|
expect(instagramContentReportStamp('2026-09-29T12:34:56.789Z')).toBe('2026-09-29T12-34-56-789Z')
|
|
})
|
|
})
|