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
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,12 @@ const mockCaptureException = jest.fn<any>();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const mockCaptureMessage = jest.fn<any>();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const mockAppendPreviousReviewSummaryHistory = jest.fn<any>();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const mockAppendReviewSummaryFooter = jest.fn<any>();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const mockBuildReviewSummaryFooter = 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>();
Expand Down Expand Up @@ -140,8 +144,14 @@ jest.mock('@sentry/nextjs', () => ({
captureMessage: mockCaptureMessage,
}));

jest.mock('@/lib/code-reviews/summary/history', () => ({
appendPreviousReviewSummaryHistory: (...args: unknown[]) =>
mockAppendPreviousReviewSummaryHistory(...args),
}));

jest.mock('@/lib/code-reviews/summary/usage-footer', () => ({
appendReviewSummaryFooter: (...args: unknown[]) => mockAppendReviewSummaryFooter(...args),
buildReviewSummaryFooter: (...args: unknown[]) => mockBuildReviewSummaryFooter(...args),
}));

jest.mock('@/lib/code-reviews/action-required', () => {
Expand Down Expand Up @@ -225,6 +235,8 @@ function makeReview(overrides: Partial<CloudAgentCodeReview> = {}): CloudAgentCo
repository_review_instructions_used: false,
repository_review_instructions_ref: null,
repository_review_instructions_truncated: false,
previous_summary_body: null,
previous_summary_head_sha: null,
model: null,
total_tokens_in: null,
total_tokens_out: null,
Expand Down Expand Up @@ -379,7 +391,15 @@ beforeEach(async () => {
mockGetSessionUsageFromBilling.mockResolvedValue(null);
mockUpdateCodeReviewUsage.mockResolvedValue(undefined);
mockUpdateCodeReviewStatusIfNonTerminal.mockResolvedValue(true);
mockAppendReviewSummaryFooter.mockReturnValue('body with footer');
mockAppendPreviousReviewSummaryHistory.mockImplementation((body: string) => body);
mockBuildReviewSummaryFooter.mockImplementation(
(footer: { usage?: unknown; reviewGuidance?: { used: boolean } }) =>
footer.usage || footer.reviewGuidance?.used ? '\n\nfooter' : ''
);
mockAppendReviewSummaryFooter.mockImplementation(
(body: string, footer: { usage?: unknown; reviewGuidance?: { used: boolean } }) =>
footer.usage || footer.reviewGuidance?.used ? 'body with footer' : body
);
mockDisableCodeReviewForActionRequiredFailure.mockResolvedValue(undefined);
({ POST } = await import('./route'));
});
Expand Down Expand Up @@ -2395,6 +2415,60 @@ describe('POST /api/internal/code-review-status/[reviewId]', () => {
});

describe('summary footer guidance', () => {
it('appends captured history to a completed GitHub summary', async () => {
const review = makeReview({
previous_summary_body: '<!-- kilo-review -->\n## Code Review Summary\n\nOld findings',
previous_summary_head_sha: 'previous-head-sha',
});
mockGetCodeReviewById.mockResolvedValue(review);
mockAppendPreviousReviewSummaryHistory.mockReturnValue('body with history');

await POST(makeRequest({ status: 'completed' }), makeParams(REVIEW_ID));

expect(mockAppendPreviousReviewSummaryHistory).toHaveBeenCalledWith(
'existing body',
review.previous_summary_body,
'previous-head-sha',
{ maxBodyCharacters: 65_536, reservedCharacters: 0 }
);
expect(mockAppendReviewSummaryFooter).toHaveBeenCalledWith('body with history', {
usage: undefined,
reviewGuidance: { used: false, ref: null, truncated: false },
});
expect(mockUpdateKiloReviewComment).toHaveBeenCalledWith(
'inst-1',
'owner',
'repo',
99,
'body with history',
'standard'
);
});

it('omits an oversized GitHub footer instead of exceeding the comment limit', async () => {
const review = makeReview({
previous_summary_body: '<!-- kilo-review -->\n## Code Review Summary\n\nOld findings',
previous_summary_head_sha: 'previous-head-sha',
model: 'anthropic/claude-sonnet-4.6',
total_tokens_in: 1000,
total_tokens_out: 200,
});
mockGetCodeReviewById.mockResolvedValue(review);
mockAppendPreviousReviewSummaryHistory.mockReturnValue('body with bounded history');
mockAppendReviewSummaryFooter.mockReturnValue('x'.repeat(65_537));

await POST(makeRequest({ status: 'completed' }), makeParams(REVIEW_ID));

expect(mockUpdateKiloReviewComment).toHaveBeenCalledWith(
'inst-1',
'owner',
'repo',
99,
'body with bounded history',
'standard'
);
});

it('updates completed GitHub summary with REVIEW.md guidance metadata when used', async () => {
const review = makeReview({
repository_review_instructions_used: true,
Expand All @@ -2415,6 +2489,12 @@ describe('POST /api/internal/code-review-status/[reviewId]', () => {
1,
'standard'
);
expect(mockAppendPreviousReviewSummaryHistory).toHaveBeenCalledWith(
'existing body',
null,
null,
{ maxBodyCharacters: 65_536, reservedCharacters: 8 }
);
expect(mockAppendReviewSummaryFooter).toHaveBeenCalledWith('existing body', {
usage: { model: 'anthropic/claude-sonnet-4.6', tokensIn: 1000, tokensOut: 200 },
reviewGuidance: { used: true, ref: 'main', truncated: false },
Expand Down Expand Up @@ -2498,7 +2578,10 @@ describe('POST /api/internal/code-review-status/[reviewId]', () => {

await POST(makeRequest({ status: 'completed' }), makeParams(REVIEW_ID));

expect(mockAppendReviewSummaryFooter).not.toHaveBeenCalled();
expect(mockAppendReviewSummaryFooter).toHaveBeenCalledWith('existing body', {
usage: undefined,
reviewGuidance: { used: false, ref: null, truncated: false },
});
expect(mockUpdateKiloReviewComment).not.toHaveBeenCalled();
});
});
Expand Down
102 changes: 52 additions & 50 deletions apps/web/src/app/api/internal/code-review-status/[reviewId]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,11 @@ import { captureException, captureMessage } from '@sentry/nextjs';
import { CALLBACK_TOKEN_SECRET } from '@/lib/config.server';
import { verifyCallbackToken } from '@kilocode/worker-utils/callback-token';
import { PLATFORM } from '@/lib/integrations/core/constants';
import { appendReviewSummaryFooter } from '@/lib/code-reviews/summary/usage-footer';
import { appendPreviousReviewSummaryHistory } from '@/lib/code-reviews/summary/history';
import {
appendReviewSummaryFooter,
buildReviewSummaryFooter,
} from '@/lib/code-reviews/summary/usage-footer';
import { APP_URL } from '@/lib/constants';
import type {
CloudAgentCodeReview,
Expand Down Expand Up @@ -394,6 +398,7 @@ async function resolveTerminalOwner(
return undefined;
}

const GITHUB_COMMENT_MAX_CHARACTERS = 65_536;
const BILLING_NOTICE_MARKER = '<!-- kilo-billing-notice -->';
const MODEL_NOT_FOUND_SUMMARY_URL = 'https://app.kilo.ai/code-reviews';
const MODEL_NOT_FOUND_CHECK_TITLE = 'Selected model is no longer available';
Expand Down Expand Up @@ -1234,7 +1239,7 @@ export async function POST(
});
}

// Add reaction to indicate review completion status AND update usage footer
// Add reaction to indicate review completion status and finalize summary metadata
if (status === 'completed' || status === 'failed') {
if (integration) {
try {
Expand Down Expand Up @@ -1286,28 +1291,40 @@ export async function POST(
}
}

// Usage footer (completed only)
// Summary history and footer (completed only)
if (status === 'completed') {
const { model, tokensIn, tokensOut } = await getReviewUsageData(reviewId);
const usage =
model && tokensIn != null && tokensOut != null
? { model, tokensIn, tokensOut }
: undefined;
const reviewGuidance = getReviewGuidanceFooterData(review);
const summaryFooter = { usage, reviewGuidance };
const reservedFooterCharacters = buildReviewSummaryFooter(summaryFooter).length;

if (usage || reviewGuidance.used) {
const existing = await findKiloReviewComment(
integration.platform_installation_id,
repoOwner,
repoName,
review.pr_number,
appType
const existing = await findKiloReviewComment(
integration.platform_installation_id,
repoOwner,
repoName,
review.pr_number,
appType
);
if (existing) {
const bodyWithHistory = appendPreviousReviewSummaryHistory(
existing.body,
review.previous_summary_body,
review.previous_summary_head_sha,
{
maxBodyCharacters: GITHUB_COMMENT_MAX_CHARACTERS,
reservedCharacters: reservedFooterCharacters,
}
);
if (existing) {
const updatedBody = appendReviewSummaryFooter(existing.body, {
usage,
reviewGuidance,
});
const bodyWithFooter = appendReviewSummaryFooter(bodyWithHistory, summaryFooter);
const updatedBody =
bodyWithFooter.length <= GITHUB_COMMENT_MAX_CHARACTERS
? bodyWithFooter
: bodyWithHistory;
if (updatedBody !== existing.body) {
await updateKiloReviewComment(
integration.platform_installation_id,
repoOwner,
Expand All @@ -1317,19 +1334,9 @@ export async function POST(
appType
);
logExceptInTest(
`[code-review-status] Updated summary comment footer on ${review.repo_full_name}#${review.pr_number}`
`[code-review-status] Updated summary comment metadata on ${review.repo_full_name}#${review.pr_number}`
);
}
} else {
logExceptInTest(
'[code-review-status] Usage data not available for footer update',
{
reviewId,
model,
tokensIn,
tokensOut,
}
);
}
}
} else if (platform === PLATFORM.GITLAB) {
Expand Down Expand Up @@ -1377,7 +1384,7 @@ export async function POST(
}
}

// Usage footer (completed only)
// Summary history and footer (completed only)
if (status === 'completed') {
const { model, tokensIn, tokensOut } = await getReviewUsageData(reviewId);
const usage =
Expand All @@ -1386,18 +1393,23 @@ export async function POST(
: undefined;
const reviewGuidance = getReviewGuidanceFooterData(review);

if (usage || reviewGuidance.used) {
const existing = await findKiloReviewNote(
accessToken,
review.repo_full_name,
review.pr_number,
instanceUrl
const existing = await findKiloReviewNote(
accessToken,
review.repo_full_name,
review.pr_number,
instanceUrl
);
if (existing) {
const bodyWithHistory = appendPreviousReviewSummaryHistory(
existing.body,
review.previous_summary_body,
review.previous_summary_head_sha
);
if (existing) {
const updatedBody = appendReviewSummaryFooter(existing.body, {
usage,
reviewGuidance,
});
const updatedBody = appendReviewSummaryFooter(bodyWithHistory, {
usage,
reviewGuidance,
});
if (updatedBody !== existing.body) {
await updateKiloReviewNote(
accessToken,
review.repo_full_name,
Expand All @@ -1407,26 +1419,16 @@ export async function POST(
instanceUrl
);
logExceptInTest(
`[code-review-status] Updated summary note footer on GitLab MR ${review.repo_full_name}!${review.pr_number}`
`[code-review-status] Updated summary note metadata on GitLab MR ${review.repo_full_name}!${review.pr_number}`
);
}
} else {
logExceptInTest(
'[code-review-status] Usage data not available for footer update',
{
reviewId,
model,
tokensIn,
tokensOut,
}
);
}
}
}
} catch (postCompletionError) {
// Non-blocking - log but don't fail the callback
logExceptInTest(
'[code-review-status] Failed to add completion reaction or usage footer:',
'[code-review-status] Failed to add completion reaction or summary metadata:',
postCompletionError
);
}
Expand Down
22 changes: 22 additions & 0 deletions apps/web/src/lib/code-reviews/db/code-reviews.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1104,6 +1104,28 @@ export async function updateCodeReviewUsage(
}
}

export async function updatePreviousReviewSummary(
reviewId: string,
summary: { body: string | null; headSha: string | null }
): Promise<void> {
try {
await db
.update(cloud_agent_code_reviews)
.set({
previous_summary_body: summary.body,
previous_summary_head_sha: summary.headSha,
updated_at: new Date().toISOString(),
})
.where(eq(cloud_agent_code_reviews.id, reviewId));
} catch (error) {
captureException(error, {
tags: { operation: 'updatePreviousReviewSummary' },
extra: { reviewId, hasBody: summary.body !== null, headSha: summary.headSha },
});
throw error;
}
}

/**
* Updates REVIEW.md usage metadata for a code review.
*/
Expand Down
Loading