test(fleet): validate shipped artifact dispositions (#770)
All checks were successful
ci/woodpecker/push/publish Pipeline was successful
ci/woodpecker/push/ci Pipeline was successful

This commit was merged in pull request #770.
This commit is contained in:
2026-07-15 01:37:12 +00:00
parent a5e8e55401
commit e9c4aa3e8b
5 changed files with 302 additions and 1 deletions

View File

@@ -0,0 +1,90 @@
import { cp, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { afterEach, describe, expect, it } from 'vitest';
import {
SHIPPED_FLEET_ARTIFACT_DISPOSITIONS,
validateShippedFleetArtifactDispositions,
} from './example-profile-dispositions.js';
const frameworkFleet = resolve(
dirname(fileURLToPath(import.meta.url)),
'..',
'..',
'framework',
'fleet',
);
const EXPECTED_DISPOSITIONS = [
'examples/coding.yaml:v1-fixture',
'examples/general.yaml:v1-fixture',
'examples/hybrid.yaml:v1-fixture',
'examples/local-canary.yaml:v1-fixture',
'examples/minimal.yaml:v1-fixture',
'examples/operator-interaction.yaml:v1-fixture',
'examples/research.yaml:v1-fixture',
'profiles/business.yaml:canonical-profile',
'profiles/marketing.yaml:canonical-profile',
'profiles/personal-assistant.yaml:canonical-profile',
'profiles/research.yaml:canonical-profile',
'profiles/software-delivery.yaml:canonical-profile',
'services/operator-interaction.yaml:canonical-service-policy',
];
const declared = SHIPPED_FLEET_ARTIFACT_DISPOSITIONS.map(
({ path, disposition }): string => `${path}:${disposition}`,
);
describe('shipped fleet example/profile/service disposition validation', (): void => {
it('enumerates every inventory artifact with an explicit executable disposition', (): void => {
expect(declared).toEqual(EXPECTED_DISPOSITIONS);
});
it('validates every shipped artifact through its declared v1 or canonical path', async (): Promise<void> => {
const results = await validateShippedFleetArtifactDispositions({ frameworkFleet });
expect(results.map(({ path, disposition }): string => `${path}:${disposition}`)).toEqual(
EXPECTED_DISPOSITIONS,
);
expect(results.filter(({ disposition }): boolean => disposition === 'v1-fixture')).toHaveLength(
7,
);
expect(
results.filter(({ disposition }): boolean => disposition === 'canonical-profile'),
).toHaveLength(5);
expect(
results.find(({ path }): boolean => path === 'services/operator-interaction.yaml'),
).toMatchObject({
disposition: 'canonical-service-policy',
});
});
let temporaryFleet: string | undefined;
afterEach(async (): Promise<void> => {
if (temporaryFleet) await rm(temporaryFleet, { recursive: true, force: true });
temporaryFleet = undefined;
});
it('fails closed when a shipped artifact lacks a declared disposition', async (): Promise<void> => {
temporaryFleet = await mkdtemp(join(tmpdir(), 'mosaic-dispositions-'));
await cp(frameworkFleet, temporaryFleet, { recursive: true });
await writeFile(join(temporaryFleet, 'examples', 'undeclared.yaml'), 'version: 1\n');
await expect(
validateShippedFleetArtifactDispositions({ frameworkFleet: temporaryFleet }),
).rejects.toThrow(/undeclared shipped fleet artifact.*examples\/undeclared.yaml/i);
});
it('rejects a v1 fixture when its explicit version declaration is removed', async (): Promise<void> => {
temporaryFleet = await mkdtemp(join(tmpdir(), 'mosaic-dispositions-'));
await cp(frameworkFleet, temporaryFleet, { recursive: true });
const fixturePath = join(temporaryFleet, 'examples', 'coding.yaml');
const fixture = await readFile(fixturePath, 'utf8');
await writeFile(fixturePath, fixture.replace(/^version: 1\n/, ''));
await expect(
validateShippedFleetArtifactDispositions({ frameworkFleet: temporaryFleet }),
).rejects.toThrow(/Fleet roster version must be 1/);
});
});

View File

@@ -0,0 +1,121 @@
import { readdir } from 'node:fs/promises';
import { basename, join } from 'node:path';
import { loadFleetRoster } from '../commands/fleet.js';
import { loadProfiles } from '../commands/fleet-profiles.js';
import {
provisionInteractionService,
readInteractionServiceProfile,
} from './interaction-service-profile.js';
export type FleetArtifactDisposition =
| 'v1-fixture'
| 'canonical-profile'
| 'canonical-service-policy';
export interface ShippedFleetArtifactDisposition {
readonly path: string;
readonly disposition: FleetArtifactDisposition;
}
export interface ValidateShippedFleetArtifactDispositionsOptions {
readonly frameworkFleet: string;
readonly rolesDir?: string;
readonly overrideDir?: string;
}
/**
* The M0 inventory in executable form. Every shipped fleet YAML asset is either
* a deliberately retained v1 fixture or validated through its canonical loader.
*/
export const SHIPPED_FLEET_ARTIFACT_DISPOSITIONS: readonly ShippedFleetArtifactDisposition[] = [
{ path: 'examples/coding.yaml', disposition: 'v1-fixture' },
{ path: 'examples/general.yaml', disposition: 'v1-fixture' },
{ path: 'examples/hybrid.yaml', disposition: 'v1-fixture' },
{ path: 'examples/local-canary.yaml', disposition: 'v1-fixture' },
{ path: 'examples/minimal.yaml', disposition: 'v1-fixture' },
{ path: 'examples/operator-interaction.yaml', disposition: 'v1-fixture' },
{ path: 'examples/research.yaml', disposition: 'v1-fixture' },
{ path: 'profiles/business.yaml', disposition: 'canonical-profile' },
{ path: 'profiles/marketing.yaml', disposition: 'canonical-profile' },
{ path: 'profiles/personal-assistant.yaml', disposition: 'canonical-profile' },
{ path: 'profiles/research.yaml', disposition: 'canonical-profile' },
{ path: 'profiles/software-delivery.yaml', disposition: 'canonical-profile' },
{ path: 'services/operator-interaction.yaml', disposition: 'canonical-service-policy' },
];
/**
* Fail closed when a fleet YAML asset is added or removed without a disposition.
* This keeps legacy v1 compatibility explicit instead of silently accepting new
* unresolved classes outside the shared resolver.
*/
export async function validateShippedFleetArtifactDispositions(
options: ValidateShippedFleetArtifactDispositionsOptions,
): Promise<readonly ShippedFleetArtifactDisposition[]> {
await assertEveryShippedArtifactIsDeclared(options.frameworkFleet);
const examples = SHIPPED_FLEET_ARTIFACT_DISPOSITIONS.filter(
({ disposition }): boolean => disposition === 'v1-fixture',
);
for (const artifact of examples) {
const roster = await loadFleetRoster(join(options.frameworkFleet, artifact.path));
if (roster.version !== 1) {
throw new Error(`v1 fixture ${artifact.path} must declare version: 1`);
}
}
const profilesDir = join(options.frameworkFleet, 'profiles');
const profiles = await loadProfiles({
profilesDir,
rolesDir: options.rolesDir ?? join(options.frameworkFleet, 'roles'),
overrideDir: options.overrideDir ?? join(options.frameworkFleet, 'roles.local'),
});
const declaredProfiles = SHIPPED_FLEET_ARTIFACT_DISPOSITIONS.filter(
({ disposition }): boolean => disposition === 'canonical-profile',
).map(({ path }): string => basename(path, '.yaml'));
const resolvedProfiles = new Set(profiles.map(({ id }): string => id));
for (const profileId of declaredProfiles) {
if (!resolvedProfiles.has(profileId)) {
throw new Error(`declared canonical profile ${profileId} did not resolve`);
}
}
const servicePolicies = SHIPPED_FLEET_ARTIFACT_DISPOSITIONS.filter(
({ disposition }): boolean => disposition === 'canonical-service-policy',
);
for (const artifact of servicePolicies) {
const serviceProfile = await readInteractionServiceProfile(
join(options.frameworkFleet, artifact.path),
);
provisionInteractionService(serviceProfile, { agentName: 'interaction-example' });
}
return SHIPPED_FLEET_ARTIFACT_DISPOSITIONS;
}
async function assertEveryShippedArtifactIsDeclared(frameworkFleet: string): Promise<void> {
const declared = new Set(SHIPPED_FLEET_ARTIFACT_DISPOSITIONS.map(({ path }): string => path));
const shipped = await listShippedFleetArtifactPaths(frameworkFleet);
for (const path of shipped) {
if (!declared.has(path)) {
throw new Error(`undeclared shipped fleet artifact: ${path}`);
}
}
for (const path of declared) {
if (!shipped.has(path)) {
throw new Error(`declared shipped fleet artifact is missing: ${path}`);
}
}
}
async function listShippedFleetArtifactPaths(frameworkFleet: string): Promise<Set<string>> {
const directories = ['examples', 'profiles', 'services'];
const paths = new Set<string>();
for (const directory of directories) {
const files = await readdir(join(frameworkFleet, directory));
for (const file of files) {
if (file.endsWith('.yaml') || file.endsWith('.yml')) paths.add(`${directory}/${file}`);
}
}
return paths;
}