teqo-backup/tests/unit/dockerignoreMatch.unit.spec.ts
Francisco Solla 9504c7ccf6 ops(OPS65): CI do main em janela de 30 min (gate .dockerignore fail-open) + matar agent-pool
- ci.yml: on push removido → schedule */30 + workflow_dispatch (fundido com a
  estrutura de job único sequencial do OPS62 X1)
- job gate (host, main-only, sem pnpm): revision do container rodando no
  homeserver (label org.opencontainers.image.revision — o compose pode mentir
  após rollback falho) + diff + classificador .dockerignore
- fail-open: revision desconhecida → suite roda (verde); incapaz de classificar
  → run vermelho (nunca blackout silencioso); deploy duplo-gated
  (needs: [checks, gate] + if explícito)
- deploy-homeserver.sh: guard 'already deployed' idempotente (mesma revision)
- agent-pool.yml removido; pool dormente (144 pnpm install/dia no tick */10)
- specs: dockerignoreMatch, ciClassifyProduction (incl. git-diff com repo
  scratch e exit codes), pins ciSkipInvariants/deployScript; HIGH_RISK_EXACT
- docs: runbook (janela/gate/verificação ao vivo/falhas), AGENTS.md,
  AGENT-OPS.md (tabela X1 fundida), skills pool dormente, changelog OPS65
- impl plan aprovado; intenção marcada entregue
2026-08-18 21:08:12 -03:00

107 lines
3.7 KiB
TypeScript

// @vitest-environment node
import { readFileSync } from 'node:fs'
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { describe, expect, it } from 'vitest'
import {
compileDockerignoreRule,
isDockerignored,
parseDockerignore,
} from '../../scripts/lib/dockerignore.mjs'
const repoRoot = join(fileURLToPath(new URL('.', import.meta.url)), '../..')
const dockerignore = readFileSync(join(repoRoot, '.dockerignore'), 'utf8')
describe('dockerignoreMatch (OPS65 production-change gate)', () => {
it('compiles the moby semantics: basename at any depth, subtree exclusion', () => {
const rules = parseDockerignore('docs\n*.log\n')
expect(isDockerignored('docs', rules)).toBe(true)
expect(isDockerignored('docs/plans/x.md', rules)).toBe(true)
expect(isDockerignored('a/docs/plans/x.md', rules)).toBe(true)
expect(isDockerignored('x.log', rules)).toBe(true)
expect(isDockerignored('a/b/x.log', rules)).toBe(true)
expect(isDockerignored('src/app/page.tsx', rules)).toBe(false)
})
it('anchors leading-slash patterns to the context root', () => {
const rules = parseDockerignore('/*.png\n')
expect(isDockerignored('a.png', rules)).toBe(true)
expect(isDockerignored('sub/b.png', rules)).toBe(false)
})
it('honours ! negations with last-match-wins', () => {
const rules = parseDockerignore('.env.*\n!.env.example\n')
expect(isDockerignored('.env.local', rules)).toBe(true)
expect(isDockerignored('.env.example', rules)).toBe(false)
expect(isDockerignored('a/.env.example', rules)).toBe(false)
})
it('supports globstar and directory-only patterns', () => {
const rules = parseDockerignore('**/fixtures\na/**/b\n')
expect(isDockerignored('x/fixtures/y.json', rules)).toBe(true)
expect(isDockerignored('a/b', rules)).toBe(true)
expect(isDockerignored('a/x/b', rules)).toBe(true)
expect(isDockerignored('a/x/c/b', rules)).toBe(true)
})
it('ignores blank lines and comments', () => {
const rules = parseDockerignore('# header\n\ndocs\n')
expect(rules).toHaveLength(1)
expect(isDockerignored('docs/x', rules)).toBe(true)
})
it('escapes regex metacharacters in plain patterns', () => {
const rules = parseDockerignore('foo.bar\n')
expect(isDockerignored('foo.bar', rules)).toBe(true)
expect(isDockerignored('fooXbar', rules)).toBe(false)
})
it('treats the repo .dockerignore as the source of truth', () => {
const rules = parseDockerignore(dockerignore)
// Anything the image needs is production: changes force the full suite.
for (const path of [
'src/app/page.tsx',
'src/migrations/20260818_x.ts',
'public/favicon.ico',
'Dockerfile',
'.dockerignore',
'AGENTS.md',
'package.json',
'pnpm-lock.yaml',
'scripts/deploy-homeserver.sh',
'scripts/ci-classify-production.mjs',
'.forgejo/workflows/ci.yml',
'.forgejo/workflows/ci-pr.yml',
'tests/unit/dockerignoreMatch.unit.spec.ts',
'.env.example',
]) {
expect(isDockerignored(path, rules), path).toBe(false)
}
// Non-artifact paths skip the suite (quiet days cost seconds).
for (const path of [
'docs/plans/x.md',
'docs/ops/teqo-1313-deploy.md',
'.agents/skills/foo/SKILL.md',
'.env.local',
'.env.test.local',
'data/tse/x.zip',
'media/x.jpg',
'foo.png',
'x.log',
'x.tsbuildinfo',
'node_modules/x',
'.next/x',
'.next-e2e/x',
]) {
expect(isDockerignored(path, rules), path).toBe(true)
}
})
it('keeps the .dockerignore itself explicit (self-referential gate)', () => {
const rule = compileDockerignoreRule('!.dockerignore')
expect(rule.negate).toBe(true)
})
})