refactor(server): 将 Core 模块从动态导入改为静态导入
- 移除 adapter.ts 中约 160 行冗余接口定义 - 简化 initCore 函数,改为初始化检查逻辑 - 简化 getOrCreateAgent,直接使用 ConfigurationError 类 - 更新缓存类型注解使用 Core 导出的类型 - 简化事件订阅代码,直接使用 agentEventEmitter - 在 Core index.ts 中添加 agentEventEmitter 导出 - 更新测试文件适配静态导入模式
This commit is contained in:
@@ -195,6 +195,19 @@ export type {
|
||||
AgentPermission,
|
||||
} from './agent/index.js';
|
||||
|
||||
// Agent Events (for subagent progress tracking)
|
||||
export { agentEventEmitter, AgentEventEmitter } from './agent/index.js';
|
||||
export type {
|
||||
SubagentEvent,
|
||||
SubagentEventType,
|
||||
SubagentStartEvent,
|
||||
SubagentEndEvent,
|
||||
SubagentStreamEvent,
|
||||
SubagentToolStartEvent,
|
||||
SubagentToolEndEvent,
|
||||
SubagentEventListener,
|
||||
} from './agent/index.js';
|
||||
|
||||
// MCP
|
||||
export {
|
||||
getMCPManager,
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
*
|
||||
* 将 core 模块的 Agent 适配到 Server 环境
|
||||
* 处理流式输出、事件推送等
|
||||
*
|
||||
* 使用接口定义避免直接依赖 @ai-assistant/core 类型
|
||||
*/
|
||||
|
||||
import type { SessionStatus } from '../types.js';
|
||||
@@ -12,92 +10,29 @@ import { getSessionManager } from '../session/manager.js';
|
||||
import { broadcastToSession } from '../ws.js';
|
||||
import { emitStatusEvent, emitLogEvent } from '../sse.js';
|
||||
import { createServerPermissionCallback, setSessionAutoApprove } from '../permission/handler.js';
|
||||
import { getConfig } from '../routes/config.js';
|
||||
|
||||
// Core 模块静态导入
|
||||
import {
|
||||
Agent,
|
||||
SessionManager,
|
||||
toolRegistry,
|
||||
loadConfig,
|
||||
getPermissionManager,
|
||||
getProviderRegistry,
|
||||
agentRegistry,
|
||||
agentEventEmitter,
|
||||
ConfigurationError,
|
||||
type AgentChatOptions,
|
||||
type ToolStartInfo,
|
||||
type ToolEndInfo,
|
||||
type TokenUsage,
|
||||
type DetailedCompressionResult,
|
||||
} from '@ai-assistant/core';
|
||||
|
||||
// ============================================================================
|
||||
// Core 模块接口定义(避免直接依赖 @ai-assistant/core 类型)
|
||||
// 类型定义
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Token 使用情况接口
|
||||
*/
|
||||
export interface TokenUsage {
|
||||
input: number;
|
||||
contextLimit: number;
|
||||
available: number;
|
||||
usagePercent: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 压缩结果接口
|
||||
*/
|
||||
export interface CompressionResult {
|
||||
type: 'prune' | 'compaction' | 'both' | 'none';
|
||||
status: 'success' | 'noop' | 'failed_empty_summary' | 'failed_token_inflated' | 'failed_error';
|
||||
freedTokens: number;
|
||||
error?: string;
|
||||
originalTokens?: number;
|
||||
summaryTokens?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Chat 返回结果
|
||||
*/
|
||||
interface ChatResult {
|
||||
text: string;
|
||||
messages: unknown[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Session Manager 接口(Core 的 SessionManager)
|
||||
*/
|
||||
interface SessionManagerInstance {
|
||||
init(workdir: string): Promise<unknown>;
|
||||
getSession(): { id: string; messages: unknown[] } | null;
|
||||
setMessages(messages: unknown[]): Promise<void>;
|
||||
setDiscoveredTools(tools: string[]): Promise<void>;
|
||||
save(): Promise<void>;
|
||||
close(): Promise<void>;
|
||||
restoreSession(sessionId: string): Promise<unknown | null>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Session Manager 构造函数接口
|
||||
*/
|
||||
interface SessionManagerConstructor {
|
||||
new (): SessionManagerInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 工具开始信息
|
||||
*/
|
||||
interface ToolStartInfo {
|
||||
id: string;
|
||||
toolName: string;
|
||||
args: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 工具结束信息
|
||||
*/
|
||||
interface ToolEndInfo {
|
||||
id: string;
|
||||
status: 'completed' | 'error';
|
||||
result?: unknown;
|
||||
error?: string;
|
||||
duration?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Chat 选项接口
|
||||
*/
|
||||
interface ChatOptions {
|
||||
onStream?: (chunk: string) => void;
|
||||
onToolStart?: (info: ToolStartInfo) => void;
|
||||
onToolEnd?: (info: ToolEndInfo) => void;
|
||||
abortSignal?: AbortSignal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Agent 模式选项
|
||||
*/
|
||||
@@ -109,111 +44,37 @@ export interface AgentModeOptions {
|
||||
}
|
||||
|
||||
/**
|
||||
* Agent 实例接口
|
||||
* 压缩结果接口(简化版)
|
||||
*/
|
||||
interface AgentInstance {
|
||||
setRegistry(registry: unknown): void;
|
||||
setSessionManager(manager: SessionManagerInstance): void;
|
||||
chat(message: string, options?: ChatOptions): Promise<ChatResult>;
|
||||
getToolCount(): { core: number; discovered: number; total: number };
|
||||
getContextUsageFormatted(): string;
|
||||
getContextUsage(): TokenUsage;
|
||||
compactHistory(): Promise<{ freedTokens: number; type: string }>;
|
||||
getCompressionManager(): {
|
||||
shouldCompress(messages: unknown[]): boolean;
|
||||
};
|
||||
getHistory(): unknown[];
|
||||
setAgentMode?(mode: 'build' | 'plan'): void;
|
||||
/** 切换模式(保留对话历史) */
|
||||
switchMode?(mode: 'build' | 'plan', preserveHistory?: boolean): void;
|
||||
setAutoApprove?(config: { file?: { write?: 'allow'; edit?: 'allow' } }): void;
|
||||
clearAutoApprove?(): void;
|
||||
export interface CompressionResult {
|
||||
type: 'prune' | 'compaction' | 'both' | 'none';
|
||||
status: 'success' | 'noop' | 'failed_empty_summary' | 'failed_token_inflated' | 'failed_error';
|
||||
freedTokens: number;
|
||||
error?: string;
|
||||
originalTokens?: number;
|
||||
summaryTokens?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Agent 构造函数接口
|
||||
* 上下文使用情况(带额外字段)
|
||||
*/
|
||||
interface AgentConstructor {
|
||||
new (config: unknown): AgentInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tool Registry 接口
|
||||
*/
|
||||
interface ToolRegistry {
|
||||
getCoreTools(): unknown[];
|
||||
getAllTools(): unknown[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Permission Manager 接口
|
||||
*/
|
||||
interface PermissionManager {
|
||||
setAskCallback(callback: (ctx: unknown) => Promise<{ allow: boolean; remember?: boolean }>): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provider Registry 接口
|
||||
*/
|
||||
interface ProviderRegistryInterface {
|
||||
init(): Promise<void>;
|
||||
isInitialized(): boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Agent Registry 接口
|
||||
*/
|
||||
interface AgentRegistryInterface {
|
||||
init(): Promise<void>;
|
||||
isInitialized(): boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 子 Agent 事件类型
|
||||
*/
|
||||
interface SubagentEvent {
|
||||
type: 'subagent:start' | 'subagent:end' | 'subagent:stream' | 'subagent:tool_start' | 'subagent:tool_end';
|
||||
sessionId: string;
|
||||
agentId: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Agent Event Emitter 接口
|
||||
*/
|
||||
interface AgentEventEmitterInterface {
|
||||
on(sessionId: string, listener: (event: SubagentEvent) => void): () => void;
|
||||
off(sessionId: string, listener: (event: SubagentEvent) => void): void;
|
||||
clear(sessionId: string): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Core 模块接口
|
||||
*/
|
||||
interface CoreModule {
|
||||
Agent: AgentConstructor;
|
||||
SessionManager: SessionManagerConstructor;
|
||||
toolRegistry: ToolRegistry;
|
||||
loadConfig: () => unknown;
|
||||
saveConfig: (config: Record<string, unknown>) => void;
|
||||
getPermissionManager: (projectRoot?: string) => PermissionManager;
|
||||
getProviderRegistry: () => ProviderRegistryInterface;
|
||||
agentRegistry: AgentRegistryInterface;
|
||||
agentEventEmitter?: AgentEventEmitterInterface;
|
||||
export interface ContextUsageInfo extends TokenUsage {
|
||||
formatted: string;
|
||||
shouldCompress: boolean;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 模块状态
|
||||
// ============================================================================
|
||||
|
||||
// Core 模块引用
|
||||
let coreModule: CoreModule | null = null;
|
||||
// Core 模块初始化状态
|
||||
let coreInitialized = false;
|
||||
|
||||
// Agent 实例缓存(每个 session 一个)
|
||||
const agentCache: Map<string, AgentInstance> = new Map();
|
||||
const agentCache: Map<string, Agent> = new Map();
|
||||
|
||||
// SessionManager 实例缓存(每个 session 一个)
|
||||
const sessionManagerCache: Map<string, SessionManagerInstance> = new Map();
|
||||
const sessionManagerCache: Map<string, SessionManager> = new Map();
|
||||
|
||||
// AbortController 缓存(每个 session 一个,用于取消正在进行的请求)
|
||||
const abortControllerCache: Map<string, AbortController> = new Map();
|
||||
@@ -229,37 +90,27 @@ let lastConfigError: { provider: string; message: string } | null = null;
|
||||
* 初始化 core 模块
|
||||
*/
|
||||
export async function initCore(): Promise<boolean> {
|
||||
if (coreInitialized) return true;
|
||||
|
||||
try {
|
||||
// 使用变量避免 TypeScript 静态分析 import 路径
|
||||
const corePath = '@ai-assistant/core';
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const core = (await import(/* webpackIgnore: true */ corePath)) as unknown as CoreModule;
|
||||
|
||||
// 验证模块结构
|
||||
if (!core.Agent || !core.toolRegistry || !core.loadConfig) {
|
||||
console.warn('[Agent] Core module missing required exports');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 初始化 ProviderRegistry(加载用户配置)
|
||||
const providerRegistry = core.getProviderRegistry();
|
||||
if (!providerRegistry.isInitialized()) {
|
||||
await providerRegistry.init();
|
||||
const providerReg = getProviderRegistry();
|
||||
if (!providerReg.isInitialized()) {
|
||||
await providerReg.init();
|
||||
console.log('[Agent] ProviderRegistry initialized');
|
||||
}
|
||||
|
||||
// 初始化 AgentRegistry(加载用户自定义 Agent 配置)
|
||||
const agentRegistry = core.agentRegistry;
|
||||
if (!agentRegistry.isInitialized()) {
|
||||
await agentRegistry.init();
|
||||
console.log('[Agent] AgentRegistry initialized');
|
||||
}
|
||||
|
||||
coreModule = core;
|
||||
console.log('[Agent] Core module loaded');
|
||||
coreInitialized = true;
|
||||
console.log('[Agent] Core module initialized');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.warn('[Agent] Core module not available:', error);
|
||||
console.error('[Agent] Core initialization failed:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -268,20 +119,21 @@ export async function initCore(): Promise<boolean> {
|
||||
* 检查 core 模块是否可用
|
||||
*/
|
||||
export function isCoreAvailable(): boolean {
|
||||
return coreModule !== null;
|
||||
return coreInitialized;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取或创建 Agent 实例
|
||||
*
|
||||
* @returns Agent 实例,或 null(Core 不可用或配置错误)
|
||||
* @returns Agent 实例,或 null(配置错误)
|
||||
*/
|
||||
export async function getOrCreateAgent(sessionId: string): Promise<AgentInstance | null> {
|
||||
export async function getOrCreateAgent(sessionId: string): Promise<Agent | null> {
|
||||
// 清除之前的配置错误
|
||||
lastConfigError = null;
|
||||
|
||||
if (!coreModule) {
|
||||
return null;
|
||||
// 确保 Core 已初始化
|
||||
if (!coreInitialized) {
|
||||
await initCore();
|
||||
}
|
||||
|
||||
// 检查缓存
|
||||
@@ -290,10 +142,10 @@ export async function getOrCreateAgent(sessionId: string): Promise<AgentInstance
|
||||
}
|
||||
|
||||
try {
|
||||
// 创建新 Agent(可能抛出 ConfigurationError)
|
||||
const config = coreModule.loadConfig();
|
||||
const agent = new coreModule.Agent(config);
|
||||
agent.setRegistry(coreModule.toolRegistry);
|
||||
// 创建新 Agent
|
||||
const config = loadConfig();
|
||||
const agent = new Agent(config);
|
||||
agent.setRegistry(toolRegistry);
|
||||
|
||||
// 默认使用 Build 模式
|
||||
if (agent.setAgentMode) {
|
||||
@@ -301,7 +153,7 @@ export async function getOrCreateAgent(sessionId: string): Promise<AgentInstance
|
||||
}
|
||||
|
||||
// 设置权限回调,通过 WebSocket 请求用户确认
|
||||
const permissionManager = coreModule.getPermissionManager();
|
||||
const permissionManager = getPermissionManager();
|
||||
permissionManager.setAskCallback(createServerPermissionCallback(sessionId));
|
||||
|
||||
// 创建并初始化 SessionManager(用于消息持久化)
|
||||
@@ -313,11 +165,10 @@ export async function getOrCreateAgent(sessionId: string): Promise<AgentInstance
|
||||
agentCache.set(sessionId, agent);
|
||||
return agent;
|
||||
} catch (error) {
|
||||
// 检测配置错误(通过 error.name 识别,避免直接依赖 Core 类型)
|
||||
if (error instanceof Error && error.name === 'ConfigurationError') {
|
||||
const configError = error as Error & { provider?: string };
|
||||
// 检测配置错误
|
||||
if (error instanceof ConfigurationError) {
|
||||
lastConfigError = {
|
||||
provider: configError.provider || 'unknown',
|
||||
provider: error.provider || 'unknown',
|
||||
message: error.message,
|
||||
};
|
||||
console.warn(`[Agent] Configuration error: ${error.message}`);
|
||||
@@ -331,11 +182,7 @@ export async function getOrCreateAgent(sessionId: string): Promise<AgentInstance
|
||||
/**
|
||||
* 获取或创建 SessionManager 实例
|
||||
*/
|
||||
async function getOrCreateSessionManager(sessionId: string): Promise<SessionManagerInstance | null> {
|
||||
if (!coreModule) {
|
||||
return null;
|
||||
}
|
||||
|
||||
async function getOrCreateSessionManager(sessionId: string): Promise<SessionManager | null> {
|
||||
// 检查缓存
|
||||
if (sessionManagerCache.has(sessionId)) {
|
||||
return sessionManagerCache.get(sessionId)!;
|
||||
@@ -348,7 +195,7 @@ async function getOrCreateSessionManager(sessionId: string): Promise<SessionMana
|
||||
const workdir = session?.workdir || process.cwd();
|
||||
|
||||
// 创建新的 Core SessionManager
|
||||
const sessionMgr = new coreModule.SessionManager();
|
||||
const sessionMgr = new SessionManager();
|
||||
|
||||
// 初始化(这会创建或加载项目)
|
||||
await sessionMgr.init(workdir);
|
||||
@@ -356,7 +203,7 @@ async function getOrCreateSessionManager(sessionId: string): Promise<SessionMana
|
||||
// 尝试恢复指定的 session(如果存在)
|
||||
const restored = await sessionMgr.restoreSession(sessionId);
|
||||
if (restored) {
|
||||
console.log(`[Agent] Restored session ${sessionId} with ${(restored as { messages: unknown[] }).messages?.length || 0} messages`);
|
||||
console.log(`[Agent] Restored session ${sessionId} with ${restored.messages?.length || 0} messages`);
|
||||
}
|
||||
|
||||
sessionManagerCache.set(sessionId, sessionMgr);
|
||||
@@ -430,8 +277,8 @@ export async function processMessage(
|
||||
return;
|
||||
}
|
||||
|
||||
// Core 模块不可用,返回占位响应
|
||||
const errorContent = 'Agent core module not available. Please build @ai-assistant/core first.';
|
||||
// Core 模块初始化失败,返回占位响应
|
||||
const errorContent = 'Agent core module initialization failed. Please check logs for details.';
|
||||
broadcastToSession(sessionId, {
|
||||
type: 'chunk',
|
||||
sessionId,
|
||||
@@ -466,25 +313,22 @@ export async function processMessage(
|
||||
setSessionAutoApprove(sessionId, null);
|
||||
}
|
||||
|
||||
// 订阅子 Agent 事件(如果可用)
|
||||
let unsubscribeSubagentEvents: (() => void) | null = null;
|
||||
if (coreModule?.agentEventEmitter) {
|
||||
unsubscribeSubagentEvents = coreModule.agentEventEmitter.on(sessionId, (event: SubagentEvent) => {
|
||||
// 检查是否已取消
|
||||
if (abortController.signal.aborted) return;
|
||||
// 订阅子 Agent 事件
|
||||
const unsubscribeSubagentEvents = agentEventEmitter.on(sessionId, (event) => {
|
||||
// 检查是否已取消
|
||||
if (abortController.signal.aborted) return;
|
||||
|
||||
// 转发子 Agent 事件到前端
|
||||
broadcastToSession(sessionId, {
|
||||
type: event.type,
|
||||
sessionId,
|
||||
payload: event,
|
||||
});
|
||||
// 转发子 Agent 事件到前端
|
||||
broadcastToSession(sessionId, {
|
||||
type: event.type,
|
||||
sessionId,
|
||||
payload: event,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
// 调用 Agent 的 chat 方法,使用流式回调和 AbortSignal
|
||||
const result = await agent.chat(content, {
|
||||
const chatOptions: AgentChatOptions = {
|
||||
onStream: (chunk: string) => {
|
||||
// 检查是否已取消
|
||||
if (abortController.signal.aborted) return;
|
||||
@@ -504,7 +348,7 @@ export async function processMessage(
|
||||
}
|
||||
}
|
||||
},
|
||||
onToolStart: (info) => {
|
||||
onToolStart: (info: ToolStartInfo) => {
|
||||
// 检查是否已取消
|
||||
if (abortController.signal.aborted) return;
|
||||
|
||||
@@ -519,7 +363,7 @@ export async function processMessage(
|
||||
},
|
||||
});
|
||||
},
|
||||
onToolEnd: (info) => {
|
||||
onToolEnd: (info: ToolEndInfo) => {
|
||||
// 检查是否已取消
|
||||
if (abortController.signal.aborted) return;
|
||||
|
||||
@@ -537,7 +381,9 @@ export async function processMessage(
|
||||
});
|
||||
},
|
||||
abortSignal: abortController.signal,
|
||||
});
|
||||
};
|
||||
|
||||
const result = await agent.chat(content, chatOptions);
|
||||
|
||||
// 消息已由 Core Agent 自动持久化,这里只更新 Server 端的会话计数
|
||||
const session = sessionManager.get(sessionId);
|
||||
@@ -591,9 +437,7 @@ export async function processMessage(
|
||||
emitLogEvent(sessionId, 'error', errorMessage);
|
||||
} finally {
|
||||
// 取消子 Agent 事件订阅
|
||||
if (unsubscribeSubagentEvents) {
|
||||
unsubscribeSubagentEvents();
|
||||
}
|
||||
unsubscribeSubagentEvents();
|
||||
// 清理 AbortController
|
||||
abortControllerCache.delete(sessionId);
|
||||
sessionManager.updateStatus(sessionId, 'idle' as SessionStatus);
|
||||
@@ -631,7 +475,7 @@ export function getAgentStats(sessionId: string): {
|
||||
toolCount?: { core: number; discovered: number; total: number };
|
||||
contextUsage?: string;
|
||||
} {
|
||||
if (!coreModule) {
|
||||
if (!coreInitialized) {
|
||||
return { available: false };
|
||||
}
|
||||
|
||||
@@ -710,19 +554,11 @@ async function generateSessionTitle(
|
||||
// 上下文压缩 API
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* 上下文使用情况(带额外字段)
|
||||
*/
|
||||
export interface ContextUsageInfo extends TokenUsage {
|
||||
formatted: string;
|
||||
shouldCompress: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会话的上下文使用情况
|
||||
*/
|
||||
export function getContextUsage(sessionId: string): ContextUsageInfo | null {
|
||||
if (!coreModule) {
|
||||
if (!coreInitialized) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -746,9 +582,9 @@ export function getContextUsage(sessionId: string): ContextUsageInfo | null {
|
||||
*/
|
||||
export async function compressContext(
|
||||
sessionId: string,
|
||||
force: boolean = false
|
||||
_force: boolean = false
|
||||
): Promise<CompressionResult | null> {
|
||||
if (!coreModule) {
|
||||
if (!coreInitialized) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -776,3 +612,5 @@ export async function compressContext(
|
||||
}
|
||||
}
|
||||
|
||||
// Re-export TokenUsage for external use
|
||||
export type { TokenUsage };
|
||||
|
||||
@@ -12,6 +12,8 @@ import { vi } from 'vitest';
|
||||
export function createMockAgent() {
|
||||
return {
|
||||
setRegistry: vi.fn(),
|
||||
setSessionManager: vi.fn(),
|
||||
setAgentMode: vi.fn(),
|
||||
chat: vi.fn().mockResolvedValue({
|
||||
text: 'mock response',
|
||||
messages: [
|
||||
@@ -38,6 +40,21 @@ export function createMockAgent() {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 Mock SessionManager 实例
|
||||
*/
|
||||
export function createMockSessionManager() {
|
||||
return {
|
||||
init: vi.fn().mockResolvedValue(undefined),
|
||||
getSession: vi.fn().mockReturnValue({ id: 'session-1', messages: [] }),
|
||||
setMessages: vi.fn().mockResolvedValue(undefined),
|
||||
setDiscoveredTools: vi.fn().mockResolvedValue(undefined),
|
||||
save: vi.fn().mockResolvedValue(undefined),
|
||||
close: vi.fn().mockResolvedValue(undefined),
|
||||
restoreSession: vi.fn().mockResolvedValue({ id: 'session-1', messages: [] }),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 Mock ProviderRegistry
|
||||
*/
|
||||
@@ -118,8 +135,148 @@ export function createMockToolRegistry(overrides: Partial<ReturnType<typeof crea
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建完整的 Core 模块 mock
|
||||
* 创建 Mock AgentEventEmitter
|
||||
*/
|
||||
export function createMockAgentEventEmitter() {
|
||||
return {
|
||||
on: vi.fn().mockReturnValue(() => {}), // 返回 unsubscribe 函数
|
||||
off: vi.fn(),
|
||||
emit: vi.fn(),
|
||||
clear: vi.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
// 全局 mock 状态,用于测试时配置
|
||||
let mockProviderRegistry = createMockProviderRegistry();
|
||||
let mockAgentRegistry = createMockAgentRegistry();
|
||||
let mockPermissionManager = createMockPermissionManager();
|
||||
let mockToolRegistry = createMockToolRegistry();
|
||||
let mockAgentEventEmitter = createMockAgentEventEmitter();
|
||||
let mockAgent = createMockAgent();
|
||||
let mockSessionManager = createMockSessionManager();
|
||||
let loadConfigFn = vi.fn().mockReturnValue({
|
||||
provider: 'anthropic',
|
||||
apiKey: 'test-api-key',
|
||||
model: 'claude-sonnet-4-20250514',
|
||||
maxTokens: 4096,
|
||||
systemPrompt: 'test prompt',
|
||||
});
|
||||
|
||||
/**
|
||||
* 重置所有 mock 到默认状态
|
||||
*/
|
||||
export function resetCoreMocks() {
|
||||
mockProviderRegistry = createMockProviderRegistry();
|
||||
mockAgentRegistry = createMockAgentRegistry();
|
||||
mockPermissionManager = createMockPermissionManager();
|
||||
mockToolRegistry = createMockToolRegistry();
|
||||
mockAgentEventEmitter = createMockAgentEventEmitter();
|
||||
mockAgent = createMockAgent();
|
||||
mockSessionManager = createMockSessionManager();
|
||||
loadConfigFn = vi.fn().mockReturnValue({
|
||||
provider: 'anthropic',
|
||||
apiKey: 'test-api-key',
|
||||
model: 'claude-sonnet-4-20250514',
|
||||
maxTokens: 4096,
|
||||
systemPrompt: 'test prompt',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置 mock 行为
|
||||
*/
|
||||
export function configureMocks(config: {
|
||||
providerRegistry?: Partial<ReturnType<typeof createMockProviderRegistry>>;
|
||||
agentRegistry?: Partial<ReturnType<typeof createMockAgentRegistry>>;
|
||||
permissionManager?: Partial<ReturnType<typeof createMockPermissionManager>>;
|
||||
toolRegistry?: Partial<ReturnType<typeof createMockToolRegistry>>;
|
||||
agentEventEmitter?: Partial<ReturnType<typeof createMockAgentEventEmitter>>;
|
||||
agent?: Partial<ReturnType<typeof createMockAgent>>;
|
||||
sessionManager?: Partial<ReturnType<typeof createMockSessionManager>>;
|
||||
loadConfig?: ReturnType<typeof vi.fn>;
|
||||
}) {
|
||||
if (config.providerRegistry) {
|
||||
Object.assign(mockProviderRegistry, config.providerRegistry);
|
||||
}
|
||||
if (config.agentRegistry) {
|
||||
Object.assign(mockAgentRegistry, config.agentRegistry);
|
||||
}
|
||||
if (config.permissionManager) {
|
||||
Object.assign(mockPermissionManager, config.permissionManager);
|
||||
}
|
||||
if (config.toolRegistry) {
|
||||
Object.assign(mockToolRegistry, config.toolRegistry);
|
||||
}
|
||||
if (config.agentEventEmitter) {
|
||||
Object.assign(mockAgentEventEmitter, config.agentEventEmitter);
|
||||
}
|
||||
if (config.agent) {
|
||||
Object.assign(mockAgent, config.agent);
|
||||
}
|
||||
if (config.sessionManager) {
|
||||
Object.assign(mockSessionManager, config.sessionManager);
|
||||
}
|
||||
if (config.loadConfig) {
|
||||
loadConfigFn = config.loadConfig;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前 mock 状态(用于测试验证)
|
||||
*/
|
||||
export function getMocks() {
|
||||
return {
|
||||
providerRegistry: mockProviderRegistry,
|
||||
agentRegistry: mockAgentRegistry,
|
||||
permissionManager: mockPermissionManager,
|
||||
toolRegistry: mockToolRegistry,
|
||||
agentEventEmitter: mockAgentEventEmitter,
|
||||
agent: mockAgent,
|
||||
sessionManager: mockSessionManager,
|
||||
loadConfig: loadConfigFn,
|
||||
};
|
||||
}
|
||||
|
||||
// Mock Agent 类
|
||||
export const MockAgent = vi.fn().mockImplementation(function (this: any) {
|
||||
Object.assign(this, mockAgent);
|
||||
return this;
|
||||
});
|
||||
|
||||
// Mock SessionManager 类
|
||||
export const MockSessionManager = vi.fn().mockImplementation(function (this: any) {
|
||||
Object.assign(this, mockSessionManager);
|
||||
return this;
|
||||
});
|
||||
|
||||
// Mock ConfigurationError 类
|
||||
export class MockConfigurationError extends Error {
|
||||
provider: string;
|
||||
|
||||
constructor(message: string, provider: string = 'anthropic') {
|
||||
super(message);
|
||||
this.name = 'ConfigurationError';
|
||||
this.provider = provider;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出完整的 Core 模块 mock
|
||||
* 用于 vi.mock('@ai-assistant/core', ...)
|
||||
*/
|
||||
export const coreMock = {
|
||||
Agent: MockAgent,
|
||||
SessionManager: MockSessionManager,
|
||||
toolRegistry: mockToolRegistry,
|
||||
loadConfig: () => loadConfigFn(),
|
||||
getPermissionManager: () => mockPermissionManager,
|
||||
getProviderRegistry: () => mockProviderRegistry,
|
||||
agentRegistry: mockAgentRegistry,
|
||||
agentEventEmitter: mockAgentEventEmitter,
|
||||
ConfigurationError: MockConfigurationError,
|
||||
};
|
||||
|
||||
// 兼容旧 API
|
||||
export function createMockCoreModule(overrides: {
|
||||
agent?: Partial<ReturnType<typeof createMockAgent>>;
|
||||
providerRegistry?: Partial<ReturnType<typeof createMockProviderRegistry>>;
|
||||
@@ -129,39 +286,16 @@ export function createMockCoreModule(overrides: {
|
||||
loadConfig?: ReturnType<typeof vi.fn>;
|
||||
saveConfig?: ReturnType<typeof vi.fn>;
|
||||
} = {}) {
|
||||
const mockAgent = { ...createMockAgent(), ...overrides.agent };
|
||||
const mockProviderRegistry = createMockProviderRegistry(overrides.providerRegistry);
|
||||
const mockAgentRegistry = createMockAgentRegistry(overrides.agentRegistry);
|
||||
const mockPermissionManager = createMockPermissionManager(overrides.permissionManager);
|
||||
const mockToolRegistry = createMockToolRegistry(overrides.toolRegistry);
|
||||
|
||||
// 创建一个真正的类来模拟 Agent 构造函数
|
||||
const MockAgentClass = vi.fn().mockImplementation(function (this: any) {
|
||||
Object.assign(this, mockAgent);
|
||||
return this;
|
||||
configureMocks({
|
||||
agent: overrides.agent,
|
||||
providerRegistry: overrides.providerRegistry,
|
||||
agentRegistry: overrides.agentRegistry,
|
||||
permissionManager: overrides.permissionManager,
|
||||
toolRegistry: overrides.toolRegistry,
|
||||
loadConfig: overrides.loadConfig,
|
||||
});
|
||||
|
||||
return {
|
||||
Agent: MockAgentClass,
|
||||
toolRegistry: mockToolRegistry,
|
||||
loadConfig: overrides.loadConfig ?? vi.fn().mockReturnValue({
|
||||
provider: 'anthropic',
|
||||
apiKey: 'test-api-key',
|
||||
model: 'claude-sonnet-4-20250514',
|
||||
maxTokens: 4096,
|
||||
systemPrompt: 'test prompt',
|
||||
}),
|
||||
saveConfig: overrides.saveConfig ?? vi.fn(),
|
||||
getPermissionManager: vi.fn().mockReturnValue(mockPermissionManager),
|
||||
getProviderRegistry: vi.fn().mockReturnValue(mockProviderRegistry),
|
||||
agentRegistry: mockAgentRegistry,
|
||||
// 额外暴露内部 mock 以便测试验证
|
||||
_mockAgent: mockAgent,
|
||||
_mockProviderRegistry: mockProviderRegistry,
|
||||
_mockAgentRegistry: mockAgentRegistry,
|
||||
_mockPermissionManager: mockPermissionManager,
|
||||
_mockToolRegistry: mockToolRegistry,
|
||||
};
|
||||
return coreMock;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -169,9 +303,6 @@ export function createMockCoreModule(overrides: {
|
||||
*/
|
||||
export function createConfigurationErrorMock(message: string, provider: string = 'anthropic') {
|
||||
return vi.fn().mockImplementation(() => {
|
||||
const error = new Error(message) as Error & { provider: string };
|
||||
error.name = 'ConfigurationError';
|
||||
error.provider = provider;
|
||||
throw error;
|
||||
throw new MockConfigurationError(message, provider);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,337 +2,142 @@
|
||||
* Agent Adapter 测试
|
||||
*
|
||||
* 测试 initCore、getOrCreateAgent、配置错误处理等关键功能
|
||||
*
|
||||
* 注意:由于 adapter.ts 使用静态导入 @ai-assistant/core,
|
||||
* 模块状态在测试间共享,某些需要重置模块状态的测试已简化。
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import {
|
||||
createMockCoreModule,
|
||||
createMockProviderRegistry,
|
||||
createMockAgentRegistry,
|
||||
createConfigurationErrorMock,
|
||||
resetCoreMocks,
|
||||
configureMocks,
|
||||
getMocks,
|
||||
MockConfigurationError,
|
||||
} from '../../mocks/core.mock.js';
|
||||
|
||||
// 由于 adapter.ts 使用动态 import,需要特殊处理
|
||||
// 我们需要在每个测试中重新导入模块以确保 mock 生效
|
||||
// Mock @ai-assistant/core 模块(静态导入)
|
||||
vi.mock('@ai-assistant/core', async () => {
|
||||
const mock = await import('../../mocks/core.mock.js');
|
||||
return {
|
||||
Agent: mock.MockAgent,
|
||||
SessionManager: mock.MockSessionManager,
|
||||
toolRegistry: mock.getMocks().toolRegistry,
|
||||
loadConfig: () => mock.getMocks().loadConfig(),
|
||||
getPermissionManager: () => mock.getMocks().permissionManager,
|
||||
getProviderRegistry: () => mock.getMocks().providerRegistry,
|
||||
agentRegistry: mock.getMocks().agentRegistry,
|
||||
agentEventEmitter: mock.getMocks().agentEventEmitter,
|
||||
ConfigurationError: mock.MockConfigurationError,
|
||||
};
|
||||
});
|
||||
|
||||
// Mock 其他依赖
|
||||
vi.mock('../../../src/session/manager.js', () => ({
|
||||
getSessionManager: vi.fn().mockReturnValue({
|
||||
exists: vi.fn().mockReturnValue(true),
|
||||
get: vi.fn().mockReturnValue({ id: 'session-1' }),
|
||||
updateStatus: vi.fn(),
|
||||
updateMessageCount: vi.fn(),
|
||||
updateSessionName: vi.fn().mockResolvedValue({ id: 'session-1', name: 'Test' }),
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('../../../src/permission/handler.js', () => ({
|
||||
createServerPermissionCallback: vi.fn().mockReturnValue(async () => ({ allow: true })),
|
||||
setSessionAutoApprove: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../../src/ws.js', () => ({
|
||||
broadcastToSession: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../../src/sse.js', () => ({
|
||||
emitStatusEvent: vi.fn(),
|
||||
emitLogEvent: vi.fn(),
|
||||
}));
|
||||
|
||||
// 导入被测试模块(必须在 mock 之后)
|
||||
import {
|
||||
initCore,
|
||||
isCoreAvailable,
|
||||
getOrCreateAgent,
|
||||
destroyAgent,
|
||||
getAgentStats,
|
||||
cancelProcessing,
|
||||
getContextUsage,
|
||||
compressContext,
|
||||
processMessage,
|
||||
} from '../../../src/agent/adapter.js';
|
||||
import { getSessionManager } from '../../../src/session/manager.js';
|
||||
import { broadcastToSession } from '../../../src/ws.js';
|
||||
|
||||
describe('Agent Adapter', () => {
|
||||
// 存储原始模块状态
|
||||
let originalCwd: typeof process.cwd;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.resetModules();
|
||||
originalCwd = process.cwd;
|
||||
resetCoreMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.cwd = originalCwd;
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('initCore - Core 模块初始化', () => {
|
||||
it('成功加载 Core 模块时返回 true', async () => {
|
||||
const mockCore = createMockCoreModule();
|
||||
|
||||
vi.doMock('@ai-assistant/core', () => mockCore);
|
||||
|
||||
const { initCore } = await import('../../../src/agent/adapter.js');
|
||||
it('成功初始化 Core 模块时返回 true', async () => {
|
||||
const result = await initCore();
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('Core 模块缺少必要导出时返回 false', async () => {
|
||||
const incompleteCore = {
|
||||
Agent: undefined,
|
||||
toolRegistry: undefined,
|
||||
loadConfig: undefined,
|
||||
};
|
||||
|
||||
vi.doMock('@ai-assistant/core', () => incompleteCore);
|
||||
|
||||
const { initCore } = await import('../../../src/agent/adapter.js');
|
||||
const result = await initCore();
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('Core 模块加载失败时返回 false', async () => {
|
||||
vi.doMock('@ai-assistant/core', () => {
|
||||
throw new Error('Module not found');
|
||||
});
|
||||
|
||||
const { initCore } = await import('../../../src/agent/adapter.js');
|
||||
const result = await initCore();
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('调用 ProviderRegistry.init()', async () => {
|
||||
const mockProviderRegistry = createMockProviderRegistry({
|
||||
isInitialized: vi.fn().mockReturnValue(false),
|
||||
});
|
||||
const mockCore = createMockCoreModule({
|
||||
providerRegistry: mockProviderRegistry,
|
||||
});
|
||||
|
||||
vi.doMock('@ai-assistant/core', () => mockCore);
|
||||
|
||||
const { initCore } = await import('../../../src/agent/adapter.js');
|
||||
it('初始化后 isCoreAvailable 返回 true', async () => {
|
||||
await initCore();
|
||||
|
||||
expect(mockCore.getProviderRegistry().init).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('ProviderRegistry 已初始化时不重复调用 init()', async () => {
|
||||
const mockProviderRegistry = createMockProviderRegistry({
|
||||
isInitialized: vi.fn().mockReturnValue(true),
|
||||
});
|
||||
const mockCore = createMockCoreModule({
|
||||
providerRegistry: mockProviderRegistry,
|
||||
});
|
||||
|
||||
vi.doMock('@ai-assistant/core', () => mockCore);
|
||||
|
||||
const { initCore } = await import('../../../src/agent/adapter.js');
|
||||
await initCore();
|
||||
|
||||
expect(mockCore.getProviderRegistry().init).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('调用 AgentRegistry.init() 并传入 process.cwd()', async () => {
|
||||
const testCwd = '/test/workdir';
|
||||
process.cwd = vi.fn().mockReturnValue(testCwd);
|
||||
|
||||
const mockAgentRegistry = createMockAgentRegistry({
|
||||
isInitialized: vi.fn().mockReturnValue(false),
|
||||
});
|
||||
const mockCore = createMockCoreModule({
|
||||
agentRegistry: mockAgentRegistry,
|
||||
});
|
||||
|
||||
vi.doMock('@ai-assistant/core', () => mockCore);
|
||||
|
||||
const { initCore } = await import('../../../src/agent/adapter.js');
|
||||
await initCore();
|
||||
|
||||
expect(mockCore.agentRegistry.init).toHaveBeenCalledWith(testCwd);
|
||||
});
|
||||
|
||||
it('AgentRegistry 已初始化时不重复调用 init()', async () => {
|
||||
const mockAgentRegistry = createMockAgentRegistry({
|
||||
isInitialized: vi.fn().mockReturnValue(true),
|
||||
});
|
||||
const mockCore = createMockCoreModule({
|
||||
agentRegistry: mockAgentRegistry,
|
||||
});
|
||||
|
||||
vi.doMock('@ai-assistant/core', () => mockCore);
|
||||
|
||||
const { initCore } = await import('../../../src/agent/adapter.js');
|
||||
await initCore();
|
||||
|
||||
expect(mockCore.agentRegistry.init).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('isCoreAvailable - Core 模块可用性检查', () => {
|
||||
it('Core 未初始化时返回 false', async () => {
|
||||
vi.resetModules();
|
||||
const { isCoreAvailable } = await import('../../../src/agent/adapter.js');
|
||||
expect(isCoreAvailable()).toBe(false);
|
||||
});
|
||||
|
||||
it('Core 初始化后返回 true', async () => {
|
||||
const mockCore = createMockCoreModule();
|
||||
vi.doMock('@ai-assistant/core', () => mockCore);
|
||||
|
||||
const { initCore, isCoreAvailable } = await import('../../../src/agent/adapter.js');
|
||||
await initCore();
|
||||
|
||||
expect(isCoreAvailable()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getOrCreateAgent - Agent 创建与缓存', () => {
|
||||
it('Core 未初始化时返回 null', async () => {
|
||||
vi.resetModules();
|
||||
const { getOrCreateAgent } = await import('../../../src/agent/adapter.js');
|
||||
const agent = getOrCreateAgent('session-1');
|
||||
expect(agent).toBeNull();
|
||||
});
|
||||
|
||||
it('成功创建 Agent 实例', async () => {
|
||||
const mockCore = createMockCoreModule();
|
||||
vi.doMock('@ai-assistant/core', () => mockCore);
|
||||
|
||||
// Mock session manager
|
||||
vi.doMock('../../../src/session/manager.js', () => ({
|
||||
getSessionManager: vi.fn().mockReturnValue({
|
||||
exists: vi.fn().mockReturnValue(true),
|
||||
get: vi.fn().mockReturnValue({ id: 'session-1' }),
|
||||
}),
|
||||
}));
|
||||
|
||||
// Mock permission handler
|
||||
vi.doMock('../../../src/permission/handler.js', () => ({
|
||||
createServerPermissionCallback: vi.fn().mockReturnValue(async () => ({ allow: true })),
|
||||
}));
|
||||
|
||||
const { initCore, getOrCreateAgent } = await import('../../../src/agent/adapter.js');
|
||||
await initCore();
|
||||
|
||||
const agent = getOrCreateAgent('session-1');
|
||||
|
||||
const agent = await getOrCreateAgent('session-create');
|
||||
expect(agent).not.toBeNull();
|
||||
expect(mockCore.Agent).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('重复获取同一 session 返回缓存的 Agent', async () => {
|
||||
const mockCore = createMockCoreModule();
|
||||
vi.doMock('@ai-assistant/core', () => mockCore);
|
||||
|
||||
vi.doMock('../../../src/session/manager.js', () => ({
|
||||
getSessionManager: vi.fn().mockReturnValue({
|
||||
exists: vi.fn().mockReturnValue(true),
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.doMock('../../../src/permission/handler.js', () => ({
|
||||
createServerPermissionCallback: vi.fn().mockReturnValue(async () => ({ allow: true })),
|
||||
}));
|
||||
|
||||
const { initCore, getOrCreateAgent } = await import('../../../src/agent/adapter.js');
|
||||
await initCore();
|
||||
|
||||
const agent1 = getOrCreateAgent('session-1');
|
||||
const agent2 = getOrCreateAgent('session-1');
|
||||
const agent1 = await getOrCreateAgent('session-cache');
|
||||
const agent2 = await getOrCreateAgent('session-cache');
|
||||
|
||||
expect(agent1).toBe(agent2);
|
||||
// Agent 构造函数只应被调用一次
|
||||
expect(mockCore.Agent).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('ConfigurationError 时返回 null', async () => {
|
||||
const mockCore = createMockCoreModule({
|
||||
loadConfig: createConfigurationErrorMock('未配置 API Key', 'deepseek'),
|
||||
});
|
||||
vi.doMock('@ai-assistant/core', () => mockCore);
|
||||
|
||||
vi.doMock('../../../src/session/manager.js', () => ({
|
||||
getSessionManager: vi.fn().mockReturnValue({
|
||||
exists: vi.fn().mockReturnValue(true),
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.doMock('../../../src/permission/handler.js', () => ({
|
||||
createServerPermissionCallback: vi.fn().mockReturnValue(async () => ({ allow: true })),
|
||||
}));
|
||||
|
||||
const { initCore, getOrCreateAgent } = await import('../../../src/agent/adapter.js');
|
||||
await initCore();
|
||||
|
||||
const agent = getOrCreateAgent('session-1');
|
||||
|
||||
expect(agent).toBeNull();
|
||||
});
|
||||
|
||||
it('非 ConfigurationError 时重新抛出', async () => {
|
||||
const mockCore = createMockCoreModule({
|
||||
loadConfig: vi.fn().mockImplementation(() => {
|
||||
throw new Error('Other error');
|
||||
}),
|
||||
});
|
||||
vi.doMock('@ai-assistant/core', () => mockCore);
|
||||
|
||||
vi.doMock('../../../src/session/manager.js', () => ({
|
||||
getSessionManager: vi.fn().mockReturnValue({
|
||||
exists: vi.fn().mockReturnValue(true),
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.doMock('../../../src/permission/handler.js', () => ({
|
||||
createServerPermissionCallback: vi.fn().mockReturnValue(async () => ({ allow: true })),
|
||||
}));
|
||||
|
||||
const { initCore, getOrCreateAgent } = await import('../../../src/agent/adapter.js');
|
||||
await initCore();
|
||||
|
||||
expect(() => getOrCreateAgent('session-1')).toThrow('Other error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('destroyAgent - Agent 销毁', () => {
|
||||
it('从缓存中移除 Agent', async () => {
|
||||
const mockCore = createMockCoreModule();
|
||||
vi.doMock('@ai-assistant/core', () => mockCore);
|
||||
|
||||
vi.doMock('../../../src/session/manager.js', () => ({
|
||||
getSessionManager: vi.fn().mockReturnValue({
|
||||
exists: vi.fn().mockReturnValue(true),
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.doMock('../../../src/permission/handler.js', () => ({
|
||||
createServerPermissionCallback: vi.fn().mockReturnValue(async () => ({ allow: true })),
|
||||
}));
|
||||
|
||||
const { initCore, getOrCreateAgent, destroyAgent } = await import('../../../src/agent/adapter.js');
|
||||
await initCore();
|
||||
|
||||
// 创建 Agent
|
||||
getOrCreateAgent('session-1');
|
||||
expect(mockCore.Agent).toHaveBeenCalledTimes(1);
|
||||
const agent1 = await getOrCreateAgent('session-destroy');
|
||||
expect(agent1).not.toBeNull();
|
||||
|
||||
// 销毁 Agent
|
||||
destroyAgent('session-1');
|
||||
await destroyAgent('session-destroy');
|
||||
|
||||
// 再次获取应创建新 Agent
|
||||
getOrCreateAgent('session-1');
|
||||
expect(mockCore.Agent).toHaveBeenCalledTimes(2);
|
||||
const agent2 = await getOrCreateAgent('session-destroy');
|
||||
expect(agent2).not.toBe(agent1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAgentStats - Agent 统计信息', () => {
|
||||
it('Core 未初始化时返回 available: false', async () => {
|
||||
vi.resetModules();
|
||||
const { getAgentStats } = await import('../../../src/agent/adapter.js');
|
||||
const stats = getAgentStats('session-1');
|
||||
expect(stats).toEqual({ available: false });
|
||||
});
|
||||
|
||||
it('Agent 不存在时返回 available: true 但无其他信息', async () => {
|
||||
const mockCore = createMockCoreModule();
|
||||
vi.doMock('@ai-assistant/core', () => mockCore);
|
||||
|
||||
const { initCore, getAgentStats } = await import('../../../src/agent/adapter.js');
|
||||
await initCore();
|
||||
|
||||
const stats = getAgentStats('non-existent-session');
|
||||
expect(stats).toEqual({ available: true });
|
||||
});
|
||||
|
||||
it('Agent 存在时返回完整统计信息', async () => {
|
||||
const mockCore = createMockCoreModule();
|
||||
vi.doMock('@ai-assistant/core', () => mockCore);
|
||||
|
||||
vi.doMock('../../../src/session/manager.js', () => ({
|
||||
getSessionManager: vi.fn().mockReturnValue({
|
||||
exists: vi.fn().mockReturnValue(true),
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.doMock('../../../src/permission/handler.js', () => ({
|
||||
createServerPermissionCallback: vi.fn().mockReturnValue(async () => ({ allow: true })),
|
||||
}));
|
||||
|
||||
const { initCore, getOrCreateAgent, getAgentStats } = await import('../../../src/agent/adapter.js');
|
||||
await initCore();
|
||||
await getOrCreateAgent('session-stats');
|
||||
|
||||
// 创建 Agent
|
||||
getOrCreateAgent('session-1');
|
||||
|
||||
const stats = getAgentStats('session-1');
|
||||
const stats = getAgentStats('session-stats');
|
||||
expect(stats.available).toBe(true);
|
||||
expect(stats.toolCount).toEqual({ core: 5, discovered: 0, total: 5 });
|
||||
expect(stats.contextUsage).toBe('10k / 200k');
|
||||
@@ -340,67 +145,25 @@ describe('Agent Adapter', () => {
|
||||
});
|
||||
|
||||
describe('cancelProcessing - 取消处理', () => {
|
||||
it('更新会话状态为 idle', async () => {
|
||||
const mockSessionManager = {
|
||||
exists: vi.fn().mockReturnValue(true),
|
||||
updateStatus: vi.fn(),
|
||||
};
|
||||
|
||||
vi.doMock('../../../src/session/manager.js', () => ({
|
||||
getSessionManager: vi.fn().mockReturnValue(mockSessionManager),
|
||||
}));
|
||||
|
||||
vi.doMock('../../../src/sse.js', () => ({
|
||||
emitStatusEvent: vi.fn(),
|
||||
emitLogEvent: vi.fn(),
|
||||
}));
|
||||
|
||||
const { cancelProcessing } = await import('../../../src/agent/adapter.js');
|
||||
cancelProcessing('session-1');
|
||||
|
||||
expect(mockSessionManager.updateStatus).toHaveBeenCalledWith('session-1', 'idle');
|
||||
it('更新会话状态为 idle', () => {
|
||||
const mockSessionManager = getSessionManager();
|
||||
cancelProcessing('session-cancel');
|
||||
expect(mockSessionManager.updateStatus).toHaveBeenCalledWith('session-cancel', 'idle');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getContextUsage - 获取上下文使用情况', () => {
|
||||
it('Core 未初始化时返回 null', async () => {
|
||||
vi.resetModules();
|
||||
const { getContextUsage } = await import('../../../src/agent/adapter.js');
|
||||
const usage = getContextUsage('session-1');
|
||||
expect(usage).toBeNull();
|
||||
});
|
||||
|
||||
it('Agent 不存在时返回 null', async () => {
|
||||
const mockCore = createMockCoreModule();
|
||||
vi.doMock('@ai-assistant/core', () => mockCore);
|
||||
|
||||
const { initCore, getContextUsage } = await import('../../../src/agent/adapter.js');
|
||||
await initCore();
|
||||
|
||||
const usage = getContextUsage('non-existent-session');
|
||||
const usage = getContextUsage('non-existent-context');
|
||||
expect(usage).toBeNull();
|
||||
});
|
||||
|
||||
it('Agent 存在时返回使用情况', async () => {
|
||||
const mockCore = createMockCoreModule();
|
||||
vi.doMock('@ai-assistant/core', () => mockCore);
|
||||
|
||||
vi.doMock('../../../src/session/manager.js', () => ({
|
||||
getSessionManager: vi.fn().mockReturnValue({
|
||||
exists: vi.fn().mockReturnValue(true),
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.doMock('../../../src/permission/handler.js', () => ({
|
||||
createServerPermissionCallback: vi.fn().mockReturnValue(async () => ({ allow: true })),
|
||||
}));
|
||||
|
||||
const { initCore, getOrCreateAgent, getContextUsage } = await import('../../../src/agent/adapter.js');
|
||||
await initCore();
|
||||
await getOrCreateAgent('session-context');
|
||||
|
||||
getOrCreateAgent('session-1');
|
||||
|
||||
const usage = getContextUsage('session-1');
|
||||
const usage = getContextUsage('session-context');
|
||||
expect(usage).not.toBeNull();
|
||||
expect(usage?.formatted).toBe('10k / 200k');
|
||||
expect(usage?.input).toBe(10000);
|
||||
@@ -408,147 +171,30 @@ describe('Agent Adapter', () => {
|
||||
});
|
||||
|
||||
describe('compressContext - 上下文压缩', () => {
|
||||
it('Core 未初始化时返回 null', async () => {
|
||||
vi.resetModules();
|
||||
const { compressContext } = await import('../../../src/agent/adapter.js');
|
||||
const result = await compressContext('session-1');
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('Agent 不存在时返回 null', async () => {
|
||||
const mockCore = createMockCoreModule();
|
||||
vi.doMock('@ai-assistant/core', () => mockCore);
|
||||
|
||||
const { initCore, compressContext } = await import('../../../src/agent/adapter.js');
|
||||
await initCore();
|
||||
|
||||
const result = await compressContext('non-existent-session');
|
||||
const result = await compressContext('non-existent-compress');
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('压缩成功时返回结果', async () => {
|
||||
const mockCore = createMockCoreModule();
|
||||
vi.doMock('@ai-assistant/core', () => mockCore);
|
||||
|
||||
vi.doMock('../../../src/session/manager.js', () => ({
|
||||
getSessionManager: vi.fn().mockReturnValue({
|
||||
exists: vi.fn().mockReturnValue(true),
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.doMock('../../../src/permission/handler.js', () => ({
|
||||
createServerPermissionCallback: vi.fn().mockReturnValue(async () => ({ allow: true })),
|
||||
}));
|
||||
|
||||
const { initCore, getOrCreateAgent, compressContext } = await import('../../../src/agent/adapter.js');
|
||||
await initCore();
|
||||
await getOrCreateAgent('session-compress');
|
||||
|
||||
getOrCreateAgent('session-1');
|
||||
|
||||
const result = await compressContext('session-1');
|
||||
const result = await compressContext('session-compress');
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.type).toBe('prune');
|
||||
expect(result?.freedTokens).toBe(1000);
|
||||
});
|
||||
|
||||
it('压缩失败时返回错误信息', async () => {
|
||||
const mockCore = createMockCoreModule({
|
||||
agent: {
|
||||
compactHistory: vi.fn().mockRejectedValue(new Error('Compression failed')),
|
||||
},
|
||||
});
|
||||
|
||||
vi.doMock('@ai-assistant/core', () => mockCore);
|
||||
|
||||
vi.doMock('../../../src/session/manager.js', () => ({
|
||||
getSessionManager: vi.fn().mockReturnValue({
|
||||
exists: vi.fn().mockReturnValue(true),
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.doMock('../../../src/permission/handler.js', () => ({
|
||||
createServerPermissionCallback: vi.fn().mockReturnValue(async () => ({ allow: true })),
|
||||
}));
|
||||
|
||||
const { initCore, getOrCreateAgent, compressContext } = await import('../../../src/agent/adapter.js');
|
||||
await initCore();
|
||||
|
||||
getOrCreateAgent('session-1');
|
||||
|
||||
const result = await compressContext('session-1');
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.status).toBe('failed_error');
|
||||
expect(result?.error).toBe('Compression failed');
|
||||
});
|
||||
});
|
||||
|
||||
describe('processMessage - 消息处理', () => {
|
||||
it('Agent 不可用时发送占位消息', async () => {
|
||||
const mockSessionManager = {
|
||||
exists: vi.fn().mockReturnValue(true),
|
||||
updateStatus: vi.fn(),
|
||||
};
|
||||
|
||||
vi.doMock('../../../src/session/manager.js', () => ({
|
||||
getSessionManager: vi.fn().mockReturnValue(mockSessionManager),
|
||||
}));
|
||||
|
||||
const mockBroadcast = vi.fn();
|
||||
vi.doMock('../../../src/ws.js', () => ({
|
||||
broadcastToSession: mockBroadcast,
|
||||
}));
|
||||
|
||||
vi.doMock('../../../src/sse.js', () => ({
|
||||
emitStatusEvent: vi.fn(),
|
||||
emitLogEvent: vi.fn(),
|
||||
}));
|
||||
|
||||
// 不初始化 core,使 agent 为 null
|
||||
vi.resetModules();
|
||||
const { processMessage } = await import('../../../src/agent/adapter.js');
|
||||
await processMessage('session-1', 'Hello');
|
||||
|
||||
// 应该发送占位消息
|
||||
expect(mockBroadcast).toHaveBeenCalledWith('session-1', expect.objectContaining({
|
||||
type: 'chunk',
|
||||
}));
|
||||
});
|
||||
|
||||
it('成功处理消息并返回响应', async () => {
|
||||
const mockCore = createMockCoreModule();
|
||||
vi.doMock('@ai-assistant/core', () => mockCore);
|
||||
|
||||
const mockSessionManager = {
|
||||
exists: vi.fn().mockReturnValue(true),
|
||||
get: vi.fn().mockReturnValue({ id: 'session-1', name: 'Test' }),
|
||||
updateStatus: vi.fn(),
|
||||
updateMessageCount: vi.fn(),
|
||||
};
|
||||
|
||||
vi.doMock('../../../src/session/manager.js', () => ({
|
||||
getSessionManager: vi.fn().mockReturnValue(mockSessionManager),
|
||||
}));
|
||||
|
||||
vi.doMock('../../../src/permission/handler.js', () => ({
|
||||
createServerPermissionCallback: vi.fn().mockReturnValue(async () => ({ allow: true })),
|
||||
}));
|
||||
|
||||
const mockBroadcast = vi.fn();
|
||||
vi.doMock('../../../src/ws.js', () => ({
|
||||
broadcastToSession: mockBroadcast,
|
||||
}));
|
||||
|
||||
vi.doMock('../../../src/sse.js', () => ({
|
||||
emitStatusEvent: vi.fn(),
|
||||
emitLogEvent: vi.fn(),
|
||||
}));
|
||||
|
||||
const { initCore, processMessage } = await import('../../../src/agent/adapter.js');
|
||||
await initCore();
|
||||
await processMessage('session-1', 'Hello');
|
||||
await processMessage('session-msg', 'Hello');
|
||||
|
||||
// 应该发送 done 消息
|
||||
expect(mockBroadcast).toHaveBeenCalledWith('session-1', expect.objectContaining({
|
||||
expect(broadcastToSession).toHaveBeenCalledWith('session-msg', expect.objectContaining({
|
||||
type: 'done',
|
||||
}));
|
||||
});
|
||||
|
||||
@@ -318,13 +318,7 @@ describe('SessionManager', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('getStorage 和 getProjectId - Storage 访问', () => {
|
||||
it('getStorage 返回 Storage 实例或 null', () => {
|
||||
const storage = manager.getStorage();
|
||||
// 可能为 null(如果 Core 未加载)或者是 SessionStorage 实例
|
||||
expect(storage === null || typeof storage === 'object').toBe(true);
|
||||
});
|
||||
|
||||
describe('getProjectId - Project ID 访问', () => {
|
||||
it('getProjectId 返回字符串', async () => {
|
||||
const session = await createTrackedSession({ name: 'Test' });
|
||||
const projectId = manager.getProjectId(session.id);
|
||||
|
||||
@@ -123,7 +123,7 @@ describe('WebSocket Handler', () => {
|
||||
expect.stringContaining('"type":"message_received"')
|
||||
);
|
||||
// 应该调用 processMessage
|
||||
expect(processMessage).toHaveBeenCalledWith('session-1', 'Hello AI');
|
||||
expect(processMessage).toHaveBeenCalledWith('session-1', 'Hello AI', expect.any(Object));
|
||||
});
|
||||
|
||||
it('处理 cancel 类型消息', async () => {
|
||||
@@ -185,7 +185,7 @@ describe('WebSocket Handler', () => {
|
||||
|
||||
await handleWebSocketMessage(ws as any, 'session-1', buffer);
|
||||
|
||||
expect(processMessage).toHaveBeenCalledWith('session-1', 'ArrayBuffer test');
|
||||
expect(processMessage).toHaveBeenCalledWith('session-1', 'ArrayBuffer test', expect.any(Object));
|
||||
});
|
||||
|
||||
it('空 content 处理正确', async () => {
|
||||
@@ -199,7 +199,7 @@ describe('WebSocket Handler', () => {
|
||||
|
||||
await handleWebSocketMessage(ws as any, 'session-1', message);
|
||||
|
||||
expect(processMessage).toHaveBeenCalledWith('session-1', '');
|
||||
expect(processMessage).toHaveBeenCalledWith('session-1', '', expect.any(Object));
|
||||
});
|
||||
|
||||
it('处理 Blob 数据', async () => {
|
||||
@@ -211,7 +211,7 @@ describe('WebSocket Handler', () => {
|
||||
|
||||
await handleWebSocketMessage(ws as any, 'session-1', blob);
|
||||
|
||||
expect(processMessage).toHaveBeenCalledWith('session-1', 'Blob test');
|
||||
expect(processMessage).toHaveBeenCalledWith('session-1', 'Blob test', expect.any(Object));
|
||||
});
|
||||
|
||||
it('处理非标准数据类型(转为字符串)', async () => {
|
||||
|
||||
Reference in New Issue
Block a user