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 => {
|
describe('[email protected]', (): void => {
|
||||||
const unitPath = resolve(process.cwd(), 'framework', 'systemd', 'user', '[email protected]');
|
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 unit = await readFile(unitPath, 'utf8');
|
||||||
const matches = unit.match(/^ConditionPathExists=(.+)$/gm) ?? [];
|
const matches = unit.match(/^ConditionPathExists=(.+)$/gm) ?? [];
|
||||||
expect(matches).toHaveLength(1);
|
expect(matches.length).toBeGreaterThan(0);
|
||||||
return matches[0]!.slice('ConditionPathExists='.length).trim();
|
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> => {
|
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
|
// unit (WantedBy=default.target) but does not start it, so without this
|
||||||
// condition a reboot between `install` and the first `apply` would run
|
// condition a reboot between `install` and the first `apply` would run
|
||||||
// ExecStart against an absent env file and fail every seat unit.
|
// 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> => {
|
it('guards exactly the file the fleet writes, so the two cannot drift apart', async (): Promise<void> => {
|
||||||
const mosaicHome = await v2Home();
|
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.
|
// Per home shape, the guard must render to exactly the file the
|
||||||
expect(rendered).toBe(join(mosaicHome, 'fleet', 'agents', 'coder0.env.generated'));
|
// 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> => {
|
it('guards a real failure — the launcher rejects an absent generated env', async (): Promise<void> => {
|
||||||
|
|||||||
Reference in New Issue
Block a user