test(#1297): make broker-dependent tests host-independent (CI root + no live broker)

Pipeline 2561 failed 5 tests that pass on a dev host, two环境 classes:

1. Un-seamed success-path start/apply tests answered the HOST's broker
   state: this dev machine has a live lease-broker socket at the default
   XDG path, so the real probe (correctly, since #1297 F3) returned true
   locally; CI has no broker, so the new start re-check refused
   broker-absent. Fixed by injecting a deterministic present-broker seam
   in exactly the tests whose property is ordering/targeting (reconciler
   start-ordering, two apply-running tests, and the CLI command spec's
   shared helper). Refusal and real-probe tests keep explicit false
   seams / explicit socket paths.

2. The placeUnitFile unlink-failure spec relies on EACCES from a 0500
   directory; CI steps run as root (apk add succeeds unprivileged-free)
   and CAP_DAC_OVERRIDE makes unlink succeed, so the abort path cannot
   be triggered there. Now it.skip under uid 0 with the reason stated;
   exercised on every non-root dev host.

The remaining unseamed apply tests use the stopped-agent path (no broker
interaction) and passed CI unchanged — verified against the 2561 log,
not assumed.
This commit is contained in:
fargo
2026-08-20 15:24:24 -05:00
parent b7d9e3b8ca
commit c9b932360a
3 changed files with 48 additions and 25 deletions
@@ -73,6 +73,10 @@ function program(
runner, runner,
reconcileDeps: { reconcileDeps: {
homeDirectory: '/home/mosaic', 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', readHolderIdentity: async () => '11111111-1111-4111-8111-111111111111',
validateRoster: async () => undefined, validateRoster: async () => undefined,
prepareProjections: async () => [{ agentName: 'coder0' }], prepareProjections: async () => [{ agentName: 'coder0' }],
+14 -3
View File
@@ -4516,7 +4516,15 @@ describe('#1297 review: the real broker probe, exercised without any seam', () =
await rm(dir, { recursive: true, force: true }); await rm(dir, { recursive: true, force: true });
}); });
it('placeUnitFile aborts with UnitPlacementError when unlink fails never copies through a live symlink', async () => { // 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 dir = await tempDir();
const unitDir = join(dir, 'systemd', 'user'); const unitDir = join(dir, 'systemd', 'user');
await mkdir(unitDir, { recursive: true }); await mkdir(unitDir, { recursive: true });
@@ -4531,7 +4539,9 @@ describe('#1297 review: the real broker probe, exercised without any seam', () =
// not root, so mode bits are enforced). // not root, so mode bits are enforced).
await chmod(unitDir, 0o500); await chmod(unitDir, 0o500);
try { try {
await expect(placeUnitFile(source, unitDir, 'x.service')).rejects.toThrow(UnitPlacementError); await expect(placeUnitFile(source, unitDir, 'x.service')).rejects.toThrow(
UnitPlacementError,
);
} finally { } finally {
await chmod(unitDir, 0o700); await chmod(unitDir, 0o700);
} }
@@ -4540,5 +4550,6 @@ describe('#1297 review: the real broker probe, exercised without any seam', () =
expect(await readFile(residueTarget, 'utf8')).toBe('RESIDUE-BYTES'); expect(await readFile(residueTarget, 'utf8')).toBe('RESIDUE-BYTES');
expect(await readlink(destination)).toBe(residueTarget); expect(await readlink(destination)).toBe(residueTarget);
await rm(dir, { recursive: true, force: true }); await rm(dir, { recursive: true, force: true });
}); },
);
}); });
@@ -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<void> => { it('command start enables and starts the broker BEFORE the holder and any agent unit', async (): Promise<void> => {
const calls: string[][] = []; const calls: string[][] = [];
const result = await run('start', { 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) => { runner: async (command, args) => {
calls.push([command, ...args]); calls.push([command, ...args]);
if (command === 'tmux' && args.includes('list-sessions')) { if (command === 'tmux' && args.includes('list-sessions')) {
@@ -232,6 +236,8 @@ describe('fleet roster-owned reconciler', (): void => {
expectedGeneration: 7, expectedGeneration: 7,
deps: deps({ deps: deps({
readRoster: async () => runningRoster, readRoster: async () => runningRoster,
// Deterministic broker presence (see start-ordering test note).
checkBrokerSocket: async () => true,
runner: async (command, args) => { runner: async (command, args) => {
calls.push([command, ...args]); calls.push([command, ...args]);
if (command === 'tmux' && args.includes('list-sessions')) { if (command === 'tmux' && args.includes('list-sessions')) {
@@ -543,6 +549,8 @@ describe('fleet roster-owned reconciler', (): void => {
expectedGeneration: 7, expectedGeneration: 7,
deps: deps({ deps: deps({
readRoster: async () => runningRoster, readRoster: async () => runningRoster,
// Deterministic broker presence (see start-ordering test note).
checkBrokerSocket: async () => true,
runner: async (command, args) => { runner: async (command, args) => {
calls.push([command, ...args]); calls.push([command, ...args]);
if (command === 'tmux' && args.includes('list-sessions')) { if (command === 'tmux' && args.includes('list-sessions')) {