mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-21 15:55:50 +00:00
chore: 移除 ultrareview preflight stub 及其测试
- 删除 src/services/api/ultrareviewPreflight.ts(自动生成的 stub) - 删除 src/commands/review/UltrareviewPreflightDialog.tsx(依赖前者的 UI stub) - 删除 src/services/api/__tests__/ultrareviewPreflight.test.ts(测试已删代码) - 同步移除 ultrareviewCommand.test.tsx 中对 UltrareviewPreflightDialog 的 mock Co-Authored-By: glm-5.2 <zai-org@claude-code-best.win>
This commit is contained in:
@@ -1,56 +0,0 @@
|
|||||||
import React, { useCallback, useRef, useState } from 'react';
|
|
||||||
import { Box, Dialog, Text } from '@anthropic/ink';
|
|
||||||
import { Select } from '../../components/CustomSelect/select.js';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
billingNote: string | null;
|
|
||||||
onConfirm: (signal: AbortSignal) => Promise<void>;
|
|
||||||
onCancel: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dialog shown when /v1/ultrareview/preflight returns action='confirm'.
|
|
||||||
* Displays the server-provided billing_note (or a generic fallback) and
|
|
||||||
* gives the user a Proceed / Cancel choice.
|
|
||||||
*/
|
|
||||||
export function UltrareviewPreflightDialog({ billingNote, onConfirm, onCancel }: Props): React.ReactNode {
|
|
||||||
const [isLaunching, setIsLaunching] = useState(false);
|
|
||||||
const abortControllerRef = useRef(new AbortController());
|
|
||||||
|
|
||||||
const handleSelect = useCallback(
|
|
||||||
(value: string) => {
|
|
||||||
if (value === 'proceed') {
|
|
||||||
setIsLaunching(true);
|
|
||||||
void onConfirm(abortControllerRef.current.signal).catch(() => setIsLaunching(false));
|
|
||||||
} else {
|
|
||||||
onCancel();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[onConfirm, onCancel],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleCancel = useCallback(() => {
|
|
||||||
abortControllerRef.current.abort();
|
|
||||||
onCancel();
|
|
||||||
}, [onCancel]);
|
|
||||||
|
|
||||||
const options = [
|
|
||||||
{ label: 'Proceed', value: 'proceed' },
|
|
||||||
{ label: 'Cancel', value: 'cancel' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const displayNote = billingNote ?? 'This run may incur additional cost.';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog title="Ultrareview — additional cost" onCancel={handleCancel} color="background">
|
|
||||||
<Box flexDirection="column" gap={1}>
|
|
||||||
<Text>{displayNote}</Text>
|
|
||||||
{isLaunching ? (
|
|
||||||
<Text color="background">Launching…</Text>
|
|
||||||
) : (
|
|
||||||
<Select options={options} onChange={handleSelect} onCancel={handleCancel} />
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -179,13 +179,10 @@ mock.module('src/components/CustomSelect/select.js', () => ({
|
|||||||
Select: 'Select',
|
Select: 'Select',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// UltrareviewOverageDialog and PreflightDialog — return a simple marker
|
// UltrareviewOverageDialog — return a simple marker
|
||||||
mock.module('src/commands/review/UltrareviewOverageDialog.js', () => ({
|
mock.module('src/commands/review/UltrareviewOverageDialog.js', () => ({
|
||||||
UltrareviewOverageDialog: () => ({ type: 'UltrareviewOverageDialog' }),
|
UltrareviewOverageDialog: () => ({ type: 'UltrareviewOverageDialog' }),
|
||||||
}));
|
}));
|
||||||
mock.module('src/commands/review/UltrareviewPreflightDialog.js', () => ({
|
|
||||||
UltrareviewPreflightDialog: () => ({ type: 'UltrareviewPreflightDialog' }),
|
|
||||||
}));
|
|
||||||
|
|
||||||
import { call } from '../ultrareviewCommand.js';
|
import { call } from '../ultrareviewCommand.js';
|
||||||
|
|
||||||
|
|||||||
@@ -1,226 +0,0 @@
|
|||||||
/**
|
|
||||||
* Regression tests for fetchUltrareviewPreflight.
|
|
||||||
* Verifies all three action enum states (proceed/confirm/blocked),
|
|
||||||
* network/HTTP error handling, and Zod schema mismatch fallback.
|
|
||||||
*/
|
|
||||||
import { afterAll, beforeAll, describe, expect, mock, test } from 'bun:test'
|
|
||||||
import { debugMock } from '../../../../tests/mocks/debug.js'
|
|
||||||
import { logMock } from '../../../../tests/mocks/log.js'
|
|
||||||
import { setupAxiosMock } from '../../../../tests/mocks/axios.js'
|
|
||||||
|
|
||||||
// Mock dependency chain before any subject import
|
|
||||||
mock.module('src/utils/debug.ts', debugMock)
|
|
||||||
mock.module('src/utils/log.ts', logMock)
|
|
||||||
mock.module('src/services/analytics/index.js', () => ({
|
|
||||||
logEvent: () => {},
|
|
||||||
}))
|
|
||||||
|
|
||||||
// Mock auth utilities
|
|
||||||
mock.module('src/utils/auth.js', () => ({
|
|
||||||
isClaudeAISubscriber: () => true,
|
|
||||||
isTeamSubscriber: () => false,
|
|
||||||
isEnterpriseSubscriber: () => false,
|
|
||||||
}))
|
|
||||||
|
|
||||||
// Mock OAuth config
|
|
||||||
mock.module('src/constants/oauth.js', () => ({
|
|
||||||
getOauthConfig: () => ({ BASE_API_URL: 'https://api.anthropic.com' }),
|
|
||||||
}))
|
|
||||||
|
|
||||||
// Mock prepareApiRequest and getOAuthHeaders
|
|
||||||
mock.module('src/utils/teleport/api.js', () => ({
|
|
||||||
prepareApiRequest: async () => ({
|
|
||||||
accessToken: 'test-token',
|
|
||||||
orgUUID: 'org-uuid-test',
|
|
||||||
}),
|
|
||||||
getOAuthHeaders: (token: string) => ({
|
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'anthropic-version': '2023-06-01',
|
|
||||||
}),
|
|
||||||
}))
|
|
||||||
|
|
||||||
// We'll mock axios at module level.
|
|
||||||
// Typed as any in test code (CLAUDE.md: mock data may use as any).
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
const mockAxiosPost = mock(async (..._args: any[]): Promise<any> => {
|
|
||||||
throw new Error('not configured')
|
|
||||||
})
|
|
||||||
|
|
||||||
const axiosHandle = setupAxiosMock()
|
|
||||||
axiosHandle.stubs.post = mockAxiosPost
|
|
||||||
axiosHandle.stubs.isAxiosError = (e: unknown) =>
|
|
||||||
typeof e === 'object' &&
|
|
||||||
e !== null &&
|
|
||||||
(e as { isAxiosError?: boolean }).isAxiosError === true
|
|
||||||
|
|
||||||
beforeAll(() => {
|
|
||||||
axiosHandle.useStubs = true
|
|
||||||
})
|
|
||||||
|
|
||||||
afterAll(() => {
|
|
||||||
axiosHandle.useStubs = false
|
|
||||||
})
|
|
||||||
|
|
||||||
import {
|
|
||||||
fetchUltrareviewPreflight,
|
|
||||||
type UltrareviewPreflightResponse,
|
|
||||||
} from '../ultrareviewPreflight.js'
|
|
||||||
|
|
||||||
describe('fetchUltrareviewPreflight', () => {
|
|
||||||
test('returns proceed action when server responds with proceed', async () => {
|
|
||||||
const serverResponse: UltrareviewPreflightResponse = {
|
|
||||||
action: 'proceed',
|
|
||||||
billing_note: null,
|
|
||||||
}
|
|
||||||
mockAxiosPost.mockImplementationOnce(async () => ({
|
|
||||||
status: 200,
|
|
||||||
data: serverResponse,
|
|
||||||
}))
|
|
||||||
|
|
||||||
const result = await fetchUltrareviewPreflight({ repo: 'owner/repo' })
|
|
||||||
expect(result).not.toBeNull()
|
|
||||||
expect(result?.action).toBe('proceed')
|
|
||||||
expect(result?.billing_note).toBeNull()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('returns confirm action with billing_note when server responds with confirm', async () => {
|
|
||||||
const serverResponse: UltrareviewPreflightResponse = {
|
|
||||||
action: 'confirm',
|
|
||||||
billing_note: 'This run will cost approximately $2.50.',
|
|
||||||
}
|
|
||||||
mockAxiosPost.mockImplementationOnce(async () => ({
|
|
||||||
status: 200,
|
|
||||||
data: serverResponse,
|
|
||||||
}))
|
|
||||||
|
|
||||||
const result = await fetchUltrareviewPreflight({ repo: 'owner/repo' })
|
|
||||||
expect(result).not.toBeNull()
|
|
||||||
expect(result?.action).toBe('confirm')
|
|
||||||
expect(result?.billing_note).toBe('This run will cost approximately $2.50.')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('returns blocked action when server responds with blocked', async () => {
|
|
||||||
const serverResponse: UltrareviewPreflightResponse = {
|
|
||||||
action: 'blocked',
|
|
||||||
billing_note: null,
|
|
||||||
}
|
|
||||||
mockAxiosPost.mockImplementationOnce(async () => ({
|
|
||||||
status: 200,
|
|
||||||
data: serverResponse,
|
|
||||||
}))
|
|
||||||
|
|
||||||
const result = await fetchUltrareviewPreflight({ repo: 'owner/repo' })
|
|
||||||
expect(result).not.toBeNull()
|
|
||||||
expect(result?.action).toBe('blocked')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('returns null on schema mismatch (invalid action value)', async () => {
|
|
||||||
mockAxiosPost.mockImplementationOnce(async () => ({
|
|
||||||
status: 200,
|
|
||||||
data: { action: 'unknown_action', billing_note: null },
|
|
||||||
}))
|
|
||||||
|
|
||||||
const result = await fetchUltrareviewPreflight({ repo: 'owner/repo' })
|
|
||||||
expect(result).toBeNull()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('returns null on network error (no response)', async () => {
|
|
||||||
const networkError = new Error('ECONNREFUSED')
|
|
||||||
;(networkError as unknown as { isAxiosError: boolean }).isAxiosError = true
|
|
||||||
mockAxiosPost.mockImplementationOnce(async () => {
|
|
||||||
throw networkError
|
|
||||||
})
|
|
||||||
|
|
||||||
const result = await fetchUltrareviewPreflight({ repo: 'owner/repo' })
|
|
||||||
expect(result).toBeNull()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('returns null on 401 Unauthorized', async () => {
|
|
||||||
const authError = new Error('Unauthorized')
|
|
||||||
;(
|
|
||||||
authError as unknown as {
|
|
||||||
isAxiosError: boolean
|
|
||||||
response: { status: number }
|
|
||||||
}
|
|
||||||
).isAxiosError = true
|
|
||||||
;(authError as unknown as { response: { status: number } }).response = {
|
|
||||||
status: 401,
|
|
||||||
}
|
|
||||||
mockAxiosPost.mockImplementationOnce(async () => {
|
|
||||||
throw authError
|
|
||||||
})
|
|
||||||
|
|
||||||
const result = await fetchUltrareviewPreflight({ repo: 'owner/repo' })
|
|
||||||
expect(result).toBeNull()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('returns null on 403 Forbidden', async () => {
|
|
||||||
const forbiddenError = new Error('Forbidden')
|
|
||||||
;(
|
|
||||||
forbiddenError as unknown as {
|
|
||||||
isAxiosError: boolean
|
|
||||||
response: { status: number }
|
|
||||||
}
|
|
||||||
).isAxiosError = true
|
|
||||||
;(forbiddenError as unknown as { response: { status: number } }).response =
|
|
||||||
{ status: 403 }
|
|
||||||
mockAxiosPost.mockImplementationOnce(async () => {
|
|
||||||
throw forbiddenError
|
|
||||||
})
|
|
||||||
|
|
||||||
const result = await fetchUltrareviewPreflight({ repo: 'owner/repo' })
|
|
||||||
expect(result).toBeNull()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('returns null on 5xx server error', async () => {
|
|
||||||
const serverError = new Error('Internal Server Error')
|
|
||||||
;(
|
|
||||||
serverError as unknown as {
|
|
||||||
isAxiosError: boolean
|
|
||||||
response: { status: number }
|
|
||||||
}
|
|
||||||
).isAxiosError = true
|
|
||||||
;(serverError as unknown as { response: { status: number } }).response = {
|
|
||||||
status: 500,
|
|
||||||
}
|
|
||||||
mockAxiosPost.mockImplementationOnce(async () => {
|
|
||||||
throw serverError
|
|
||||||
})
|
|
||||||
|
|
||||||
const result = await fetchUltrareviewPreflight({ repo: 'owner/repo' })
|
|
||||||
expect(result).toBeNull()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('passes pr_number to request body when provided', async () => {
|
|
||||||
mockAxiosPost.mockImplementationOnce(
|
|
||||||
async (_url: unknown, body: unknown) => {
|
|
||||||
const b = body as { pr_number: number }
|
|
||||||
expect(b.pr_number).toBe(42)
|
|
||||||
return { status: 200, data: { action: 'proceed', billing_note: null } }
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
const result = await fetchUltrareviewPreflight({
|
|
||||||
repo: 'owner/repo',
|
|
||||||
pr_number: 42,
|
|
||||||
})
|
|
||||||
expect(result?.action).toBe('proceed')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('passes confirm flag to request body when provided', async () => {
|
|
||||||
mockAxiosPost.mockImplementationOnce(
|
|
||||||
async (_url: unknown, body: unknown) => {
|
|
||||||
const b = body as { confirm: boolean }
|
|
||||||
expect(b.confirm).toBe(true)
|
|
||||||
return { status: 200, data: { action: 'proceed', billing_note: null } }
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
const result = await fetchUltrareviewPreflight({
|
|
||||||
repo: 'owner/repo',
|
|
||||||
confirm: true,
|
|
||||||
})
|
|
||||||
expect(result?.action).toBe('proceed')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
import axios from 'axios'
|
|
||||||
import z from 'zod/v4'
|
|
||||||
import { getOauthConfig } from '../../constants/oauth.js'
|
|
||||||
import { logForDebugging } from '../../utils/debug.js'
|
|
||||||
import { getOAuthHeaders, prepareApiRequest } from '../../utils/teleport/api.js'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Zod schema for the /v1/ultrareview/preflight response.
|
|
||||||
* Based on binary-extracted schema: vq.object({action: vq.enum([...]), billing_note: ...})
|
|
||||||
*/
|
|
||||||
const UltrareviewPreflightSchema = z.object({
|
|
||||||
action: z.enum(['proceed', 'confirm', 'blocked']),
|
|
||||||
billing_note: z.string().nullable().optional(),
|
|
||||||
})
|
|
||||||
|
|
||||||
export type UltrareviewPreflightResponse = z.infer<
|
|
||||||
typeof UltrareviewPreflightSchema
|
|
||||||
>
|
|
||||||
|
|
||||||
export type UltrareviewPreflightArgs = {
|
|
||||||
repo: string
|
|
||||||
pr_number?: number
|
|
||||||
pr_url?: string
|
|
||||||
confirm?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* POST /v1/ultrareview/preflight — server-side gate before launch.
|
|
||||||
*
|
|
||||||
* Returns the preflight result (proceed / confirm / blocked) or null on any
|
|
||||||
* failure (network error, auth error, schema mismatch). Callers must treat
|
|
||||||
* null as "fallback to direct launch" to preserve existing behavior.
|
|
||||||
*
|
|
||||||
* The `confirm` flag should be set to true when the user has already
|
|
||||||
* acknowledged the billing dialog (or passed --confirm on the CLI), which
|
|
||||||
* skips the server-side confirm prompt and gets a direct proceed/blocked.
|
|
||||||
*/
|
|
||||||
export async function fetchUltrareviewPreflight(
|
|
||||||
args: UltrareviewPreflightArgs,
|
|
||||||
): Promise<UltrareviewPreflightResponse | null> {
|
|
||||||
try {
|
|
||||||
const { accessToken, orgUUID } = await prepareApiRequest()
|
|
||||||
|
|
||||||
const body: Record<string, unknown> = {
|
|
||||||
repo: args.repo,
|
|
||||||
}
|
|
||||||
if (args.pr_number !== undefined) {
|
|
||||||
body.pr_number = args.pr_number
|
|
||||||
}
|
|
||||||
if (args.pr_url !== undefined) {
|
|
||||||
body.pr_url = args.pr_url
|
|
||||||
}
|
|
||||||
if (args.confirm !== undefined) {
|
|
||||||
body.confirm = args.confirm
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await axios.post(
|
|
||||||
`${getOauthConfig().BASE_API_URL}/v1/ultrareview/preflight`,
|
|
||||||
body,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
...getOAuthHeaders(accessToken),
|
|
||||||
'x-organization-uuid': orgUUID,
|
|
||||||
},
|
|
||||||
timeout: 10000,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
const parsed = UltrareviewPreflightSchema.safeParse(response.data)
|
|
||||||
if (!parsed.success) {
|
|
||||||
logForDebugging(
|
|
||||||
`fetchUltrareviewPreflight: schema mismatch — ${parsed.error.message}`,
|
|
||||||
)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
return parsed.data
|
|
||||||
} catch (error) {
|
|
||||||
logForDebugging(`fetchUltrareviewPreflight failed: ${error}`)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user