fix(mosaic): preserve v1 agent name semantics
All checks were successful
ci/woodpecker/pr/ci Pipeline was successful

This commit is contained in:
Jarvis
2026-07-16 07:01:51 -05:00
parent fce489269a
commit 355571182f
2 changed files with 33 additions and 3 deletions

View File

@@ -387,6 +387,32 @@ describe('v1-to-v2 migration preview', (): void => {
expect(preview.canonicalRoster?.agents.map(({ name }) => name)).toEqual(['coder0']); expect(preview.canonicalRoster?.agents.map(({ name }) => name)).toEqual(['coder0']);
}); });
it('matches production v1 rejection for a whitespace-padded agent name', async () => {
const source = v1Source.replace('name: coder0', 'name: " coder0 "');
const rosterPath = join(await mkdtemp(join(tmpdir(), 'v1-agent-name-parity-')), 'roster.yaml');
temporaryDirectories.push(dirname(rosterPath));
await writeFile(rosterPath, source);
await expect(loadFleetRoster(rosterPath)).rejects.toThrow('Invalid fleet agent name');
const preview = await previewV1ToV2Migration({
source,
decisions: decisions(),
observations: observations(),
personaDirs: await semanticDirs(),
});
expect(preview.status).toBe('blocked');
expect(preview.blockers).toContainEqual({
code: 'invalid-v1-field-type',
path: 'agents[0].name',
detail: 'A valid agent name is required.',
});
expect(preview).not.toHaveProperty('canonicalRoster');
expect(preview).not.toHaveProperty('canonicalYaml');
expect(preview.evidence).not.toHaveProperty('candidate');
});
it.each([ it.each([
['socket_name', " socket_name: ''\n holder_session: _holder"], ['socket_name', " socket_name: ''\n holder_session: _holder"],
['socketName', " socketName: ''\n holder_session: _holder"], ['socketName', " socketName: ''\n holder_session: _holder"],

View File

@@ -386,7 +386,7 @@ export async function previewV1ToV2Migration(
for (let index = 0; index < rawAgents.length; index += 1) { for (let index = 0; index < rawAgents.length; index += 1) {
const rawAgent = record(rawAgents[index]); const rawAgent = record(rawAgents[index]);
const name = stringValue(rawAgent.name); const name = exactStringValue(rawAgent.name);
const path = `agents[${index}]`; const path = `agents[${index}]`;
if (!name) { if (!name) {
blockers.push(blocker('invalid-agent-name', `${path}.name`, 'Agent name is required.')); blockers.push(blocker('invalid-agent-name', `${path}.name`, 'Agent name is required.'));
@@ -538,7 +538,7 @@ export async function previewV1ToV2Migration(
rawAgents rawAgents
.map((candidate) => record(candidate)) .map((candidate) => record(candidate))
.filter((candidate) => classifyAgentLocality(candidate, decisions.fleetHost) !== 'remote') .filter((candidate) => classifyAgentLocality(candidate, decisions.fleetHost) !== 'remote')
.map((candidate) => stringValue(candidate.name)) .map((candidate) => exactStringValue(candidate.name))
.filter((name): name is string => name !== undefined), .filter((name): name is string => name !== undefined),
); );
for (const decisionName of Object.keys(decisions.agents)) { for (const decisionName of Object.keys(decisions.agents)) {
@@ -936,7 +936,7 @@ function validateV1FieldCatalog(raw: Record<string, unknown>, blockers: Migratio
path, path,
blockers, blockers,
); );
const name = stringValue(agent.name); const name = exactStringValue(agent.name);
if (!name || !AGENT_NAME_PATTERN.test(name)) { if (!name || !AGENT_NAME_PATTERN.test(name)) {
blockers.push( blockers.push(
blocker('invalid-v1-field-type', `${path}.name`, 'A valid agent name is required.'), blocker('invalid-v1-field-type', `${path}.name`, 'A valid agent name is required.'),
@@ -1420,6 +1420,10 @@ function record(value: unknown): Record<string, unknown> {
: {}; : {};
} }
function exactStringValue(value: unknown): string | undefined {
return typeof value === 'string' ? value : undefined;
}
function trimmedString(value: unknown): string | undefined { function trimmedString(value: unknown): string | undefined {
return typeof value === 'string' ? value.trim() : undefined; return typeof value === 'string' ? value.trim() : undefined;
} }