teqo-backup/tests/unit/contentPieceProfileWindow.unit.spec.ts
Francisco Solla 8608aeb317 feat(C235): importa qualquer janela de publicações do perfil na Central
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.
2026-09-29 13:23:01 -03:00

223 lines
7.5 KiB
TypeScript

// @vitest-environment node
import { describe, expect, it } from 'vitest'
import {
contentPieceProfileFeedIdentityUrls,
contentPieceProfilePeriodBounds,
contentPieceProfilePeriodError,
contentPieceProfileWindowLabel,
contentPieceProfileWindowStartReached,
planContentPieceProfileWindow,
} from '@/lib/contentPieceProfileWindow'
// C235 — the window owner: the civil-date bounds of the operator's period, the
// recency filter over the feed timestamps, the identity dedupe (Central and
// feed itself) and the honest coverage check. Pure: no Payload, no network.
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('planContentPieceProfileWindow', () => {
const from = '2026-08-01T03:00:00.000Z'
const to = '2026-09-21T03:00:00.000Z'
it('keeps the window novelties, newest first, and classifies each link-only kind', () => {
const plan = planContentPieceProfileWindow({
posts: [
post('NOVO'),
post('CARROSSEL', { mediaType: 'CAROUSEL_ALBUM', mediaUrl: null }),
post('PROTEGIDO', { mediaUrl: null }),
],
from,
to,
})
expect(plan.found).toBe(3)
expect(plan.existingCount).toBe(0)
expect(plan.outsideWindow).toBe(0)
expect(plan.malformed).toBe(0)
expect(plan.window).toEqual({ from, to })
expect(plan.candidates.map((candidate) => candidate.shortcode)).toEqual([
'NOVO',
'CARROSSEL',
'PROTEGIDO',
])
expect(plan.candidates.map((candidate) => candidate.linkOnlyReason)).toEqual([
null,
'carrossel',
'indisponivel',
])
})
it('drops what falls outside the window, the malformed and the feed twins', () => {
const plan = planContentPieceProfileWindow({
posts: [
post('JA', { permalink: 'https://www.instagram.com/reel/JA/' }),
post('ANTES', { timestamp: '2026-07-31T23:59:59.000Z' }),
post('DEPOIS', { timestamp: '2026-09-22T00:00:00.000Z' }),
post('SEMID', { permalink: 'https://example.com/no-id/' }),
post('REPETIDA'),
post('REPETIDA', { permalink: 'https://www.instagram.com/p/REPETIDA/' }),
],
existingSourceUrls: ['https://www.instagram.com/p/JA/'],
from,
to,
})
expect(plan.found).toBe(3)
expect(plan.existingCount).toBe(1)
expect(plan.existingShortcodes).toEqual(['JA'])
expect(plan.outsideWindow).toBe(2)
expect(plan.malformed).toBe(1)
expect(plan.feedDuplicates).toBe(1)
expect(plan.candidates.map((candidate) => candidate.shortcode)).toEqual(['REPETIDA'])
})
it('treats an unbounded window as the whole feed (the recency mode)', () => {
const plan = planContentPieceProfileWindow({
posts: [
post('ANTIGA', { timestamp: '2020-01-01T00:00:00.000Z' }),
post('NOVA', { timestamp: '2026-09-20T12:00:00.000Z' }),
],
})
expect(plan.window).toEqual({ from: null, to: null })
expect(plan.found).toBe(2)
expect(plan.outsideWindow).toBe(0)
expect(plan.candidates.map((candidate) => candidate.shortcode)).toEqual(['ANTIGA', 'NOVA'])
})
it('refuses an invalid bound', () => {
expect(() => planContentPieceProfileWindow({ posts: [], from: 'ontem' })).toThrow(/janela/)
expect(() => planContentPieceProfileWindow({ posts: [], to: 'amanhã' })).toThrow(/janela/)
})
})
describe('contentPieceProfilePeriodError', () => {
const today = '2026-09-29'
it('accepts a real, ordered, non-future period', () => {
expect(contentPieceProfilePeriodError({ since: '2026-08-01', today })).toBeNull()
expect(
contentPieceProfilePeriodError({ since: '2026-08-01', until: '2026-08-31', today }),
).toBeNull()
expect(contentPieceProfilePeriodError({ since: today, today })).toBeNull()
})
it('rejects a malformed or impossible date', () => {
expect(contentPieceProfilePeriodError({ since: '01/08/2026', today })).toBe(
'Informe uma data inicial válida.',
)
expect(contentPieceProfilePeriodError({ since: '2026-02-30', today })).toBe(
'Informe uma data inicial válida.',
)
expect(
contentPieceProfilePeriodError({ since: '2026-08-01', until: '2026-02-30', today }),
).toBe('Informe uma data final válida.')
})
it('rejects an inverted or future period', () => {
expect(
contentPieceProfilePeriodError({ since: '2026-08-31', until: '2026-08-01', today }),
).toBe('A data final não pode ser anterior à data inicial.')
expect(contentPieceProfilePeriodError({ since: '2026-10-01', today })).toBe(
'A data inicial não pode estar no futuro.',
)
})
})
describe('contentPieceProfilePeriodBounds', () => {
it('resolves the start of the since day and the exclusive end of the until day in Bahia', () => {
expect(contentPieceProfilePeriodBounds({ since: '2026-08-01' })).toEqual({
fromIso: '2026-08-01T03:00:00.000Z',
toIso: null,
})
expect(contentPieceProfilePeriodBounds({ since: '2026-08-01', until: '2026-08-31' })).toEqual({
fromIso: '2026-08-01T03:00:00.000Z',
toIso: '2026-09-01T03:00:00.000Z',
})
})
it('refuses an impossible date defensively', () => {
expect(() => contentPieceProfilePeriodBounds({ since: '2026-13-01' })).toThrow(
'Informe uma data inicial válida.',
)
})
})
describe('contentPieceProfileWindowStartReached', () => {
const fromIso = '2026-08-01T03:00:00.000Z'
it('is reached when the walk got a post older than the window start', () => {
expect(
contentPieceProfileWindowStartReached({
posts: [post('NOVA'), post('ANTES', { timestamp: '2026-07-31T23:59:59.000Z' })],
fromIso,
}),
).toBe(true)
})
it('is not reached when every post is still inside the window', () => {
expect(contentPieceProfileWindowStartReached({ posts: [post('NOVA')], fromIso })).toBe(false)
expect(contentPieceProfileWindowStartReached({ posts: [], fromIso })).toBe(false)
})
it('is reached when there is no bound to reach', () => {
expect(contentPieceProfileWindowStartReached({ posts: [], fromIso: null })).toBe(true)
})
})
describe('contentPieceProfileWindowLabel', () => {
it('labels the recency and the civil period', () => {
expect(contentPieceProfileWindowLabel({ mode: 'recent' })).toBe('Recentes')
expect(contentPieceProfileWindowLabel({ mode: 'period', since: '2026-08-01' })).toBe(
'01/08/2026 → hoje',
)
expect(
contentPieceProfileWindowLabel({
mode: 'period',
since: '2026-08-01',
until: '2026-08-31',
}),
).toBe('01/08/2026 → 31/08/2026')
})
})
describe('contentPieceProfileFeedIdentityUrls', () => {
it('spells every canonical kind of each parseable post once', () => {
expect(
contentPieceProfileFeedIdentityUrls([
post('A'),
post('B', { permalink: 'https://www.instagram.com/p/B/' }),
post('A', { permalink: 'https://www.instagram.com/p/A/' }),
post('X', { permalink: 'https://example.com/x/' }),
]),
).toEqual([
'https://www.instagram.com/p/A/',
'https://www.instagram.com/reel/A/',
'https://www.instagram.com/tv/A/',
'https://www.instagram.com/p/B/',
'https://www.instagram.com/reel/B/',
'https://www.instagram.com/tv/B/',
])
})
})