Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
6ad74aa
feat(agent-task): add Qovery service context
RemiBonnet Sep 15, 2026
5416c69
fix(agent-task): align context cards
RemiBonnet Sep 15, 2026
2d67e42
fix(agent-task): streamline context selection
RemiBonnet Sep 15, 2026
a9cf213
fix(agent-task): reset service context selection
RemiBonnet Sep 15, 2026
9e05a48
fix(agent-task): style context reset action
RemiBonnet Sep 15, 2026
550888e
fix(agent-task): hide empty context reset
RemiBonnet Sep 15, 2026
3d3ba01
fix(agent-task): update service context label
RemiBonnet Sep 15, 2026
662cfa1
fix(agent-task): align context badge typography
RemiBonnet Sep 15, 2026
244ca1b
fix(agent-task): restore empty context choices
RemiBonnet Sep 15, 2026
92d3d40
fix(agent-task): list Qovery context services
RemiBonnet Sep 15, 2026
dc7afb8
fix(agent-task): clarify service context copy
RemiBonnet Sep 15, 2026
be92e52
fix(agent-task): hide completed select all action
RemiBonnet Sep 15, 2026
4792d03
fix(agent-task): refine service import modal
RemiBonnet Sep 15, 2026
b7bde49
fix(agent-task): preserve select all spacing
RemiBonnet Sep 15, 2026
dec18ad
feat(agent-task): connect Qovery MCP to service context
RemiBonnet Sep 15, 2026
ded735e
fix(agent-task): align context styling
RemiBonnet Sep 15, 2026
2afb25b
fix(agent-task): clarify required Qovery MCP
RemiBonnet Sep 15, 2026
c145bbb
fix(agent-task): keep default cursor on required MCP
RemiBonnet Sep 15, 2026
e8dd497
fix(agent-task): synchronize Qovery MCP setup
RemiBonnet Sep 15, 2026
10466cc
fix(agent-task): select existing Qovery MCP
RemiBonnet Sep 15, 2026
eb1fc58
fix(agent-task): show preconfigured Qovery MCP
RemiBonnet Sep 15, 2026
91f594e
fix(agent-task): display required Qovery MCP selection
RemiBonnet Sep 15, 2026
a2c1614
fix(agent-task): preserve template during configuration redirect
RemiBonnet Sep 15, 2026
7d9cc95
fix(agent-task): address context review feedback
RemiBonnet Sep 15, 2026
bb28a0a
fix(agent-task): harden Qovery MCP setup
RemiBonnet Sep 15, 2026
121d1f4
fix(agent-task): lock template Qovery MCP
RemiBonnet Sep 15, 2026
976b1b5
fix(agent-task): prevent duplicate context saves
RemiBonnet Sep 15, 2026
5cf0b86
feat(agent-task): persist Qovery service context
RemiBonnet Sep 15, 2026
bced05c
fix(agent-task): address context integration feedback
RemiBonnet Sep 15, 2026
b0f6d83
fix(agent-task): preserve context settings
RemiBonnet Sep 15, 2026
476861e
fix(agent-task): uppercase git provider labels
RemiBonnet Sep 15, 2026
cc57144
fix(agent-task): harden service context flow
RemiBonnet Sep 15, 2026
f8a6090
test(agent-task): avoid modal overlay ordering
RemiBonnet Sep 15, 2026
dacb7fa
fix(agent-task): keep modal behavior unchanged
RemiBonnet Sep 15, 2026
28df3ee
fix(agent-task): preserve valid service context
RemiBonnet Sep 15, 2026
7d43b09
fix(agent-task): harden Qovery MCP setup
RemiBonnet Sep 15, 2026
7b55b18
fix(agent-task): delimit generated context prompt
RemiBonnet Sep 15, 2026
40f4f7f
fix(agent-task): guard context inventories
RemiBonnet Sep 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ function RouteComponent() {
<Navigate
to="/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/agentic-workflow/configuration"
params={{ organizationId, projectId, environmentId }}
search={(search) => search}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Necessary to keep template URL

replace
/>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ function RouteComponent() {

return (
<AgenticWorkflowCreationFlow
requiresQoveryMcp={selectedTemplate?.requiresQoveryMcp}
seed={selectedTemplate?.seed}
variablesSeed={selectedTemplate?.variables}
Comment on lines +33 to 35

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we could pass the selectedTemplate altogether instead of having multiple props

onExit={() =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,16 @@ describe('MCP server mutations', () => {
expect(result).toEqual(mockData)
})

it('should create the read-only Qovery MCP server', async () => {
const mockData = { id: 'qovery-mcp', name: 'Qovery MCP', url: 'https://mcp.qovery.com/mcp' }
jest.spyOn(MCPServersApi.prototype, 'createQoveryMcpServer').mockResolvedValue({ data: mockData } as never)

const result = await mutations.createQoveryMcpServer({ organizationId: 'org-1' })

expect(MCPServersApi.prototype.createQoveryMcpServer).toHaveBeenCalledWith('org-1', undefined)
expect(result).toEqual(mockData)
})

it('should edit an MCP server', async () => {
const mockData = { id: 'mcp-1', ...mcpServerRequest }
jest.spyOn(MCPServersApi.prototype, 'editMcpServer').mockResolvedValue({ data: mockData } as never)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import {
OrganizationWebhookApi,
type OrganizationWebhookCreateRequest,
PlanEnum,
type QoveryMcpServerRequest,
type TfVarsDiscoveryMode,
} from 'qovery-typescript-axios'
import { match } from 'ts-pattern'
Expand Down Expand Up @@ -648,6 +649,16 @@ export const mutations = {
const response = await mcpServersApi.createMcpServer(organizationId, mcpServerRequest)
return response.data
},
async createQoveryMcpServer({
organizationId,
qoveryMcpServerRequest,
}: {
organizationId: string
qoveryMcpServerRequest?: QoveryMcpServerRequest
}) {
const response = await mcpServersApi.createQoveryMcpServer(organizationId, qoveryMcpServerRequest)
return response.data
},
async editMcpServer({
mcpServerId,
mcpServerRequest,
Expand Down
1 change: 1 addition & 0 deletions libs/domains/organizations/feature/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ export * from './lib/hooks/use-parse-terraform-variables-from-git-repo/use-parse
export * from './lib/hooks/use-list-tfvars-files-from-git-repo/use-list-tfvars-files-from-git-repo'
export * from './lib/hooks/use-mcp-servers/use-mcp-servers'
export * from './lib/hooks/use-create-mcp-server/use-create-mcp-server'
export * from './lib/hooks/use-create-qovery-mcp-server/use-create-qovery-mcp-server'
export * from './lib/hooks/use-edit-mcp-server/use-edit-mcp-server'
export * from './lib/hooks/use-delete-mcp-server/use-delete-mcp-server'
export * from './lib/invoice-banner/invoice-banner'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { mutations } from '@qovery/domains/organizations/data-access'
import { queries } from '@qovery/state/util-queries'

export function useCreateQoveryMcpServer() {
const queryClient = useQueryClient()

return useMutation(mutations.createQoveryMcpServer, {
onSuccess(_, { organizationId }) {
queryClient.invalidateQueries({
queryKey: queries.organizations.mcpServers({ organizationId }).queryKey,
})
},
meta: {
notifyOnSuccess: {
title: 'Qovery MCP has been created',
},
notifyOnError: true,
},
})
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import { McpServerScope } from 'qovery-typescript-axios'
import { type UseFormReturn } from 'react-hook-form'
import * as organizationsDomain from '@qovery/domains/organizations/feature'
import { renderWithProviders, screen } from '@qovery/shared/util-tests'
import * as servicesDomain from '@qovery/domains/services/feature'
import { renderWithProviders, screen, waitFor } from '@qovery/shared/util-tests'
import { type AgenticWorkflowSettingsFormValues } from '../agentic-workflow-settings'
import { AgenticWorkflowSettingsFormHarness } from '../agentic-workflow-settings-test-utils'
import { AgenticWorkflowConnectionsSettings } from './agentic-workflow-connections-settings'

const useMcpServersSpy = jest.spyOn(organizationsDomain, 'useMcpServers') as jest.Mock
const useCreateQoveryMcpServerSpy = jest.spyOn(organizationsDomain, 'useCreateQoveryMcpServer') as jest.Mock
const useContextServicesSpy = jest.spyOn(servicesDomain, 'useAgenticWorkflowContextServices') as jest.Mock
const refetchMcpServers = jest.fn()

jest.mock('@tanstack/react-router', () => ({
...jest.requireActual('@tanstack/react-router'),
Expand All @@ -13,6 +19,7 @@ jest.mock('@tanstack/react-router', () => ({

describe('AgenticWorkflowConnectionsSettings', () => {
beforeEach(() => {
refetchMcpServers.mockReset().mockResolvedValue({ data: [], isError: false })
useMcpServersSpy.mockReturnValue({
data: [
{
Expand All @@ -23,8 +30,12 @@ describe('AgenticWorkflowConnectionsSettings', () => {
attachable: true,
},
],
isError: false,
isLoading: false,
refetch: refetchMcpServers,
})
useContextServicesSpy.mockReturnValue({ data: [], isError: false, isLoading: false })
useCreateQoveryMcpServerSpy.mockReturnValue({ mutateAsync: jest.fn() })
})

it('renders Git context and MCPs with settings-specific confirmation labels', async () => {
Expand All @@ -49,7 +60,9 @@ describe('AgenticWorkflowConnectionsSettings', () => {
dockerFragment: 'RUN apt-get update',
}}
>
{(form) => <AgenticWorkflowConnectionsSettings form={form} gitTokensLoading={false} />}
{(form) => (
<AgenticWorkflowConnectionsSettings environmentId="environment-1" form={form} gitTokensLoading={false} />
)}
</AgenticWorkflowSettingsFormHarness>
)

Expand All @@ -59,4 +72,170 @@ describe('AgenticWorkflowConnectionsSettings', () => {
await userEvent.click(screen.getByRole('button', { name: 'Manage context' }))
expect(screen.getByRole('button', { name: 'Apply changes' })).toBeInTheDocument()
})

it('shows and edits persisted Qovery service context', async () => {
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
const qoveryMcpServer = {
id: 'qovery-mcp',
name: 'qovery',
url: 'https://mcp.qovery.com/mcp',
scope: McpServerScope.ORGANIZATION,
attachable: true,
}
useMcpServersSpy.mockReturnValue({ data: [qoveryMcpServer], isLoading: false })
useContextServicesSpy.mockReturnValue({
data: [{ id: 'service-1', name: 'api', type: 'APPLICATION' }],
isLoading: false,
})
let settingsForm: UseFormReturn<AgenticWorkflowSettingsFormValues> | undefined
const { userEvent } = renderWithProviders(
<AgenticWorkflowSettingsFormHarness
values={{
contextServiceIds: ['service-1'],
mcpServerIds: [qoveryMcpServer.id],
requiredMcpServerIds: [qoveryMcpServer.id],
}}
>
{(form) => {
settingsForm = form
return (
<AgenticWorkflowConnectionsSettings environmentId="environment-1" form={form} gitTokensLoading={false} />
)
}}
</AgenticWorkflowSettingsFormHarness>
)

expect(screen.getByText('api')).toBeInTheDocument()
const lockedMcpButton = screen.getByRole('button', {
name: /MCP Qovery: This MCP is required by the selected Qovery service context/,
})
expect(lockedMcpButton).toBeDisabled()
await userEvent.hover(lockedMcpButton.parentElement as HTMLElement)
expect(await screen.findByRole('tooltip')).toHaveTextContent(
'This MCP is required by the selected Qovery service context and cannot be removed.'
)
await userEvent.click(screen.getByRole('button', { name: 'Manage Qovery service context' }))
expect(screen.getByRole('checkbox', { name: 'api' })).toBeChecked()
await userEvent.click(screen.getByRole('checkbox', { name: 'api' }))
await userEvent.click(screen.getByRole('button', { name: 'Confirm' }))

expect(settingsForm?.getValues('contextServiceIds')).toEqual([])
expect(settingsForm?.getValues('requiredMcpServerIds')).toEqual([qoveryMcpServer.id])
expect(screen.getByText('Add Qovery services')).toBeInTheDocument()
})

it('creates an attachable Qovery MCP instead of selecting an unavailable one', async () => {
const unavailableQoveryMcpServer = {
id: 'unavailable-qovery-mcp',
name: 'qovery',
url: 'https://mcp.qovery.com/mcp',
scope: McpServerScope.USER,
attachable: false,
}
const createdQoveryMcpServer = {
id: 'created-qovery-mcp',
name: 'qovery',
url: 'https://mcp.qovery.com/mcp',
scope: McpServerScope.ORGANIZATION,
}
const createQoveryMcpServer = jest.fn().mockResolvedValue(createdQoveryMcpServer)
useMcpServersSpy.mockReturnValue({ data: [unavailableQoveryMcpServer], isLoading: false })
useContextServicesSpy.mockReturnValue({
data: [{ id: 'service-1', name: 'api', type: 'APPLICATION' }],
isLoading: false,
})
useCreateQoveryMcpServerSpy.mockReturnValue({ mutateAsync: createQoveryMcpServer })
let settingsForm: UseFormReturn<AgenticWorkflowSettingsFormValues> | undefined
const { userEvent } = renderWithProviders(
<AgenticWorkflowSettingsFormHarness>
{(form) => {
settingsForm = form
return (
<AgenticWorkflowConnectionsSettings environmentId="environment-1" form={form} gitTokensLoading={false} />
)
}}
</AgenticWorkflowSettingsFormHarness>
)

await userEvent.click(screen.getByRole('button', { name: /^Add Qovery services/ }))
await userEvent.click(screen.getByRole('checkbox', { name: 'api' }))
await userEvent.click(screen.getByRole('button', { name: 'Confirm' }))

expect(createQoveryMcpServer).toHaveBeenCalledWith({ organizationId: 'organization-1' })
expect(settingsForm?.getValues('mcpServerIds')).toEqual([createdQoveryMcpServer.id])
expect(settingsForm?.getValues('requiredMcpServerIds')).toEqual([createdQoveryMcpServer.id])
})

it('does not create a Qovery MCP when the MCP inventory cannot be loaded', async () => {
const createQoveryMcpServer = jest.fn()
refetchMcpServers.mockResolvedValue({
data: undefined,
error: new Error('MCP servers failed to load'),
isError: true,
})
useMcpServersSpy.mockReturnValue({
data: undefined,
isError: true,
isLoading: false,
refetch: refetchMcpServers,
})
useContextServicesSpy.mockReturnValue({
data: [{ id: 'service-1', name: 'api', type: 'APPLICATION' }],
isError: false,
isLoading: false,
})
useCreateQoveryMcpServerSpy.mockReturnValue({ mutateAsync: createQoveryMcpServer })
const { userEvent } = renderWithProviders(
<AgenticWorkflowSettingsFormHarness>
{(form) => (
<AgenticWorkflowConnectionsSettings environmentId="environment-1" form={form} gitTokensLoading={false} />
)}
</AgenticWorkflowSettingsFormHarness>
)

await userEvent.click(screen.getByRole('button', { name: /^Add Qovery services/ }))
await userEvent.click(screen.getByRole('checkbox', { name: 'api' }))
await userEvent.click(screen.getByRole('button', { name: 'Confirm' }))

await waitFor(() => expect(refetchMcpServers).toHaveBeenCalled())
expect(createQoveryMcpServer).not.toHaveBeenCalled()
expect(screen.getByText('Unable to add the selected services. Try again.')).toBeInTheDocument()
})

it('prevents editing service context when the context inventory cannot be loaded', () => {
useContextServicesSpy.mockReturnValue({ data: undefined, isError: true, isLoading: false })

renderWithProviders(
<AgenticWorkflowSettingsFormHarness values={{ contextServiceIds: ['service-1'] }}>
{(form) => (
<AgenticWorkflowConnectionsSettings environmentId="environment-1" form={form} gitTokensLoading={false} />
)}
</AgenticWorkflowSettingsFormHarness>
)

expect(screen.getByRole('button', { name: /^Add Qovery services/ })).toBeDisabled()
})

it.each([
{ contextServicesLoading: true, mcpServersLoading: false },
{ contextServicesLoading: false, mcpServersLoading: true },
])(
'prevents editing persisted Qovery service context while dependencies are loading',
({ contextServicesLoading, mcpServersLoading }) => {
useMcpServersSpy.mockReturnValue({ data: [], isLoading: mcpServersLoading })
useContextServicesSpy.mockReturnValue({
data: [{ id: 'service-1', name: 'api', type: 'APPLICATION' }],
isLoading: contextServicesLoading,
})

renderWithProviders(
<AgenticWorkflowSettingsFormHarness values={{ contextServiceIds: ['service-1'] }}>
{(form) => (
<AgenticWorkflowConnectionsSettings environmentId="environment-1" form={form} gitTokensLoading={false} />
)}
</AgenticWorkflowSettingsFormHarness>
)

expect(screen.getByRole('button', { name: 'Manage Qovery service context' })).toBeDisabled()
}
)
})
Loading
Loading