diff --git a/packages/frontend/editor-ui/src/features/agents/__tests__/useAgentChannelSetup.test.ts b/packages/frontend/editor-ui/src/features/agents/__tests__/useAgentChannelSetup.test.ts new file mode 100644 index 00000000000..3bee2c04ddb --- /dev/null +++ b/packages/frontend/editor-ui/src/features/agents/__tests__/useAgentChannelSetup.test.ts @@ -0,0 +1,100 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import { useAgentChannelSetup } from '../composables/useAgentChannelSetup'; + +const { + fetchAllCredentialsForWorkflowMock, + fetchProjectMock, + projectsStoreMock, + setCredentialsMock, +} = vi.hoisted(() => ({ + fetchAllCredentialsForWorkflowMock: vi.fn(), + fetchProjectMock: vi.fn(), + projectsStoreMock: { + currentProject: null as { id: string; scopes?: string[] } | null, + personalProject: null as { id: string; scopes?: string[] } | null, + myProjects: [] as Array<{ id: string; scopes?: string[] }>, + fetchProject: vi.fn(), + }, + setCredentialsMock: vi.fn(), +})); + +vi.mock('@n8n/stores/useRootStore', () => ({ + useRootStore: () => ({ restApiContext: {} }), +})); + +vi.mock('@/app/stores/ui.store', () => ({ + useUIStore: () => ({ + isModalActiveById: {}, + openNewCredential: vi.fn(), + openExistingCredential: vi.fn(), + }), +})); + +vi.mock('@/features/credentials/credentials.store', () => ({ + useCredentialsStore: () => ({ + setCredentials: setCredentialsMock, + fetchAllCredentialsForWorkflow: fetchAllCredentialsForWorkflowMock, + getCredentialTypeByName: vi.fn(), + }), +})); + +vi.mock('@/features/collaboration/projects/projects.store', () => ({ + useProjectsStore: () => projectsStoreMock, +})); + +function createChannelSetup() { + return useAgentChannelSetup({ + projectId: () => 'artifact-project', + agentId: () => 'agent-1', + currentIntegration: null, + connectedCredentials: {}, + fetchStatus: vi.fn().mockResolvedValue(undefined), + isIntegrationConnected: () => false, + }); +} + +describe('useAgentChannelSetup', () => { + beforeEach(() => { + vi.clearAllMocks(); + projectsStoreMock.currentProject = null; + projectsStoreMock.personalProject = null; + projectsStoreMock.myProjects = []; + projectsStoreMock.fetchProject = fetchProjectMock; + fetchAllCredentialsForWorkflowMock.mockResolvedValue([]); + fetchProjectMock.mockResolvedValue({ + id: 'artifact-project', + name: 'Artifact project', + icon: null, + type: 'team', + description: null, + createdAt: '2026-01-01T00:00:00.000Z', + updatedAt: '2026-01-01T00:00:00.000Z', + relations: [], + scopes: ['credential:create'], + rolesManaged: false, + }); + }); + + it('uses project scopes already available in the store', async () => { + projectsStoreMock.myProjects = [{ id: 'artifact-project', scopes: ['credential:create'] }]; + const { credentialPermissions, loadChannelState } = createChannelSetup(); + + await loadChannelState([]); + + expect(credentialPermissions.value.create).toBe(true); + expect(fetchProjectMock).not.toHaveBeenCalled(); + }); + + it('loads scopes when an artifact project is missing from the store', async () => { + // AGENT-443: artifact mode supplies its project through props rather than the route. + const { credentialPermissions, loadChannelState } = createChannelSetup(); + + expect(credentialPermissions.value.create).toBe(false); + + await loadChannelState([]); + + expect(fetchProjectMock).toHaveBeenCalledWith('artifact-project'); + expect(credentialPermissions.value.create).toBe(true); + }); +}); diff --git a/packages/frontend/editor-ui/src/features/agents/components/AgentChannelModal.vue b/packages/frontend/editor-ui/src/features/agents/components/AgentChannelModal.vue index a4d8856ecd3..2c4496433f7 100644 --- a/packages/frontend/editor-ui/src/features/agents/components/AgentChannelModal.vue +++ b/packages/frontend/editor-ui/src/features/agents/components/AgentChannelModal.vue @@ -11,6 +11,7 @@ import { } from '@n8n/design-system'; import type { IconName } from '@n8n/design-system/components/N8nIcon/icons'; import { useI18n } from '@n8n/i18n'; +import { FocusScope } from 'reka-ui'; import { computed, ref, watch } from 'vue'; import { useAgentChannelSetup } from '../composables/useAgentChannelSetup'; import { useAgentIntegrationStatus } from '../composables/useAgentIntegrationStatus'; @@ -245,6 +246,15 @@ watch( @interact-outside="(e) => e.preventDefault()" @update:open="$emit('update:open', $event)" > + + + + @@ -277,7 +287,7 @@ watch( - + @@ -467,6 +477,13 @@ watch( + +