chore: consolidate new foundation and archive v1 (#1495)
This commit is contained in:
@@ -0,0 +1,177 @@
|
||||
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 <name>` 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/<uid>', () => {
|
||||
expect(resolveLeaseBrokerSocketForPreflight({}, 1002)).toBe(
|
||||
'/run/user/1002/mosaic-lease/broker.sock',
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user