kimi-code/packages/node-sdk/test/session-cancel.test.ts
2026-05-22 15:54:50 +08:00

171 lines
5.3 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 type { KimiError, 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 { KimiHarness } = await import('#/index');
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 = new KimiHarness({ 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('cancels an active manual compaction and emits compaction.cancelled', 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 = new KimiHarness({ homeDir, identity: TEST_IDENTITY });
try {
const session = await harness.createSession({ id: 'ses_cancel_compaction', workDir });
const started = waitForSDKEvent(session, (event) => event.type === 'compaction.started');
const cancelled = waitForSDKEvent(session, (event) => event.type === 'compaction.cancelled');
await session.compact({ instruction: 'Keep the compact test pending.' });
await expect(started).resolves.toMatchObject({
type: 'compaction.started',
sessionId: session.id,
trigger: 'manual',
});
await session.cancelCompaction();
await expect(cancelled).resolves.toMatchObject({
type: 'compaction.cancelled',
sessionId: session.id,
});
} 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 = new KimiHarness({ 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();
}
});
});
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');
}