From 01d8b16474e4b2d2dce684b6c251b93dc5a65088 Mon Sep 17 00:00:00 2001 From: Jarvis Date: Thu, 16 Jul 2026 04:10:38 -0500 Subject: [PATCH] fix(fleet): align migration preview and clean CI Co-Authored-By: Claude Opus 4.8 --- .../mosaic/src/fleet/v1-v2-migration.spec.ts | 64 +++++++++++++++++++ packages/mosaic/src/fleet/v1-v2-migration.ts | 40 +++++++++++- packages/mosaic/turbo.json | 8 +++ 3 files changed, 111 insertions(+), 1 deletion(-) create mode 100644 packages/mosaic/turbo.json diff --git a/packages/mosaic/src/fleet/v1-v2-migration.spec.ts b/packages/mosaic/src/fleet/v1-v2-migration.spec.ts index 6b14df8..544610b 100644 --- a/packages/mosaic/src/fleet/v1-v2-migration.spec.ts +++ b/packages/mosaic/src/fleet/v1-v2-migration.spec.ts @@ -1136,6 +1136,70 @@ describe('v1-to-v2 migration preview', (): void => { expect(preview.environment.map(({ agent }) => agent)).toEqual(['coder0']); }); + it.each([ + [ + 'tmux connector with matrix settings', + 'connector:\n kind: tmux\n matrix:\n homeserver_url: https://rejected-matrix.example.test\n user_id: "@bot:example.test"\n room_id: "!room:example.test"\n', + 'connector.matrix', + 'https://rejected-matrix.example.test', + ], + [ + 'tmux connector with discord settings', + 'connector:\n kind: tmux\n discord:\n channel_id: rejected-discord-channel\n', + 'connector.discord', + 'rejected-discord-channel', + ], + [ + 'discord connector with matrix settings', + 'connector:\n kind: discord\n discord:\n channel_id: channel\n matrix:\n homeserver_url: https://rejected-matrix.example.test\n user_id: "@bot:example.test"\n room_id: "!room:example.test"\n', + 'connector.matrix', + 'https://rejected-matrix.example.test', + ], + [ + 'matrix connector with discord settings', + 'connector:\n kind: matrix\n matrix:\n homeserver_url: https://matrix.example.test\n user_id: "@bot:example.test"\n room_id: "!room:example.test"\n discord:\n channel_id: rejected-discord-channel\n', + 'connector.discord', + 'rejected-discord-channel', + ], + ] as const)( + 'blocks %s to match production v1 connector exclusivity', + async (_label, connectorSource, expectedPath, rejectedValue) => { + const preview = await previewV1ToV2Migration({ + source: v1Source.replace(/connector:\n[\s\S]*$/, connectorSource), + decisions: decisions(), + observations: observations(), + personaDirs: await semanticDirs(), + }); + + expect(preview.status).toBe('blocked'); + expect(preview.blockers).toContainEqual( + expect.objectContaining({ code: 'invalid-connector', path: expectedPath }), + ); + expect(preview).not.toHaveProperty('canonicalRoster'); + expect(preview).not.toHaveProperty('canonicalYaml'); + expect(JSON.stringify(preview.blockers)).not.toContain(rejectedValue); + }, + ); + + it('blocks whitespace-padded connector kinds to match exact production v1 parsing', async () => { + const rejectedKind = ' discord '; + const preview = await previewV1ToV2Migration({ + source: v1Source.replace('kind: discord', `kind: "${rejectedKind}"`), + decisions: decisions(), + observations: observations(), + personaDirs: await semanticDirs(), + }); + + expect(preview.status).toBe('blocked'); + expect(preview.blockers).toContainEqual( + expect.objectContaining({ code: 'invalid-connector', path: 'connector.kind' }), + ); + expect(preview).not.toHaveProperty('canonicalRoster'); + expect(preview).not.toHaveProperty('canonicalYaml'); + expect(preview.evidence).not.toHaveProperty('candidate'); + expect(JSON.stringify(preview.blockers)).not.toContain(rejectedKind); + }); + it('blocks malformed schema-only remote and connector fields', async () => { const source = v1Source .replace('host: build.example.test', 'host: 42') diff --git a/packages/mosaic/src/fleet/v1-v2-migration.ts b/packages/mosaic/src/fleet/v1-v2-migration.ts index 7ba9fea..d1bd00f 100644 --- a/packages/mosaic/src/fleet/v1-v2-migration.ts +++ b/packages/mosaic/src/fleet/v1-v2-migration.ts @@ -1005,10 +1005,48 @@ function validateConnector( ): void { if (!present) return; rejectUnknownKeys(connector, new Set(['kind', 'matrix', 'discord']), 'connector', blockers); - const kind = stringValue(connector.kind); + const kind = typeof connector.kind === 'string' ? connector.kind : undefined; if (!kind || !['tmux', 'discord', 'matrix'].includes(kind)) { blockers.push(blocker('invalid-connector', 'connector.kind', 'Unsupported connector kind.')); } + if (kind === 'tmux') { + if (connector.matrix !== undefined) { + blockers.push( + blocker( + 'invalid-connector', + 'connector.matrix', + 'Tmux connector must not define Matrix connector data.', + ), + ); + } + if (connector.discord !== undefined) { + blockers.push( + blocker( + 'invalid-connector', + 'connector.discord', + 'Tmux connector must not define Discord connector data.', + ), + ); + } + } + if (kind === 'discord' && connector.matrix !== undefined) { + blockers.push( + blocker( + 'invalid-connector', + 'connector.matrix', + 'Discord connector must not define Matrix connector data.', + ), + ); + } + if (kind === 'matrix' && connector.discord !== undefined) { + blockers.push( + blocker( + 'invalid-connector', + 'connector.discord', + 'Matrix connector must not define Discord connector data.', + ), + ); + } if (kind === 'matrix' && connector.matrix === undefined) { blockers.push( blocker('invalid-connector', 'connector.matrix', 'Matrix connector data is required.'), diff --git a/packages/mosaic/turbo.json b/packages/mosaic/turbo.json new file mode 100644 index 0000000..d3d66de --- /dev/null +++ b/packages/mosaic/turbo.json @@ -0,0 +1,8 @@ +{ + "extends": ["//"], + "tasks": { + "test": { + "dependsOn": ["^build", "build"] + } + } +}