diff --git a/packages/mosaic/src/commands/fleet-roster-v2-dispatch.spec.ts b/packages/mosaic/src/commands/fleet-roster-v2-dispatch.spec.ts index 12d039a0..2d073ec4 100644 --- a/packages/mosaic/src/commands/fleet-roster-v2-dispatch.spec.ts +++ b/packages/mosaic/src/commands/fleet-roster-v2-dispatch.spec.ts @@ -211,12 +211,23 @@ describe('mosaic fleet install — roster v2', (): void => { describe('mosaic-agent@.service', (): void => { const unitPath = resolve(process.cwd(), 'framework', 'systemd', 'user', 'mosaic-agent@.service'); - /** The single `ConditionPathExists=` value declared by the unit template. */ - async function conditionPath(): Promise { + /** + * Every `ConditionPathExists=` value declared by the unit template, in file + * order, `|` triggering prefix included. + * + * Until #1410 this helper pinned `toHaveLength(1)` — a count assertion, not + * a content assertion, and the count pin was itself the defect: when #1408 + * required a second triggering line (the `%h/.mosaic` brain-home shape), + * this spec was a second consumer of the unit template that the shell suite + * and the enumeration guard could not see, so the fix failed here first + * (CI 2651 — the installation-documentation.spec.ts lesson again). Assert + * content per line, never count. + */ + async function conditionPaths(): Promise { const unit = await readFile(unitPath, 'utf8'); const matches = unit.match(/^ConditionPathExists=(.+)$/gm) ?? []; - expect(matches).toHaveLength(1); - return matches[0]!.slice('ConditionPathExists='.length).trim(); + expect(matches.length).toBeGreaterThan(0); + return matches.map((line) => line.slice('ConditionPathExists='.length).trim()); } it('will not attempt a seat before the reconciler has written its env', async (): Promise => { @@ -224,7 +235,26 @@ describe('mosaic-agent@.service', (): void => { // unit (WantedBy=default.target) but does not start it, so without this // condition a reboot between `install` and the first `apply` would run // ExecStart against an absent env file and fail every seat unit. - expect(await conditionPath()).toBe('%h/.config/mosaic/fleet/agents/%i.env.generated'); + // + // Two lines since #1408: the env projection lives under %h/.config/mosaic + // on framework-home hosts and under %h/.mosaic on brain-home hosts. Both + // carry the `|` triggering prefix — systemd ANDs same-type conditions + // unless every line is triggering (then they OR), and a bare spelling + // would demand BOTH home shapes on one host, which is never true, so + // every seat would silently skip. + expect(await conditionPaths()).toEqual([ + '|%h/.config/mosaic/fleet/agents/%i.env.generated', + '|%h/.mosaic/fleet/agents/%i.env.generated', + ]); + }); + + it('refuses the bare ANDed spelling on every condition line', async (): Promise => { + // Invariant ported from test-fleet-units.sh, held separately from the + // literal pin above so it survives future edits to the path set: every + // ConditionPathExists line must stay triggering (`|`). + for (const value of await conditionPaths()) { + expect(value.startsWith('|')).toBe(true); + } }); /** @@ -246,10 +276,18 @@ describe('mosaic-agent@.service', (): void => { */ it('guards exactly the file the fleet writes, so the two cannot drift apart', async (): Promise => { const mosaicHome = await v2Home(); - const rendered = (await conditionPath()).replace('%h', tempHome!).replace('%i', 'coder0'); + const rendered = (await conditionPaths()).map((value) => + value.replace(/^\|/, '').replace('%h', tempHome!).replace('%i', 'coder0'), + ); - // The path an installed fleet actually places for this agent. - expect(rendered).toBe(join(mosaicHome, 'fleet', 'agents', 'coder0.env.generated')); + // Per home shape, the guard must render to exactly the file the + // reconciler writes there: mosaicHome (%h/.config/mosaic) on + // framework-home hosts, %h/.mosaic on brain-home hosts (#1408) — each + // line pinned to its file, not merely present. + expect(rendered).toEqual([ + join(mosaicHome, 'fleet', 'agents', 'coder0.env.generated'), + join(tempHome!, '.mosaic', 'fleet', 'agents', 'coder0.env.generated'), + ]); }); it('guards a real failure — the launcher rejects an absent generated env', async (): Promise => {