mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-08-04 14:02:22 +00:00
* feat(protocol): extract shared protocol package from agent-core - add `@moonshot-ai/protocol` package with REST/WS schemas, envelopes, error codes, event types, and display schemas\n- migrate agent-core `events.ts` and `display/schemas.ts` to re-export from protocol - add centralized `onUnexpectedError` handler for safe emitter listener callbacks - reject forkSession when source session has an active running turn - add protocol schema tests and unexpectedError handler tests
193 lines
6.1 KiB
TypeScript
193 lines
6.1 KiB
TypeScript
import { writeFile } from 'node:fs/promises';
|
|
import { join } from 'node:path';
|
|
|
|
import type * as KosongModule from '@moonshot-ai/kosong';
|
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
import { createKimiHarness, type KimiError, type Event } from '#/index';
|
|
|
|
import { makeTempDir, removeTempDirs, waitForSDKEvent } from './session-runtime-helpers';
|
|
import { TEST_IDENTITY } from './test-identity';
|
|
|
|
vi.mock('@moonshot-ai/kosong', async (importOriginal) => {
|
|
const actual = await importOriginal<typeof KosongModule>();
|
|
return {
|
|
...actual,
|
|
createProvider: () => ({
|
|
name: 'fake',
|
|
modelName: 'fake-model',
|
|
thinkingEffort: null,
|
|
async generate(
|
|
_systemPrompt: string,
|
|
_tools: unknown,
|
|
_history: unknown,
|
|
options?: { readonly signal?: AbortSignal },
|
|
) {
|
|
await waitForAbort(options?.signal);
|
|
throwAbortError();
|
|
},
|
|
withThinking() {
|
|
return this;
|
|
},
|
|
}),
|
|
};
|
|
});
|
|
|
|
const tempDirs: string[] = [];
|
|
|
|
afterEach(async () => {
|
|
await removeTempDirs(tempDirs);
|
|
});
|
|
|
|
describe('Session.cancel', () => {
|
|
it('cancels an active streaming turn and emits turn_ended(cancelled)', async () => {
|
|
const homeDir = await makeTempDir(tempDirs, 'kimi-sdk-cancel-home-');
|
|
const workDir = await makeTempDir(tempDirs, 'kimi-sdk-cancel-work-');
|
|
await writeFakeModelConfig(homeDir);
|
|
const harness = createKimiHarness({ homeDir, identity: TEST_IDENTITY });
|
|
|
|
try {
|
|
const session = await harness.createSession({ id: 'ses_cancel_active_turn', workDir });
|
|
const events: Event[] = [];
|
|
const unsubscribe = session.onEvent((event) => {
|
|
events.push(event);
|
|
});
|
|
const started = waitForSDKEvent(session, (event) => event.type === 'turn.started');
|
|
const ended = waitForSDKEvent(session, (event) => event.type === 'turn.ended');
|
|
|
|
await session.prompt('start a turn that will be cancelled');
|
|
const startedEvent = await started;
|
|
await session.cancel();
|
|
const endedEvent = await ended;
|
|
unsubscribe();
|
|
|
|
expect(startedEvent).toMatchObject({
|
|
type: 'turn.started',
|
|
sessionId: session.id,
|
|
});
|
|
expect(endedEvent).toMatchObject({
|
|
type: 'turn.ended',
|
|
sessionId: session.id,
|
|
turnId: startedEvent.type === 'turn.started' ? startedEvent.turnId : undefined,
|
|
reason: 'cancelled',
|
|
});
|
|
expect(events).toContainEqual(expect.objectContaining({ type: 'turn.started' }));
|
|
expect(events).toContainEqual(expect.objectContaining({ type: 'turn.ended' }));
|
|
} finally {
|
|
await harness.close();
|
|
}
|
|
});
|
|
|
|
it('rejects manual compaction on an empty session with compaction.unable', async () => {
|
|
const homeDir = await makeTempDir(tempDirs, 'kimi-sdk-cancel-compact-home-');
|
|
const workDir = await makeTempDir(tempDirs, 'kimi-sdk-cancel-compact-work-');
|
|
await writeFakeModelConfig(homeDir);
|
|
const harness = createKimiHarness({ homeDir, identity: TEST_IDENTITY });
|
|
|
|
try {
|
|
const session = await harness.createSession({ id: 'ses_cancel_compaction', workDir });
|
|
|
|
await expect(session.compact({ instruction: 'Keep the compact test pending.' })).rejects.toMatchObject({
|
|
name: 'KimiError',
|
|
code: 'compaction.unable',
|
|
} satisfies Partial<KimiError>);
|
|
} finally {
|
|
await harness.close();
|
|
}
|
|
});
|
|
|
|
it('rejects after the session is closed', async () => {
|
|
const homeDir = await makeTempDir(tempDirs, 'kimi-sdk-cancel-home-');
|
|
const workDir = await makeTempDir(tempDirs, 'kimi-sdk-cancel-work-');
|
|
const harness = createKimiHarness({ homeDir, identity: TEST_IDENTITY });
|
|
|
|
try {
|
|
const session = await harness.createSession({ id: 'ses_cancel_closed', workDir });
|
|
await session.close();
|
|
|
|
await expect(session.cancel()).rejects.toMatchObject({
|
|
name: 'KimiError',
|
|
code: 'session.closed',
|
|
} satisfies Partial<KimiError>);
|
|
await expect(session.cancelCompaction()).rejects.toMatchObject({
|
|
name: 'KimiError',
|
|
code: 'session.closed',
|
|
} satisfies Partial<KimiError>);
|
|
} finally {
|
|
await harness.close();
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('KimiHarness.forkSession', () => {
|
|
it('rejects while the source session has an active turn', async () => {
|
|
const homeDir = await makeTempDir(tempDirs, 'kimi-sdk-fork-active-home-');
|
|
const workDir = await makeTempDir(tempDirs, 'kimi-sdk-fork-active-work-');
|
|
await writeFakeModelConfig(homeDir);
|
|
const harness = createKimiHarness({ homeDir, identity: TEST_IDENTITY });
|
|
|
|
try {
|
|
const session = await harness.createSession({ id: 'ses_fork_active_turn', workDir });
|
|
const started = waitForSDKEvent(session, (event) => event.type === 'turn.started');
|
|
const ended = waitForSDKEvent(session, (event) => event.type === 'turn.ended');
|
|
|
|
await session.prompt('keep this turn active');
|
|
await started;
|
|
try {
|
|
await expect(
|
|
harness.forkSession({
|
|
id: session.id,
|
|
forkId: 'ses_fork_active_child',
|
|
}),
|
|
).rejects.toMatchObject({
|
|
name: 'KimiError',
|
|
code: 'session.fork_active_turn',
|
|
} satisfies Partial<KimiError>);
|
|
} finally {
|
|
await session.cancel().catch(() => undefined);
|
|
await ended.catch(() => undefined);
|
|
}
|
|
} finally {
|
|
await harness.close();
|
|
}
|
|
});
|
|
});
|
|
|
|
async function writeFakeModelConfig(homeDir: string): Promise<void> {
|
|
await writeFile(
|
|
join(homeDir, 'config.toml'),
|
|
`
|
|
default_model = "fake-model"
|
|
|
|
[providers.local]
|
|
type = "kimi"
|
|
base_url = "https://example.test/v1"
|
|
api_key = "sk-test"
|
|
|
|
[models.fake-model]
|
|
provider = "local"
|
|
model = "fake-model"
|
|
max_context_size = 1000
|
|
`,
|
|
'utf-8',
|
|
);
|
|
}
|
|
|
|
function waitForAbort(signal: AbortSignal | undefined): Promise<void> {
|
|
if (signal?.aborted === true) {
|
|
return Promise.resolve();
|
|
}
|
|
return new Promise((resolve) => {
|
|
signal?.addEventListener(
|
|
'abort',
|
|
() => {
|
|
resolve();
|
|
},
|
|
{ once: true },
|
|
);
|
|
});
|
|
}
|
|
|
|
function throwAbortError(): never {
|
|
throw new DOMException('The operation was aborted.', 'AbortError');
|
|
}
|