mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-18 06:15:51 +00:00
feat: 添加 Provider Registry、StatusLine、Cache Stats 和其他增强
- providerRegistry: OpenAI 兼容 provider 切换(Cerebras/Groq/DeepSeek/Qwen) - StatusLine: 增强状态栏(缓存命中率、TTL 倒计时、自定义 shell 命令) - cacheStats: 缓存命中率和 token 签名追踪 - ultrareviewPreflight: 代码审查预检服务 - SkillsMenu/filterSkills: 技能菜单过滤增强 - MagicDocs/langfuse prompts: 提示词更新 - claude.ts: API 客户端更新 Co-Authored-By: glm-5-turbo <zai-org@claude-code-best.win>
This commit is contained in:
56
src/commands/review/UltrareviewPreflightDialog.tsx
Normal file
56
src/commands/review/UltrareviewPreflightDialog.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
312
src/commands/review/__tests__/ultrareviewCommand.test.tsx
Normal file
312
src/commands/review/__tests__/ultrareviewCommand.test.tsx
Normal file
@@ -0,0 +1,312 @@
|
||||
/**
|
||||
* Regression tests for `ultrareviewCommand.call` (src/commands/review/
|
||||
* ultrareviewCommand.tsx). The previous version of `call` made an axios
|
||||
* preflight POST and branched on `action: proceed | blocked | confirm`;
|
||||
* that integration was removed and `call` now branches on `checkOverageGate()`'s
|
||||
* four `kind` values: `not-enabled`, `low-balance`, `needs-confirm`, `proceed`.
|
||||
*
|
||||
* These tests verify each branch:
|
||||
* - `proceed` → forwards billingNote and args to `launchRemoteReview`,
|
||||
* calls `onDone(text)`, returns null
|
||||
* - `not-enabled` → onDone with paywall message + `display: 'system'`,
|
||||
* returns null, does NOT launch
|
||||
* - `low-balance` → onDone with balance-too-low message including the
|
||||
* available amount, returns null, does NOT launch
|
||||
* - `needs-confirm` → returns the React `UltrareviewOverageDialog` element,
|
||||
* does NOT call onDone, does NOT launch
|
||||
* - `proceed` + null launch result → onDone with "failed to launch" message
|
||||
* - `proceed` + arg pass-through → args (e.g. PR number) reach launchRemoteReview
|
||||
* verbatim (call doesn't parse them itself)
|
||||
*/
|
||||
import { afterAll, beforeEach, 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';
|
||||
|
||||
// Pre-import the real react and ink modules so we can delegate after this
|
||||
// suite. Bun's mock.module is process-global / last-write-wins; without
|
||||
// delegation the stub createElement / stub ink components leak into other
|
||||
// test files (e.g. SnapshotUpdateDialog.test.tsx, AgentsPlatformView.test.tsx)
|
||||
// that need real React.createElement and real Box/Text components.
|
||||
const _realReactMod = (await import('react')) as Record<string, unknown> & {
|
||||
default?: Record<string, unknown>;
|
||||
};
|
||||
const _realInkMod = (await import('@anthropic/ink')) as Record<string, unknown>;
|
||||
let _useStubReactForUltrareview = true;
|
||||
let _useStubInkForUltrareview = true;
|
||||
afterAll(() => {
|
||||
_useStubReactForUltrareview = false;
|
||||
_useStubInkForUltrareview = false;
|
||||
// The handle reference exists by the time afterAll runs (TDZ resolves via
|
||||
// closure). Flip useStubs off so the spread-real fall-through kicks in for
|
||||
// any test file that runs after this one in the same process.
|
||||
_ultrareviewAxiosHandle.useStubs = false;
|
||||
});
|
||||
|
||||
// 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.module('src/services/analytics/growthbook.js', () => ({
|
||||
getFeatureValue_CACHED_MAY_BE_STALE: () => null,
|
||||
}));
|
||||
|
||||
// Mock auth utilities
|
||||
mock.module('src/utils/auth.js', () => ({
|
||||
isClaudeAISubscriber: () => true,
|
||||
isTeamSubscriber: () => false,
|
||||
isEnterpriseSubscriber: () => false,
|
||||
}));
|
||||
|
||||
// Mock checkOverageGate with a mutable gate result so each test can drive
|
||||
// the four branches in ultrareviewCommand.call (not-enabled, low-balance,
|
||||
// needs-confirm, proceed). launchRemoteReview captures args for the
|
||||
// args-forwarding test, and its return value is mutable too — `null` triggers
|
||||
// the "failed to launch" onDone branch.
|
||||
type GateResult =
|
||||
| { kind: 'proceed'; billingNote: string }
|
||||
| { kind: 'not-enabled' }
|
||||
| { kind: 'low-balance'; available: number }
|
||||
| { kind: 'needs-confirm' };
|
||||
let _gateResult: GateResult = { kind: 'proceed', billingNote: '' };
|
||||
let _launchResult: Array<{ type: 'text'; text: string }> | null = [{ type: 'text', text: 'Launched successfully.' }];
|
||||
const _capturedLaunchArgs: string[] = [];
|
||||
mock.module('src/commands/review/reviewRemote.js', () => ({
|
||||
checkOverageGate: async () => _gateResult,
|
||||
confirmOverage: () => {},
|
||||
launchRemoteReview: async (args: string) => {
|
||||
_capturedLaunchArgs.push(args);
|
||||
return _launchResult;
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock OAuth config so real fetchUltrareviewPreflight can run
|
||||
mock.module('src/constants/oauth.js', () => ({
|
||||
getOauthConfig: () => ({ BASE_API_URL: 'https://api.anthropic.com' }),
|
||||
}));
|
||||
|
||||
// Mock prepareApiRequest so real fetchUltrareviewPreflight skips auth
|
||||
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',
|
||||
}),
|
||||
}));
|
||||
|
||||
// Mock axios — per-test responses set via mockAxiosPost.mockImplementationOnce
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const mockAxiosPost = mock(
|
||||
async (..._args: any[]): Promise<any> => ({
|
||||
status: 200,
|
||||
data: { action: 'proceed', billing_note: null },
|
||||
}),
|
||||
);
|
||||
|
||||
// Spread real axios + flag-gate stubs so the per-test mockAxiosPost stops
|
||||
// leaking into later test files (mock.module is process-global). Default ON
|
||||
// for this suite; afterAll above flips _useStubReactForUltrareview, but here
|
||||
// we tie axios cleanup to the helper's own flag — see suite-level afterAll.
|
||||
const _ultrareviewAxiosHandle = setupAxiosMock();
|
||||
_ultrareviewAxiosHandle.useStubs = true;
|
||||
_ultrareviewAxiosHandle.stubs.post = mockAxiosPost;
|
||||
_ultrareviewAxiosHandle.stubs.isAxiosError = (e: unknown) =>
|
||||
typeof e === 'object' && e !== null && (e as { isAxiosError?: boolean }).isAxiosError === true;
|
||||
|
||||
// Mock detectCurrentRepositoryWithHost
|
||||
mock.module('src/utils/detectRepository.js', () => ({
|
||||
detectCurrentRepositoryWithHost: async () => ({
|
||||
host: 'github.com',
|
||||
owner: 'testowner',
|
||||
name: 'testrepo',
|
||||
}),
|
||||
}));
|
||||
|
||||
// Minimal mock for React/Ink so we don't need a full renderer.
|
||||
// Preserve any explicit `children` prop when no varargs children are passed
|
||||
// — otherwise consumers who pass `children` via the props object (e.g.
|
||||
// SnapshotUpdateDialog.ts uses `React.createElement(Dialog, { ..., children })`)
|
||||
// see their array overwritten with `[]`. mock.module is process-global so this
|
||||
// mock survives into other test files in the same run; afterAll flips the flag
|
||||
// so we delegate to real React thereafter.
|
||||
mock.module('react', () => {
|
||||
const stubCreateElement = (type: unknown, props: unknown, ...children: unknown[]) => {
|
||||
const propsObj = (props ?? {}) as Record<string, unknown>;
|
||||
const finalChildren = children.length > 0 ? children : 'children' in propsObj ? propsObj.children : [];
|
||||
return {
|
||||
$$typeof: Symbol.for('react.element'),
|
||||
type,
|
||||
props: { ...propsObj, children: finalChildren },
|
||||
};
|
||||
};
|
||||
const realCreate = ((_realReactMod.default as Record<string, unknown> | undefined)?.createElement ??
|
||||
_realReactMod.createElement) as (...args: unknown[]) => unknown;
|
||||
const createElement = (...args: unknown[]) =>
|
||||
_useStubReactForUltrareview ? stubCreateElement(args[0], args[1], ...args.slice(2)) : realCreate(...args);
|
||||
return {
|
||||
..._realReactMod,
|
||||
default: {
|
||||
...((_realReactMod.default as Record<string, unknown> | undefined) ?? {}),
|
||||
createElement,
|
||||
},
|
||||
createElement,
|
||||
};
|
||||
});
|
||||
|
||||
// Spread real ink + flag-gate the stub components. Without spread, the bare
|
||||
// { Box: 'Box', Dialog: 'Dialog', Text: 'Text' } leaks into every later test
|
||||
// file (e.g. AgentsPlatformView.test.tsx) that imports @anthropic/ink — those
|
||||
// consumers receive strings instead of real components and rendering breaks.
|
||||
mock.module('@anthropic/ink', () => {
|
||||
if (_useStubInkForUltrareview) {
|
||||
return {
|
||||
..._realInkMod,
|
||||
Box: 'Box',
|
||||
Dialog: 'Dialog',
|
||||
Text: 'Text',
|
||||
};
|
||||
}
|
||||
return _realInkMod;
|
||||
});
|
||||
|
||||
mock.module('src/components/CustomSelect/select.js', () => ({
|
||||
Select: 'Select',
|
||||
}));
|
||||
|
||||
// UltrareviewOverageDialog and PreflightDialog — return a simple marker
|
||||
mock.module('src/commands/review/UltrareviewOverageDialog.js', () => ({
|
||||
UltrareviewOverageDialog: () => ({ type: 'UltrareviewOverageDialog' }),
|
||||
}));
|
||||
mock.module('src/commands/review/UltrareviewPreflightDialog.js', () => ({
|
||||
UltrareviewPreflightDialog: () => ({ type: 'UltrareviewPreflightDialog' }),
|
||||
}));
|
||||
|
||||
import { call } from '../ultrareviewCommand.js';
|
||||
|
||||
const makeContext = () =>
|
||||
({
|
||||
abortController: { signal: {} },
|
||||
}) as Parameters<typeof call>[1];
|
||||
|
||||
describe('ultrareviewCommand.call: gate branches', () => {
|
||||
// Reset gate + launch state between tests so a previous test's mutation
|
||||
// doesn't leak into the next.
|
||||
beforeEach(() => {
|
||||
_gateResult = { kind: 'proceed', billingNote: '' };
|
||||
_launchResult = [{ type: 'text', text: 'Launched successfully.' }];
|
||||
_capturedLaunchArgs.length = 0;
|
||||
});
|
||||
|
||||
test('proceed gate: forwards billingNote to launchRemoteReview, calls onDone, returns null', async () => {
|
||||
_gateResult = { kind: 'proceed', billingNote: ' Free review 1 of 5.' };
|
||||
|
||||
const messages: string[] = [];
|
||||
const onDone = (msg: string) => messages.push(msg);
|
||||
|
||||
const result = await call(onDone as Parameters<typeof call>[0], makeContext(), '');
|
||||
|
||||
expect(result).toBeNull();
|
||||
expect(messages.length).toBe(1);
|
||||
expect(messages[0]).toContain('Launched successfully');
|
||||
// launchRemoteReview was invoked exactly once with the empty args.
|
||||
expect(_capturedLaunchArgs).toEqual(['']);
|
||||
});
|
||||
|
||||
test('not-enabled gate: onDone with paywall message, returns null', async () => {
|
||||
_gateResult = { kind: 'not-enabled' };
|
||||
|
||||
const messages: string[] = [];
|
||||
const opts: Array<unknown> = [];
|
||||
const onDone = (msg: string, opt: unknown) => {
|
||||
messages.push(msg);
|
||||
opts.push(opt);
|
||||
};
|
||||
|
||||
const result = await call(onDone as Parameters<typeof call>[0], makeContext(), '');
|
||||
|
||||
expect(result).toBeNull();
|
||||
expect(messages).toHaveLength(1);
|
||||
expect(messages[0]).toContain('Free ultrareviews used');
|
||||
expect(messages[0]).toContain('claude.ai/settings/billing');
|
||||
expect((opts[0] as { display: string }).display).toBe('system');
|
||||
// launchRemoteReview must NOT be called when paywalled.
|
||||
expect(_capturedLaunchArgs).toEqual([]);
|
||||
});
|
||||
|
||||
test('low-balance gate: onDone with balance-too-low message including available amount, returns null', async () => {
|
||||
_gateResult = { kind: 'low-balance', available: 4.5 };
|
||||
|
||||
const messages: string[] = [];
|
||||
const opts: Array<unknown> = [];
|
||||
const onDone = (msg: string, opt: unknown) => {
|
||||
messages.push(msg);
|
||||
opts.push(opt);
|
||||
};
|
||||
|
||||
const result = await call(onDone as Parameters<typeof call>[0], makeContext(), '');
|
||||
|
||||
expect(result).toBeNull();
|
||||
expect(messages).toHaveLength(1);
|
||||
expect(messages[0]).toContain('Balance too low');
|
||||
expect(messages[0]).toContain('$4.50');
|
||||
expect(messages[0]).toContain('claude.ai/settings/billing');
|
||||
expect((opts[0] as { display: string }).display).toBe('system');
|
||||
expect(_capturedLaunchArgs).toEqual([]);
|
||||
});
|
||||
|
||||
test('needs-confirm gate: returns UltrareviewOverageDialog React element, does not launch', async () => {
|
||||
_gateResult = { kind: 'needs-confirm' };
|
||||
|
||||
const messages: string[] = [];
|
||||
const onDone = (msg: string) => messages.push(msg);
|
||||
|
||||
const result = await call(onDone as Parameters<typeof call>[0], makeContext(), '');
|
||||
|
||||
// Returns a React element rather than null.
|
||||
expect(result).not.toBeNull();
|
||||
expect(typeof result).toBe('object');
|
||||
const element = result as { type: unknown };
|
||||
expect(element.type).toBeDefined();
|
||||
// No onDone call until the user interacts with the dialog.
|
||||
expect(messages).toEqual([]);
|
||||
expect(_capturedLaunchArgs).toEqual([]);
|
||||
});
|
||||
|
||||
test('proceed gate + launchRemoteReview returns null: onDone with failure message', async () => {
|
||||
_gateResult = { kind: 'proceed', billingNote: '' };
|
||||
_launchResult = null; // teleport / non-github failure path
|
||||
|
||||
const messages: string[] = [];
|
||||
const opts: Array<unknown> = [];
|
||||
const onDone = (msg: string, opt: unknown) => {
|
||||
messages.push(msg);
|
||||
opts.push(opt);
|
||||
};
|
||||
|
||||
const result = await call(onDone as Parameters<typeof call>[0], makeContext(), '');
|
||||
|
||||
expect(result).toBeNull();
|
||||
expect(messages).toHaveLength(1);
|
||||
expect(messages[0]).toContain('Ultrareview failed to launch');
|
||||
expect((opts[0] as { display: string }).display).toBe('system');
|
||||
});
|
||||
|
||||
test('proceed gate: forwards args (e.g. PR number) verbatim to launchRemoteReview', async () => {
|
||||
_gateResult = { kind: 'proceed', billingNote: '' };
|
||||
|
||||
const messages: string[] = [];
|
||||
const onDone = (msg: string) => messages.push(msg);
|
||||
|
||||
await call(onDone as Parameters<typeof call>[0], makeContext(), '42');
|
||||
|
||||
// ultrareviewCommand.call doesn't parse args itself — launchRemoteReview
|
||||
// is responsible for PR-number detection. So we only assert pass-through.
|
||||
expect(_capturedLaunchArgs).toEqual(['42']);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user