test(#1408): dispatch spec asserts both triggering condition lines per home shape
ci/woodpecker/pr/ci Pipeline was successful
ci/woodpecker/pr/ci Pipeline was successful
The conditionPath() helper pinned toHaveLength(1) — a count assertion,
and the count pin was the defect: the unit's second triggering line
(brain-home shape, #1408) failed this spec (CI 2651) because it was a
second consumer of the unit template invisible to the shell suite and
the enumeration guard.
- helper returns ALL ConditionPathExists values, asserts content not count
- literal pin covers both |-prefixed lines in order
- new test: every condition line must keep the | triggering prefix
(ported from test-fleet-units.sh's bare-ANDed-spelling refusal)
- drift-apart guard now per path: each line renders to exactly the file
the reconciler writes for that home shape (orch-01 requirement)
Sabotage control: unit reverted to base f45928c fails exactly the three
condition tests; restored unit passes 11/11.
This commit is contained in:
@@ -211,12 +211,23 @@ describe('mosaic fleet install — roster v2', (): void => {
|
||||
describe('[email protected]', (): void => {
|
||||
const unitPath = resolve(process.cwd(), 'framework', 'systemd', 'user', '[email protected]');
|
||||
|
||||
/** The single `ConditionPathExists=` value declared by the unit template. */
|
||||
async function conditionPath(): Promise<string> {
|
||||
/**
|
||||
* 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<string[]> {
|
||||
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<void> => {
|
||||
@@ -224,7 +235,26 @@ describe('[email protected]', (): 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<void> => {
|
||||
// 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('[email protected]', (): void => {
|
||||
*/
|
||||
it('guards exactly the file the fleet writes, so the two cannot drift apart', async (): Promise<void> => {
|
||||
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<void> => {
|
||||
|
||||
Reference in New Issue
Block a user