mirror of
https://github.com/wu736139669/hapi.git
synced 2026-10-14 20:26:05 +00:00
202 lines
6.3 KiB
TypeScript
202 lines
6.3 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
import { MessageQueue2 } from '@/utils/MessageQueue2';
|
|
import type { EnhancedMode } from './loop';
|
|
|
|
const harness = vi.hoisted(() => ({
|
|
notifications: [] as Array<{ method: string; params: unknown }>,
|
|
registerRequestCalls: [] as string[],
|
|
initializeCalls: [] as unknown[]
|
|
}));
|
|
|
|
vi.mock('./codexAppServerClient', () => {
|
|
class MockCodexAppServerClient {
|
|
private notificationHandler: ((method: string, params: unknown) => void) | null = null;
|
|
|
|
async connect(): Promise<void> {}
|
|
|
|
async initialize(params: unknown): Promise<{ protocolVersion: number }> {
|
|
harness.initializeCalls.push(params);
|
|
return { protocolVersion: 1 };
|
|
}
|
|
|
|
setNotificationHandler(handler: ((method: string, params: unknown) => void) | null): void {
|
|
this.notificationHandler = handler;
|
|
}
|
|
|
|
registerRequestHandler(method: string): void {
|
|
harness.registerRequestCalls.push(method);
|
|
}
|
|
|
|
async startThread(): Promise<{ thread: { id: string }; model: string }> {
|
|
return { thread: { id: 'thread-anonymous' }, model: 'gpt-5.4' };
|
|
}
|
|
|
|
async resumeThread(): Promise<{ thread: { id: string }; model: string }> {
|
|
return { thread: { id: 'thread-anonymous' }, model: 'gpt-5.4' };
|
|
}
|
|
|
|
async startTurn(): Promise<{ turn: Record<string, never> }> {
|
|
const started = { turn: {} };
|
|
harness.notifications.push({ method: 'turn/started', params: started });
|
|
this.notificationHandler?.('turn/started', started);
|
|
|
|
const completed = { status: 'Completed', turn: {} };
|
|
harness.notifications.push({ method: 'turn/completed', params: completed });
|
|
this.notificationHandler?.('turn/completed', completed);
|
|
|
|
return { turn: {} };
|
|
}
|
|
|
|
async interruptTurn(): Promise<Record<string, never>> {
|
|
return {};
|
|
}
|
|
|
|
async disconnect(): Promise<void> {}
|
|
}
|
|
|
|
return { CodexAppServerClient: MockCodexAppServerClient };
|
|
});
|
|
|
|
vi.mock('./utils/buildHapiMcpBridge', () => ({
|
|
buildHapiMcpBridge: async () => ({
|
|
server: {
|
|
stop: () => {}
|
|
},
|
|
mcpServers: {}
|
|
})
|
|
}));
|
|
|
|
import { codexRemoteLauncher } from './codexRemoteLauncher';
|
|
|
|
type FakeAgentState = {
|
|
requests: Record<string, unknown>;
|
|
completedRequests: Record<string, unknown>;
|
|
};
|
|
|
|
function createMode(): EnhancedMode {
|
|
return {
|
|
permissionMode: 'default',
|
|
collaborationMode: 'default'
|
|
};
|
|
}
|
|
|
|
function createSessionStub() {
|
|
const queue = new MessageQueue2<EnhancedMode>((mode) => JSON.stringify(mode));
|
|
queue.push('hello from launcher test', createMode());
|
|
queue.close();
|
|
|
|
const sessionEvents: Array<{ type: string; [key: string]: unknown }> = [];
|
|
const codexMessages: unknown[] = [];
|
|
const thinkingChanges: boolean[] = [];
|
|
const foundSessionIds: string[] = [];
|
|
let currentModel: string | null | undefined;
|
|
let agentState: FakeAgentState = {
|
|
requests: {},
|
|
completedRequests: {}
|
|
};
|
|
|
|
const rpcHandlers = new Map<string, (params: unknown) => unknown>();
|
|
const client = {
|
|
rpcHandlerManager: {
|
|
registerHandler(method: string, handler: (params: unknown) => unknown) {
|
|
rpcHandlers.set(method, handler);
|
|
}
|
|
},
|
|
updateAgentState(handler: (state: FakeAgentState) => FakeAgentState) {
|
|
agentState = handler(agentState);
|
|
},
|
|
sendAgentMessage(message: unknown) {
|
|
codexMessages.push(message);
|
|
},
|
|
sendUserMessage(_text: string) {},
|
|
sendSessionEvent(event: { type: string; [key: string]: unknown }) {
|
|
sessionEvents.push(event);
|
|
}
|
|
};
|
|
|
|
const session = {
|
|
path: '/tmp/hapi-update',
|
|
logPath: '/tmp/hapi-update/test.log',
|
|
client,
|
|
queue,
|
|
codexArgs: undefined,
|
|
codexCliOverrides: undefined,
|
|
sessionId: null as string | null,
|
|
thinking: false,
|
|
getPermissionMode() {
|
|
return 'default' as const;
|
|
},
|
|
setModel(nextModel: string | null) {
|
|
currentModel = nextModel;
|
|
},
|
|
getModel() {
|
|
return currentModel;
|
|
},
|
|
onThinkingChange(nextThinking: boolean) {
|
|
session.thinking = nextThinking;
|
|
thinkingChanges.push(nextThinking);
|
|
},
|
|
onSessionFound(id: string) {
|
|
session.sessionId = id;
|
|
foundSessionIds.push(id);
|
|
},
|
|
sendAgentMessage(message: unknown) {
|
|
client.sendAgentMessage(message);
|
|
},
|
|
sendSessionEvent(event: { type: string; [key: string]: unknown }) {
|
|
client.sendSessionEvent(event);
|
|
},
|
|
sendUserMessage(text: string) {
|
|
client.sendUserMessage(text);
|
|
}
|
|
};
|
|
|
|
return {
|
|
session,
|
|
sessionEvents,
|
|
codexMessages,
|
|
thinkingChanges,
|
|
foundSessionIds,
|
|
rpcHandlers,
|
|
getModel: () => currentModel,
|
|
getAgentState: () => agentState
|
|
};
|
|
}
|
|
|
|
describe('codexRemoteLauncher', () => {
|
|
afterEach(() => {
|
|
harness.notifications = [];
|
|
harness.registerRequestCalls = [];
|
|
harness.initializeCalls = [];
|
|
});
|
|
|
|
it('finishes a turn and emits ready when task lifecycle events omit turn_id', async () => {
|
|
const {
|
|
session,
|
|
sessionEvents,
|
|
thinkingChanges,
|
|
foundSessionIds,
|
|
getModel
|
|
} = createSessionStub();
|
|
|
|
const exitReason = await codexRemoteLauncher(session as never);
|
|
|
|
expect(exitReason).toBe('exit');
|
|
expect(foundSessionIds).toContain('thread-anonymous');
|
|
expect(getModel()).toBe('gpt-5.4');
|
|
expect(harness.initializeCalls).toEqual([{
|
|
clientInfo: {
|
|
name: 'hapi-codex-client',
|
|
version: '1.0.0'
|
|
},
|
|
capabilities: {
|
|
experimentalApi: true
|
|
}
|
|
}]);
|
|
expect(harness.notifications.map((entry) => entry.method)).toEqual(['turn/started', 'turn/completed']);
|
|
expect(sessionEvents.filter((event) => event.type === 'ready').length).toBeGreaterThanOrEqual(1);
|
|
expect(thinkingChanges).toContain(true);
|
|
expect(session.thinking).toBe(false);
|
|
});
|
|
});
|