fix(#792): guard fleet regen roster reads
All checks were successful
ci/woodpecker/pr/ci Pipeline was successful
All checks were successful
ci/woodpecker/pr/ci Pipeline was successful
This commit is contained in:
@@ -19,6 +19,7 @@ Make expected missing or malformed fleet roster configuration fail with an actio
|
|||||||
- 2026-07-16: Added a shared roster read/parse guard and routed v1 fleet commands plus v1/v2 selection through Commander’s actionable nonzero error path. V2 command modules already return structured nonzero JSON errors for their guarded reads.
|
- 2026-07-16: Added a shared roster read/parse guard and routed v1 fleet commands plus v1/v2 selection through Commander’s actionable nonzero error path. V2 command modules already return structured nonzero JSON errors for their guarded reads.
|
||||||
- 2026-07-16: Replaced installer heading `echo` with format-safe `printf`; added a regression check for the scoped package heading.
|
- 2026-07-16: Replaced installer heading `echo` with format-safe `printf`; added a regression check for the scoped package heading.
|
||||||
- 2026-07-16: Rebuilt CLI and manually verified `fleet ps` with no roster prints the initialization hint, exits 1, and has no stack trace.
|
- 2026-07-16: Rebuilt CLI and manually verified `fleet ps` with no roster prints the initialization hint, exits 1, and has no stack trace.
|
||||||
|
- 2026-07-17: Rebased #818 onto `origin/main` at `9ddc6fbd` (#791 PR3). The added `fleet regen` command had a canonical roster read in its sibling module; it now uses the same missing-roster guard and Commander exit path. Internal NORTH_STAR, preset, and post-write invariant reads remain intentionally unguarded.
|
||||||
|
|
||||||
## Verification
|
## Verification
|
||||||
|
|
||||||
@@ -27,6 +28,7 @@ Make expected missing or malformed fleet roster configuration fail with an actio
|
|||||||
- `pnpm lint` — PASS
|
- `pnpm lint` — PASS
|
||||||
- `pnpm format:check` — PASS
|
- `pnpm format:check` — PASS
|
||||||
- `pnpm --filter @mosaicstack/mosaic exec vitest run src/commands/fleet.spec.ts src/commands/install-heading.spec.ts` — PASS (209 tests)
|
- `pnpm --filter @mosaicstack/mosaic exec vitest run src/commands/fleet.spec.ts src/commands/install-heading.spec.ts` — PASS (209 tests)
|
||||||
|
- `pnpm --filter @mosaicstack/mosaic exec vitest run src/commands/fleet-regen-command.spec.ts` — PASS (27 tests, including missing canonical roster)
|
||||||
- Instrumented Vitest coverage is unavailable because `@vitest/coverage-v8` is not declared in this repository. Each branch added in the roster guard has direct unit coverage.
|
- Instrumented Vitest coverage is unavailable because `@vitest/coverage-v8` is not declared in this repository. Each branch added in the roster guard has direct unit coverage.
|
||||||
|
|
||||||
## Risks / blockers
|
## Risks / blockers
|
||||||
|
|||||||
@@ -150,6 +150,17 @@ describe('projectRosterV2AgentGeneratedEnv', (): void => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('mosaic fleet regen', (): void => {
|
describe('mosaic fleet regen', (): void => {
|
||||||
|
it('reports a missing canonical roster with the shared initialization hint', async (): Promise<void> => {
|
||||||
|
const home = await mkdtemp(join(tmpdir(), 'mosaic-fleet-regen-missing-roster-'));
|
||||||
|
cleanup = home;
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
program(home, recordingRunner([])).parseAsync(['node', 'mosaic', 'fleet', 'regen', '--json']),
|
||||||
|
).rejects.toThrow(
|
||||||
|
`No fleet roster found at ${join(home, 'fleet', 'roster.yaml')}. Run \`mosaic fleet init\``,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('is dry-run by default: reports the plan and writes nothing', async (): Promise<void> => {
|
it('is dry-run by default: reports the plan and writes nothing', async (): Promise<void> => {
|
||||||
const home = await fleetHome();
|
const home = await fleetHome();
|
||||||
const calls: string[][] = [];
|
const calls: string[][] = [];
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { readFile, stat } from 'node:fs/promises';
|
import { stat } from 'node:fs/promises';
|
||||||
import { homedir } from 'node:os';
|
import { homedir } from 'node:os';
|
||||||
import { join, relative, resolve } from 'node:path';
|
import { join, relative, resolve } from 'node:path';
|
||||||
import type { Command } from 'commander';
|
import type { Command } from 'commander';
|
||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
validateRosterV2Semantics,
|
validateRosterV2Semantics,
|
||||||
type FleetRosterV2,
|
type FleetRosterV2,
|
||||||
} from '../fleet/roster-v2.js';
|
} from '../fleet/roster-v2.js';
|
||||||
|
import { FleetRosterConfigurationError, readFleetRosterText } from '../fleet/fleet-roster-v1.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* `mosaic fleet regen` — recovery-framed regeneration of the roster-derived
|
* `mosaic fleet regen` — recovery-framed regeneration of the roster-derived
|
||||||
@@ -303,7 +304,7 @@ function defaultReadRoster(
|
|||||||
mosaicHome: string,
|
mosaicHome: string,
|
||||||
): (rosterPath: string) => Promise<FleetRosterV2> {
|
): (rosterPath: string) => Promise<FleetRosterV2> {
|
||||||
return async (rosterPath: string): Promise<FleetRosterV2> => {
|
return async (rosterPath: string): Promise<FleetRosterV2> => {
|
||||||
const roster = parseRosterV2(await readFile(rosterPath, 'utf8'), 'yaml');
|
const roster = parseRosterV2(await readFleetRosterText(rosterPath), 'yaml');
|
||||||
// Enforce the SAME semantic gate as reconcile/plan/verify (persona resolution
|
// Enforce the SAME semantic gate as reconcile/plan/verify (persona resolution
|
||||||
// + protected-class tool-policy match) so regen cannot project a roster the
|
// + protected-class tool-policy match) so regen cannot project a roster the
|
||||||
// rest of the fleet surface would reject.
|
// rest of the fleet surface would reject.
|
||||||
@@ -433,6 +434,10 @@ export function registerFleetRegenCommand(fleetCommand: Command, deps: FleetRege
|
|||||||
// the operator must finish/verify (and clear the lock) before restarting.
|
// the operator must finish/verify (and clear the lock) before restarting.
|
||||||
if (result.incomplete || result.cleanup) process.exitCode = 1;
|
if (result.incomplete || result.cleanup) process.exitCode = 1;
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
|
if (error instanceof FleetRosterConfigurationError) {
|
||||||
|
fleetCommand.error(error.message, { code: 'fleet.roster', exitCode: 1 });
|
||||||
|
return;
|
||||||
|
}
|
||||||
process.exitCode = 1;
|
process.exitCode = 1;
|
||||||
const message = error instanceof Error ? error.message : String(error);
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
process.stderr.write(`mosaic fleet regen failed: ${message}\n`);
|
process.stderr.write(`mosaic fleet regen failed: ${message}\n`);
|
||||||
|
|||||||
Reference in New Issue
Block a user