import { describe, it, expect, afterEach, vi } from 'vitest'; import fs from 'fs'; import os from 'os'; import path from 'path'; import { parse } from 'yaml'; import { composeProfile } from '../composeCommand'; let tmp = ''; afterEach(() => { if (tmp && fs.existsSync(tmp)) fs.rmSync(tmp, { recursive: true, force: true }); tmp = ''; }); function write(p: string, content: string) { fs.mkdirSync(path.dirname(p), { recursive: true }); fs.writeFileSync(p, content); } describe('composeProfile — end-to-end project inheritance', () => { it('composes an inherited project and writes output.yaml + integrity report', async () => { tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'compose-')); // base project write(path.join(tmp, 'base/partials/main.yaml'), `externals: []\n`); write( path.join(tmp, 'base/partials/Base.yaml'), `pushers:\n - id: BASE\n name: base\n step: original\n`, ); // integration inherits base and customizes BASE + adds LOCAL write( path.join(tmp, 'integration/partials/main.yaml'), `externals:\n - type: project\n path: ../../base\n weight: 10\n`, ); write( path.join(tmp, 'integration/partials/Local.yaml'), `pushers:\n - id: BASE\n step: overridden\n - id: LOCAL\n name: local\n`, ); await composeProfile(path.join(tmp, 'integration'), false); const outPath = path.join(tmp, 'integration/output.yaml'); const reportPath = path.join(tmp, 'integration/output.report.md'); expect(fs.existsSync(outPath)).toBe(true); expect(fs.existsSync(reportPath)).toBe(true); const out = parse(fs.readFileSync(outPath, 'utf8')); const pushers: any[] = out.pushers; const base = pushers.find((p) => p.id === 'BASE'); const local = pushers.find((p) => p.id === 'LOCAL'); expect(local).toBeTruthy(); // local component added // local (generation 0) wins over inherited base value expect(base.step).toBe('overridden'); // inherited field preserved from base expect(base.name).toBe('base'); const report = fs.readFileSync(reportPath, 'utf8'); expect(report).toContain('Merge Integrity Report'); expect(report).toContain('base'); // composed project listed }); it('does not falsely flag tokenized ids as LOST, nor emit externals NOTE noise (CR fixes)', async () => { tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'compose-')); // base uses a variable-token id and a non-empty externals list write(path.join(tmp, 'base/partials/main.yaml'), `externals: []\n`); write( path.join(tmp, 'base/partials/variables.yaml'), `variables:\n '**BaseId**': resolved-base\n`, ); write( path.join(tmp, 'base/partials/Base.yaml'), `pushers:\n - id: '**BaseId**'\n name: base\n`, ); write( path.join(tmp, 'integration/partials/main.yaml'), `externals:\n - type: project\n path: ../../base\n weight: 10\n`, ); let warned = ''; const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); try { await composeProfile(path.join(tmp, 'integration'), false); warned = warn.mock.calls.flat().join('\n'); // capture BEFORE restore (restore resets calls) } finally { warn.mockRestore(); } // token id resolved in output const out = parse( fs.readFileSync(path.join(tmp, 'integration/output.yaml'), 'utf8'), ); expect(out.pushers.find((p: any) => p.id === 'resolved-base')).toBeTruthy(); // report has no real LOST finding and no wholesale-overwrite NOTE // (the counts table always lists a "⚠ LOST | 0" row, so match the warning phrasing instead) const report = fs.readFileSync( path.join(tmp, 'integration/output.report.md'), 'utf8', ); expect(report).toContain('= inherited'); expect(report).not.toMatch(/LOST \(unexplained\)/); expect(report).not.toMatch(/overwritten wholesale/); // and no LOST/NOTE warning was printed to console expect(warned).not.toMatch(/LOST|overwritten wholesale/); }); it('warns on a replacement toKey collision across projects (Story 1.6 AC)', async () => { tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'compose-')); write(path.join(tmp, 'base/partials/main.yaml'), `externals: []\n`); write( path.join(tmp, 'base/partials/B.yaml'), `pushers:\n - id: '**A**'\n - id: '**B**'\n`, ); // two project refs map different fromKeys to the SAME toKey => guaranteed collision write( path.join(tmp, 'integration/partials/main.yaml'), `externals:\n - type: project\n path: ../../base\n replacements:\n - fromKey: '**A**'\n toKey: '**X**'\n - type: project\n path: ../../base\n replacements:\n - fromKey: '**B**'\n toKey: '**X**'\n`, ); let warned = ''; const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); try { await composeProfile(path.join(tmp, 'integration'), false); warned = warn.mock.calls.flat().join('\n'); // capture BEFORE restore (restore resets calls) } finally { warn.mockRestore(); } expect(warned).toMatch( /toKey "\*\*X\*\*" is produced from multiple distinct fromKeys/, ); }); it('writes no report when there is no inheritance', async () => { tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'compose-')); write(path.join(tmp, 'solo/partials/main.yaml'), `externals: []\n`); write(path.join(tmp, 'solo/partials/A.yaml'), `pushers:\n - id: A\n`); await composeProfile(path.join(tmp, 'solo'), false); expect(fs.existsSync(path.join(tmp, 'solo/output.yaml'))).toBe(true); expect(fs.existsSync(path.join(tmp, 'solo/output.report.md'))).toBe(false); }); });