kimi-code / packages /agent-core-v2 /test /session /sessionTitle /sessionTitleService.test.ts
SaylorTwift's picture
SaylorTwift HF Staff
Add files using upload-large-folder tool
f0634fb verified
Raw History Blame Contribute Delete
21.8 kB
import { afterEach, beforeEach, describe, expect, it, vi, type Mock } from 'vitest';
import { OAuthConnectionError, OAuthUnauthorizedError } from '@moonshot-ai/kimi-code-oauth';
import { DisposableStore, type IDisposable } from '#/_base/di/lifecycle';
import { type IAgentScopeHandle } from '#/_base/di/scope';
import { LifecycleScope } from '#/app/scopes';
import { createServices, type TestInstantiationService } from '#/_base/di/test';
import { Emitter } from '#/_base/event';
import { IOAuthService } from '#/app/auth/auth';
import { IEventService } from '#/app/event/event';
import type { Event2 } from '#/app/event/event2';
import { IHostRequestHeaders } from '#/llm-adapter/model/host-request-headers';
import {
IProviderService,
type OAuthRef,
type ProviderConfig,
} from '#/llm-adapter/provider/provider';
import { ISessionContext, makeSessionContext } from '#/session/sessionContext/sessionContext';
import {
IAgentLifecycleService,
MAIN_AGENT_ID,
} from '#/session/agentLifecycle/agentLifecycle';
import {
IAgentTitlePromptSource,
type TitleDigestExcerpt,
type TitleTurnExcerpt,
} from '#/session/sessionTitle/agentTitlePromptSource';
import { ISessionTitleService } from '#/session/sessionTitle/sessionTitle';
import { SessionTitleService } from '#/session/sessionTitle/sessionTitleService';
import {
ISessionMetadata,
type SessionMeta,
type SessionMetaPatch,
type SessionMetadataChangedEvent,
} from '#/session/sessionMetadata/sessionMetadata';
import { SessionMetaUpdated } from '#/session/sessionMetadata/sessionMetaEvents';
import { registerLogServices } from '../../_base/log/stubs';
import { stubProviderService } from '../../app/provider/stubs';
const SESSION_ID = 'sess-1';
const MANAGED_PROVIDER: ProviderConfig = {
type: 'kimi',
baseUrl: 'https://api.example.test/coding/v1',
oauth: { storage: 'file', key: 'kimi-code' },
};
class FakeEventService implements IEventService {
declare readonly _serviceBrand: undefined;
private readonly emitter = new Emitter<Event2>();
readonly onDidPublish = this.emitter.event;
readonly published: Event2[] = [];
publish(event: Event2): void {
this.published.push(event);
this.emitter.fire(event);
}
subscribe(handler: (event: Event2) => void): IDisposable {
return this.emitter.event(handler);
}
}
class FakeSessionMetadata implements ISessionMetadata {
declare readonly _serviceBrand: undefined;
readonly ready = Promise.resolve();
private readonly emitter = new Emitter<SessionMetadataChangedEvent>();
readonly onDidChangeMetadata = this.emitter.event;
meta: SessionMeta;
constructor() {
this.meta = {
id: SESSION_ID,
createdAt: 0,
updatedAt: 0,
archived: false,
};
}
read(): Promise<SessionMeta> {
return Promise.resolve(this.meta);
}
update(patch: SessionMetaPatch): Promise<void> {
this.meta = { ...this.meta, ...patch };
this.emitter.fire({ changed: Object.keys(patch) as (keyof SessionMeta)[] });
return Promise.resolve();
}
setTitle(title: string): Promise<void> {
return this.update({ title, titleKind: 'custom' });
}
async setGeneratedTitleIfUncustomized(
title: string,
opts?: { force?: boolean },
): Promise<boolean> {
if (opts?.force !== true && this.meta.titleKind === 'custom') return false;
await this.update({ title, titleKind: 'generated' });
return true;
}
setArchived(archived: boolean): Promise<void> {
return this.update({ archived });
}
registerAgent(): Promise<void> {
return Promise.resolve();
}
}
function createPendingFetch() {
let markStarted!: () => void;
let resolveResponse!: (response: Response) => void;
const started = new Promise<void>((resolve) => {
markStarted = resolve;
});
const response = new Promise<Response>((resolve) => {
resolveResponse = resolve;
});
return {
fetch: async () => {
markStarted();
return response;
},
started,
resolve: resolveResponse,
};
}
describe('SessionTitleService', () => {
let disposables: DisposableStore;
let ix: TestInstantiationService;
let events: FakeEventService;
let metadata: FakeSessionMetadata;
let providers: Record<string, ProviderConfig>;
let fetchMock: Mock<(url: string, init?: RequestInit) => Promise<Response>>;
let tokenError: Error | undefined;
let forceTokenError: Error | undefined;
let resolvedOAuthRefs: Array<OAuthRef | undefined>;
let titlePrompts: readonly string[];
let promptSourceImpl: (limit: number) => Promise<readonly string[]>;
let turnExcerpt: TitleTurnExcerpt;
let digestExcerpt: TitleDigestExcerpt;
let tokenCalls: boolean[];
beforeEach(() => {
tokenError = undefined;
forceTokenError = undefined;
resolvedOAuthRefs = [];
titlePrompts = [];
promptSourceImpl = async (limit) => titlePrompts.slice(0, limit);
turnExcerpt = {};
digestExcerpt = { turns: [] };
tokenCalls = [];
providers = { 'managed:kimi-code': MANAGED_PROVIDER };
metadata = new FakeSessionMetadata();
events = new FakeEventService();
fetchMock = vi.fn<(url: string, init?: RequestInit) => Promise<Response>>(
async () =>
new Response(JSON.stringify({ title: '็”Ÿๆˆ็š„ๆ ‡้ข˜' }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
}),
);
vi.stubGlobal('fetch', fetchMock);
disposables = new DisposableStore();
ix = createServices(disposables, {
base: [registerLogServices],
additionalServices: (reg) => {
reg.defineInstance(
ISessionContext,
makeSessionContext({
sessionId: SESSION_ID,
workspaceId: 'ws-1',
sessionDir: '/tmp/sess-1',
sessionScope: 'sessions/sess-1',
cwd: '/tmp',
}),
);
reg.defineInstance(ISessionMetadata, metadata);
const promptSource: IAgentTitlePromptSource = {
_serviceBrand: undefined,
firstUserPrompts: (limit) => promptSourceImpl(limit),
firstTurnExcerpt: async () => turnExcerpt,
digestExcerpt: async () => digestExcerpt,
};
const mainAgent: IAgentScopeHandle = {
id: MAIN_AGENT_ID,
kind: LifecycleScope.Agent,
accessor: { get: <T>() => promptSource as T },
dispose: () => undefined,
};
reg.definePartialInstance(IAgentLifecycleService, {
handleOf: () => mainAgent,
});
reg.defineInstance(IEventService, events);
reg.defineInstance(IProviderService, stubProviderService(providers));
reg.definePartialInstance(IOAuthService, {
resolveTokenProvider: (_provider, oauthRef) => {
resolvedOAuthRefs.push(oauthRef);
return {
getAccessToken: async (options) => {
tokenCalls.push(options?.force === true);
if (tokenError !== undefined) throw tokenError;
if (options?.force === true && forceTokenError !== undefined) {
throw forceTokenError;
}
return 'test-token';
},
};
},
});
reg.defineInstance(IHostRequestHeaders, {
headers: { 'User-Agent': 'test' },
thirdPartyHeaders: {},
});
reg.define(ISessionTitleService, SessionTitleService);
},
});
ix.get(ISessionTitleService);
});
afterEach(() => {
disposables.dispose();
vi.unstubAllGlobals();
vi.unstubAllEnvs();
});
it('replaces the easy title with the generated one', async () => {
titlePrompts = ['ๅธฎๆˆ‘็œ‹ไธ€ไธ‹่ฟ™ไธช Go ็š„ nil pointer ๆŠฅ้”™'];
const title = await ix.get(ISessionTitleService).generateTitle();
expect(title).toBe('็”Ÿๆˆ็š„ๆ ‡้ข˜');
expect(metadata.meta.title).toBe('็”Ÿๆˆ็š„ๆ ‡้ข˜');
expect(metadata.meta.titleKind).toBe('generated');
const [, init] = fetchMock.mock.calls[0]!;
expect(JSON.parse(init?.body as string)).toEqual({
method: 'chat_title',
params: { chat_content: 'user: ๅธฎๆˆ‘็œ‹ไธ€ไธ‹่ฟ™ไธช Go ็š„ nil pointer ๆŠฅ้”™' },
});
expect(new Headers(init?.headers as Record<string, string>).get('authorization')).toBe(
'Bearer test-token',
);
const rebroadcast = events.published.find(
(event): event is SessionMetaUpdated =>
event.type === 'session.meta.updated' &&
(event as SessionMetaUpdated).payload.patch.title === '็”Ÿๆˆ็š„ๆ ‡้ข˜',
);
expect(rebroadcast).toBeDefined();
});
it('composes the title input from the recorded prompts in order', async () => {
titlePrompts = ['ๅ…ˆๅธฎๆˆ‘ๆญไธ€ไธช Vite ้กน็›ฎ', 'ๅŠ ไธŠ่ทฏ็”ฑ', '็Žฐๅœจ้…ไธ€ไธ‹ ESLint'];
await ix.get(ISessionTitleService).generateTitle();
const [, init] = fetchMock.mock.calls[0]!;
expect(JSON.parse(init?.body as string)).toEqual({
method: 'chat_title',
params: {
chat_content: 'user: ๅ…ˆๅธฎๆˆ‘ๆญไธ€ไธช Vite ้กน็›ฎ\nuser: ๅŠ ไธŠ่ทฏ็”ฑ\nuser: ็Žฐๅœจ้…ไธ€ไธ‹ ESLint',
},
});
});
it('truncates each prompt to the per-prompt budget, keeping the head', async () => {
titlePrompts = ['ๅพˆ้•ฟ็š„่พ“ๅ…ฅ'.repeat(400), '็ฌฌไบŒๆก'];
await ix.get(ISessionTitleService).generateTitle();
const [, init] = fetchMock.mock.calls[0]!;
const body = JSON.parse(init?.body as string) as { params: { chat_content: string } };
expect(body.params.chat_content).toBe(`user: ${'ๅพˆ้•ฟ็š„่พ“ๅ…ฅ'.repeat(80)}\nuser: ็ฌฌไบŒๆก`);
});
it('returns unavailable when only a slash activation updated lastPrompt', async () => {
await metadata.update({ lastPrompt: '/compact' });
await expect(ix.get(ISessionTitleService).generateTitle()).resolves.toBeUndefined();
expect(fetchMock).not.toHaveBeenCalled();
});
it('does nothing without a managed OAuth provider', async () => {
delete providers['managed:kimi-code'];
titlePrompts = ['hello'];
await expect(ix.get(ISessionTitleService).generateTitle()).resolves.toBeUndefined();
expect(fetchMock).not.toHaveBeenCalled();
});
it('never overwrites a custom title set while generation is in flight', async () => {
const pendingFetch = createPendingFetch();
fetchMock.mockImplementationOnce(pendingFetch.fetch);
titlePrompts = ['hello'];
const generation = ix.get(ISessionTitleService).generateTitle();
await pendingFetch.started;
await metadata.setTitle('user ๅ–็š„ๆ ‡้ข˜');
pendingFetch.resolve(
new Response(JSON.stringify({ title: '็”Ÿๆˆ็š„ๆ ‡้ข˜' }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
}),
);
await expect(generation).resolves.toBeUndefined();
expect(metadata.meta.title).toBe('user ๅ–็š„ๆ ‡้ข˜');
expect(metadata.meta.titleKind).toBe('custom');
});
it('skips generation when the current title was already generated', async () => {
await metadata.setGeneratedTitleIfUncustomized('ๅทฒ็”Ÿๆˆ็š„ๆ ‡้ข˜');
titlePrompts = ['hello'];
await expect(ix.get(ISessionTitleService).generateTitle()).resolves.toBeUndefined();
expect(fetchMock).not.toHaveBeenCalled();
expect(metadata.meta.title).toBe('ๅทฒ็”Ÿๆˆ็š„ๆ ‡้ข˜');
});
it('force regenerates an already-generated title', async () => {
await metadata.setGeneratedTitleIfUncustomized('ๅทฒ็”Ÿๆˆ็š„ๆ ‡้ข˜');
titlePrompts = ['hello'];
await expect(
ix.get(ISessionTitleService).generateTitle({ force: true }),
).resolves.toBe('็”Ÿๆˆ็š„ๆ ‡้ข˜');
expect(fetchMock).toHaveBeenCalledTimes(1);
expect(metadata.meta.title).toBe('็”Ÿๆˆ็š„ๆ ‡้ข˜');
expect(metadata.meta.titleKind).toBe('generated');
});
it('force overwrites a custom title and drops its custom marking', async () => {
await metadata.setTitle('user ๅ–็š„ๆ ‡้ข˜');
titlePrompts = ['hello'];
await expect(
ix.get(ISessionTitleService).generateTitle({ force: true }),
).resolves.toBe('็”Ÿๆˆ็š„ๆ ‡้ข˜');
expect(metadata.meta.title).toBe('็”Ÿๆˆ็š„ๆ ‡้ข˜');
expect(metadata.meta.titleKind).toBe('generated');
});
it('force still degrades when the backend request fails', async () => {
fetchMock.mockImplementationOnce(async () => new Response('', { status: 500 }));
await metadata.setTitle('user ๅ–็š„ๆ ‡้ข˜');
titlePrompts = ['hello'];
await expect(
ix.get(ISessionTitleService).generateTitle({ force: true }),
).resolves.toBeUndefined();
expect(metadata.meta.title).toBe('user ๅ–็š„ๆ ‡้ข˜');
expect(metadata.meta.titleKind).toBe('custom');
});
it('first_turn composes the opening prompt with the first reply, within budget', async () => {
turnExcerpt = { user: 'ๆœ€ๅˆ็š„้—ฎ้ข˜', assistant: '็ฌฌไธ€่ฝฎ็š„ๅ›ž็ญ”' };
await expect(
ix.get(ISessionTitleService).generateTitle({ source: 'first_turn' }),
).resolves.toBe('็”Ÿๆˆ็š„ๆ ‡้ข˜');
const [, init] = fetchMock.mock.calls[0]!;
expect(JSON.parse(init?.body as string)).toEqual({
method: 'chat_title',
params: { chat_content: 'user: ๆœ€ๅˆ็š„้—ฎ้ข˜\nassistant: ็ฌฌไธ€่ฝฎ็š„ๅ›ž็ญ”' },
});
});
it('first_turn is strict: no assistant reply yet means unavailable', async () => {
turnExcerpt = { user: 'ๅชๆœ‰้—ฎ้ข˜' };
await expect(
ix.get(ISessionTitleService).generateTitle({ source: 'first_turn' }),
).resolves.toBeUndefined();
expect(fetchMock).not.toHaveBeenCalled();
});
it('first_turn truncates each segment to its budget', async () => {
turnExcerpt = { user: '้—ฎ'.repeat(500), assistant: '็ญ”'.repeat(1000) };
await expect(
ix.get(ISessionTitleService).generateTitle({ source: 'first_turn' }),
).resolves.toBe('็”Ÿๆˆ็š„ๆ ‡้ข˜');
const [, init] = fetchMock.mock.calls[0]!;
const content = (JSON.parse(init?.body as string) as { params: { chat_content: string } })
.params.chat_content;
expect(content).toBe(`user: ${'้—ฎ'.repeat(400)}\nassistant: ${'็ญ”'.repeat(300)}`);
});
it('digest composes every turn as interleaved user/assistant lines', async () => {
digestExcerpt = {
turns: [
{ user: 'ๅผ€ๅœบ', assistant: 'ๅผ€ๅœบๅ›ž็ญ”' },
{ user: 'ๆœ€ๆ–ฐ่ฟฝ้—ฎ', assistant: 'ๅฝ“ๅ‰่ฟ›ๅฑ•' },
],
};
await expect(
ix.get(ISessionTitleService).generateTitle({ source: 'digest' }),
).resolves.toBe('็”Ÿๆˆ็š„ๆ ‡้ข˜');
let [, init] = fetchMock.mock.calls[0]!;
expect(JSON.parse(init?.body as string)).toEqual({
method: 'chat_title',
params: {
chat_content: 'user: ๅผ€ๅœบ\nassistant: ๅผ€ๅœบๅ›ž็ญ”\nuser: ๆœ€ๆ–ฐ่ฟฝ้—ฎ\nassistant: ๅฝ“ๅ‰่ฟ›ๅฑ•',
},
});
fetchMock.mockClear();
digestExcerpt = { turns: [{ user: 'ๅผ€ๅœบ', assistant: undefined }] };
await expect(
ix.get(ISessionTitleService).generateTitle({ force: true, source: 'digest' }),
).resolves.toBe('็”Ÿๆˆ็š„ๆ ‡้ข˜');
[, init] = fetchMock.mock.calls[0]!;
expect(JSON.parse(init?.body as string)).toEqual({
method: 'chat_title',
params: { chat_content: 'user: ๅผ€ๅœบ' },
});
});
it('digest truncates each segment to its budget', async () => {
digestExcerpt = {
turns: [{ user: '้—ฎ'.repeat(300), assistant: '็ญ”'.repeat(300) }],
};
await expect(
ix.get(ISessionTitleService).generateTitle({ source: 'digest' }),
).resolves.toBe('็”Ÿๆˆ็š„ๆ ‡้ข˜');
const [, init] = fetchMock.mock.calls[0]!;
const content = (JSON.parse(init?.body as string) as { params: { chat_content: string } })
.params.chat_content;
expect(content).toBe(`user: ${'้—ฎ'.repeat(200)}\nassistant: ${'็ญ”'.repeat(200)}`);
});
it('digest elides the middle turns when the input exceeds the total budget', async () => {
digestExcerpt = {
turns: Array.from({ length: 30 }, (_, i) => ({
user: `็ฌฌ${i}ไธช${'้—ฎ'.repeat(180)}`,
assistant: `็ฌฌ${i}ไธช${'็ญ”'.repeat(180)}`,
})),
};
await expect(
ix.get(ISessionTitleService).generateTitle({ source: 'digest' }),
).resolves.toBe('็”Ÿๆˆ็š„ๆ ‡้ข˜');
const [, init] = fetchMock.mock.calls[0]!;
const content = (JSON.parse(init?.body as string) as { params: { chat_content: string } })
.params.chat_content;
expect(content.length).toBeLessThanOrEqual(3000);
expect(content.startsWith('user: ็ฌฌ0ไธช')).toBe(true);
expect(content).toContain('\n...\n');
expect(content.split('\n...\n')[1]?.startsWith('user: ')).toBe(true);
expect(content.endsWith(`assistant: ็ฌฌ29ไธช${'็ญ”'.repeat(180)}`)).toBe(true);
});
it('digest is unavailable when the window yields no segments at all', async () => {
digestExcerpt = { turns: [] };
await expect(
ix.get(ISessionTitleService).generateTitle({ source: 'digest' }),
).resolves.toBeUndefined();
expect(fetchMock).not.toHaveBeenCalled();
});
it('keeps the current title when the backend request fails', async () => {
fetchMock.mockImplementationOnce(async () => new Response('', { status: 500 }));
titlePrompts = ['hello'];
await metadata.update({ title: 'hello', titleKind: 'replaceable' });
await expect(ix.get(ISessionTitleService).generateTitle()).resolves.toBeUndefined();
expect(metadata.meta.title).toBe('hello');
expect(tokenCalls).toEqual([false]);
});
it('retries once with a force-refreshed token on a 401', async () => {
fetchMock.mockImplementationOnce(async () => new Response('', { status: 401 }));
titlePrompts = ['hello'];
await expect(ix.get(ISessionTitleService).generateTitle()).resolves.toBe('็”Ÿๆˆ็š„ๆ ‡้ข˜');
expect(metadata.meta.title).toBe('็”Ÿๆˆ็š„ๆ ‡้ข˜');
expect(fetchMock).toHaveBeenCalledTimes(2);
expect(tokenCalls).toEqual([false, true]);
});
it('gives up when the 401 persists after the force refresh', async () => {
fetchMock.mockImplementation(async () => new Response('', { status: 401 }));
titlePrompts = ['hello'];
await expect(ix.get(ISessionTitleService).generateTitle()).resolves.toBeUndefined();
expect(metadata.meta.title).toBeUndefined();
expect(fetchMock).toHaveBeenCalledTimes(2);
expect(tokenCalls).toEqual([false, true]);
});
it('degrades when the force refresh after a 401 fails', async () => {
fetchMock.mockImplementationOnce(async () => new Response('', { status: 401 }));
forceTokenError = new OAuthUnauthorizedError('refresh rejected');
titlePrompts = ['hello'];
await expect(ix.get(ISessionTitleService).generateTitle()).resolves.toBeUndefined();
expect(metadata.meta.title).toBeUndefined();
expect(fetchMock).toHaveBeenCalledTimes(1);
expect(tokenCalls).toEqual([false, true]);
});
it('returns unavailable when the OAuth token is missing or revoked', async () => {
tokenError = new OAuthUnauthorizedError('re-login required');
titlePrompts = ['hello'];
const svc = ix.get(ISessionTitleService);
await expect(svc.generateTitle()).resolves.toBeUndefined();
expect(fetchMock).not.toHaveBeenCalled();
});
it('returns unavailable when OAuth token retrieval has an operational failure', async () => {
tokenError = new OAuthConnectionError('connection failed');
titlePrompts = ['hello'];
await expect(ix.get(ISessionTitleService).generateTitle()).resolves.toBeUndefined();
expect(fetchMock).not.toHaveBeenCalled();
});
it('propagates unexpected token provider failures', async () => {
tokenError = new Error('unexpected failure');
titlePrompts = ['hello'];
await expect(ix.get(ISessionTitleService).generateTitle()).rejects.toThrow(
'unexpected failure',
);
expect(fetchMock).not.toHaveBeenCalled();
});
it('includes environment custom headers', async () => {
vi.stubEnv('KIMI_CODE_CUSTOM_HEADERS', 'X-Proxy-Header: from-env\n');
titlePrompts = ['hello'];
await ix.get(ISessionTitleService).generateTitle();
const [, init] = fetchMock.mock.calls[0]!;
const headers = new Headers(init?.headers as Record<string, string>);
expect(headers.get('x-proxy-header')).toBe('from-env');
expect(headers.get('user-agent')).toBe('test');
});
it('pairs the environment endpoint with its credential slot when it overrides persisted config', async () => {
vi.stubEnv('KIMI_CODE_BASE_URL', 'https://api.env.example.test/coding/v1');
vi.stubEnv('KIMI_CODE_OAUTH_HOST', 'https://auth.env.example.test');
titlePrompts = ['hello'];
await ix.get(ISessionTitleService).generateTitle();
expect(fetchMock.mock.calls[0]?.[0]).toBe('https://api.env.example.test/coding/v1/tools');
expect(resolvedOAuthRefs[0]).toMatchObject({
storage: 'file',
oauthHost: 'https://auth.env.example.test',
});
expect(resolvedOAuthRefs[0]?.key).not.toBe(MANAGED_PROVIDER.oauth?.key);
});
it('shares an in-flight generation between concurrent requests', async () => {
const pendingFetch = createPendingFetch();
fetchMock.mockImplementationOnce(pendingFetch.fetch);
titlePrompts = ['hello'];
const first = ix.get(ISessionTitleService).generateTitle();
const second = ix.get(ISessionTitleService).generateTitle();
await pendingFetch.started;
pendingFetch.resolve(
new Response(JSON.stringify({ title: '็”Ÿๆˆ็š„ๆ ‡้ข˜' }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
}),
);
await expect(first).resolves.toBe('็”Ÿๆˆ็š„ๆ ‡้ข˜');
await expect(second).resolves.toBe('็”Ÿๆˆ็š„ๆ ‡้ข˜');
expect(fetchMock).toHaveBeenCalledTimes(1);
});
it('returns unavailable without calling the backend when no prompt was seen', async () => {
await expect(ix.get(ISessionTitleService).generateTitle()).resolves.toBeUndefined();
expect(fetchMock).not.toHaveBeenCalled();
});
});