fix(#1408): mosaic-agent@ condition arms on either home shape (#1410)
ci/woodpecker/push/publish Pipeline was canceled
ci/woodpecker/push/publish Pipeline was canceled
Co-authored-by: veronica <[email protected]>
This commit was merged in pull request #1410.
This commit is contained in:
@@ -11,7 +11,17 @@ PartOf=mosaic-tmux-holder.service
|
|||||||
# launcher would fail the unit. A skipped unit is the honest state for "enabled
|
# launcher would fail the unit. A skipped unit is the honest state for "enabled
|
||||||
# but not yet configured"; systemd re-evaluates the condition on every start, so
|
# but not yet configured"; systemd re-evaluates the condition on every start, so
|
||||||
# the seat comes up on the next start once the reconciler has written env.
|
# the seat comes up on the next start once the reconciler has written env.
|
||||||
ConditionPathExists=%h/.config/mosaic/fleet/agents/%i.env.generated
|
#
|
||||||
|
# #1408: the reconciler writes projections into the BRAIN home when one is
|
||||||
|
# active (~/.mosaic/fleet/agents, mirroring start-agent-session.sh's brain-home
|
||||||
|
# resolution), and into MOSAIC_HOME on a legacy single-tree host. A single
|
||||||
|
# config-home condition therefore skipped every seat on brain-home estates —
|
||||||
|
# measured on two estates: 27 projections vs 0, and 5 vs 0, gate never fired.
|
||||||
|
# Two TRIGGERING conditions (the `|` prefix ORs same-type conditions, which
|
||||||
|
# otherwise AND): either shape arms the unit; the launcher still resolves the
|
||||||
|
# authoritative copy itself.
|
||||||
|
ConditionPathExists=|%h/.config/mosaic/fleet/agents/%i.env.generated
|
||||||
|
ConditionPathExists=|%h/.mosaic/fleet/agents/%i.env.generated
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=oneshot
|
Type=oneshot
|
||||||
|
|||||||
@@ -32,6 +32,17 @@ if grep -qF -- '/bin/bash -lc' "$HOLDER"; then
|
|||||||
fail "holder must not start tmux through a login shell"
|
fail "holder must not start tmux through a login shell"
|
||||||
fi
|
fi
|
||||||
grep -qF 'Requires=mosaic-tmux-holder.service' "$AGENT" || fail "agent does not require holder"
|
grep -qF 'Requires=mosaic-tmux-holder.service' "$AGENT" || fail "agent does not require holder"
|
||||||
|
# #1408: the projection condition must arm on EITHER home shape. Both lines must
|
||||||
|
# carry the `|` triggering prefix — same-type conditions without it AND together,
|
||||||
|
# which can never be true (one file cannot exist at two paths), so a bare-spelling
|
||||||
|
# regression would disable autostart everywhere while reading as "has a condition".
|
||||||
|
grep -qF 'ConditionPathExists=|%h/.config/mosaic/fleet/agents/%i.env.generated' "$AGENT" || \
|
||||||
|
fail "agent lacks triggering condition for the config home projection"
|
||||||
|
grep -qF 'ConditionPathExists=|%h/.mosaic/fleet/agents/%i.env.generated' "$AGENT" || \
|
||||||
|
fail "agent lacks triggering condition for the brain home projection (#1408)"
|
||||||
|
if grep -qE '^ConditionPathExists=[^|]' "$AGENT"; then
|
||||||
|
fail "agent has a non-triggering ConditionPathExists — same-type conditions AND, re-arming #1408"
|
||||||
|
fi
|
||||||
grep -qF 'start-agent-session.sh' "$AGENT" || fail "agent unit does not call start-agent-session.sh"
|
grep -qF 'start-agent-session.sh' "$AGENT" || fail "agent unit does not call start-agent-session.sh"
|
||||||
if grep -qE '^Environment(File)?=' "$AGENT" "$INTERACTION"; then
|
if grep -qE '^Environment(File)?=' "$AGENT" "$INTERACTION"; then
|
||||||
fail "agent units must not accept ambient or projection environment before strict parsing"
|
fail "agent units must not accept ambient or projection environment before strict parsing"
|
||||||
|
|||||||
@@ -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