diff --git a/packages/mosaic/src/commands/fleet-reconciler-command.spec.ts b/packages/mosaic/src/commands/fleet-reconciler-command.spec.ts index 0c335e29..5a32c5d9 100644 --- a/packages/mosaic/src/commands/fleet-reconciler-command.spec.ts +++ b/packages/mosaic/src/commands/fleet-reconciler-command.spec.ts @@ -73,6 +73,10 @@ function program( runner, reconcileDeps: { homeDirectory: '/home/mosaic', + // Deterministic broker presence: without a seam the reconciler probes the + // REAL host socket (#1297 F3), making every CLI start test answer the + // host's broker state instead of its own property. + checkBrokerSocket: async () => true, readHolderIdentity: async () => '11111111-1111-4111-8111-111111111111', validateRoster: async () => undefined, prepareProjections: async () => [{ agentName: 'coder0' }], diff --git a/packages/mosaic/src/commands/fleet.spec.ts b/packages/mosaic/src/commands/fleet.spec.ts index 5fe8096a..627f8a13 100644 --- a/packages/mosaic/src/commands/fleet.spec.ts +++ b/packages/mosaic/src/commands/fleet.spec.ts @@ -4516,29 +4516,40 @@ describe('#1297 review: the real broker probe, exercised without any seam', () = await rm(dir, { recursive: true, force: true }); }); - it('placeUnitFile aborts with UnitPlacementError when unlink fails — never copies through a live symlink', async () => { - const dir = await tempDir(); - const unitDir = join(dir, 'systemd', 'user'); - await mkdir(unitDir, { recursive: true }); - // By-path residue: destination is a symlink pointing somewhere else. - const residueTarget = join(dir, 'residue-target'); - await writeFile(residueTarget, 'RESIDUE-BYTES'); - const destination = join(unitDir, 'x.service'); - await symlink(residueTarget, destination); - const source = join(dir, 'seed.service'); - await writeFile(source, 'UNIT-BYTES'); - // Read-only unit dir: unlink now fails EACCES (test runs as the owner, - // not root, so mode bits are enforced). - await chmod(unitDir, 0o500); - try { - await expect(placeUnitFile(source, unitDir, 'x.service')).rejects.toThrow(UnitPlacementError); - } finally { - await chmod(unitDir, 0o700); - } - // The copy-through never happened: residue bytes intact, destination - // still the symlink (abort, not overwrite-through). - expect(await readFile(residueTarget, 'utf8')).toBe('RESIDUE-BYTES'); - expect(await readlink(destination)).toBe(residueTarget); - await rm(dir, { recursive: true, force: true }); - }); + // EACCES-based unlink failure requires a non-root uid: root bypasses + // directory mode bits (CAP_DAC_OVERRIDE), so the abort path cannot be + // triggered this way under CI's root runner. Skipped there, exercised on + // every non-root dev host. + const itUnlessRoot = + typeof process.getuid === 'function' && process.getuid() === 0 ? it.skip : it; + itUnlessRoot( + 'placeUnitFile aborts with UnitPlacementError when unlink fails — never copies through a live symlink', + async () => { + const dir = await tempDir(); + const unitDir = join(dir, 'systemd', 'user'); + await mkdir(unitDir, { recursive: true }); + // By-path residue: destination is a symlink pointing somewhere else. + const residueTarget = join(dir, 'residue-target'); + await writeFile(residueTarget, 'RESIDUE-BYTES'); + const destination = join(unitDir, 'x.service'); + await symlink(residueTarget, destination); + const source = join(dir, 'seed.service'); + await writeFile(source, 'UNIT-BYTES'); + // Read-only unit dir: unlink now fails EACCES (test runs as the owner, + // not root, so mode bits are enforced). + await chmod(unitDir, 0o500); + try { + await expect(placeUnitFile(source, unitDir, 'x.service')).rejects.toThrow( + UnitPlacementError, + ); + } finally { + await chmod(unitDir, 0o700); + } + // The copy-through never happened: residue bytes intact, destination + // still the symlink (abort, not overwrite-through). + expect(await readFile(residueTarget, 'utf8')).toBe('RESIDUE-BYTES'); + expect(await readlink(destination)).toBe(residueTarget); + await rm(dir, { recursive: true, force: true }); + }, + ); }); diff --git a/packages/mosaic/src/fleet/fleet-reconciler.spec.ts b/packages/mosaic/src/fleet/fleet-reconciler.spec.ts index 49164e65..b1b99d54 100644 --- a/packages/mosaic/src/fleet/fleet-reconciler.spec.ts +++ b/packages/mosaic/src/fleet/fleet-reconciler.spec.ts @@ -180,6 +180,10 @@ describe('fleet roster-owned reconciler', (): void => { it('command start enables and starts the broker BEFORE the holder and any agent unit', async (): Promise => { const calls: string[][] = []; const result = await run('start', { + // Deterministic broker presence: without the seam this test answers the + // HOST's broker state (passes on a machine with a live broker, refuses + // on CI), not the ordering property it exists for (#1297 follow-up). + checkBrokerSocket: async () => true, runner: async (command, args) => { calls.push([command, ...args]); if (command === 'tmux' && args.includes('list-sessions')) { @@ -232,6 +236,8 @@ describe('fleet roster-owned reconciler', (): void => { expectedGeneration: 7, deps: deps({ readRoster: async () => runningRoster, + // Deterministic broker presence (see start-ordering test note). + checkBrokerSocket: async () => true, runner: async (command, args) => { calls.push([command, ...args]); if (command === 'tmux' && args.includes('list-sessions')) { @@ -543,6 +549,8 @@ describe('fleet roster-owned reconciler', (): void => { expectedGeneration: 7, deps: deps({ readRoster: async () => runningRoster, + // Deterministic broker presence (see start-ordering test note). + checkBrokerSocket: async () => true, runner: async (command, args) => { calls.push([command, ...args]); if (command === 'tmux' && args.includes('list-sessions')) {