Files
zclaw_openfang/desktop/tests/stabilization.test.ts
iven 4a23bbeda6
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
fix: update chatStore tests for sub-store refactoring
Tests were referencing old monolithic useChatStore API. Updated to
use useConversationStore for conversation/agent/model state and
useChatStore for message operations. 10→0 failures.
2026-04-06 11:57:46 +08:00

347 lines
10 KiB
TypeScript

/**
* Stabilization Core Path Tests
*
* Covers the 4 critical paths from STABILIZATION_DIRECTIVE.md 5:
* 1. Skill execution -- invoke -> no crash -> result
* 2. Hand trigger -- emit event -> frontend receives notification
* 3. Message sending -- Store -> invoke -> streaming response
* 4. Config sync -- SaaS pull -> Store update
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { invoke } from '@tauri-apps/api/core';
import { useChatStore, type Message } from '../src/store/chatStore';
// --- Shared mocks ---
vi.mock('@tauri-apps/api/core', () => ({
invoke: vi.fn(),
}));
vi.mock('@tauri-apps/api/event', () => ({
listen: vi.fn(() => Promise.resolve(() => {})),
emit: vi.fn(() => Promise.resolve()),
}));
vi.mock('../src/lib/tauri-gateway', () => ({
isTauriRuntime: () => false,
getGatewayClient: vi.fn(),
startLocalGateway: vi.fn(),
stopLocalGateway: vi.fn(),
getLocalGatewayStatus: vi.fn(),
getLocalGatewayAuth: vi.fn(),
prepareLocalGatewayForTauri: vi.fn(),
approveLocalGatewayDevicePairing: vi.fn(),
getZclawProcessList: vi.fn(),
getZclawProcessLogs: vi.fn(),
getUnsupportedLocalGatewayStatus: vi.fn(() => ({
supported: false,
cliAvailable: false,
runtimeSource: null,
runtimePath: null,
serviceLabel: null,
serviceLoaded: false,
serviceStatus: null,
configOk: false,
port: null,
portStatus: null,
probeUrl: null,
listenerPids: [],
error: null,
raw: {},
})),
}));
vi.mock('../src/lib/gateway-client', () => ({
getGatewayClient: vi.fn(() => ({
chatStream: vi.fn(),
chat: vi.fn(),
onAgentStream: vi.fn(() => () => {}),
getState: vi.fn(() => 'disconnected'),
})),
}));
vi.mock('../src/lib/intelligence-client', () => ({
intelligenceClient: {
compactor: {
checkThreshold: vi.fn(() => Promise.resolve({ should_compact: false, current_tokens: 0, urgency: 'none' })),
compact: vi.fn(() => Promise.resolve({ compacted_messages: [] })),
},
memory: {
search: vi.fn(() => Promise.resolve([])),
},
identity: {
buildPrompt: vi.fn(() => Promise.resolve('')),
},
reflection: {
recordConversation: vi.fn(() => Promise.resolve()),
shouldReflect: vi.fn(() => Promise.resolve(false)),
reflect: vi.fn(() => Promise.resolve()),
},
},
}));
vi.mock('../src/lib/memory-extractor', () => ({
getMemoryExtractor: vi.fn(() => ({
extractFromConversation: vi.fn(() => Promise.resolve([])),
})),
}));
vi.mock('../src/lib/agent-swarm', () => ({
getAgentSwarm: vi.fn(() => ({
runAgent: vi.fn(),
})),
}));
vi.mock('../src/lib/speech-synth', () => ({
speechSynth: {
speak: vi.fn(() => Promise.resolve()),
},
}));
vi.mock('../src/lib/logger', () => ({
createLogger: () => ({
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
}),
}));
vi.mock('../src/store/chat/conversationStore', () => {
const state = {
currentAgent: { id: 'test-agent-1', name: 'Test Agent' },
sessionKey: 'test-session-1',
currentModel: 'default',
conversations: [],
currentConversationId: null,
agents: [{ id: 'test-agent-1', name: 'Test Agent' }],
};
return {
useConversationStore: {
getState: () => state,
setState: vi.fn((partial: Record<string, unknown>) => {
if (typeof partial === 'function') {
Object.assign(state, partial(state));
} else {
Object.assign(state, partial);
}
}),
subscribe: vi.fn(),
persist: {
hasHydrated: () => true,
},
...(Object.fromEntries(
Object.keys(state).map((k) => [k, vi.fn()])
)),
},
};
});
// --- 1. Skill Execution ---
describe('Skill execution (SEC2-P0-01)', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('should invoke skill_execute with real agentId/sessionId (not empty strings)', async () => {
const mockResult = {
success: true,
output: { text: 'Skill executed successfully' },
duration_ms: 150,
};
vi.mocked(invoke).mockResolvedValueOnce(mockResult);
// Dynamically import to apply mocks
const { KernelClient } = await import('../src/lib/kernel-client');
const { installSkillMethods } = await import('../src/lib/kernel-skills');
const client = new KernelClient();
installSkillMethods({ prototype: Object.getPrototypeOf(client) } as any);
const result = await (client as any).executeSkill('test-skill-1', { query: 'hello' });
expect(invoke).toHaveBeenCalledWith('skill_execute', expect.objectContaining({
id: 'test-skill-1',
context: expect.objectContaining({
agentId: expect.any(String),
sessionId: expect.any(String),
}),
input: { query: 'hello' },
}));
// Verify agentId is NOT empty string (the core P0 bug)
const callArgs = vi.mocked(invoke).mock.calls[0][1] as any;
expect(callArgs.context.agentId).not.toBe('');
expect(callArgs.context.sessionId).not.toBe('');
expect(result.success).toBe(true);
});
it('should not crash when skill_execute returns an error', async () => {
vi.mocked(invoke).mockRejectedValueOnce(new Error('Skill not found'));
const { KernelClient } = await import('../src/lib/kernel-client');
const { installSkillMethods } = await import('../src/lib/kernel-skills');
const client = new KernelClient();
installSkillMethods({ prototype: Object.getPrototypeOf(client) } as any);
await expect(
(client as any).executeSkill('nonexistent-skill')
).rejects.toThrow('Skill not found');
});
});
// --- 2. Hand Trigger Event ---
describe('Hand execution event (SEC2-P1-03)', () => {
it('should add hand message to chatStore when hand-execution-complete is received', () => {
const store = useChatStore.getState();
const initialCount = store.messages.length;
// Simulate the event handler logic from ChatArea.tsx
const payload = {
approvalId: 'approval-1',
handId: 'browser',
success: true,
error: null,
};
store.addMessage({
id: `hand_complete_${Date.now()}`,
role: 'hand',
content: `Hand ${payload.handId} 执行完成`,
timestamp: new Date(),
handName: payload.handId,
handStatus: 'completed',
handResult: payload,
});
const updated = useChatStore.getState();
expect(updated.messages.length).toBe(initialCount + 1);
const handMsg = updated.messages[updated.messages.length - 1];
expect(handMsg.role).toBe('hand');
expect(handMsg.handName).toBe('browser');
expect(handMsg.handStatus).toBe('completed');
});
it('should handle failed hand execution correctly', () => {
const store = useChatStore.getState();
const initialCount = store.messages.length;
store.addMessage({
id: `hand_complete_${Date.now()}`,
role: 'hand',
content: 'Hand researcher 执行失败: timeout exceeded',
timestamp: new Date(),
handName: 'researcher',
handStatus: 'failed',
handResult: { handId: 'researcher', success: false, error: 'timeout exceeded' },
});
const updated = useChatStore.getState();
const handMsg = updated.messages[updated.messages.length - 1];
expect(handMsg.handStatus).toBe('failed');
expect(handMsg.content).toContain('失败');
});
});
// --- 3. Message Sending ---
describe('Message sending flow', () => {
beforeEach(() => {
vi.clearAllMocks();
// Reset store state
useChatStore.setState({ messages: [], isStreaming: false });
});
it('should add user message to store when sending', () => {
const store = useChatStore.getState();
const initialCount = store.messages.length;
store.addMessage({
id: 'user-test-1',
role: 'user',
content: 'Hello, test message',
timestamp: new Date(),
});
const updated = useChatStore.getState();
expect(updated.messages.length).toBe(initialCount + 1);
const userMsg = updated.messages.find((m: Message) => m.id === 'user-test-1');
expect(userMsg).toBeDefined();
expect(userMsg!.role).toBe('user');
expect(userMsg!.content).toBe('Hello, test message');
});
it('should add assistant streaming message', () => {
const store = useChatStore.getState();
store.addMessage({
id: 'assistant-test-1',
role: 'assistant',
content: 'Streaming response...',
timestamp: new Date(),
streaming: true,
});
const updated = useChatStore.getState();
const assistantMsg = updated.messages.find((m: Message) => m.id === 'assistant-test-1');
expect(assistantMsg).toBeDefined();
expect(assistantMsg!.streaming).toBe(true);
});
it('should handle tool messages with toolName and toolOutput', () => {
const store = useChatStore.getState();
store.addMessage({
id: 'tool-test-1',
role: 'tool',
content: 'Tool executed',
timestamp: new Date(),
toolName: 'web_search',
toolInput: '{"query": "test"}',
toolOutput: '{"results": []}',
});
const updated = useChatStore.getState();
const toolMsg = updated.messages.find((m: Message) => m.id === 'tool-test-1');
expect(toolMsg).toBeDefined();
expect(toolMsg!.toolName).toBe('web_search');
expect(toolMsg!.toolOutput).toBeDefined();
});
});
// --- 4. Config Sync ---
describe('Config sync (SaaS to Store)', () => {
it('should invoke saas-client with correct /api/v1 prefix for templates', async () => {
vi.mocked(invoke).mockResolvedValueOnce([]);
const { KernelClient } = await import('../src/lib/kernel-client');
const { installHandMethods } = await import('../src/lib/kernel-hands');
const client = new KernelClient();
installHandMethods({ prototype: Object.getPrototypeOf(client) } as any);
// Verify the client was created without crash
expect(client).toBeDefined();
});
it('should handle store update cycle correctly', () => {
// Simulate a config sync: external data arrives -> store updates
useChatStore.setState({ isStreaming: false });
expect(useChatStore.getState().isStreaming).toBe(false);
useChatStore.setState({ isStreaming: true });
expect(useChatStore.getState().isStreaming).toBe(true);
useChatStore.setState({ isStreaming: false });
expect(useChatStore.getState().isStreaming).toBe(false);
});
});