mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-08-12 18:27:39 +00:00
161 lines
5.3 KiB
TypeScript
161 lines
5.3 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest';
|
|
import type { CoreAPI, RPCMethods } from '@moonshot-ai/agent-core';
|
|
|
|
import { SDKRpcClientBase } from '../src/rpc';
|
|
import { Session } from '../src/session';
|
|
|
|
class CapturingRpc extends SDKRpcClientBase {
|
|
readonly promptCalls: unknown[] = [];
|
|
readonly enterPlanCalls: unknown[] = [];
|
|
readonly cancelPlanCalls: unknown[] = [];
|
|
readonly getPlanCalls: unknown[] = [];
|
|
readonly clearPlanCalls: unknown[] = [];
|
|
readonly setModelCalls: unknown[] = [];
|
|
private getRpcDelay: Promise<void> | undefined;
|
|
private getRpcCallCount = 0;
|
|
private readonly getRpcWaiters = new Set<() => void>();
|
|
|
|
delayGetRpcUntil(promise: Promise<void>): void {
|
|
this.getRpcDelay = promise;
|
|
}
|
|
|
|
waitForGetRpcCalls(count: number): Promise<void> {
|
|
if (this.getRpcCallCount >= count) return Promise.resolve();
|
|
return new Promise<void>((resolve) => {
|
|
const check = () => {
|
|
if (this.getRpcCallCount < count) return;
|
|
this.getRpcWaiters.delete(check);
|
|
resolve();
|
|
};
|
|
this.getRpcWaiters.add(check);
|
|
});
|
|
}
|
|
|
|
protected async getRpc(): Promise<RPCMethods<CoreAPI>> {
|
|
this.getRpcCallCount += 1;
|
|
for (const waiter of this.getRpcWaiters) waiter();
|
|
if (this.getRpcDelay !== undefined) await this.getRpcDelay;
|
|
return {
|
|
prompt: async (input: unknown) => {
|
|
this.promptCalls.push(input);
|
|
},
|
|
setModel: async (input: unknown) => {
|
|
this.setModelCalls.push(input);
|
|
return { model: 'captured-model' };
|
|
},
|
|
enterPlan: async (input: unknown) => {
|
|
this.enterPlanCalls.push(input);
|
|
},
|
|
cancelPlan: async (input: unknown) => {
|
|
this.cancelPlanCalls.push(input);
|
|
},
|
|
getPlan: async (input: unknown) => {
|
|
this.getPlanCalls.push(input);
|
|
return null;
|
|
},
|
|
clearPlan: async (input: unknown) => {
|
|
this.clearPlanCalls.push(input);
|
|
},
|
|
} as unknown as RPCMethods<CoreAPI>;
|
|
}
|
|
}
|
|
|
|
describe('Session.prompt input normalization', () => {
|
|
it('passes multimodal prompt parts through to the core RPC client', async () => {
|
|
const prompt = vi.fn(async () => {});
|
|
const session = new Session({
|
|
id: 'ses_multimodal_prompt',
|
|
workDir: '/tmp/work',
|
|
rpc: { prompt } as unknown as SDKRpcClientBase,
|
|
});
|
|
const input = [
|
|
{ type: 'text', text: 'describe these' },
|
|
{ type: 'image_url', imageUrl: { url: 'data:image/png;base64,AAAA' } },
|
|
{ type: 'video_url', videoUrl: { url: 'ms://file-123', id: 'file-123' } },
|
|
] as const;
|
|
|
|
await session.prompt(input);
|
|
|
|
expect(prompt).toHaveBeenCalledWith({
|
|
sessionId: 'ses_multimodal_prompt',
|
|
input,
|
|
});
|
|
});
|
|
|
|
it('starts btw and returns the forked agent id', async () => {
|
|
const startBtw = vi.fn(async () => 'agent-btw');
|
|
const session = new Session({
|
|
id: 'ses_btw_start',
|
|
workDir: '/tmp/work',
|
|
rpc: { startBtw } as unknown as SDKRpcClientBase,
|
|
});
|
|
|
|
await expect(session.startBtw()).resolves.toBe('agent-btw');
|
|
expect(startBtw).toHaveBeenCalledWith({
|
|
sessionId: 'ses_btw_start',
|
|
});
|
|
});
|
|
|
|
it('scopes interactive agent id across awaited session operations', async () => {
|
|
const rpc = new CapturingRpc();
|
|
const session = new Session({
|
|
id: 'ses_scoped_agent',
|
|
workDir: '/tmp/work',
|
|
rpc,
|
|
});
|
|
|
|
await rpc.withInteractiveAgent('agent-btw', async () => {
|
|
await Promise.resolve();
|
|
await session.prompt('side question');
|
|
await session.setPlanMode(true);
|
|
await session.getPlan();
|
|
await session.clearPlan();
|
|
await session.setPlanMode(false);
|
|
expect(rpc.interactiveAgentId).toBe('agent-btw');
|
|
});
|
|
|
|
expect(rpc.interactiveAgentId).toBe('main');
|
|
expect(rpc.promptCalls).toEqual([
|
|
{
|
|
sessionId: 'ses_scoped_agent',
|
|
agentId: 'agent-btw',
|
|
input: [{ type: 'text', text: 'side question' }],
|
|
},
|
|
]);
|
|
expect(rpc.enterPlanCalls).toEqual([{ sessionId: 'ses_scoped_agent', agentId: 'agent-btw' }]);
|
|
expect(rpc.getPlanCalls).toEqual([{ sessionId: 'ses_scoped_agent', agentId: 'agent-btw' }]);
|
|
expect(rpc.clearPlanCalls).toEqual([{ sessionId: 'ses_scoped_agent', agentId: 'agent-btw' }]);
|
|
expect(rpc.cancelPlanCalls).toEqual([{ sessionId: 'ses_scoped_agent', agentId: 'agent-btw' }]);
|
|
});
|
|
|
|
it('isolates overlapping interactive agent scopes while RPC resolution is pending', async () => {
|
|
let releaseRpc!: () => void;
|
|
const getRpcDelay = new Promise<void>((resolve) => {
|
|
releaseRpc = resolve;
|
|
});
|
|
const rpc = new CapturingRpc();
|
|
rpc.delayGetRpcUntil(getRpcDelay);
|
|
const session = new Session({
|
|
id: 'ses_overlapping_agents',
|
|
workDir: '/tmp/work',
|
|
rpc,
|
|
});
|
|
|
|
const first = rpc.withInteractiveAgent('agent-a', () => session.setModel('model-a'));
|
|
const second = rpc.withInteractiveAgent('agent-b', () => session.setModel('model-b'));
|
|
await rpc.waitForGetRpcCalls(2);
|
|
|
|
expect(rpc.setModelCalls).toEqual([]);
|
|
releaseRpc();
|
|
await Promise.all([first, second]);
|
|
|
|
expect(rpc.interactiveAgentId).toBe('main');
|
|
expect(rpc.setModelCalls).toHaveLength(2);
|
|
expect(rpc.setModelCalls).toEqual(
|
|
expect.arrayContaining([
|
|
{ sessionId: 'ses_overlapping_agents', agentId: 'agent-a', model: 'model-a' },
|
|
{ sessionId: 'ses_overlapping_agents', agentId: 'agent-b', model: 'model-b' },
|
|
]),
|
|
);
|
|
});
|
|
});
|