From 948f1300d1c366d226c5286b32a779d0809d0068 Mon Sep 17 00:00:00 2001 From: Cascade Bot Date: Mon, 27 Apr 2026 11:39:49 +0000 Subject: [PATCH 1/4] feat(alerting): add configurable PM results list for alerting agent + enable UX --- src/agents/definitions/alerting.yaml | 6 +- src/agents/shared/promptContext.ts | 10 ++ src/backends/secretOrchestrator.ts | 15 +++ src/sentry/integration.ts | 8 ++ .../unit/agents/shared/promptContext.test.ts | 81 ++++++++++++ tests/unit/sentry/integration.test.ts | 40 ++++++ .../projects/integration-alerting-tab.tsx | 122 +++++++++++++++++- .../components/projects/integration-form.tsx | 6 +- 8 files changed, 283 insertions(+), 5 deletions(-) diff --git a/src/agents/definitions/alerting.yaml b/src/agents/definitions/alerting.yaml index 7cc638aa..9277e7f9 100644 --- a/src/agents/definitions/alerting.yaml +++ b/src/agents/definitions/alerting.yaml @@ -58,9 +58,12 @@ prompts: 2. Read the relevant source files to understand context and confirm the root cause 3. Summarize: what failed, why, and which code path is responsible <% if (it.backlogListId) { %> - 4. Create a bug fix work item in the backlog (list/status ID: <%= it.backlogListId %>) with: + 4. Create a bug fix investigation work item in the backlog (list/status ID: <%= it.backlogListId %>) with: - Title: short, actionable description (e.g. "Fix: NullPointerException in PaymentService.charge") - Description: root cause, error details, affected file/function, link to the alert issue + <% } else if (it.workItemId) { %> + 4. Post a comment on the current work item (<%= it.workItemId %>) summarising the investigation: + root cause, affected file/function, and recommended fix approach <% } %> 5. Call Finish when done @@ -69,3 +72,4 @@ hint: | Focus on frames from application code (not third-party library frames). Check the event timeline/breadcrumbs for the action or request that preceded the failure. Keep the bug work item description concise and actionable. + If no backlog list is configured, post investigation findings as a comment on the triggering work item. diff --git a/src/agents/shared/promptContext.ts b/src/agents/shared/promptContext.ts index c27d7a79..1b4dcc10 100644 --- a/src/agents/shared/promptContext.ts +++ b/src/agents/shared/promptContext.ts @@ -51,6 +51,10 @@ function getPromptTerminology(pmType: string | undefined) { * Shared by the llmist agent lifecycle (agents/base.ts) and the adapter * (backends/adapter.ts) so both backends use consistent prompt context * building logic including PM-type normalization and work item noun i18n. + * + * @param alertingResultsContainerId - Optional PM container ID from Sentry integration config. + * Used as a fallback `backlogListId` when no PM backlog is configured on the project. + * Populated by `secretOrchestrator` for alerting agent runs. */ export function buildPromptContext( workItemId: string | undefined, @@ -64,17 +68,23 @@ export function buildPromptContext( originalWorkItemUrl: string; detectedAgentType: string; }, + alertingResultsContainerId?: string, ): PromptContext { const pmProvider = getPMProviderOrNull(); const listIds = getListIds(project); const terminology = getPromptTerminology(pmProvider?.type); + // Fall back to the Sentry-configured results container when no PM backlog is set. + const backlogListId = + listIds.backlogListId ?? (alertingResultsContainerId ? alertingResultsContainerId : undefined); + return { workItemId, workItemUrl: workItemId && pmProvider ? pmProvider.getWorkItemUrl(workItemId) : undefined, projectId: project.id, baseBranch: project.baseBranch, ...listIds, + backlogListId, pmType: pmProvider?.type, ...terminology, maxInFlightItems: project.maxInFlightItems ?? 1, diff --git a/src/backends/secretOrchestrator.ts b/src/backends/secretOrchestrator.ts index 405a010d..c2f39442 100644 --- a/src/backends/secretOrchestrator.ts +++ b/src/backends/secretOrchestrator.ts @@ -9,6 +9,7 @@ import type { createAgentLogger } from '../agents/utils/logging.js'; import { mergeEngineSettings } from '../config/engineSettings.js'; import { loadPartials } from '../db/repositories/partialsRepository.js'; import { withGitHubToken } from '../github/client.js'; +import { getSentryIntegrationConfig } from '../sentry/integration.js'; import type { AgentInput, CascadeConfig, ProjectConfig } from '../types/index.js'; import { getDashboardUrl } from '../utils/runLink.js'; import { createNativeToolRuntimeArtifacts } from './nativeToolRuntime.js'; @@ -59,11 +60,25 @@ export async function buildExecutionPlan( } : undefined; + // For alerting agents, look up Sentry `resultsContainerId` as a fallback + // backlogListId when no PM backlog status/list is configured on the project. + let alertingResultsContainerId: string | undefined; + if (agentType === 'alerting') { + try { + const sentryConfig = await getSentryIntegrationConfig(project.id); + alertingResultsContainerId = sentryConfig?.resultsContainerId; + } catch { + // Non-fatal — proceed without the fallback container + } + } + const promptContext: PromptContext = buildPromptContext( workItemId, project, input.triggerType, prContext, + undefined, + alertingResultsContainerId, ); // Load DB partials for template include resolution diff --git a/src/sentry/integration.ts b/src/sentry/integration.ts index 54acdb18..ddbc1459 100644 --- a/src/sentry/integration.ts +++ b/src/sentry/integration.ts @@ -14,6 +14,11 @@ import { getIntegrationByProjectAndCategory } from '../db/repositories/integrati export interface SentryIntegrationConfig { /** Sentry organization slug (e.g. "my-company") */ organizationSlug: string; + /** + * PM container ID where the alerting agent creates investigation work items. + * Maps to `backlogListId` in the prompt context when no PM backlog is configured. + */ + resultsContainerId?: string; } // ============================================================================ @@ -35,5 +40,8 @@ export async function getSentryIntegrationConfig( return { organizationSlug: config.organizationSlug, + ...(typeof config.resultsContainerId === 'string' + ? { resultsContainerId: config.resultsContainerId } + : {}), }; } diff --git a/tests/unit/agents/shared/promptContext.test.ts b/tests/unit/agents/shared/promptContext.test.ts index a7346125..5016b4fc 100644 --- a/tests/unit/agents/shared/promptContext.test.ts +++ b/tests/unit/agents/shared/promptContext.test.ts @@ -467,6 +467,87 @@ describe('buildPromptContext', () => { }); }); + describe('with alertingResultsContainerId fallback', () => { + beforeEach(() => { + const mockProvider = createMockPMProvider(); + mockProvider.type = 'trello'; + mockProvider.getWorkItemUrl = vi.fn((id: string) => `https://trello.com/c/${id}`); + mockGetPMProvider.mockReturnValue(mockProvider); + }); + + it('uses alertingResultsContainerId as backlogListId when PM backlog is not set', () => { + const projectWithoutBacklog = makeProject({ + trello: { + boardId: 'board1', + lists: { + splitting: 'list1', + planning: 'list2', + todo: 'list3', + // no backlog + inProgress: 'list-in-progress', + inReview: 'list-in-review', + merged: 'list-merged', + }, + labels: { readyToProcess: 'label1', processed: 'label2' }, + }, + }); + const ctx = buildPromptContext( + 'card123', + projectWithoutBacklog as never, + undefined, + undefined, + undefined, + 'sentry-container-id', + ); + expect(ctx.backlogListId).toBe('sentry-container-id'); + }); + + it('PM backlogListId takes precedence over alertingResultsContainerId', () => { + const ctx = buildPromptContext( + 'card123', + makeProject() as never, + undefined, + undefined, + undefined, + 'sentry-container-id', + ); + // makeProject has trello.lists.backlog = 'list-backlog' + expect(ctx.backlogListId).toBe('list-backlog'); + }); + + it('alertingResultsContainerId is ignored when it is undefined', () => { + const ctx = buildPromptContext( + 'card123', + makeProject() as never, + undefined, + undefined, + undefined, + undefined, + ); + // still gets backlog from PM config + expect(ctx.backlogListId).toBe('list-backlog'); + }); + + it('backlogListId remains undefined when neither PM backlog nor alertingResultsContainerId is set', () => { + const projectWithoutBacklog = makeProject({ + trello: { + boardId: 'board1', + lists: { + splitting: 'list1', + planning: 'list2', + todo: 'list3', + inProgress: 'list-in-progress', + inReview: 'list-in-review', + merged: 'list-merged', + }, + labels: { readyToProcess: 'label1', processed: 'label2' }, + }, + }); + const ctx = buildPromptContext('card123', projectWithoutBacklog as never); + expect(ctx.backlogListId).toBeUndefined(); + }); + }); + describe('without PM provider (no PM context — e.g. debug agent from dashboard)', () => { beforeEach(() => { mockGetPMProvider.mockReturnValue(null); diff --git a/tests/unit/sentry/integration.test.ts b/tests/unit/sentry/integration.test.ts index 08264b05..aba67428 100644 --- a/tests/unit/sentry/integration.test.ts +++ b/tests/unit/sentry/integration.test.ts @@ -96,6 +96,46 @@ describe('sentry/integration', () => { expect(result).toEqual({ organizationSlug: 'acme-corp' }); }); + it('returns resultsContainerId when present in config', async () => { + mockGetIntegrationByProjectAndCategory.mockResolvedValueOnce({ + id: 'int-3', + provider: 'sentry', + config: { organizationSlug: 'my-org', resultsContainerId: 'list-backlog-123' }, + }); + + const result = await getSentryIntegrationConfig('proj-3'); + + expect(result).toEqual({ + organizationSlug: 'my-org', + resultsContainerId: 'list-backlog-123', + }); + }); + + it('omits resultsContainerId when absent from config', async () => { + mockGetIntegrationByProjectAndCategory.mockResolvedValueOnce({ + id: 'int-4', + provider: 'sentry', + config: { organizationSlug: 'my-org' }, + }); + + const result = await getSentryIntegrationConfig('proj-4'); + + expect(result).toEqual({ organizationSlug: 'my-org' }); + expect(result?.resultsContainerId).toBeUndefined(); + }); + + it('omits resultsContainerId when it is not a string', async () => { + mockGetIntegrationByProjectAndCategory.mockResolvedValueOnce({ + id: 'int-5', + provider: 'sentry', + config: { organizationSlug: 'my-org', resultsContainerId: 42 }, + }); + + const result = await getSentryIntegrationConfig('proj-5'); + + expect(result?.resultsContainerId).toBeUndefined(); + }); + it('queries using projectId and alerting category', async () => { mockGetIntegrationByProjectAndCategory.mockResolvedValueOnce(null); diff --git a/web/src/components/projects/integration-alerting-tab.tsx b/web/src/components/projects/integration-alerting-tab.tsx index 3363f497..257dad6c 100644 --- a/web/src/components/projects/integration-alerting-tab.tsx +++ b/web/src/components/projects/integration-alerting-tab.tsx @@ -2,7 +2,7 @@ * Alerting (Sentry) integration tab component. */ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; -import { Trash2 } from 'lucide-react'; +import { Info, Trash2 } from 'lucide-react'; import { useState } from 'react'; import { CopyButton } from '@/components/ui/copy-button.js'; import { Input } from '@/components/ui/input.js'; @@ -11,6 +11,77 @@ import { API_URL } from '@/lib/api.js'; import { trpc, trpcClient } from '@/lib/trpc.js'; import { ProjectSecretField } from './project-secret-field.js'; +// ============================================================================ +// PM Container Picker +// ============================================================================ + +interface ContainerPickerProps { + projectId: string; + pmProvider: string; + value: string; + onChange: (id: string) => void; +} + +function PMContainerPicker({ projectId, pmProvider, value, onChange }: ContainerPickerProps) { + const containersMutation = useMutation({ + mutationFn: async () => { + return (await trpcClient.pm.discovery.discover.mutate({ + providerId: pmProvider, + capability: 'containers', + args: {}, + projectId, + })) as Array<{ id: string; name: string }>; + }, + }); + + return ( +
+
+ + +
+ {containersMutation.isError && ( +

{containersMutation.error.message}

+ )} +

+ Or enter the ID manually:{' '} + onChange(e.target.value)} + placeholder="container-id" + className="ml-1 inline-block h-6 rounded border border-input bg-background px-2 text-xs" + /> +

+
+ ); +} + // ============================================================================ // Alerting Tab (Sentry) // ============================================================================ @@ -18,15 +89,20 @@ import { ProjectSecretField } from './project-secret-field.js'; interface AlertingTabProps { projectId: string; alertingIntegration?: Record; + /** PM provider slug (e.g. "trello", "jira", "linear") when a PM integration is configured. */ + pmProvider?: string; } -export function AlertingTab({ projectId, alertingIntegration }: AlertingTabProps) { +export function AlertingTab({ projectId, alertingIntegration, pmProvider }: AlertingTabProps) { const queryClient = useQueryClient(); const existingConfig = (alertingIntegration?.config as Record) ?? {}; const [organizationSlug, setOrganizationSlug] = useState( (existingConfig.organizationSlug as string) ?? '', ); + const [resultsContainerId, setResultsContainerId] = useState( + (existingConfig.resultsContainerId as string) ?? '', + ); const [verifyResult, setVerifyResult] = useState<{ id: string; @@ -80,7 +156,10 @@ export function AlertingTab({ projectId, alertingIntegration }: AlertingTabProps projectId, category: 'alerting', provider: 'sentry', - config: { organizationSlug }, + config: { + organizationSlug, + ...(resultsContainerId ? { resultsContainerId } : {}), + }, }); }, onSuccess: () => { @@ -106,6 +185,17 @@ export function AlertingTab({ projectId, alertingIntegration }: AlertingTabProps return (
+ {/* Agent enablement info box */} +
+ +
+ Enable the Alerting Agent — After saving this + integration, go to the Agents tab and enable the{' '} + alerting agent type so Sentry alerts trigger + investigation runs automatically. +
+
+ {/* Organization Slug */}
@@ -123,6 +213,32 @@ export function AlertingTab({ projectId, alertingIntegration }: AlertingTabProps
+ {/* Investigation Results List */} +
+ +

+ The PM list or status where the alerting agent creates investigation work items. Used as + the target container when the agent creates bug fix cards. +

+ {pmProvider ? ( + + ) : ( + setResultsContainerId(e.target.value)} + placeholder="List ID or status name (configure PM integration to use a picker)" + /> + )} +
+ +
+ {/* Credentials */}
diff --git a/web/src/components/projects/integration-form.tsx b/web/src/components/projects/integration-form.tsx index 328bed69..f5901ee5 100644 --- a/web/src/components/projects/integration-form.tsx +++ b/web/src/components/projects/integration-form.tsx @@ -99,7 +99,11 @@ export function IntegrationForm({ projectId }: { projectId: string }) { {activeTab === 'scm' && } {activeTab === 'alerting' && ( - + )}
); From 738cb817eda37fde1cc1eaa8a9485e226a89c8da Mon Sep 17 00:00:00 2001 From: Cascade Bot Date: Mon, 27 Apr 2026 15:46:59 +0000 Subject: [PATCH 2/4] fix(alerting): map PM provider to correct discovery capability in container picker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the non-existent `containers` capability (which caused NOT_IMPLEMENTED errors for every provider) with provider-specific mappings: Trello→`boards`, JIRA→`projects`, Linear→`teams`. Disable the Fetch button with an explanatory tooltip for unknown providers. Also simplify the `alertingResultsContainerId ?? undefined` ternary to a plain `??` expression in promptContext.ts. Co-Authored-By: Claude Sonnet 4.6 --- src/agents/shared/promptContext.ts | 3 +-- .../projects/integration-alerting-tab.tsx | 26 +++++++++++++++++-- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/src/agents/shared/promptContext.ts b/src/agents/shared/promptContext.ts index 1b4dcc10..72412f97 100644 --- a/src/agents/shared/promptContext.ts +++ b/src/agents/shared/promptContext.ts @@ -75,8 +75,7 @@ export function buildPromptContext( const terminology = getPromptTerminology(pmProvider?.type); // Fall back to the Sentry-configured results container when no PM backlog is set. - const backlogListId = - listIds.backlogListId ?? (alertingResultsContainerId ? alertingResultsContainerId : undefined); + const backlogListId = listIds.backlogListId ?? alertingResultsContainerId; return { workItemId, diff --git a/web/src/components/projects/integration-alerting-tab.tsx b/web/src/components/projects/integration-alerting-tab.tsx index 257dad6c..187d2880 100644 --- a/web/src/components/projects/integration-alerting-tab.tsx +++ b/web/src/components/projects/integration-alerting-tab.tsx @@ -22,12 +22,33 @@ interface ContainerPickerProps { onChange: (id: string) => void; } +/** + * Maps a PM provider slug to its discovery capability that returns container-like items. + * Trello → "boards", JIRA → "projects", Linear → "teams". + * Falls back to undefined (disabling the fetch button) for unknown providers. + */ +function containerCapabilityForProvider( + provider: string, +): 'boards' | 'projects' | 'teams' | undefined { + const map: Record = { + trello: 'boards', + jira: 'projects', + linear: 'teams', + }; + return map[provider]; +} + function PMContainerPicker({ projectId, pmProvider, value, onChange }: ContainerPickerProps) { + const capability = containerCapabilityForProvider(pmProvider); + const containersMutation = useMutation({ mutationFn: async () => { + if (!capability) { + throw new Error(`No container discovery capability mapped for provider "${pmProvider}"`); + } return (await trpcClient.pm.discovery.discover.mutate({ providerId: pmProvider, - capability: 'containers', + capability, args: {}, projectId, })) as Array<{ id: string; name: string }>; @@ -59,7 +80,8 @@ function PMContainerPicker({ projectId, pmProvider, value, onChange }: Container
{containersMutation.isError && ( @@ -96,7 +143,7 @@ function PMContainerPicker({ projectId, pmProvider, value, onChange }: Container type="text" value={value} onChange={(e) => onChange(e.target.value)} - placeholder="container-id" + placeholder="list-id or status-name" className="ml-1 inline-block h-6 rounded border border-input bg-background px-2 text-xs" />

@@ -104,6 +151,49 @@ function PMContainerPicker({ projectId, pmProvider, value, onChange }: Container ); } +/** + * Renders the Investigation Results container input. + * Shows a `PMContainerPicker` when the provider supports dropdown discovery, + * or falls back to a plain text `Input` for Trello and unconfigured projects. + * Extracted to keep `AlertingTab`'s cognitive complexity below the project limit. + */ +function ContainerInput({ + projectId, + pmProvider, + pmConfig, + value, + onChange, +}: { + projectId: string; + pmProvider: string | undefined; + pmConfig: Record | undefined; + value: string; + onChange: (v: string) => void; +}) { + if (pmProvider && providerPickerConfig(pmProvider)) { + return ( + + ); + } + const placeholder = pmProvider + ? `Enter list ID manually (no picker available for ${pmProvider})` + : 'List ID or status name (configure PM integration to use a picker)'; + return ( + onChange(e.target.value)} + placeholder={placeholder} + /> + ); +} + // ============================================================================ // Alerting Tab (Sentry) // ============================================================================ @@ -113,9 +203,16 @@ interface AlertingTabProps { alertingIntegration?: Record; /** PM provider slug (e.g. "trello", "jira", "linear") when a PM integration is configured. */ pmProvider?: string; + /** The project's existing PM integration config, used to drive the container picker. */ + pmConfig?: Record; } -export function AlertingTab({ projectId, alertingIntegration, pmProvider }: AlertingTabProps) { +export function AlertingTab({ + projectId, + alertingIntegration, + pmProvider, + pmConfig, +}: AlertingTabProps) { const queryClient = useQueryClient(); const existingConfig = (alertingIntegration?.config as Record) ?? {}; @@ -242,21 +339,13 @@ export function AlertingTab({ projectId, alertingIntegration, pmProvider }: Aler The PM list or status where the alerting agent creates investigation work items. Used as the target container when the agent creates bug fix cards.

- {pmProvider ? ( - - ) : ( - setResultsContainerId(e.target.value)} - placeholder="List ID or status name (configure PM integration to use a picker)" - /> - )} +

diff --git a/web/src/components/projects/integration-form.tsx b/web/src/components/projects/integration-form.tsx index f5901ee5..12bab445 100644 --- a/web/src/components/projects/integration-form.tsx +++ b/web/src/components/projects/integration-form.tsx @@ -103,6 +103,7 @@ export function IntegrationForm({ projectId }: { projectId: string }) { projectId={projectId} alertingIntegration={alertingIntegration} pmProvider={pmIntegration ? pmProvider : undefined} + pmConfig={pmIntegration ? (pmIntegration.config as Record) : undefined} /> )} From ddb90ea425649a817193574a011462ffe961bc2a Mon Sep 17 00:00:00 2001 From: Cascade Bot Date: Wed, 29 Apr 2026 10:25:48 +0000 Subject: [PATCH 4/4] test: add coverage for buildExecutionPlan in secretOrchestrator --- .../unit/backends/secretOrchestrator.test.ts | 148 +++++++++++++++++- 1 file changed, 147 insertions(+), 1 deletion(-) diff --git a/tests/unit/backends/secretOrchestrator.test.ts b/tests/unit/backends/secretOrchestrator.test.ts index 923fca17..74e76f2f 100644 --- a/tests/unit/backends/secretOrchestrator.test.ts +++ b/tests/unit/backends/secretOrchestrator.test.ts @@ -1,14 +1,75 @@ +/* biome-ignore lint/suspicious/noExplicitAny: test mocks */ import { beforeEach, describe, expect, it, vi } from 'vitest'; vi.mock('../../../src/utils/runLink.js', () => ({ getDashboardUrl: vi.fn(), })); -import { injectRunLinkSecrets } from '../../../src/backends/secretOrchestrator.js'; +// Mock everything that buildExecutionPlan might call +vi.mock('../../../src/agents/shared/modelResolution.js', () => ({ + resolveModelConfig: vi.fn().mockResolvedValue({ + systemPrompt: 'system', + taskPrompt: 'task', + model: 'claude', + maxIterations: 10, + }), +})); + +vi.mock('../../../src/agents/shared/promptContext.js', () => ({ + buildPromptContext: vi.fn().mockReturnValue({}), +})); + +vi.mock('../../../src/db/repositories/partialsRepository.js', () => ({ + loadPartials: vi.fn().mockResolvedValue(new Map()), +})); + +vi.mock('../../../src/sentry/integration.js', () => ({ + getSentryIntegrationConfig: vi.fn(), +})); + +vi.mock('../../../src/agents/definitions/profiles.js', () => ({ + getAgentProfile: vi.fn().mockReturnValue({ + fetchContext: vi.fn().mockResolvedValue({}), + finishHooks: {}, + filterTools: vi.fn().mockReturnValue([]), + }), +})); + +vi.mock('../../../src/agents/definitions/toolManifests.js', () => ({ + getToolManifests: vi.fn().mockReturnValue([]), +})); + +vi.mock('../../../src/backends/registry.js', () => ({ + isNativeToolEngineDefinition: vi.fn().mockReturnValue(false), +})); + +vi.mock('../../../src/agents/definitions/index.js', () => ({ + needsGitStateStopHooks: vi.fn().mockReturnValue(false), +})); + +vi.mock('../../../src/backends/secretBuilder.js', () => ({ + augmentProjectSecrets: vi.fn().mockResolvedValue({}), + resolveGitHubToken: vi.fn(), + injectGitHubAckCommentId: vi.fn(), + injectProgressCommentId: vi.fn(), +})); + +vi.mock('../../../src/backends/sidecarManager.js', () => ({ + createCompletionArtifacts: vi.fn().mockReturnValue({}), +})); + +import { buildPromptContext } from '../../../src/agents/shared/promptContext.js'; +import { + buildExecutionPlan, + injectRunLinkSecrets, +} from '../../../src/backends/secretOrchestrator.js'; +import { getSentryIntegrationConfig } from '../../../src/sentry/integration.js'; import type { ProjectConfig } from '../../../src/types/index.js'; import { getDashboardUrl } from '../../../src/utils/runLink.js'; const mockGetDashboardUrl = vi.mocked(getDashboardUrl); +const mockGetSentryIntegrationConfig = vi.mocked(getSentryIntegrationConfig); +const mockBuildPromptContext = vi.mocked(buildPromptContext); function makeProject(overrides?: Partial): ProjectConfig { return { @@ -24,6 +85,91 @@ function makeProject(overrides?: Partial): ProjectConfig { beforeEach(() => { mockGetDashboardUrl.mockReturnValue(undefined); + vi.clearAllMocks(); +}); + +describe('buildExecutionPlan', () => { + it('fetches sentry config for alerting agent', async () => { + mockGetSentryIntegrationConfig.mockResolvedValueOnce({ + organizationSlug: 'org', + resultsContainerId: 'sentry-container-123', + }); + + const project = makeProject(); + await buildExecutionPlan( + 'alerting', + { project, config: {}, triggerType: 'sentry:issue-created' } as unknown as any, + '/repo', + {} as unknown as any, + {} as unknown as any, + 'token', + false, + 'claude-code', + {} as unknown as any, + ); + + expect(mockGetSentryIntegrationConfig).toHaveBeenCalledWith('test-project'); + expect(mockBuildPromptContext).toHaveBeenCalledWith( + undefined, + project, + 'sentry:issue-created', + undefined, + undefined, + 'sentry-container-123', + ); + }); + + it('does not fetch sentry config for non-alerting agent', async () => { + const project = makeProject(); + await buildExecutionPlan( + 'implementation', + { project, config: {}, triggerType: 'manual' } as unknown as any, + '/repo', + {} as unknown as any, + {} as unknown as any, + 'token', + false, + 'claude-code', + {} as unknown as any, + ); + + expect(mockGetSentryIntegrationConfig).not.toHaveBeenCalled(); + expect(mockBuildPromptContext).toHaveBeenCalledWith( + undefined, + project, + 'manual', + undefined, + undefined, + undefined, + ); + }); + + it('handles sentry config failure gracefully', async () => { + mockGetSentryIntegrationConfig.mockRejectedValueOnce(new Error('DB failure')); + + const project = makeProject(); + await buildExecutionPlan( + 'alerting', + { project, config: {}, triggerType: 'sentry:issue-created' } as unknown as any, + '/repo', + {} as unknown as any, + {} as unknown as any, + 'token', + false, + 'claude-code', + {} as unknown as any, + ); + + expect(mockGetSentryIntegrationConfig).toHaveBeenCalledWith('test-project'); + expect(mockBuildPromptContext).toHaveBeenCalledWith( + undefined, + project, + 'sentry:issue-created', + undefined, + undefined, + undefined, + ); + }); }); describe('injectRunLinkSecrets', () => {