import { beforeEach, afterEach, expect, test } from 'bun:test' import { effectScope, ref } from 'vue' import { createMCPConnectionDraft, mcpConnectionSettings, type MCPConnection } from '@/app/integrations/mcp' import { useMCPConnectionSettings } from '@/app/integrations/mcp/settings/use' const connection: MCPConnection = { id: 'mcp-test', name: 'Test', enabled: true, transport: { type: 'streamable-http', url: 'https://example.com/mcp' }, authentication: { type: 'none' } } let previousConnections = mcpConnectionSettings.value.connections beforeEach(() => { previousConnections = mcpConnectionSettings.value.connections mcpConnectionSettings.value.connections = [structuredClone(connection)] }) afterEach(() => { mcpConnectionSettings.value.connections = previousConnections }) test('MCP save commits staged clearing without changing a new draft', async () => { const scope = effectScope() let finish: () => void = () => undefined const pending = new Promise((resolve) => { finish = resolve }) const saved: boolean[] = [] const token = ref('') try { const state = scope.run(() => useMCPConnectionSettings( ref(createMCPConnectionDraft()), token, ref({ bearerTokenRequired: 'Required' }), { status: async () => 'configured', setCredential: async () => pending, save: (draft) => { saved.push(draft.enabled) return connection }, remove: async () => undefined } ) ) if (!state) throw new Error('Missing scope') state.draft.value.id = connection.id state.clearCredential() expect(saved).toEqual([]) const clearing = state.save() state.startAdd() state.draft.value.enabled = true token.value = 'new-key' finish() await clearing expect(saved).toEqual([false, false]) expect(state.credentialCleared.value).toBe(false) expect(state.draft.value.enabled).toBe(true) expect(token.value).toBe('new-key') } finally { scope.stop() } }) test('MCP edit ignores credential status after another draft opens', async () => { const previous = mcpConnectionSettings.value.connections mcpConnectionSettings.value.connections = [connection] const scope = effectScope() let finish: () => void = () => undefined const pending = new Promise((resolve) => { finish = resolve }) try { const state = scope.run(() => useMCPConnectionSettings( ref(createMCPConnectionDraft()), ref(''), ref({ bearerTokenRequired: 'Required' }), { status: async () => { await pending return 'configured' }, setCredential: async () => undefined, save: () => connection, remove: async () => undefined } ) ) if (!state) throw new Error('Missing scope') const editing = state.startEdit(connection.id) state.startAdd() finish() expect(await editing).toBe(false) expect(state.tokenStatus.value).toBe('missing') } finally { scope.stop() mcpConnectionSettings.value.connections = previous } }) test('MCP failed writes preserve replacements and staged removals', async () => { const scope = effectScope() const token = ref('replacement') try { const state = scope.run(() => useMCPConnectionSettings( ref(createMCPConnectionDraft()), token, ref({ bearerTokenRequired: 'Required' }), { status: async () => 'configured', setCredential: async () => { throw new Error('Offline') }, save: () => connection, remove: async () => undefined } ) ) if (!state) throw new Error('Missing scope') state.draft.value.id = connection.id expect(await state.save()).toBe('partial') expect(state.error.value).toBe('Offline') expect(token.value).toBe('replacement') state.clearCredential() expect(state.error.value).toBe('') expect(token.value).toBe('') expect(state.credentialCleared.value).toBe(true) expect(await state.save()).toBe('partial') expect(state.error.value).toBe('Offline') expect(state.credentialCleared.value).toBe(true) } finally { scope.stop() } }) test('failed bearer write never enables the saved connection', async () => { const scope = effectScope() const saved: boolean[] = [] try { const state = scope.run(() => useMCPConnectionSettings( ref(createMCPConnectionDraft()), ref('token'), ref({ bearerTokenRequired: 'Required' }), { status: async () => 'configured', setCredential: async () => { throw new Error('Write failed') }, save: (draft) => { saved.push(draft.enabled) return connection }, remove: async () => undefined } ) ) if (!state) throw new Error('Missing scope') state.draft.value.authenticationType = 'bearer' state.draft.value.enabled = true expect(await state.save()).toBe('partial') expect(saved).toEqual([false]) } finally { scope.stop() } }) test('clear never persists unsaved fields and cancel discards removal', async () => { const scope = effectScope() const names: string[] = [] try { const state = scope.run(() => useMCPConnectionSettings( ref(createMCPConnectionDraft()), ref(''), ref({ bearerTokenRequired: 'Required' }), { status: async () => 'configured', setCredential: async () => undefined, save: (draft) => { names.push(draft.name) expect(draft.url).toBe(connection.transport.url) return connection }, remove: async () => undefined } ) ) if (!state) throw new Error('Missing scope') state.draft.value.id = connection.id state.draft.value.name = '' state.draft.value.url = 'invalid' state.clearCredential() expect(names).toEqual([]) expect(state.draft.value.url).toBe('invalid') expect(state.credentialCleared.value).toBe(true) state.cancel() expect(state.credentialCleared.value).toBe(false) } finally { scope.stop() } }) test('status lookup failure opens the editor with an error', async () => { const scope = effectScope() try { const state = scope.run(() => useMCPConnectionSettings( ref(createMCPConnectionDraft()), ref(''), ref({ bearerTokenRequired: 'Required' }), { status: async () => { throw new Error('Locked') }, setCredential: async () => undefined, save: () => connection, remove: async () => undefined } ) ) if (!state) throw new Error('Missing scope') expect(await state.startEdit(connection.id)).toBe(true) expect(state.error.value).toBe('Locked') } finally { scope.stop() } }) test('serializes overlapping credential replacements', async () => { const scope = effectScope() const writes: string[] = [] let finish: () => void = () => undefined const blocked = new Promise((resolve) => { finish = resolve }) const token = ref('first') try { const state = scope.run(() => useMCPConnectionSettings( ref(createMCPConnectionDraft()), token, ref({ bearerTokenRequired: 'Required' }), { status: async () => 'configured', setCredential: async (_id, value) => { writes.push(value) if (value === 'first') await blocked }, save: () => connection, remove: async () => undefined } ) ) if (!state) throw new Error('Missing scope') state.draft.value.id = connection.id state.draft.value.authenticationType = 'bearer' const first = state.save() await Promise.resolve() token.value = 'second' const second = state.save() await Promise.resolve() expect(writes).toEqual(['first']) finish() await Promise.all([first, second]) expect(writes).toEqual(['first', 'second']) } finally { finish() scope.stop() } }) test('reopened editors serialize writes to the same connection while other connections proceed', async () => { const firstScope = effectScope() const secondScope = effectScope() let finish: () => void = () => undefined const blocked = new Promise((resolve) => { finish = resolve }) const writes: string[] = [] const services = { status: async () => 'configured' as const, setCredential: async (_id: string, value: string) => { writes.push(value) if (value === 'first') await blocked }, save: (draft: { id: `mcp-${string}` | null }) => ({ ...connection, id: draft.id ?? connection.id }), remove: async () => undefined } try { const first = firstScope.run(() => useMCPConnectionSettings( ref(createMCPConnectionDraft()), ref('first'), ref({ bearerTokenRequired: 'Required' }), services ) ) const second = secondScope.run(() => useMCPConnectionSettings( ref(createMCPConnectionDraft()), ref('second'), ref({ bearerTokenRequired: 'Required' }), services ) ) const other = secondScope.run(() => useMCPConnectionSettings( ref(createMCPConnectionDraft()), ref('other'), ref({ bearerTokenRequired: 'Required' }), services ) ) if (!first || !second || !other) throw new Error('Missing scope') for (const editor of [first, second, other]) { editor.draft.value.id = connection.id editor.draft.value.authenticationType = 'bearer' } other.draft.value.id = 'mcp-other' const savingFirst = first.save() await Promise.resolve() firstScope.stop() const savingSecond = second.save() await other.save() expect(writes).toEqual(['first', 'other']) finish() await Promise.all([savingFirst, savingSecond]) expect(writes).toEqual(['first', 'other', 'second']) } finally { finish() firstScope.stop() secondScope.stop() } }) test('blank-token save rechecks status after a different editor clears the credential', async () => { const scope = effectScope() let configured = true const enabled: boolean[] = [] const services = { status: async () => (configured ? ('configured' as const) : ('missing' as const)), setCredential: async (_id: string, value: string) => { configured = Boolean(value) }, save: (draft: { enabled: boolean }) => { enabled.push(draft.enabled) return connection }, remove: async () => undefined } try { const first = scope.run(() => useMCPConnectionSettings( ref(createMCPConnectionDraft()), ref(''), ref({ bearerTokenRequired: 'Required' }), services ) ) const second = scope.run(() => useMCPConnectionSettings( ref(createMCPConnectionDraft()), ref(''), ref({ bearerTokenRequired: 'Required' }), services ) ) if (!first || !second) throw new Error('Missing scope') await first.startEdit(connection.id) await second.startEdit(connection.id) second.draft.value.authenticationType = 'bearer' second.draft.value.enabled = true first.clearCredential() expect(configured).toBe(true) const clearing = first.save() const saving = second.save() await clearing expect(await saving).toBe('partial') expect(second.error.value).toBe('Required') expect(enabled).toEqual([false, false, false]) } finally { scope.stop() } }) test('cancel clears secrets and ignores a pending credential lookup', async () => { const scope = effectScope() const token = ref('') const lookup = Promise.withResolvers<'configured'>() try { const state = scope.run(() => useMCPConnectionSettings( ref(createMCPConnectionDraft()), token, ref({ bearerTokenRequired: 'Required' }), { status: () => lookup.promise, save: () => connection, setCredential: async () => undefined, remove: async () => undefined } ) ) if (!state) throw new Error('Missing scope') const editing = state.startEdit(connection.id) expect(state.busy.value).toBe(true) token.value = 'discard-me' state.cancel() expect(token.value).toBe('') expect(state.draft.value.id).toBeNull() lookup.resolve('configured') expect(await editing).toBe(false) expect(state.busy.value).toBe(false) expect(state.tokenStatus.value).toBe('missing') token.value = 'discard-on-close' scope.stop() expect(token.value).toBe('') } finally { lookup.resolve('configured') scope.stop() } })