Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
14 changes: 14 additions & 0 deletions apps/web/src/app/(app)/code-reviews/ReviewAgentPageClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { useEffect } from 'react';
import { toast } from 'sonner';
import { ReviewConfigForm } from '@/components/code-reviews/ReviewConfigForm';
import { CodeReviewActionRequiredAlert } from '@/components/code-reviews/CodeReviewActionRequiredAlert';
import { CodeReviewJobsCard } from '@/components/code-reviews/CodeReviewJobsCard';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { Badge } from '@/components/ui/badge';
Expand Down Expand Up @@ -56,6 +57,11 @@ export function ReviewAgentPageClient({
trpc.personalReviewAgent.getGitLabStatus.queryOptions()
);

const { data: selectedConfigData } = useQuery(
trpc.personalReviewAgent.getReviewConfig.queryOptions({ platform: selectedPlatform })
);
const selectedActionRequired = selectedConfigData?.actionRequired ?? null;

const isGitHubAppInstalled =
githubStatusData?.connected && githubStatusData?.integration?.isValid;
const isGitLabConnected = gitlabStatusData?.connected && gitlabStatusData?.integration?.isValid;
Expand Down Expand Up @@ -151,6 +157,10 @@ export function ReviewAgentPageClient({
</Alert>
)}

{selectedPlatform === 'github' && selectedActionRequired && (
<CodeReviewActionRequiredAlert actionRequired={selectedActionRequired} />
)}

{/* GitHub Configuration Tabs */}
<Tabs defaultValue="config" className="w-full">
<TabsList className="grid w-full max-w-2xl grid-cols-2">
Expand Down Expand Up @@ -211,6 +221,10 @@ export function ReviewAgentPageClient({
</Alert>
)}

{selectedPlatform === 'gitlab' && selectedActionRequired && (
<CodeReviewActionRequiredAlert actionRequired={selectedActionRequired} />
)}

{/* GitLab Configuration Tabs */}
<Tabs defaultValue="config" className="w-full">
<TabsList className="grid w-full max-w-2xl grid-cols-2">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { useEffect } from 'react';
import { toast } from 'sonner';
import { ReviewConfigForm } from '@/components/code-reviews/ReviewConfigForm';
import { CodeReviewActionRequiredAlert } from '@/components/code-reviews/CodeReviewActionRequiredAlert';
import { CodeReviewJobsCard } from '@/components/code-reviews/CodeReviewJobsCard';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { Badge } from '@/components/ui/badge';
Expand Down Expand Up @@ -63,6 +64,14 @@ export function ReviewAgentPageClient({
})
);

const { data: selectedConfigData } = useQuery(
trpc.organizations.reviewAgent.getReviewConfig.queryOptions({
organizationId,
platform: selectedPlatform,
})
);
const selectedActionRequired = selectedConfigData?.actionRequired ?? null;

const isGitHubAppInstalled =
githubStatusData?.connected && githubStatusData?.integration?.isValid;
const isGitLabConnected = gitlabStatusData?.connected && gitlabStatusData?.integration?.isValid;
Expand Down Expand Up @@ -158,6 +167,13 @@ export function ReviewAgentPageClient({
</Alert>
)}

{selectedPlatform === 'github' && selectedActionRequired && (
<CodeReviewActionRequiredAlert
actionRequired={selectedActionRequired}
organizationId={organizationId}
/>
)}

{/* GitHub Configuration Tabs */}
<Tabs defaultValue="config" className="w-full">
<TabsList className="grid w-full max-w-2xl grid-cols-2">
Expand Down Expand Up @@ -218,6 +234,13 @@ export function ReviewAgentPageClient({
</Alert>
)}

{selectedPlatform === 'gitlab' && selectedActionRequired && (
<CodeReviewActionRequiredAlert
actionRequired={selectedActionRequired}
organizationId={organizationId}
/>
)}

{/* GitLab Configuration Tabs */}
<Tabs defaultValue="config" className="w-full">
<TabsList className="grid w-full max-w-2xl grid-cols-2">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ type Props = {
};

const CATEGORY_COLORS: Record<string, string> = {
'Action Required': 'bg-yellow-500',
'Rate Limited': 'bg-amber-500',
Timeout: 'bg-orange-500',
'Context Window Exceeded': 'bg-purple-500',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ const mockCaptureMessage = jest.fn<any>();
const mockAppendReviewSummaryFooter = jest.fn<any>();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const mockRetryReviewFresh = jest.fn<any>();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const mockDisableCodeReviewForActionRequiredFailure = jest.fn<any>();

// --- Module mocks ---

Expand Down Expand Up @@ -142,6 +144,15 @@ jest.mock('@/lib/code-reviews/summary/usage-footer', () => ({
appendReviewSummaryFooter: (...args: unknown[]) => mockAppendReviewSummaryFooter(...args),
}));

jest.mock('@/lib/code-reviews/action-required', () => {
const actual = jest.requireActual<Record<string, unknown>>('@/lib/code-reviews/action-required');
return {
...actual,
disableCodeReviewForActionRequiredFailure: (...args: unknown[]) =>
mockDisableCodeReviewForActionRequiredFailure(...args),
};
});

jest.mock('@/lib/constants', () => ({
APP_URL: 'https://test.kilo.ai',
}));
Expand Down Expand Up @@ -334,6 +345,7 @@ beforeEach(async () => {
mockUpdateCodeReviewUsage.mockResolvedValue(undefined);
mockUpdateCodeReviewStatusIfNonTerminal.mockResolvedValue(true);
mockAppendReviewSummaryFooter.mockReturnValue('body with footer');
mockDisableCodeReviewForActionRequiredFailure.mockResolvedValue(undefined);
({ POST } = await import('./route'));
});

Expand Down Expand Up @@ -479,6 +491,96 @@ describe('POST /api/internal/code-review-status/[reviewId]', () => {
);
});

it('infers BYOK invalid-key callbacks as action-required failures', async () => {
mockGetCodeReviewById.mockResolvedValue(makeReview());

const response = await POST(
makeRequest({
status: 'failed',
errorMessage:
'[BYOK] Your API key is invalid or has been revoked. Please check your API key configuration.',
}),
makeParams(REVIEW_ID)
);

expect(response.status).toBe(200);
expect(mockUpdateCodeReviewStatus).toHaveBeenCalledWith(
REVIEW_ID,
'failed',
expect.objectContaining({
terminalReason: 'byok_invalid_key',
})
);
expect(mockCreateInfraRetryAttemptIfMissing).not.toHaveBeenCalled();
expect(mockRetryReviewFresh).not.toHaveBeenCalled();
expect(mockDisableCodeReviewForActionRequiredFailure).toHaveBeenCalledWith(
expect.objectContaining({
owner: { type: 'user', id: 'user-1', userId: 'user-1' },
platform: 'github',
reviewId: REVIEW_ID,
reason: 'byok_invalid_key',
})
);
expect(mockUpdateCheckRun).toHaveBeenCalledWith(
'inst-1',
'owner',
'repo',
12345,
expect.objectContaining({
conclusion: 'action_required',
output: expect.objectContaining({ title: 'BYOK API key needs attention' }),
}),
'standard'
);
});

it('infers GitHub installation and IP allow-list callback failures', async () => {
mockGetCodeReviewById.mockResolvedValue(makeReview());

await POST(
makeRequest({
status: 'failed',
errorMessage:
'Dispatch failed: GitHub token or active app installation required for this repository (no_installation_found)',
}),
makeParams(REVIEW_ID)
);

expect(mockUpdateCodeReviewStatus).toHaveBeenLastCalledWith(
REVIEW_ID,
'failed',
expect.objectContaining({ terminalReason: 'github_installation_required' })
);

jest.clearAllMocks();
mockGetCodeReviewById.mockResolvedValue(makeReview());
mockUpdateCodeReviewAttemptForCallback.mockImplementation(async params =>
makeAttempt({
status: params.status,
error_message: params.errorMessage ?? null,
terminal_reason: params.terminalReason ?? null,
})
);
mockGetLatestCodeReviewAttempt.mockResolvedValue(makeAttempt());
mockGetIntegrationById.mockResolvedValue(makeIntegration());
mockDisableCodeReviewForActionRequiredFailure.mockResolvedValue(undefined);

await POST(
makeRequest({
status: 'failed',
errorMessage:
'Although you appear to have the correct authorization credentials, the `acme` organization has an IP allow list enabled, and 192.0.2.1 is not permitted.',
}),
makeParams(REVIEW_ID)
);

expect(mockUpdateCodeReviewStatus).toHaveBeenLastCalledWith(
REVIEW_ID,
'failed',
expect.objectContaining({ terminalReason: 'github_ip_allow_list' })
);
});

it('keeps interrupted non-billing callbacks as cancelled', async () => {
mockGetCodeReviewById.mockResolvedValue(makeReview());

Expand Down
Loading
Loading