Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
审计发现 1 CRITICAL + 4 HIGH + 4 MEDIUM + 4 LOW, 修复如下: CRITICAL: - TS seam 测试改为 JSON round-trip 验证 (12 测试覆盖 10 事件类型) HIGH: - post_conversation_hook 拦截路径 driver=None 加 debug 日志 - schedule intercept channel send 失败回退 LLM (return Ok(None)) MEDIUM: - DeltaBuffer.flush() 先 mutation 再 clear, 防止异常丢数据 - ModelsAPI.tsx 去重: 改用 model-config.ts 导出 (消除 2 函数+1 接口+2 常量) - boot_with_driver docstring 记录跳过 agent 恢复 TypeScript 0 错误, Rust 76 kernel 测试通过, TS 12 seam 测试通过
223 lines
8.0 KiB
TypeScript
223 lines
8.0 KiB
TypeScript
/**
|
|
* Chat seam tests — verify request/response type contracts
|
|
*
|
|
* Validates that TypeScript types match the Rust serde-serialized format.
|
|
* Uses round-trip JSON serialization to catch field name mismatches
|
|
* (e.g., if Rust changes `rename_all = "camelCase"` or adds/removes fields).
|
|
*/
|
|
import { describe, it, expect } from 'vitest';
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Rust side: StreamChatRequest (camelCase via serde rename_all)
|
|
// ---------------------------------------------------------------------------
|
|
interface StreamChatRequest {
|
|
agentId: string;
|
|
sessionId: string;
|
|
message: string;
|
|
thinkingEnabled?: boolean;
|
|
reasoningEffort?: string;
|
|
planMode?: boolean;
|
|
subagentEnabled?: boolean;
|
|
model?: string;
|
|
}
|
|
|
|
interface ChatRequest {
|
|
agentId: string;
|
|
message: string;
|
|
thinkingEnabled?: boolean;
|
|
reasoningEffort?: string;
|
|
planMode?: boolean;
|
|
subagentEnabled?: boolean;
|
|
model?: string;
|
|
}
|
|
|
|
interface ChatResponse {
|
|
content: string;
|
|
inputTokens: number;
|
|
outputTokens: number;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Rust side: StreamChatEvent (tagged union, tag = "type")
|
|
// ---------------------------------------------------------------------------
|
|
type StreamChatEvent =
|
|
| { type: 'delta'; delta: string }
|
|
| { type: 'thinkingDelta'; delta: string }
|
|
| { type: 'toolStart'; name: string; input: unknown }
|
|
| { type: 'toolEnd'; name: string; output: unknown }
|
|
| { type: 'subtaskStatus'; taskId: string; description: string; status: string; detail?: string }
|
|
| { type: 'iterationStart'; iteration: number; maxIterations: number }
|
|
| { type: 'handStart'; name: string; params: unknown }
|
|
| { type: 'handEnd'; name: string; result: unknown }
|
|
| { type: 'complete'; inputTokens: number; outputTokens: number }
|
|
| { type: 'error'; message: string };
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Simulated Rust serde output — these strings represent what Rust would emit.
|
|
// If a field name changes in Rust, the JSON.parse round-trip will fail here.
|
|
// ---------------------------------------------------------------------------
|
|
const RUST_STREAM_CHAT_REQUEST = `{
|
|
"agentId": "agent-1",
|
|
"sessionId": "sess-abc",
|
|
"message": "Hello",
|
|
"thinkingEnabled": true,
|
|
"reasoningEffort": "high",
|
|
"planMode": false,
|
|
"subagentEnabled": true,
|
|
"model": "gpt-4o"
|
|
}`;
|
|
|
|
const RUST_CHAT_RESPONSE = `{
|
|
"content": "Hello back!",
|
|
"inputTokens": 10,
|
|
"outputTokens": 5
|
|
}`;
|
|
|
|
const RUST_EVENT_DELTA = `{"type":"delta","delta":"Hello world"}`;
|
|
const RUST_EVENT_THINKING = `{"type":"thinkingDelta","delta":"thinking..."}`;
|
|
const RUST_EVENT_TOOL_START = `{"type":"toolStart","name":"web_search","input":{"query":"test"}}`;
|
|
const RUST_EVENT_TOOL_END = `{"type":"toolEnd","name":"web_search","output":{"results":[]}}`;
|
|
const RUST_EVENT_HAND_START = `{"type":"handStart","name":"hand_quiz","params":{"topic":"math"}}`;
|
|
const RUST_EVENT_HAND_END = `{"type":"handEnd","name":"hand_quiz","result":{"questions":[]}}`;
|
|
const RUST_EVENT_SUBTASK = `{"type":"subtaskStatus","taskId":"t1","description":"Research","status":"running","detail":"Searching"}`;
|
|
const RUST_EVENT_ITERATION = `{"type":"iterationStart","iteration":2,"maxIterations":10}`;
|
|
const RUST_EVENT_COMPLETE = `{"type":"complete","inputTokens":100,"outputTokens":50}`;
|
|
const RUST_EVENT_ERROR = `{"type":"error","message":"已取消"}`;
|
|
|
|
describe('Chat Seam: request format contract (JSON round-trip)', () => {
|
|
it('StreamChatRequest parses from simulated Rust output', () => {
|
|
const req: StreamChatRequest = JSON.parse(RUST_STREAM_CHAT_REQUEST);
|
|
expect(req.agentId).toBe('agent-1');
|
|
expect(req.sessionId).toBe('sess-abc');
|
|
expect(req.message).toBe('Hello');
|
|
expect(req.thinkingEnabled).toBe(true);
|
|
expect(req.reasoningEffort).toBe('high');
|
|
expect(req.planMode).toBe(false);
|
|
expect(req.subagentEnabled).toBe(true);
|
|
expect(req.model).toBe('gpt-4o');
|
|
});
|
|
|
|
it('StreamChatRequest optional fields are camelCase', () => {
|
|
const req: StreamChatRequest = {
|
|
agentId: 'a',
|
|
sessionId: 's',
|
|
message: 'm',
|
|
thinkingEnabled: true,
|
|
reasoningEffort: 'high',
|
|
planMode: false,
|
|
subagentEnabled: true,
|
|
model: 'gpt-4o',
|
|
};
|
|
// Verify camelCase naming by serializing and checking no snake_case
|
|
const json = JSON.stringify(req);
|
|
expect(json).not.toContain('thinking_enabled');
|
|
expect(json).not.toContain('reasoning_effort');
|
|
expect(json).not.toContain('plan_mode');
|
|
expect(json).not.toContain('subagent_enabled');
|
|
});
|
|
|
|
it('ChatResponse parses from simulated Rust output', () => {
|
|
const resp: ChatResponse = JSON.parse(RUST_CHAT_RESPONSE);
|
|
expect(resp.content).toBe('Hello back!');
|
|
expect(resp.inputTokens).toBe(10);
|
|
expect(resp.outputTokens).toBe(5);
|
|
});
|
|
});
|
|
|
|
describe('Chat Seam: StreamChatEvent format contract (JSON round-trip)', () => {
|
|
it('delta event parses from simulated Rust output', () => {
|
|
const event: StreamChatEvent = JSON.parse(RUST_EVENT_DELTA);
|
|
expect(event.type).toBe('delta');
|
|
if (event.type === 'delta') {
|
|
expect(event.delta).toBe('Hello world');
|
|
}
|
|
});
|
|
|
|
it('thinkingDelta event parses correctly', () => {
|
|
const event: StreamChatEvent = JSON.parse(RUST_EVENT_THINKING);
|
|
expect(event.type).toBe('thinkingDelta');
|
|
if (event.type === 'thinkingDelta') {
|
|
expect(event.delta).toBe('thinking...');
|
|
}
|
|
});
|
|
|
|
it('toolStart/toolEnd events parse with correct fields', () => {
|
|
const start: StreamChatEvent = JSON.parse(RUST_EVENT_TOOL_START);
|
|
const end: StreamChatEvent = JSON.parse(RUST_EVENT_TOOL_END);
|
|
|
|
if (start.type === 'toolStart') {
|
|
expect(start.name).toBe('web_search');
|
|
expect(start.input).toBeDefined();
|
|
}
|
|
if (end.type === 'toolEnd') {
|
|
expect(end.name).toBe('web_search');
|
|
expect(end.output).toBeDefined();
|
|
}
|
|
});
|
|
|
|
it('handStart/handEnd events have correct structure', () => {
|
|
const start: StreamChatEvent = JSON.parse(RUST_EVENT_HAND_START);
|
|
const end: StreamChatEvent = JSON.parse(RUST_EVENT_HAND_END);
|
|
|
|
if (start.type === 'handStart') {
|
|
expect(start.name).toMatch(/^hand_/);
|
|
expect(start.params).toBeDefined();
|
|
}
|
|
if (end.type === 'handEnd') {
|
|
expect(end.name).toMatch(/^hand_/);
|
|
expect(end.result).toBeDefined();
|
|
}
|
|
});
|
|
|
|
it('subtaskStatus event parses all fields including optional detail', () => {
|
|
const event: StreamChatEvent = JSON.parse(RUST_EVENT_SUBTASK);
|
|
if (event.type === 'subtaskStatus') {
|
|
expect(event.taskId).toBe('t1');
|
|
expect(event.description).toBe('Research');
|
|
expect(event.status).toBe('running');
|
|
expect(event.detail).toBe('Searching');
|
|
}
|
|
});
|
|
|
|
it('iterationStart event parses iteration and maxIterations', () => {
|
|
const event: StreamChatEvent = JSON.parse(RUST_EVENT_ITERATION);
|
|
if (event.type === 'iterationStart') {
|
|
expect(event.iteration).toBe(2);
|
|
expect(event.maxIterations).toBe(10);
|
|
}
|
|
});
|
|
|
|
it('complete event has token counts', () => {
|
|
const event: StreamChatEvent = JSON.parse(RUST_EVENT_COMPLETE);
|
|
if (event.type === 'complete') {
|
|
expect(event.inputTokens).toBeGreaterThanOrEqual(0);
|
|
expect(event.outputTokens).toBeGreaterThanOrEqual(0);
|
|
}
|
|
});
|
|
|
|
it('error event has message field', () => {
|
|
const event: StreamChatEvent = JSON.parse(RUST_EVENT_ERROR);
|
|
if (event.type === 'error') {
|
|
expect(event.message).toBeTruthy();
|
|
}
|
|
});
|
|
|
|
it('all 10 StreamChatEvent variants are represented in Rust output', () => {
|
|
const variants = [
|
|
RUST_EVENT_DELTA, RUST_EVENT_THINKING,
|
|
RUST_EVENT_TOOL_START, RUST_EVENT_TOOL_END,
|
|
RUST_EVENT_SUBTASK, RUST_EVENT_ITERATION,
|
|
RUST_EVENT_HAND_START, RUST_EVENT_HAND_END,
|
|
RUST_EVENT_COMPLETE, RUST_EVENT_ERROR,
|
|
];
|
|
const types = variants.map(v => JSON.parse(v).type);
|
|
expect(types).toEqual([
|
|
'delta', 'thinkingDelta',
|
|
'toolStart', 'toolEnd',
|
|
'subtaskStatus', 'iterationStart',
|
|
'handStart', 'handEnd',
|
|
'complete', 'error',
|
|
]);
|
|
});
|
|
});
|