// @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') }) })