GitNexus/gitnexus-web/test/unit/agent-history.test.ts
Octopus 0fa547ccdc
Some checks are pending
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
Gitleaks / gitleaks (push) Waiting to run
Publish / Classify release event (push) Waiting to run
Publish / RC guard (marker + release-PR skip) (push) Blocked by required conditions
Publish / ci (push) Blocked by required conditions
Publish / Publish to npm (push) Blocked by required conditions
Publish / Build & Push RC Docker images (push) Blocked by required conditions
Scorecard / Scorecard analysis (push) Waiting to run
Skill copy sync / shipped skills drift guard (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-web) (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-cli) (push) Waiting to run
feat: refresh MiniMax model and endpoint configuration (#2780)
2026-08-11 18:11:47 +00:00

457 lines
13 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import {
buildLangChainMessages,
createChatModel,
serializeAgentHistoryMessages,
type AgentMessage,
} from '../../src/core/llm/agent';
import {
buildDeepSeekRequestMessages,
DeepSeekChatOpenAI,
DeepSeekChatOpenAICompletions,
} from '../../src/core/llm/deepseek-chat-model';
import { MINIMAX_ANTHROPIC_BASE_URLS, MINIMAX_MODEL_IDS } from '../../src/core/llm/types';
describe('buildLangChainMessages', () => {
it('reconstructs assistant tool-call turns for replay', () => {
const messages: AgentMessage[] = [
{ role: 'user', content: 'Check the weather' },
{
role: 'assistant',
content: 'Let me check that.',
reasoningContent: '',
toolCalls: [
{
id: 'call_weather',
name: 'get_weather',
args: { location: 'Hangzhou' },
type: 'tool_call',
},
],
},
{
role: 'tool',
content: 'Cloudy 7~13°C',
toolCallId: 'call_weather',
name: 'get_weather',
},
];
const langChainMessages = buildLangChainMessages(messages);
expect(langChainMessages).toHaveLength(3);
expect((langChainMessages[1] as any).additional_kwargs.reasoning_content).toBe('');
expect((langChainMessages[1] as any).tool_calls).toEqual([
{
id: 'call_weather',
name: 'get_weather',
args: { location: 'Hangzhou' },
type: 'tool_call',
},
]);
expect((langChainMessages[2] as any).tool_call_id).toBe('call_weather');
});
it('preserves MiniMax image and video content blocks', () => {
const content = [
{ type: 'text' as const, text: 'Compare these inputs.' },
{
type: 'image' as const,
source: { type: 'url' as const, url: 'https://example.com/image.png' },
},
{
type: 'video' as const,
source: { type: 'url' as const, url: 'https://example.com/video.mp4', fps: 1 },
},
];
const [message] = buildLangChainMessages([{ role: 'user', content }]);
expect((message as any).content).toEqual(content);
});
});
describe('serializeAgentHistoryMessages', () => {
it('captures assistant and tool messages from a completed turn', () => {
const serialized = serializeAgentHistoryMessages(
[
{ _getType: () => 'human', content: 'old prompt' },
{
_getType: () => 'ai',
content: 'Let me check that.',
additional_kwargs: { reasoning_content: 'Need weather tool.' },
tool_calls: [
{
id: 'call_weather',
name: 'get_weather',
args: { location: 'Hangzhou' },
type: 'tool_call',
},
],
},
{
_getType: () => 'tool',
content: 'Cloudy 7~13°C',
tool_call_id: 'call_weather',
name: 'get_weather',
},
{
_getType: () => 'ai',
content: 'Tomorrow will be cloudy.',
additional_kwargs: { reasoning_content: 'Result received.' },
},
],
1,
);
expect(serialized).toEqual([
{
role: 'assistant',
content: 'Let me check that.',
reasoningContent: 'Need weather tool.',
toolCalls: [
{
id: 'call_weather',
name: 'get_weather',
args: { location: 'Hangzhou' },
type: 'tool_call',
},
],
},
{
role: 'tool',
content: 'Cloudy 7~13°C',
toolCallId: 'call_weather',
name: 'get_weather',
},
{
role: 'assistant',
content: 'Tomorrow will be cloudy.',
},
]);
});
});
describe('buildDeepSeekRequestMessages', () => {
it('preserves reasoning_content on assistant tool-call messages', () => {
const requestMessages = buildDeepSeekRequestMessages(
buildLangChainMessages([
{ role: 'user', content: '如何支持Gitlab Repo' },
{
role: 'assistant',
content: '',
reasoningContent: 'I should inspect the repository support flow first.',
toolCalls: [
{
id: 'call_1',
name: 'search',
args: { query: 'Gitlab repo support' },
type: 'tool_call',
},
],
},
{
role: 'tool',
content: 'No matches',
toolCallId: 'call_1',
name: 'search',
},
]),
);
expect(requestMessages).toEqual([
{ role: 'user', content: '如何支持Gitlab Repo' },
{
role: 'assistant',
content: '',
reasoning_content: 'I should inspect the repository support flow first.',
tool_calls: [
{
id: 'call_1',
type: 'function',
function: {
name: 'search',
arguments: '{"query":"Gitlab repo support"}',
},
},
],
},
{
role: 'tool',
content: 'No matches',
name: 'search',
tool_call_id: 'call_1',
},
]);
});
});
it('drops reasoning_content from assistant messages without tool calls', () => {
const messages = buildLangChainMessages([
{ role: 'user', content: 'Hello' },
{
role: 'assistant',
content: 'Hi there',
reasoningContent: 'I should greet the user.',
},
]);
const requestMessages = buildDeepSeekRequestMessages(messages);
expect(requestMessages).toEqual([
{ role: 'user', content: 'Hello' },
{ role: 'assistant', content: 'Hi there' },
]);
});
it('drops reasoningContent from serialized assistant messages without tool calls', () => {
const serialized = serializeAgentHistoryMessages(
[
{
_getType: () => 'ai',
content: 'Simple answer.',
additional_kwargs: { reasoning_content: 'Thinking about it.' },
},
],
0,
);
expect(serialized).toEqual([
{
role: 'assistant',
content: 'Simple answer.',
},
]);
});
describe('createChatModel', () => {
it('configures MiniMax-M3 adaptive thinking on the China endpoint', () => {
const model = createChatModel({
provider: 'minimax',
apiKey: 'minimax-test-key',
model: MINIMAX_MODEL_IDS[0],
baseUrl: MINIMAX_ANTHROPIC_BASE_URLS.cn_zh,
thinkingMode: 'adaptive',
temperature: 0.1,
} as any) as any;
expect(model.model).toBe(MINIMAX_MODEL_IDS[0]);
expect(model.clientOptions.baseURL).toBe(MINIMAX_ANTHROPIC_BASE_URLS.cn_zh);
expect(model.thinking).toEqual({ type: 'adaptive' });
expect(model.temperature).toBeUndefined();
});
it('supports disabled thinking for MiniMax-M3', () => {
const model = createChatModel({
provider: 'minimax',
apiKey: 'minimax-test-key',
model: MINIMAX_MODEL_IDS[0],
thinkingMode: 'disabled',
temperature: 0.1,
} as any) as any;
expect(model.thinking).toEqual({ type: 'disabled' });
expect(model.temperature).toBe(0.1);
});
it('keeps MiniMax-M2.7 thinking always on', () => {
const model = createChatModel({
provider: 'minimax',
apiKey: 'minimax-test-key',
model: MINIMAX_MODEL_IDS[1],
thinkingMode: 'disabled',
temperature: 0.1,
} as any) as any;
expect(model.invocationParams({}).thinking).toBeUndefined();
expect(model.temperature).toBeUndefined();
});
it('keeps DeepSeek model subclasses on withConfig clones used for tool binding', () => {
const model = createChatModel({
provider: 'deepseek',
apiKey: 'test-key',
model: 'deepseek-v4-flash',
temperature: 0.1,
} as any) as any;
expect(model).toBeInstanceOf(DeepSeekChatOpenAI);
expect(model.completions).toBeInstanceOf(DeepSeekChatOpenAICompletions);
const clonedModel = model.withConfig({ tools: [] }) as any;
expect(clonedModel).toBeInstanceOf(DeepSeekChatOpenAI);
expect(clonedModel.completions).toBeInstanceOf(DeepSeekChatOpenAICompletions);
});
it('uses DeepSeek serialization on withConfig clones', async () => {
const model = createChatModel({
provider: 'deepseek',
apiKey: 'test-key',
model: 'deepseek-v4-flash',
temperature: 0.1,
} as any) as any;
const clonedModel = model.withConfig({ tools: [] }) as any;
clonedModel.completions.streaming = false;
let capturedRequest: any;
clonedModel.completions.client = {
chat: {
completions: {
create: async (request: any) => {
capturedRequest = request;
return {
choices: [
{
message: { role: 'assistant', content: 'ok' },
finish_reason: 'stop',
},
],
};
},
},
},
};
await clonedModel.completions._generate(
buildLangChainMessages([
{ role: 'user', content: 'Check the weather' },
{
role: 'assistant',
content: '',
reasoningContent: 'Need the weather tool.',
toolCalls: [
{
id: 'call_weather',
name: 'get_weather',
args: { location: 'Hangzhou' },
type: 'tool_call',
},
],
},
{
role: 'tool',
content: 'Cloudy 7~13°C',
toolCallId: 'call_weather',
name: 'get_weather',
},
]),
{ stream: false },
);
expect(capturedRequest.messages[1].reasoning_content).toBe('Need the weather tool.');
expect(capturedRequest.messages[1].tool_calls[0].function.arguments).toBe(
'{"location":"Hangzhou"}',
);
expect(capturedRequest.messages[2].tool_call_id).toBe('call_weather');
});
it('preserves reasoning_content through the streaming path used by DeepSeek tool calls', async () => {
const model = createChatModel({
provider: 'deepseek',
apiKey: 'test-key',
model: 'deepseek-v4-flash',
temperature: 0.1,
} as any) as any;
model.completions.streaming = true;
async function* mockStream() {
yield {
id: 'chatcmpl-1',
model: 'deepseek-v4-flash',
choices: [
{
index: 0,
delta: {
role: 'assistant',
reasoning_content: 'Need the weather tool.',
},
},
],
};
yield {
id: 'chatcmpl-1',
model: 'deepseek-v4-flash',
choices: [
{
index: 0,
delta: {
tool_calls: [
{
index: 0,
id: 'call_weather',
type: 'function',
function: {
name: 'get_weather',
arguments: '{"location":"Hangzhou"}',
},
},
],
},
finish_reason: 'tool_calls',
},
],
};
}
model.completions.client = {
chat: {
completions: {
create: async () => mockStream(),
},
},
};
let streamedMessage: any;
for await (const chunk of model.completions._streamResponseChunks(
buildLangChainMessages([{ role: 'user', content: 'Check the weather' }]),
{},
)) {
streamedMessage = streamedMessage ? streamedMessage.concat(chunk.message) : chunk.message;
}
expect(streamedMessage.additional_kwargs.reasoning_content).toBe('Need the weather tool.');
expect(streamedMessage.tool_calls).toEqual([
{
id: 'call_weather',
name: 'get_weather',
args: { location: 'Hangzhou' },
type: 'tool_call',
},
]);
expect(serializeAgentHistoryMessages([streamedMessage], 0)).toEqual([
{
role: 'assistant',
content: '',
reasoningContent: 'Need the weather tool.',
toolCalls: [
{
id: 'call_weather',
name: 'get_weather',
args: { location: 'Hangzhou' },
type: 'tool_call',
},
],
},
]);
});
it('rejects overlapping DeepSeek requests before reusing active messages', async () => {
const model = createChatModel({
provider: 'deepseek',
apiKey: 'test-key',
model: 'deepseek-v4-flash',
temperature: 0.1,
} as any) as any;
model.completions.activeMessages = buildLangChainMessages([{ role: 'user', content: 'busy' }]);
await expect(
model.completions._generate(
buildLangChainMessages([{ role: 'user', content: 'Check the weather' }]),
{ stream: false },
),
).rejects.toThrow('DeepSeekChatOpenAICompletions does not support overlapping requests');
});
});