import { describe, it, expect } from 'vitest'; import { resolve, assembleVariables } from '@beehexa/hexasync-template-compose'; import { makeVirtualIO } from './fixtures'; const ROOT = '/vfs'; describe('projectResolver — single-level inheritance (Story 1.2)', () => { it('expands one type: project into the referenced project raw partials', async () => { const io = makeVirtualIO({ '/vfs/integration/partials/main.yaml': `externals:\n - type: project\n path: ../../base\n`, '/vfs/integration/partials/Local_Pusher.yaml': `pushers:\n - id: LOCAL\n`, '/vfs/base/partials/main.yaml': `externals: []\n`, '/vfs/base/partials/Base_Pusher.yaml': `pushers:\n - id: BASE\n`, }); const r = await resolve( '/vfs/integration/partials', (await io.readFileIfExists('/vfs/integration/partials/main.yaml'))!, io, ); const paths = r.worklist.map((w) => w.path); expect(paths).toContain('/vfs/base/partials/Base_Pusher.yaml'); expect(paths).toContain('/vfs/integration/partials/Local_Pusher.yaml'); expect(r.hasInheritance).toBe(true); // WorklistItem carries the full provenance superset shape (AD-5). const base = r.worklist.find((w) => w.path.includes('Base_Pusher'))!; expect(base).toMatchObject({ generation: 1, project: 'base' }); }); it('reports no inheritance when only type: local / globbed partials exist', async () => { const io = makeVirtualIO({ '/vfs/p/partials/main.yaml': `externals: []\n`, '/vfs/p/partials/A.yaml': `pushers:\n - id: A\n`, }); const r = await resolve( '/vfs/p/partials', (await io.readFileIfExists('/vfs/p/partials/main.yaml'))!, io, ); expect(r.hasInheritance).toBe(false); expect(r.worklist.every((w) => w.generation === 0)).toBe(true); }); }); describe('projectResolver — transitive & diamond (Story 1.3)', () => { const diamond = () => makeVirtualIO({ '/vfs/int/partials/main.yaml': `externals:\n - type: project\n path: ../../metrics\n`, '/vfs/int/partials/Int.yaml': `pushers:\n - id: INT\n`, '/vfs/metrics/partials/main.yaml': `externals:\n - type: project\n path: ../../misa\n weight: 10\n - type: project\n path: ../../sapo\n weight: 20\n`, '/vfs/metrics/partials/Met.yaml': `pushers:\n - id: MET\n`, '/vfs/misa/partials/main.yaml': `externals:\n - type: project\n path: ../../shared\n`, '/vfs/misa/partials/Misa.yaml': `pushers:\n - id: MISA\n`, '/vfs/sapo/partials/main.yaml': `externals:\n - type: project\n path: ../../shared\n`, '/vfs/sapo/partials/Sapo.yaml': `pushers:\n - id: SAPO\n`, '/vfs/shared/partials/main.yaml': `externals: []\n`, '/vfs/shared/partials/Shared.yaml': `pushers:\n - id: SHARED\n`, }); it('resolves all projects across levels', async () => { const io = diamond(); const r = await resolve( '/vfs/int/partials', (await io.readFileIfExists('/vfs/int/partials/main.yaml'))!, io, ); const projects = r.projects.map((p) => p.project).sort(); expect(projects).toEqual(['int', 'metrics', 'misa', 'sapo', 'shared']); }); it('dedupes a shared ancestor (diamond) keeping the shallowest occurrence', async () => { const io = diamond(); const r = await resolve( '/vfs/int/partials', (await io.readFileIfExists('/vfs/int/partials/main.yaml'))!, io, ); const sharedRows = r.projects.filter((p) => p.project === 'shared'); expect(sharedRows).toHaveLength(1); // not added twice }); it('throws with the full cycle path on a cyclic reference', async () => { const io = makeVirtualIO({ '/vfs/a/partials/main.yaml': `externals:\n - type: project\n path: ../../b\n`, '/vfs/b/partials/main.yaml': `externals:\n - type: project\n path: ../../a\n`, }); await expect( resolve( '/vfs/a/partials', (await io.readFileIfExists('/vfs/a/partials/main.yaml'))!, io, ), ).rejects.toThrow(/inheritance cycle detected/); }); it('treats missing/empty externals as [] and still globs local partials', async () => { const io = makeVirtualIO({ '/vfs/p/partials/main.yaml': `pushers:\n - id: ONLY\n`, // no externals key at all '/vfs/p/partials/Extra.yaml': `pushers:\n - id: EXTRA\n`, }); const r = await resolve( '/vfs/p/partials', (await io.readFileIfExists('/vfs/p/partials/main.yaml'))!, io, ); expect(r.worklist.map((w) => w.path)).toContain( '/vfs/p/partials/Extra.yaml', ); }); }); describe('projectResolver — precedence (Story 1.4)', () => { it('orders phases: inherited (deepest first, weight asc) then local last', async () => { const io = makeVirtualIO({ '/vfs/int/partials/main.yaml': `externals:\n - type: project\n path: ../../misa\n weight: 10\n - type: project\n path: ../../sapo\n weight: 20\n`, '/vfs/int/partials/Local.yaml': `pushers:\n - id: LOCAL\n`, '/vfs/misa/partials/main.yaml': `externals: []\n`, '/vfs/misa/partials/M.yaml': `pushers:\n - id: M\n`, '/vfs/sapo/partials/main.yaml': `externals: []\n`, '/vfs/sapo/partials/S.yaml': `pushers:\n - id: S\n`, }); const r = await resolve( '/vfs/int/partials', (await io.readFileIfExists('/vfs/int/partials/main.yaml'))!, io, ); const order = r.worklist.map((w) => w.project); // gen 1 siblings first (misa w10 before sapo w20 => sapo wins/later), local last. expect(order.indexOf('misa')).toBeLessThan(order.indexOf('sapo')); expect(order.indexOf('sapo')).toBeLessThan(order.lastIndexOf('int')); }); it('local files: lower index merges last (wins)', async () => { const io = makeVirtualIO({ '/vfs/p/partials/main.yaml': `externals:\n - type: local\n path: ./High.yaml\n index: 5\n - type: local\n path: ./Low.yaml\n index: 1\n`, '/vfs/p/partials/High.yaml': `pushers:\n - id: H\n`, '/vfs/p/partials/Low.yaml': `pushers:\n - id: L\n`, }); const r = await resolve( '/vfs/p/partials', (await io.readFileIfExists('/vfs/p/partials/main.yaml'))!, io, ); const idx = (name: string) => r.worklist.findIndex((w) => w.path.includes(name)); expect(idx('High.yaml')).toBeLessThan(idx('Low.yaml')); // lower index (Low) is later => wins }); it('sibling type:project order follows array sequence when no index', async () => { const io = makeVirtualIO({ '/vfs/d/partials/main.yaml': `externals:\n - type: project\n path: ../../b\n - type: project\n path: ../../c\n`, '/vfs/b/partials/main.yaml': `externals: []\n`, '/vfs/b/partials/B.yaml': `pushers:\n - id: X\n`, '/vfs/c/partials/main.yaml': `externals: []\n`, '/vfs/c/partials/C.yaml': `pushers:\n - id: X\n`, }); const r = await resolve( '/vfs/d/partials', (await io.readFileIfExists('/vfs/d/partials/main.yaml'))!, io, ); const order = r.worklist.map((w) => w.project); // array order [b, c] => b composes first, c later (overwrites b). expect(order.indexOf('b')).toBeLessThan(order.indexOf('c')); }); it('explicit index reorders sibling type:project compose sequence', async () => { const io = makeVirtualIO({ '/vfs/d/partials/main.yaml': `externals:\n - type: project\n path: ../../b\n index: 1\n - type: project\n path: ../../c\n index: 0\n`, '/vfs/b/partials/main.yaml': `externals: []\n`, '/vfs/b/partials/B.yaml': `pushers:\n - id: X\n`, '/vfs/c/partials/main.yaml': `externals: []\n`, '/vfs/c/partials/C.yaml': `pushers:\n - id: X\n`, }); const r = await resolve( '/vfs/d/partials', (await io.readFileIfExists('/vfs/d/partials/main.yaml'))!, io, ); const order = r.worklist.map((w) => w.project); // index says c(0) before b(1) => c composes first, b later (overwrites c). expect(order.indexOf('c')).toBeLessThan(order.indexOf('b')); }); it('throws on a non-finite weight', async () => { const io = makeVirtualIO({ '/vfs/p/partials/main.yaml': `externals:\n - type: project\n path: ../../b\n weight: not-a-number\n`, '/vfs/b/partials/main.yaml': `externals: []\n`, }); await expect( resolve( '/vfs/p/partials', (await io.readFileIfExists('/vfs/p/partials/main.yaml'))!, io, ), ).rejects.toThrow(/invalid weight/); }); }); describe('assembleVariables — flat pool nearest-wins (Story 1.5)', () => { it('current project overrides ancestor on token collision', async () => { const sets = [ { project: 'anc', generation: 2, variables: { '**X**': 'ancestor', '**Y**': 'y' }, }, { project: 'cur', generation: 0, variables: { '**X**': 'current' } }, ]; const pool = assembleVariables(sets); expect(pool['**X**']).toBe('current'); expect(pool['**Y**']).toBe('y'); }); }); describe('projectResolver — per-project replacements (Story 1.6)', () => { it('stamps the project ref replacements onto every discovered partial', async () => { const io = makeVirtualIO({ '/vfs/int/partials/main.yaml': `externals:\n - type: project\n path: ../../base\n replacements:\n - fromKey: '**GEN**'\n toKey: '**INT**'\n`, '/vfs/base/partials/main.yaml': `externals: []\n`, '/vfs/base/partials/B.yaml': `pushers:\n - id: '**GEN**'\n`, }); const r = await resolve( '/vfs/int/partials', (await io.readFileIfExists('/vfs/int/partials/main.yaml'))!, io, ); const b = r.worklist.find((w) => w.path.includes('B.yaml'))!; expect(b.replacements).toEqual([{ fromKey: '**GEN**', toKey: '**INT**' }]); }); }); void ROOT;