import assert from 'node:assert/strict' import { spawnSync } from 'node:child_process' import fs from 'node:fs' import path from 'node:path' import process from 'node:process' // eslint-disable-next-line test/no-import-node-test -- Exercise actual CI exit codes and report files. import test from 'node:test' import { ciContext, writeContext } from '../scripts/ci-context.mjs' import { evaluateJobs, renderSummary, requiredJobs } from '../scripts/ci-summary.mjs' const success = () => Object.fromEntries(requiredJobs.map(job => [job, { result: 'success' }])) test('CI summary requires every configured group and does not mutate provider results', () => { assert.deepEqual(requiredJobs, ['checks', 'coverage', 'browser-smoke', 'browser-consumers']) const needs = success() const copy = structuredClone(needs) const report = evaluateJobs(needs) assert.equal(report.passed, true) assert.deepEqual(needs, copy) assert.deepEqual(report.rows.map(row => row.job), requiredJobs) }) for (const result of ['failure', 'cancelled', 'skipped', 'unknown', undefined]) { test(`CI summary fails when any required matrix group returns ${result}`, () => { for (const job of requiredJobs) { const needs = success() needs[job].result = result assert.equal(evaluateJobs(needs).passed, false) delete needs[job] assert.equal(evaluateJobs(needs).passed, false) } }) } test('CI rejects malformed or additional job results and does not render arbitrary status text', () => { for (const input of [null, [], 'success', {}, { ...success(), extra: { result: 'success' } }]) assert.equal(evaluateJobs(input).passed, false) const needs = success() needs.checks.result = '' assert(!renderSummary(evaluateJobs(needs)).includes(' { assert(path.dirname(root) === path.resolve('refactor/.cache') && path.basename(root).startsWith('ci-summary-test-')) fs.rmSync(root, { recursive: true, force: true }) }) return root } test('CI context uses exact source/tool/lock inputs and writes absolute cache paths via environment files', (t) => { const root = fixture(t) const output = path.join(root, 'outputs') const envFile = path.join(root, 'environment') const env = { GITHUB_OUTPUT: output, GITHUB_ENV: envFile, RUNNER_TEMP: path.join(root, 'runner temp') } const context = writeContext(root, env) assert.match(context.source, /^[a-f0-9]{40}$/) assert.equal(context.yarn, '1.22.22') assert(fs.readFileSync(output, 'utf8').includes(`playwright=${context.playwright}\n`)) assert(fs.readFileSync(envFile, 'utf8').includes(`PLAYWRIGHT_BROWSERS_PATH=${context.browserCache}\n`)) assert.equal(context.yarnCache, path.join(env.RUNNER_TEMP, 'artplayer-yarn-cache')) fs.appendFileSync(path.join(root, 'yarn.lock'), '\n') assert.notEqual(ciContext(root, env).lockSha256, context.lockSha256) const manifest = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')) manifest.packageManager = 'npm@1.22.22' fs.writeFileSync(path.join(root, 'package.json'), JSON.stringify(manifest)) assert.throws(() => ciContext(root, env), { name: 'AssertionError' }) }) test('CI command returns real failure codes and preserves reports for success, failure and invalid JSON', (t) => { const root = fixture(t) const stepSummary = path.join(root, 'step-summary.md') for (const [needs, expected] of [[JSON.stringify(success()), 0], [JSON.stringify({ ...success(), coverage: { result: 'cancelled' } }), 1], ['{bad', 1]]) { const result = spawnSync(process.execPath, [path.resolve('scripts/ci-summary.mjs')], { cwd: root, encoding: 'utf8', env: { ...process.env, ARTPLAYER_CI_NEEDS: needs, GITHUB_STEP_SUMMARY: stepSummary } }) assert.equal(result.status, expected, result.stderr) const report = JSON.parse(fs.readFileSync(path.join(root, 'refactor/.cache/ci/summary.json'), 'utf8')) assert.equal(report.passed, expected === 0) assert(fs.readFileSync(path.join(root, 'refactor/.cache/ci/summary.md'), 'utf8').includes(expected === 0 ? '**passed**' : '**failed**')) } assert(fs.readFileSync(stepSummary, 'utf8').includes('cancelled')) })