Skip to content
Draft
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: 13 additions & 1 deletion .github/workflows/deploy-workers.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ concurrency:

jobs:
deploy-manual:
if: inputs.worker != ''
if: inputs.worker != '' && inputs.worker != 'services/isolate-review'
runs-on: ${{ vars.RUNNER_DEFAULT_LABEL || 'ubuntu-latest' }}
timeout-minutes: 15
name: Deploy ${{ inputs.worker }}
Expand All @@ -51,6 +51,17 @@ jobs:
- name: Checkout code
uses: useblacksmith/checkout@41cdeedae8edb2e684ba22896a5fd2a3cb85db6b # v1

- name: Validate requested Worker
env:
WORKER_DIRECTORY: ${{ inputs.worker }}
run: |
SERVICES_DIRECTORY="$(realpath "$GITHUB_WORKSPACE/services")"
WORKER_DIRECTORY="$(realpath "$WORKER_DIRECTORY")"
EXCLUDED_DIRECTORY="$(realpath "$SERVICES_DIRECTORY/isolate-review")"
if [[ "$WORKER_DIRECTORY" != "$SERVICES_DIRECTORY/"* || "$WORKER_DIRECTORY" == "$EXCLUDED_DIRECTORY" || "$WORKER_DIRECTORY" == "$EXCLUDED_DIRECTORY/"* ]]; then
exit 1
fi

- name: Setup pnpm
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4.4.0

Expand Down Expand Up @@ -158,6 +169,7 @@ jobs:

# Workers excluded from this workflow (they have custom deploy pipelines):
EXCLUDED=(
services/isolate-review
services/kiloclaw # Docker-based deploy in deploy-production.yml
services/gastown # Deployed separately
services/wasteland # Deployed separately
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -136,3 +136,6 @@ run-milvus-test.sh
!.env.test
!.envrc
.playwright-mcp

# isolate-review e2e artifacts
services/isolate-review/scripts/last-e2e/
9 changes: 9 additions & 0 deletions ENVIRONMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,15 @@ When `VERCEL_TARGET_ENV` is absent in local development or a script process, tra
- `STAGING_AUTH_TOKEN` - Auth token for the staging deployment dispatcher env. `[SECRET]`
- `PROD_AUTH_TOKEN` - Auth token for the production deployment dispatcher env. `[SECRET]`

### Isolate Review

- `KILO_GATEWAY_URL` - OpenRouter-compatible gateway base URL for `services/isolate-review`. Local `dev:env` points it at Next.js `/api/openrouter`. Production omits it and defaults to `https://api.kilo.ai/api/openrouter`. [SERVER]
- `GITHUB_API_URL` - Optional GitHub REST API origin for `services/isolate-review`. Blank or omitted defaults to `https://api.github.com`. [SERVER]
- `GIT_CLONE_URL_TEMPLATE` - Optional git clone URL template for `services/isolate-review`. Substitutes `{owner}` and `{repo}`. Blank or omitted defaults to `https://github.com/{owner}/{repo}.git`. [SERVER]
- `NEXTAUTH_SECRET` - Shared JWT signing secret used by isolate-review to validate the authenticated Kilo bearer against the current user's token pepper. `[SECRET]`
- `INTERNAL_API_SECRET` - Shared secret sent in `x-internal-api-key` by authenticated server-side callers of isolate-review. `[SECRET]`
- `ISOLATE_REVIEW_WORKER_URL` - Server-only base URL for the web app's isolate-review client. [SERVER]

### Other Services

- `DOCKER_SOCKET` - Path or URL for the Docker daemon socket; used by `services/cloud-agent-next/scripts/docker-privileged-proxy.mjs`. [SERVER]
Expand Down
3 changes: 3 additions & 0 deletions apps/web/.env.development.local.example
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ CLOUD_AGENT_R2_ATTACHMENTS_BUCKET_NAME=cloud-agent-attachments-dev
# @url cloudflare-code-review-infra
CODE_REVIEW_WORKER_URL=http://localhost:8789

# @url cloudflare-isolate-review
ISOLATE_REVIEW_WORKER_URL=http://localhost:8819

# @url cloudflare-auto-fix-infra
AUTO_FIX_URL=http://localhost:8792

Expand Down
236 changes: 200 additions & 36 deletions apps/web/src/lib/code-reviews/dispatch/dispatch-pending-reviews.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ const mockPrepareReviewPayload = jest.fn();
const mockSendCodeReviewDisabledEmail = jest.fn();
const mockGetIntegrationById = jest.fn();
const mockUpdateCheckRun = jest.fn();
const mockLogExceptInTest = jest.fn();
const mockReviewIsStillReserved = jest.fn();

jest.mock('@/lib/code-reviews/client/code-review-worker-client', () => ({
codeReviewWorkerClient: {
Expand Down Expand Up @@ -41,6 +43,19 @@ jest.mock('@sentry/nextjs', () => ({
captureException: jest.fn(),
}));

jest.mock('@/lib/utils.server', () => ({
...jest.requireActual<typeof utilsServer>('@/lib/utils.server'),
logExceptInTest: (...args: unknown[]) => mockLogExceptInTest(...args),
}));

jest.mock('@/lib/code-reviews/db/code-reviews', () => ({
...jest.requireActual<typeof codeReviewsDb>('../db/code-reviews'),
reviewIsStillReserved: (...args: unknown[]) => mockReviewIsStillReserved(...args),
}));

import { createHash, randomUUID } from 'node:crypto';
import type * as utilsServer from '@/lib/utils.server';
import type * as codeReviewsDb from '../db/code-reviews';
import { db } from '@/lib/drizzle';
import { insertTestUser } from '@/tests/helpers/user.helper';
import {
Expand All @@ -56,6 +71,8 @@ import { eq } from 'drizzle-orm';
import { or } from 'drizzle-orm';
import { tryDispatchPendingReviews } from './dispatch-pending-reviews';
import { cronPendingCodeReviewCreatedAtWindowSql } from './dispatch-constants';
import { appendCodeReviewAnalyticsPromptAppendix } from '../analytics/contracts';
import type { CodeReviewPayload } from '../triggers/prepare-review-payload';
import {
cancelSupersededReviewsForPR,
updateRepositoryReviewInstructionsMetadata,
Expand All @@ -64,6 +81,7 @@ import {
const REPO = `test-org/dispatch-pending-${Date.now()}`;
const FUNDED_BALANCE_MICRODOLLARS = 5_000_001;
const DEFAULT_TIER_BALANCE_MICRODOLLARS = 5_000_000;
const DISPATCH_PROMPT_DIAGNOSTICS_MESSAGE = '[dispatchReview] Worker dispatch prompt diagnostics';

type ReviewStatus = 'pending' | 'queued' | 'running';
type ReviewOwner = { type: 'user'; id: string } | { type: 'org'; id: string };
Expand Down Expand Up @@ -145,6 +163,9 @@ describe('tryDispatchPendingReviews', () => {
mockSendCodeReviewDisabledEmail.mockResolvedValue({ sent: true });
mockGetIntegrationById.mockResolvedValue(null);
mockUpdateCheckRun.mockResolvedValue(undefined);
mockReviewIsStillReserved.mockImplementation(
jest.requireActual<typeof codeReviewsDb>('../db/code-reviews').reviewIsStillReserved
);
});

afterEach(async () => {
Expand All @@ -166,6 +187,8 @@ describe('tryDispatchPendingReviews', () => {
mockSendCodeReviewDisabledEmail.mockReset();
mockGetIntegrationById.mockReset();
mockUpdateCheckRun.mockReset();
mockLogExceptInTest.mockReset();
mockReviewIsStillReserved.mockReset();
});

afterAll(async () => {
Expand Down Expand Up @@ -1327,6 +1350,10 @@ describe('tryDispatchPendingReviews', () => {
activeCount: 0,
});
expect(mockDispatchReview).not.toHaveBeenCalled();
expect(mockLogExceptInTest).not.toHaveBeenCalledWith(
DISPATCH_PROMPT_DIAGNOSTICS_MESSAGE,
expect.anything()
);
expect(storedReview?.status).toBe('cancelled');
expect(storedReview?.terminal_reason).toBe('superseded');
});
Expand Down Expand Up @@ -1596,46 +1623,176 @@ describe('tryDispatchPendingReviews', () => {
);
});

it('snapshots analytics enrollment and appends the protocol only when enabled', async () => {
const timestamp = minutesAgo(1);
const owner = { type: 'org', id: testOrganizationId } satisfies ReviewOwner;
mockGetAgentConfigForOwner.mockResolvedValue({
id: 'test-agent-config',
config: { review_analytics_enabled: true },
is_enabled: true,
runtime_state: {},
});
it.each([
{ preference: true, persistedDecision: undefined, variant: 'max' },
{ preference: false, persistedDecision: undefined, variant: undefined },
{ preference: false, persistedDecision: true, variant: 'xhigh' },
{ preference: true, persistedDecision: false, variant: undefined },
])(
'logs only actual dispatch prompt diagnostics with analytics preference=$preference, persisted=$persistedDecision',
async ({ preference, persistedDecision, variant }) => {
const timestamp = minutesAgo(1);
const owner = { type: 'org', id: testOrganizationId } satisfies ReviewOwner;
const preparedPrompt = 'Review this change: café.\n';
const model = 'openai/gpt-5';
const analyticsEnabled = persistedDecision ?? preference;
mockGetAgentConfigForOwner.mockResolvedValue({
id: 'test-agent-config',
config: {
review_analytics_enabled: preference,
model_slug: 'anthropic/claude-sonnet-4.6',
thinking_effort: 'high',
},
is_enabled: true,
runtime_state: {},
});
mockPrepareReviewPayload.mockImplementation((params: { reviewId: string }) => ({
reviewId: params.reviewId,
authToken: 'test-dispatch-auth-token',
sessionInput: {
prompt: preparedPrompt,
model,
variant,
githubToken: 'test-github-token',
},
}));

const [review] = await db
.insert(cloud_agent_code_reviews)
.values(
reviewValues({
owner,
const [review] = await db
.insert(cloud_agent_code_reviews)
.values(
reviewValues({ owner, status: 'pending', createdAt: timestamp, updatedAt: timestamp })
)
.returning({ id: cloud_agent_code_reviews.id });
if (persistedDecision !== undefined) {
await db.insert(cloud_agent_code_review_attempts).values({
code_review_id: review.id,
attempt_number: 1,
status: 'pending',
createdAt: timestamp,
updatedAt: timestamp,
})
)
.returning({ id: cloud_agent_code_reviews.id });

await tryDispatchPendingReviews({
type: 'org',
id: testOrganizationId,
userId: testUser.id,
});
analytics_enabled_at_dispatch: persistedDecision,
});
}

await tryDispatchPendingReviews({ ...owner, userId: testUser.id });

const [attempt] = await db
.select()
.from(cloud_agent_code_review_attempts)
.where(eq(cloud_agent_code_review_attempts.code_review_id, review.id));
const dispatchedPayload = mockDispatchReview.mock.calls[0]?.[0] as
| CodeReviewPayload
| undefined;
if (!attempt || !dispatchedPayload) {
throw new Error('Expected a persisted attempt and worker dispatch');
}

expect(mockPrepareReviewPayload).toHaveBeenCalledTimes(1);
expect(mockDispatchReview).toHaveBeenCalledTimes(1);
expect(attempt.analytics_enabled_at_dispatch).toBe(analyticsEnabled);
expect(dispatchedPayload.sessionInput.prompt).toBe(
analyticsEnabled ? appendCodeReviewAnalyticsPromptAppendix(preparedPrompt) : preparedPrompt
);
expect(
mockLogExceptInTest.mock.calls.filter(
([message]) => message === DISPATCH_PROMPT_DIAGNOSTICS_MESSAGE
)
).toEqual([
[
DISPATCH_PROMPT_DIAGNOSTICS_MESSAGE,
{
reviewId: review.id,
attemptId: attempt.id,
promptSha256: createHash('sha256')
.update(dispatchedPayload.sessionInput.prompt, 'utf8')
.digest('hex'),
promptLength: dispatchedPayload.sessionInput.prompt.length,
model,
variant: variant ?? null,
analytics_enabled_at_dispatch: analyticsEnabled,
packagedCliVersion: '7.4.20',
},
],
]);
}
);

it.each(['admitted', 'cancelled', 'reclaimed'] as const)(
'withholds prompt diagnostics until the final reservation recheck resolves: %s',
async outcome => {
const timestamp = minutesAgo(1);
const owner = { type: 'org', id: testOrganizationId } satisfies ReviewOwner;
const recheckStarted = createDeferred<void>();
const releaseRecheck = createDeferred<void>();
const { reviewIsStillReserved } =
jest.requireActual<typeof codeReviewsDb>('../db/code-reviews');
mockGetAgentConfigForOwner.mockResolvedValue({
id: 'test-agent-config',
config: { review_analytics_enabled: true },
is_enabled: true,
runtime_state: {},
});
mockReviewIsStillReserved
.mockImplementationOnce(reviewIsStillReserved)
.mockImplementationOnce(reviewIsStillReserved)
.mockImplementationOnce(async (reviewId: string, reservationId: string) => {
recheckStarted.resolve(undefined);
await releaseRecheck.promise;
return reviewIsStillReserved(reviewId, reservationId);
});
const [review] = await db
.insert(cloud_agent_code_reviews)
.values(
reviewValues({ owner, status: 'pending', createdAt: timestamp, updatedAt: timestamp })
)
.returning({ id: cloud_agent_code_reviews.id });

const [attempt] = await db
.select()
.from(cloud_agent_code_review_attempts)
.where(eq(cloud_agent_code_review_attempts.code_review_id, review.id));
const dispatchedPayload = mockDispatchReview.mock.calls[0]?.[0];
const dispatch = tryDispatchPendingReviews({ ...owner, userId: testUser.id });
await recheckStarted.promise;

expect(attempt?.analytics_enabled_at_dispatch).toBe(true);
expect(dispatchedPayload.sessionInput.prompt).toContain('kilo-review-analytics:v1');
expect(dispatchedPayload.sessionInput.prompt.match(/kilo-review-analytics:v1/g)).toHaveLength(
1
);
});
expect(mockDispatchReview).not.toHaveBeenCalled();
expect(mockLogExceptInTest).not.toHaveBeenCalledWith(
DISPATCH_PROMPT_DIAGNOSTICS_MESSAGE,
expect.anything()
);
const attempt = await db.query.cloud_agent_code_review_attempts.findFirst({
where: eq(cloud_agent_code_review_attempts.code_review_id, review.id),
});
expect(attempt?.analytics_enabled_at_dispatch).toBe(true);

if (outcome !== 'admitted') {
await db
.update(cloud_agent_code_reviews)
.set(
outcome === 'cancelled'
? { status: 'cancelled' }
: { dispatch_reservation_id: randomUUID() }
)
.where(eq(cloud_agent_code_reviews.id, review.id));
}
releaseRecheck.resolve(undefined);
const result = await dispatch;

const dispatchCount = outcome === 'admitted' ? 1 : 0;
expect(result).toEqual({
dispatched: dispatchCount,
notDispatched: 1 - dispatchCount,
activeCount: dispatchCount,
});
expect(mockDispatchReview).toHaveBeenCalledTimes(dispatchCount);
expect(
mockLogExceptInTest.mock.calls.filter(
([message]) => message === DISPATCH_PROMPT_DIAGNOSTICS_MESSAGE
)
).toHaveLength(dispatchCount);
if (outcome === 'admitted') {
const diagnosticCallIndex = mockLogExceptInTest.mock.calls.findIndex(
([message]) => message === DISPATCH_PROMPT_DIAGNOSTICS_MESSAGE
);
expect(mockLogExceptInTest.mock.invocationCallOrder[diagnosticCallIndex]).toBeLessThan(
mockDispatchReview.mock.invocationCallOrder[0]
);
}
}
);

it('forces analytics off for Bitbucket even when its stored config enables collection', async () => {
const timestamp = minutesAgo(1);
Expand Down Expand Up @@ -1746,6 +1903,13 @@ describe('tryDispatchPendingReviews', () => {

const dispatchedPayload = mockDispatchReview.mock.calls[0]?.[0];
expect(dispatchedPayload.sessionInput.prompt).toBe('Review this change.');
expect(mockLogExceptInTest).toHaveBeenCalledWith(
DISPATCH_PROMPT_DIAGNOSTICS_MESSAGE,
expect.objectContaining({
analytics_enabled_at_dispatch: true,
promptSha256: createHash('sha256').update('Review this change.', 'utf8').digest('hex'),
})
);
});

it('keeps an existing organization analytics snapshot after collection is disabled', async () => {
Expand Down
14 changes: 14 additions & 0 deletions apps/web/src/lib/code-reviews/dispatch/dispatch-pending-reviews.ts
Original file line number Diff line number Diff line change
Expand Up @@ -601,6 +601,20 @@ async function dispatchReservedReview(reservation: ReservedReview, owner: Owner)
return false;
}

logExceptInTest('[dispatchReview] Worker dispatch prompt diagnostics', {
reviewId: review.id,
attemptId: attempt.id,
promptSha256: crypto
.createHash('sha256')
.update(dispatchPayload.sessionInput.prompt, 'utf8')
.digest('hex'),
promptLength: dispatchPayload.sessionInput.prompt.length,
model: dispatchPayload.sessionInput.model,
variant: dispatchPayload.sessionInput.variant ?? null,
analytics_enabled_at_dispatch: attempt.analytics_enabled_at_dispatch,
packagedCliVersion: '7.4.20',
});

try {
await codeReviewWorkerClient.dispatchReview({
...dispatchPayload,
Expand Down
Loading
Loading