From 355571182f0559c7be2d9bff67b1dab7b31b4d93 Mon Sep 17 00:00:00 2001 From: Jarvis Date: Thu, 16 Jul 2026 07:01:51 -0500 Subject: [PATCH] fix(mosaic): preserve v1 agent name semantics --- .../mosaic/src/fleet/v1-v2-migration.spec.ts | 26 +++++++++++++++++++ packages/mosaic/src/fleet/v1-v2-migration.ts | 10 ++++--- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/packages/mosaic/src/fleet/v1-v2-migration.spec.ts b/packages/mosaic/src/fleet/v1-v2-migration.spec.ts index 3adc255..f23656f 100644 --- a/packages/mosaic/src/fleet/v1-v2-migration.spec.ts +++ b/packages/mosaic/src/fleet/v1-v2-migration.spec.ts @@ -387,6 +387,32 @@ describe('v1-to-v2 migration preview', (): void => { 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([ ['socket_name', " socket_name: ''\n holder_session: _holder"], ['socketName', " socketName: ''\n holder_session: _holder"], diff --git a/packages/mosaic/src/fleet/v1-v2-migration.ts b/packages/mosaic/src/fleet/v1-v2-migration.ts index aa57dba..4c3cb9e 100644 --- a/packages/mosaic/src/fleet/v1-v2-migration.ts +++ b/packages/mosaic/src/fleet/v1-v2-migration.ts @@ -386,7 +386,7 @@ export async function previewV1ToV2Migration( for (let index = 0; index < rawAgents.length; index += 1) { const rawAgent = record(rawAgents[index]); - const name = stringValue(rawAgent.name); + const name = exactStringValue(rawAgent.name); const path = `agents[${index}]`; if (!name) { blockers.push(blocker('invalid-agent-name', `${path}.name`, 'Agent name is required.')); @@ -538,7 +538,7 @@ export async function previewV1ToV2Migration( rawAgents .map((candidate) => record(candidate)) .filter((candidate) => classifyAgentLocality(candidate, decisions.fleetHost) !== 'remote') - .map((candidate) => stringValue(candidate.name)) + .map((candidate) => exactStringValue(candidate.name)) .filter((name): name is string => name !== undefined), ); for (const decisionName of Object.keys(decisions.agents)) { @@ -936,7 +936,7 @@ function validateV1FieldCatalog(raw: Record, blockers: Migratio path, blockers, ); - const name = stringValue(agent.name); + const name = exactStringValue(agent.name); if (!name || !AGENT_NAME_PATTERN.test(name)) { blockers.push( blocker('invalid-v1-field-type', `${path}.name`, 'A valid agent name is required.'), @@ -1420,6 +1420,10 @@ function record(value: unknown): Record { : {}; } +function exactStringValue(value: unknown): string | undefined { + return typeof value === 'string' ? value : undefined; +} + function trimmedString(value: unknown): string | undefined { return typeof value === 'string' ? value.trim() : undefined; }