import { lstat, mkdir, mkdtemp, readFile, rm, symlink, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, describe, expect, it } from 'vitest'; import { placeUnitFile, resolveLeaseBrokerSocketForPreflight } from './fleet.js'; /** * Unit-placement regression harness for #1292. * * The two measured defects this suite pins: * 1. `systemctl enable ` does NOT rewrite an existing by-path * wants-symlink — so placement must remove stale residue explicitly, and * acceptance asserts on the RESULTING SYMLINK TARGET, never on the enable * call's argument (asserting the call cannot see where the link ended up). * 2. Node's copyFile FOLLOWS a by-path symlink at the destination and * overwrites the SEED template. Acceptance asserts on the SEED's bytes * AND mtime — unchanged — which is the only check that can redden for * finding 2. The symlink-target assertion catches finding 1; these are * different defects with different failure modes. * * Fixtures are entirely inside tmpdirs (source template, active systemd dir, * wants dir) — no real host paths are touched by this suite. */ describe('placeUnitFile (#1292 unit placement)', () => { const cleanup: string[] = []; afterEach(async () => { while (cleanup.length > 0) { await rm(cleanup.pop()!, { recursive: true, force: true }); } }); async function fixture() { const root = await mkdtemp(join(tmpdir(), 'place-unit-')); cleanup.push(root); const seedDir = join(root, 'seed'); const activeDir = join(root, 'active'); await mkdir(seedDir, { recursive: true }); await mkdir(activeDir, { recursive: true }); const seedTemplate = join(seedDir, 'unit-under-test.service'); await writeFile( seedTemplate, '[Unit]\nDescription=seed template\n[Service]\nType=oneshot\nExecStart=/bin/true\n[Install]\nWantedBy=default.target\n', ); const activeSource = join(root, 'active-source.service'); await writeFile( activeSource, '[Unit]\nDescription=active copy v2\n[Service]\nType=oneshot\nExecStart=/bin/true\n[Install]\nWantedBy=default.target\n', ); return { root, seedDir, activeDir, seedTemplate, activeSource }; } it('places a regular file on a clean host (negative control: no residue anywhere)', async () => { const f = await fixture(); const result = await placeUnitFile(f.activeSource, f.activeDir, 'unit-under-test.service'); expect(result.unlinkedDestinationSymlink).toBe(false); expect(result.removedStaleWantsSymlink).toBe(false); const info = await lstat(join(f.activeDir, 'unit-under-test.service')); expect(info.isSymbolicLink()).toBe(false); expect(await readFile(join(f.activeDir, 'unit-under-test.service'), 'utf8')).toContain( 'active copy v2', ); // Seed untouched by construction — but assert it, so the clean-host case // cannot silently regress into seed-mutation. expect(await readFile(f.seedTemplate, 'utf8')).toContain('seed template'); }); it('by-path residue: unlinks destination symlink, places the file, seed bytes AND mtime unchanged (finding 2)', async () => { const f = await fixture(); const seedBefore = await readFile(f.seedTemplate, 'utf8'); const mtimeBefore = (await lstat(f.seedTemplate)).mtimeMs; // The fomo-lin convention: by-path enable left a symlink AT the unit name // pointing at the seed template, plus a wants-symlink doing the same. await symlink(f.seedTemplate, join(f.activeDir, 'unit-under-test.service')); const wantsDir = join(f.activeDir, 'default.target.wants'); await mkdir(wantsDir, { recursive: true }); await symlink(f.seedTemplate, join(wantsDir, 'unit-under-test.service')); const result = await placeUnitFile(f.activeSource, f.activeDir, 'unit-under-test.service'); expect(result.unlinkedDestinationSymlink).toBe(true); expect(result.removedStaleWantsSymlink).toBe(true); // FINDING 2's check: the seed is byte-identical and its mtime did not move. expect(await readFile(f.seedTemplate, 'utf8')).toBe(seedBefore); expect((await lstat(f.seedTemplate)).mtimeMs).toBe(mtimeBefore); // The destination is now a regular file carrying the ACTIVE content. const destInfo = await lstat(join(f.activeDir, 'unit-under-test.service')); expect(destInfo.isSymbolicLink()).toBe(false); expect(await readFile(join(f.activeDir, 'unit-under-test.service'), 'utf8')).toContain( 'active copy v2', ); }); it('by-path residue: no wants-symlink remains pointing at the seed (finding 1 residue cleared)', async () => { const f = await fixture(); await symlink(f.seedTemplate, join(f.activeDir, 'unit-under-test.service')); const wantsDir = join(f.activeDir, 'default.target.wants'); await mkdir(wantsDir, { recursive: true }); await symlink(f.seedTemplate, join(wantsDir, 'unit-under-test.service')); await placeUnitFile(f.activeSource, f.activeDir, 'unit-under-test.service'); // After placement the stale wants link is GONE (enable-by-name recreates // it correctly). A link still present must not point at the seed. try { const link = await lstat(join(wantsDir, 'unit-under-test.service')); if (link.isSymbolicLink()) { const target = await readFile(join(wantsDir, 'unit-under-test.service'), 'utf8').catch( async () => '', ); expect(target).not.toContain('seed template'); } } catch { // absent wants link — the expected post-placement state } }); it('idempotence: second placement on a reconciled host is a no-op producing the identical final state', async () => { const f = await fixture(); // Reconciled starting state: regular file at the name, wants link to the active copy. await writeFile( join(f.activeDir, 'unit-under-test.service'), await readFile(f.activeSource, 'utf8'), ); const wantsDir = join(f.activeDir, 'default.target.wants'); await mkdir(wantsDir, { recursive: true }); await symlink( join(f.activeDir, 'unit-under-test.service'), join(wantsDir, 'unit-under-test.service'), ); const before = await readFile(join(f.activeDir, 'unit-under-test.service'), 'utf8'); const result = await placeUnitFile(f.activeSource, f.activeDir, 'unit-under-test.service'); // No destructive step fired: no unlink, no wants removal. expect(result.unlinkedDestinationSymlink).toBe(false); expect(result.removedStaleWantsSymlink).toBe(false); // Identical final state. expect(await readFile(join(f.activeDir, 'unit-under-test.service'), 'utf8')).toBe(before); const link = await lstat(join(wantsDir, 'unit-under-test.service')); expect(link.isSymbolicLink()).toBe(true); }); it('double install on by-path residue converges to the identical reconciled state', async () => { const f = await fixture(); await symlink(f.seedTemplate, join(f.activeDir, 'unit-under-test.service')); const wantsDir = join(f.activeDir, 'default.target.wants'); await mkdir(wantsDir, { recursive: true }); await symlink(f.seedTemplate, join(wantsDir, 'unit-under-test.service')); await placeUnitFile(f.activeSource, f.activeDir, 'unit-under-test.service'); const first = await readFile(join(f.activeDir, 'unit-under-test.service'), 'utf8'); const secondRun = await placeUnitFile(f.activeSource, f.activeDir, 'unit-under-test.service'); const second = await readFile(join(f.activeDir, 'unit-under-test.service'), 'utf8'); expect(secondRun.unlinkedDestinationSymlink).toBe(false); expect(second).toBe(first); }); }); describe('resolveLeaseBrokerSocketForPreflight (#1292 preflight resolution)', () => { it('explicit MOSAIC_LEASE_BROKER_SOCKET wins', () => { expect( resolveLeaseBrokerSocketForPreflight({ MOSAIC_LEASE_BROKER_SOCKET: '/custom/sock' }, 1000), ).toBe('/custom/sock'); }); it('XDG_RUNTIME_DIR next', () => { expect(resolveLeaseBrokerSocketForPreflight({ XDG_RUNTIME_DIR: '/run/user/1001' }, 1000)).toBe( '/run/user/1001/mosaic-lease/broker.sock', ); }); it('falls back to /run/user/', () => { expect(resolveLeaseBrokerSocketForPreflight({}, 1002)).toBe( '/run/user/1002/mosaic-lease/broker.sock', ); }); });