/** * `copyAnsibleRoleFilesDirs` had no test, and celilo#925 is what that cost. * * A module's Ansible role `files/` directory holds static assets — most often a * compiled binary — that cannot go through the utf-8 template pipeline. They are * copied verbatim on every generate. Except they were not: the copy omitted * `force`, which Node defaults to true and bun does not, so the FIRST generate * populated `generated/` and no later one ever replaced it. * * Nothing surfaced it. `cp` reports no error, so the caller's try/catch caught * nothing; Ansible then installed the first binary forever and reported `ok`, * unchanged, while the module's version field advanced past it. On the live * fleet that read as a successful deploy of code that never shipped. * * The overwrite case is therefore the point of this file. A test that only * copied into an empty directory would have passed throughout. */ import { describe, expect, test } from 'bun:test'; import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { copyAnsibleRoleFilesDirs } from './generator'; function moduleWithRoleFile(binary: string): string { const root = mkdtempSync(join(tmpdir(), 'celilo-rolefiles-')); const filesDir = join(root, 'ansible', 'roles', 'demo', 'files'); mkdirSync(filesDir, { recursive: true }); writeFileSync(join(filesDir, 'demo-linux-x86_64'), binary); return root; } const generatedBinary = (out: string): string => readFileSync(join(out, 'ansible', 'roles', 'demo', 'files', 'demo-linux-x86_64'), 'utf-8'); describe('copyAnsibleRoleFilesDirs', () => { test('populates an empty generated tree', async () => { const modulePath = moduleWithRoleFile('v1'); const outputPath = mkdtempSync(join(tmpdir(), 'celilo-out-')); await copyAnsibleRoleFilesDirs(modulePath, outputPath); expect(generatedBinary(outputPath)).toBe('v1'); }); /** * celilo#925 in one assertion. This is the case that regressed, and the only * one that can catch it: the destination already exists, and a rebuilt * artifact has to replace it. */ test('OVERWRITES an artifact a previous generate already placed', async () => { const outputPath = mkdtempSync(join(tmpdir(), 'celilo-out-')); await copyAnsibleRoleFilesDirs(moduleWithRoleFile('v1'), outputPath); expect(generatedBinary(outputPath)).toBe('v1'); // The module is rebuilt at a new version; generate runs again. await copyAnsibleRoleFilesDirs(moduleWithRoleFile('v2'), outputPath); expect(generatedBinary(outputPath)).toBe('v2'); }); test('a module with no role files/ directory is not an error', async () => { const root = mkdtempSync(join(tmpdir(), 'celilo-norole-')); mkdirSync(join(root, 'ansible', 'roles', 'demo', 'tasks'), { recursive: true }); const outputPath = mkdtempSync(join(tmpdir(), 'celilo-out-')); await copyAnsibleRoleFilesDirs(root, outputPath); }); });