teqo/tests/unit/ciClassifyProduction.unit.spec.ts
Francisco Solla 9d7e4a1414 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
- job check (host, sem pnpm): SHA deployado via ssh + diff + classificador
  .dockerignore (scripts/ci-classify-production.mjs, lib/dockerignore.mjs)
- suite fail-open no gate; deploy duplo-gated (checks + production)
- deploy-homeserver.sh: guard 'already deployed' (idempotente)
- agent-pool.yml removido; pool dormente
- specs: dockerignoreMatch, ciClassifyProduction, pins ciSkipInvariants/deployScript
- docs: runbook, AGENTS.md, AGENT-OPS.md, changelog
2026-08-18 20:06:22 -03:00

116 lines
4 KiB
TypeScript

// @vitest-environment node
import { execFileSync } from 'node:child_process'
import { mkdtempSync, readFileSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
import { classifyProduction } from '../../scripts/ci-classify-production.mjs'
import { parseDockerignore } from '../../scripts/lib/dockerignore.mjs'
const repoRoot = join(fileURLToPath(new URL('.', import.meta.url)), '../..')
const cliPath = join(repoRoot, 'scripts/ci-classify-production.mjs')
const realRules = parseDockerignore(readFileSync(join(repoRoot, '.dockerignore'), 'utf8'))
let fixturesDir: string
let filesFrom: string
beforeAll(() => {
fixturesDir = mkdtempSync(join(tmpdir(), 'ci-classify-production-'))
filesFrom = join(fixturesDir, 'changed.txt')
})
afterAll(() => {
execFileSync('rm', ['-rf', fixturesDir])
})
const runCli = (args: string[]) => {
const env = { ...process.env, GITHUB_EVENT_NAME: 'schedule' }
return execFileSync('node', [cliPath, ...args], { encoding: 'utf8', env })
}
const writeChanged = (paths: string[]) => writeFileSync(filesFrom, paths.join('\n'))
describe('classifyProduction (pure)', () => {
it('keeps paths the docker build context receives', () => {
const { production, kept, skipped } = classifyProduction(
['src/app/page.tsx', 'docs/plans/x.md', 'package.json'],
realRules,
)
expect(production).toBe(true)
expect(kept).toEqual(['src/app/page.tsx', 'package.json'])
expect(skipped).toEqual(['docs/plans/x.md'])
})
it('reports no production change when every path is ignored', () => {
const { production, kept, skipped } = classifyProduction(
['docs/ops/teqo-1313-deploy.md', '.agents/skills/foo/SKILL.md'],
realRules,
)
expect(production).toBe(false)
expect(kept).toEqual([])
expect(skipped).toHaveLength(2)
})
})
describe('ci-classify-production CLI (OPS65 gate)', () => {
it('reports JSON with production, changed, kept and skipped', () => {
writeChanged(['docs/plans/x.md', 'src/lib/foo.ts'])
const result = JSON.parse(runCli(['--files-from', filesFrom]))
expect(result.production).toBe(true)
expect(result.kept).toEqual(['src/lib/foo.ts'])
expect(result.skipped).toEqual(['docs/plans/x.md'])
})
it('--value prints only the boolean', () => {
writeChanged(['docs/plans/x.md'])
expect(runCli(['--files-from', filesFrom, '--value']).trim()).toBe('false')
writeChanged(['src/app/page.tsx'])
expect(runCli(['--files-from', filesFrom, '--value']).trim()).toBe('true')
})
it('an empty diff means nothing to do (production=false)', () => {
writeChanged([])
const result = JSON.parse(runCli(['--files-from', filesFrom]))
expect(result.production).toBe(false)
expect(result.reason).toContain('nada a fazer')
})
it('fails open when the deployed SHA is unknown', () => {
const result = JSON.parse(runCli(['--deployed', '']))
expect(result.production).toBe(true)
expect(result.reason).toContain('sem SHA deployado')
})
it('fails open without any input at all', () => {
const result = JSON.parse(runCli([]))
expect(result.production).toBe(true)
})
it('fails open on an unreadable .dockerignore', () => {
writeChanged(['src/app/page.tsx'])
const env = { ...process.env, GITHUB_EVENT_NAME: 'schedule' }
const out = execFileSync('node', [cliPath, '--files-from', filesFrom], {
encoding: 'utf8',
env,
cwd: fixturesDir,
})
const result = JSON.parse(out)
expect(result.production).toBe(true)
expect(result.reason).toContain('.dockerignore ilegível')
})
it('workflow_dispatch always runs the full pipeline', () => {
const env = { ...process.env, GITHUB_EVENT_NAME: 'workflow_dispatch' }
const out = execFileSync('node', [cliPath, '--files-from', filesFrom], {
encoding: 'utf8',
env,
})
const result = JSON.parse(out)
expect(result.production).toBe(true)
expect(result.reason).toContain('workflow_dispatch')
})
})