mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-07-09 17:29:12 +00:00
chore: fix lint issues across packages
- remove unused imports and variables - add void to floating promises - drop redundant return-await - bind unbound method references - replace == null with strict null/undefined checks - add explicit sort comparators and Array.from helpers - rename background_tasks capability to tasks - migrate event sink to event bus in tests
This commit is contained in:
parent
5b57a3297a
commit
79ffb7b347
98 changed files with 446 additions and 336 deletions
|
|
@ -202,7 +202,7 @@ function describeApproval(display: ToolInputDisplay, action: string): string {
|
|||
return `search: ${display.query ?? ''}`.trim();
|
||||
case 'todo_list':
|
||||
return `update todo list (${String(display.items?.length ?? 0)} items)`;
|
||||
case 'background_task':
|
||||
case 'task':
|
||||
return `${display.status ?? 'background'} task ${display.task_id ?? ''}: ${
|
||||
display.description ?? ''
|
||||
}`.trim();
|
||||
|
|
@ -334,7 +334,7 @@ function adaptDisplay(display: ToolInputDisplay): DisplayBlock[] {
|
|||
return [];
|
||||
case 'todo_list':
|
||||
return [];
|
||||
case 'background_task':
|
||||
case 'task':
|
||||
return [];
|
||||
default:
|
||||
return [];
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
* (default) rewrite all consumer files (src + test) EXCEPT src/index.ts
|
||||
* --only=<reldir> limit consumer rewriting to one barrel, e.g. app/event
|
||||
* --entry regenerate src/index.ts only (no consumer rewriting)
|
||||
* --delete-barrels delete every domain barrel (src/**/index.ts except entry)
|
||||
* --delete-barrels delete every domain barrel (per-domain src index.ts except entry)
|
||||
* --list-registers print the top-level register* files (coverage set)
|
||||
* --verify-coverage exit non-zero if any register file is unreachable from entry
|
||||
* --dry-run report planned edits without writing
|
||||
|
|
@ -65,7 +65,7 @@ function resolveName(barrel, name) {
|
|||
break;
|
||||
}
|
||||
}
|
||||
if (leafName == null) leafName = (sym && sym.getName()) || name;
|
||||
if (leafName === null) leafName = (sym && sym.getName()) || name;
|
||||
return { leafFile: leaf.getFilePath(), leafName };
|
||||
}
|
||||
|
||||
|
|
@ -118,7 +118,7 @@ function allLeavesUnderDir(dirAbs) {
|
|||
}
|
||||
};
|
||||
walk(dirAbs);
|
||||
return out.sort();
|
||||
return out.sort((a, b) => a.localeCompare(b));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -128,7 +128,7 @@ function rewriteConsumerFile(sf, onlyBarrelPath) {
|
|||
const report = { imports: 0, exports: 0, manuals: [], sideEffects: 0 };
|
||||
|
||||
// Imports.
|
||||
for (const decl of [...sf.getImportDeclarations()]) {
|
||||
for (const decl of sf.getImportDeclarations()) {
|
||||
const barrel = barrelOfDecl(decl);
|
||||
if (!barrel) continue;
|
||||
if (onlyBarrelPath && barrel.getFilePath() !== onlyBarrelPath) continue;
|
||||
|
|
@ -187,7 +187,7 @@ function rewriteConsumerFile(sf, onlyBarrelPath) {
|
|||
}
|
||||
|
||||
// Exports.
|
||||
for (const decl of [...sf.getExportDeclarations()]) {
|
||||
for (const decl of sf.getExportDeclarations()) {
|
||||
const barrel = barrelOfDecl(decl);
|
||||
if (!barrel) continue;
|
||||
if (onlyBarrelPath && barrel.getFilePath() !== onlyBarrelPath) continue;
|
||||
|
|
|
|||
|
|
@ -19,8 +19,6 @@ import { fileURLToPath } from 'node:url';
|
|||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const SRC_ROOT = join(__dirname, '..', 'src');
|
||||
|
||||
const SCOPE_OF = ['App', 'Session', 'Agent'];
|
||||
|
||||
const SCOPE_DIRS = new Set(['app', 'session', 'agent']);
|
||||
|
||||
/** Resolve a `src/`-relative file path to its domain, skipping the scope tier. */
|
||||
|
|
|
|||
|
|
@ -1039,7 +1039,7 @@ export function summarize(graph: Graph): string {
|
|||
const byKind = new Map<string, number>();
|
||||
for (const e of graph.edges) byKind.set(e.kind, (byKind.get(e.kind) ?? 0) + 1);
|
||||
const kindSummary = [...byKind.entries()]
|
||||
.sort()
|
||||
.sort((a, b) => a[0].localeCompare(b[0]))
|
||||
.map(([k, n]) => `${k}=${n}`)
|
||||
.join(' ');
|
||||
return `services=${graph.services.length} edges=${graph.edges.length} ${kindSummary}`;
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ export function App(): JSX.Element {
|
|||
const queryParams = useMemo(() => readQueryParams(window.location.search), []);
|
||||
|
||||
const domains = useMemo(
|
||||
() => [...new Set(graph.services.map((s) => s.domain))].sort(),
|
||||
() => [...new Set(graph.services.map((s) => s.domain))].sort((a, b) => a.localeCompare(b)),
|
||||
[],
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ try {
|
|||
});
|
||||
} catch (err) {
|
||||
const code = err && typeof err === 'object' && 'status' in err ? err.status : 'unknown';
|
||||
log(`tsc exited ${code} (non-fatal; declarations are still emitted)`);
|
||||
log(`tsc exited ${String(code)} (non-fatal; declarations are still emitted)`);
|
||||
}
|
||||
|
||||
// 2. Detect impl files + registered class names (AST only).
|
||||
|
|
|
|||
|
|
@ -138,7 +138,7 @@ export class AsyncEmitter<T extends IWaitUntil> extends Emitter<T> {
|
|||
continue;
|
||||
}
|
||||
|
||||
Object.freeze(thenables);
|
||||
void Object.freeze(thenables);
|
||||
const settled = await Promise.allSettled(thenables);
|
||||
for (const result of settled) {
|
||||
if (result.status === 'rejected') {
|
||||
|
|
|
|||
|
|
@ -575,15 +575,6 @@ function compactionCancelledReason(active: ActiveCompaction | null): Error {
|
|||
return error;
|
||||
}
|
||||
|
||||
function isTodoItem(value: unknown): value is TodoItem {
|
||||
if (value === null || typeof value !== 'object') return false;
|
||||
const item = value as { title?: unknown; status?: unknown };
|
||||
return (
|
||||
typeof item.title === 'string' &&
|
||||
(item.status === 'pending' || item.status === 'in_progress' || item.status === 'done')
|
||||
);
|
||||
}
|
||||
|
||||
// Construct eagerly (not delayed): the service registers turn and loop hooks
|
||||
// (onLaunched / beforeStep / afterStep / onError) that drive auto
|
||||
// compaction. With delayed instantiation the eager `accessor.get(IAgentFullCompactionService)`
|
||||
|
|
|
|||
|
|
@ -112,7 +112,7 @@ export class AgentLLMRequesterService implements IAgentLLMRequesterService {
|
|||
signal?: AbortSignal,
|
||||
): Promise<LLMRequestFinish> {
|
||||
signal?.throwIfAborted();
|
||||
return await this.requestWithRetry(overrides, onPart, signal);
|
||||
return this.requestWithRetry(overrides, onPart, signal);
|
||||
}
|
||||
|
||||
private async requestWithRetry(
|
||||
|
|
@ -170,7 +170,7 @@ export class AgentLLMRequesterService implements IAgentLLMRequesterService {
|
|||
overrides,
|
||||
attempt === 1 ? undefined : { attempt: `${String(attempt)}/${String(maxAttempts)}` },
|
||||
);
|
||||
return await this.runRequest(request, onPart, signal);
|
||||
return this.runRequest(request, onPart, signal);
|
||||
}
|
||||
|
||||
private logRequestFailure(
|
||||
|
|
@ -286,7 +286,7 @@ export class AgentLLMRequesterService implements IAgentLLMRequesterService {
|
|||
model: request.model.name,
|
||||
...request.logFields,
|
||||
});
|
||||
return await run(true);
|
||||
return run(true);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -300,7 +300,7 @@ export class AgentRPCService implements IAgentRPCService {
|
|||
}
|
||||
|
||||
async activateSkill(payload: ActivateSkillPayload): Promise<void> {
|
||||
this.skills.activate(payload);
|
||||
void this.skills.activate(payload);
|
||||
await this.updatePromptMetadata(promptMetadataTextFromSkill(payload));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -467,7 +467,9 @@ export class AgentTaskService extends Disposable implements IAgentTaskService {
|
|||
entry.foregroundSignalCleanup = undefined;
|
||||
this.applyDetachTimeout(entry);
|
||||
try {
|
||||
const onDetach = entry.onDetachFn ?? entry.task?.onDetach;
|
||||
const onDetach =
|
||||
entry.onDetachFn ??
|
||||
(entry.task === undefined ? undefined : entry.task.onDetach?.bind(entry.task));
|
||||
onDetach?.();
|
||||
} catch {
|
||||
/* detach has already succeeded; hooks must not make RPC fail */
|
||||
|
|
@ -546,7 +548,9 @@ export class AgentTaskService extends Disposable implements IAgentTaskService {
|
|||
|
||||
if (!graceful) {
|
||||
try {
|
||||
const forceStop = entry.forceStopFn ?? entry.task?.forceStop;
|
||||
const forceStop =
|
||||
entry.forceStopFn ??
|
||||
(entry.task === undefined ? undefined : entry.task.forceStop?.bind(entry.task));
|
||||
await forceStop?.();
|
||||
} catch {
|
||||
/* best effort */
|
||||
|
|
|
|||
|
|
@ -176,7 +176,7 @@ export class AgentWireRecordService extends Disposable implements IAgentWireReco
|
|||
restoredRecords !== undefined &&
|
||||
this.log !== undefined
|
||||
) {
|
||||
this.log.rewrite(this.wireScope, WIRE_RECORD_FILENAME, restoredRecords);
|
||||
void this.log.rewrite(this.wireScope, WIRE_RECORD_FILENAME, restoredRecords);
|
||||
await this.log.flush();
|
||||
}
|
||||
if (completed) {
|
||||
|
|
|
|||
|
|
@ -20,9 +20,6 @@ import { ENV_MODEL_PROVIDER_KEY } from '#/app/provider/provider';
|
|||
/** Reserved key for the env-driven synthetic model alias. */
|
||||
export const ENV_MODEL_ALIAS_KEY = '__kimi_env_model__';
|
||||
|
||||
const ALLOWED_TYPES = ['kimi', 'anthropic', 'openai'] as const;
|
||||
type EnvProviderType = (typeof ALLOWED_TYPES)[number];
|
||||
|
||||
/** Default context window (256K) used when KIMI_MODEL_MAX_CONTEXT_SIZE is unset. */
|
||||
const DEFAULT_MAX_CONTEXT_SIZE = 262144;
|
||||
|
||||
|
|
|
|||
|
|
@ -212,9 +212,9 @@ export class ModelImpl implements Model {
|
|||
`Model "${this.id}" (protocol=${this.protocol}) does not support video upload`,
|
||||
);
|
||||
}
|
||||
const uploadVideo = provider.uploadVideo;
|
||||
const uploadVideo = provider.uploadVideo.bind(provider);
|
||||
return this.runWithAuthRefresh((auth) =>
|
||||
uploadVideo.call(provider, input, { signal: options?.signal, auth }),
|
||||
uploadVideo(input, { signal: options?.signal, auth }),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -43,7 +43,6 @@ import {
|
|||
type ProviderCredentialState,
|
||||
type RefreshProviderModelsOptions,
|
||||
IModelCatalogService,
|
||||
modelIdsForProvider,
|
||||
toProtocolModel,
|
||||
toProtocolProvider,
|
||||
} from './modelCatalog';
|
||||
|
|
@ -186,8 +185,8 @@ export class ModelCatalogService implements IModelCatalogService {
|
|||
if (patch.defaultModel !== undefined) {
|
||||
await this.config.set(DEFAULT_MODEL_SECTION, patch.defaultModel);
|
||||
}
|
||||
if (patch.defaultThinking !== undefined) {
|
||||
await this.config.set(DEFAULT_THINKING_SECTION, patch.defaultThinking);
|
||||
if (patch['defaultThinking'] !== undefined) {
|
||||
await this.config.set(DEFAULT_THINKING_SECTION, patch['defaultThinking']);
|
||||
}
|
||||
return this.readUserConfigShape();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { HttpFetchError, type UrlFetcher, type UrlFetchResult } from '../tools/fetch-url-types';
|
||||
|
||||
export interface BearerTokenProvider {
|
||||
interface BearerTokenProvider {
|
||||
getAccessToken(options?: { readonly force?: boolean | undefined }): Promise<string>;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ export interface ComparisonOp {
|
|||
}
|
||||
|
||||
export type QueryFilter = {
|
||||
readonly [field: string]: unknown | ComparisonOp;
|
||||
readonly [field: string]: unknown;
|
||||
};
|
||||
|
||||
export interface IQuery<T> {
|
||||
|
|
|
|||
|
|
@ -759,7 +759,7 @@ function parseRgJsonOutput(
|
|||
}
|
||||
}
|
||||
|
||||
for (const p of [...fileBuf.keys()]) {
|
||||
for (const p of fileBuf.keys()) {
|
||||
finalize(p);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -294,7 +294,7 @@ export class WireService extends Disposable implements IWireService {
|
|||
});
|
||||
return;
|
||||
}
|
||||
const dehydrate = model.blobs.dehydrate;
|
||||
const dehydrate = model.blobs.dehydrate.bind(model.blobs);
|
||||
const transform: PartsTransformer = (parts) =>
|
||||
this.blobService!.offloadParts(
|
||||
parts as readonly ContentPart[],
|
||||
|
|
|
|||
|
|
@ -182,7 +182,6 @@ describe('AgentLifecycleService', () => {
|
|||
ix.stub(ILogService, noopLog);
|
||||
ix.stub(IAgentPluginService, {
|
||||
_serviceBrand: undefined,
|
||||
appendFreshSessionStartReminder: async () => {},
|
||||
});
|
||||
ix.stub(IAgentToolRegistryService, {
|
||||
_serviceBrand: undefined,
|
||||
|
|
|
|||
|
|
@ -11,7 +11,8 @@ import { describe, expect, it, vi } from 'vitest';
|
|||
|
||||
import type { ExecutableToolContext, ExecutableToolResult, ToolExecution } from '#/agent/tool/toolContract';
|
||||
import { LocalFetchURLProvider } from '#/app/web/providers/local-fetch-url';
|
||||
import { FetchURLTool, type UrlFetcher, type UrlFetchResult } from '#/app/web/tools/fetch-url';
|
||||
import { FetchURLTool } from '#/app/web/tools/fetch-url';
|
||||
import type { UrlFetcher, UrlFetchResult } from '#/app/web/tools/fetch-url-types';
|
||||
|
||||
function isPromiseLike(value: ToolExecution | Promise<ToolExecution>): value is Promise<ToolExecution> {
|
||||
return typeof (value as Promise<ToolExecution>).then === 'function';
|
||||
|
|
@ -70,7 +71,10 @@ describe('FetchURLTool abort signal', () => {
|
|||
const result = await execute(tool, 'https://example.com', controller.signal);
|
||||
|
||||
expect(result.isError).toBe(true);
|
||||
expect(String(result.output)).toContain('boom');
|
||||
if (typeof result.output !== 'string') {
|
||||
throw new Error('expected string error output');
|
||||
}
|
||||
expect(result.output).toContain('boom');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
* storage is exercised.
|
||||
*/
|
||||
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi, type Mock } from 'vitest';
|
||||
import { resolveKimiCodeRuntimeAuth } from '@moonshot-ai/kimi-code-oauth';
|
||||
|
||||
import { DisposableStore } from '#/_base/di/lifecycle';
|
||||
|
|
@ -42,7 +42,7 @@ const deviceAuth = {
|
|||
const flush = (): Promise<void> => new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
interface FakeToolkit {
|
||||
readonly login: ReturnType<typeof vi.fn>;
|
||||
readonly login: Mock<(...args: any[]) => any>;
|
||||
readonly logout: ReturnType<typeof vi.fn>;
|
||||
readonly getCachedAccessToken: ReturnType<typeof vi.fn>;
|
||||
readonly tokenProvider: ReturnType<typeof vi.fn>;
|
||||
|
|
@ -109,7 +109,7 @@ describe('OAuthService', () => {
|
|||
});
|
||||
events = [];
|
||||
toolkit = {
|
||||
login: vi.fn(),
|
||||
login: vi.fn<(...args: any[]) => any>(),
|
||||
logout: vi.fn().mockResolvedValue({ providerName: OAUTH_PROVIDER, ok: true }),
|
||||
getCachedAccessToken: vi.fn().mockResolvedValue(undefined),
|
||||
tokenProvider: vi.fn().mockReturnValue({ getAccessToken: async () => 'access-token' }),
|
||||
|
|
@ -185,7 +185,7 @@ describe('OAuthService', () => {
|
|||
|
||||
it('startLogin resolves a device-code flow and flips to authenticated on success', async () => {
|
||||
stubManagedModelsFetch();
|
||||
toolkit.login.mockImplementation(async (_provider, options) => {
|
||||
toolkit.login.mockImplementation((_provider, options) => {
|
||||
options.onDeviceCode(deviceAuth);
|
||||
return { providerName: OAUTH_PROVIDER, ok: true };
|
||||
});
|
||||
|
|
@ -210,7 +210,7 @@ describe('OAuthService', () => {
|
|||
|
||||
it('provisions the managed provider through the provider service after login', async () => {
|
||||
stubManagedModelsFetch();
|
||||
toolkit.login.mockImplementation(async (_provider, options) => {
|
||||
toolkit.login.mockImplementation((_provider, options) => {
|
||||
options.onDeviceCode(deviceAuth);
|
||||
return { providerName: OAUTH_PROVIDER, ok: true };
|
||||
});
|
||||
|
|
@ -232,7 +232,7 @@ describe('OAuthService', () => {
|
|||
it('startLogin resolves a default oauth ref for the managed provider without oauth config', async () => {
|
||||
providers[OAUTH_PROVIDER] = { type: 'kimi', baseUrl: 'https://api.example.com' };
|
||||
stubManagedModelsFetch();
|
||||
toolkit.login.mockImplementation(async (_provider, options) => {
|
||||
toolkit.login.mockImplementation((_provider, options) => {
|
||||
options.onDeviceCode(deviceAuth);
|
||||
return { providerName: OAUTH_PROVIDER, ok: true };
|
||||
});
|
||||
|
|
@ -312,7 +312,7 @@ describe('OAuthService', () => {
|
|||
it('keeps a device-code login authenticated when model fetch is unavailable after authorization', async () => {
|
||||
const fetchMock = vi.fn().mockRejectedValue(new Error('network disabled in test'));
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
toolkit.login.mockImplementation(async (_provider, options) => {
|
||||
toolkit.login.mockImplementation((_provider, options) => {
|
||||
options.onDeviceCode(deviceAuth);
|
||||
return { providerName: OAUTH_PROVIDER, ok: true };
|
||||
});
|
||||
|
|
@ -329,7 +329,7 @@ describe('OAuthService', () => {
|
|||
|
||||
it('refreshes managed models and sets the default model after a device-code login succeeds', async () => {
|
||||
const fetchMock = stubManagedModelsFetch();
|
||||
toolkit.login.mockImplementation(async (_provider, options) => {
|
||||
toolkit.login.mockImplementation((_provider, options) => {
|
||||
options.onDeviceCode(deviceAuth);
|
||||
return { providerName: OAUTH_PROVIDER, ok: true };
|
||||
});
|
||||
|
|
@ -356,7 +356,7 @@ describe('OAuthService', () => {
|
|||
});
|
||||
|
||||
it('keeps an in-flight OAuth flow alive when unrelated providers change', async () => {
|
||||
toolkit.login.mockImplementation(async (_provider, options) => {
|
||||
toolkit.login.mockImplementation((_provider, options) => {
|
||||
options.onDeviceCode(deviceAuth);
|
||||
return new Promise(() => { });
|
||||
});
|
||||
|
|
@ -370,7 +370,7 @@ describe('OAuthService', () => {
|
|||
});
|
||||
|
||||
it('aborts an in-flight OAuth flow when its provider is removed from config', async () => {
|
||||
toolkit.login.mockImplementation(async (_provider, options) => {
|
||||
toolkit.login.mockImplementation((_provider, options) => {
|
||||
options.onDeviceCode(deviceAuth);
|
||||
return new Promise(() => { });
|
||||
});
|
||||
|
|
@ -385,7 +385,7 @@ describe('OAuthService', () => {
|
|||
|
||||
it('cancelLogin aborts a pending flow and marks it cancelled', async () => {
|
||||
let capturedSignal: AbortSignal | undefined;
|
||||
toolkit.login.mockImplementation(async (_provider, options) => {
|
||||
toolkit.login.mockImplementation((_provider, options) => {
|
||||
capturedSignal = options.signal;
|
||||
options.onDeviceCode(deviceAuth);
|
||||
return new Promise(() => { });
|
||||
|
|
@ -400,7 +400,7 @@ describe('OAuthService', () => {
|
|||
});
|
||||
|
||||
it('logout delegates to the toolkit and clears any pending flow', async () => {
|
||||
toolkit.login.mockImplementation(async (_provider, options) => {
|
||||
toolkit.login.mockImplementation((_provider, options) => {
|
||||
options.onDeviceCode(deviceAuth);
|
||||
return new Promise(() => { });
|
||||
});
|
||||
|
|
@ -785,7 +785,7 @@ describe('AuthSummaryService', () => {
|
|||
type: 'kimi',
|
||||
oauth: { storage: 'file', key: 'oauth/kimi-code' },
|
||||
};
|
||||
oauthStatus.mockImplementation(async (name: string) => {
|
||||
oauthStatus.mockImplementation((name: string) => {
|
||||
if (name === OTHER_OAUTH) throw new Error('No OAuth manager configured');
|
||||
return { loggedIn: true, provider: name };
|
||||
});
|
||||
|
|
|
|||
|
|
@ -10,16 +10,17 @@
|
|||
import { toDisposable } from '#/_base/di/lifecycle';
|
||||
import type { ServiceRegistration } from '#/_base/di/test';
|
||||
import { createHooks } from '#/hooks';
|
||||
import type { Hooks } from '#/hooks';
|
||||
import { buildContextCompactionShape } from '#/agent/contextMemory/compactionHandoff';
|
||||
import {
|
||||
IAgentContextMemoryService,
|
||||
type ContextCompactionInput,
|
||||
type ContextCompactionResult,
|
||||
} from '#/agent/contextMemory/contextMemory';
|
||||
import { computeUndoCut } from '#/agent/contextMemory/contextOps';
|
||||
import { computeUndoCut, type UndoCut } from '#/agent/contextMemory/contextOps';
|
||||
import { ensureMessageId } from '#/agent/contextMemory/messageId';
|
||||
import type { ContextMessage } from '#/agent/contextMemory/types';
|
||||
import { IEventBus } from '#/app/event/eventBus';
|
||||
import { EventBusService } from '#/app/event/eventBusService';
|
||||
import { IAgentWireRecordService } from '#/agent/wireRecord/wireRecord';
|
||||
|
||||
/**
|
||||
|
|
@ -52,21 +53,22 @@ export interface StubContextMemory extends IAgentContextMemoryService {
|
|||
* and fires `onSpliced`, mirroring `AgentContextMemoryService.applySplice` enough
|
||||
* for collaborators (e.g. `DynamicInjectorService`) to react to splices.
|
||||
*/
|
||||
export function stubContextMemory(): StubContextMemory {
|
||||
function publishSplice(
|
||||
eventBus: IEventBus | undefined,
|
||||
input: {
|
||||
start: number;
|
||||
deleteCount: number;
|
||||
messages: readonly ContextMessage[];
|
||||
tokens?: number;
|
||||
},
|
||||
): void {
|
||||
eventBus?.publish({ type: 'context.spliced', ...input });
|
||||
}
|
||||
|
||||
export function stubContextMemory(eventBus?: IEventBus): StubContextMemory {
|
||||
const messages: ContextMessage[] = [];
|
||||
const hooks = {
|
||||
onSpliced: createHooks(['onSpliced'])['onSpliced'],
|
||||
} as unknown as Hooks<{
|
||||
onSpliced: {
|
||||
start: number;
|
||||
deleteCount: number;
|
||||
messages: ContextMessage[];
|
||||
tokens?: number;
|
||||
};
|
||||
}>;
|
||||
return {
|
||||
_serviceBrand: undefined,
|
||||
hooks,
|
||||
get messages() {
|
||||
return messages;
|
||||
},
|
||||
|
|
@ -75,20 +77,20 @@ export function stubContextMemory(): StubContextMemory {
|
|||
const stamped = inserted.map(ensureMessageId);
|
||||
const start = messages.length;
|
||||
messages.push(...stamped);
|
||||
void hooks.onSpliced.run({ start, deleteCount: 0, messages: [...stamped] });
|
||||
publishSplice(eventBus, { start, deleteCount: 0, messages: [...stamped] });
|
||||
},
|
||||
clear: () => {
|
||||
const deleteCount = messages.length;
|
||||
if (deleteCount === 0) return;
|
||||
messages.splice(0, deleteCount);
|
||||
void hooks.onSpliced.run({ start: 0, deleteCount, messages: [] });
|
||||
publishSplice(eventBus, { start: 0, deleteCount, messages: [] });
|
||||
},
|
||||
undo: (count) => {
|
||||
const cut = computeUndoCut(messages, count);
|
||||
if (cut.cutIndex >= 0 && cut.removedCount >= count) {
|
||||
const deleteCount = messages.length - cut.cutIndex;
|
||||
messages.splice(cut.cutIndex, deleteCount);
|
||||
void hooks.onSpliced.run({ start: cut.cutIndex, deleteCount, messages: [] });
|
||||
publishSplice(eventBus, { start: cut.cutIndex, deleteCount, messages: [] });
|
||||
}
|
||||
return cut;
|
||||
},
|
||||
|
|
@ -96,7 +98,7 @@ export function stubContextMemory(): StubContextMemory {
|
|||
const shape = buildContextCompactionShape(messages, input);
|
||||
const previousLength = messages.length;
|
||||
messages.splice(0, previousLength, ...shape.messages);
|
||||
void hooks.onSpliced.run({
|
||||
publishSplice(eventBus, {
|
||||
start: 0,
|
||||
deleteCount: previousLength,
|
||||
messages: [...shape.messages],
|
||||
|
|
@ -109,7 +111,7 @@ export function stubContextMemory(): StubContextMemory {
|
|||
splice: (start, deleteCount, inserted, tokens) => {
|
||||
const stamped = inserted.map(ensureMessageId);
|
||||
messages.splice(start, deleteCount, ...stamped);
|
||||
void hooks.onSpliced.run({
|
||||
publishSplice(eventBus, {
|
||||
start,
|
||||
deleteCount,
|
||||
messages: [...stamped],
|
||||
|
|
@ -119,6 +121,46 @@ export function stubContextMemory(): StubContextMemory {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* DI-constructible variant of {@link stubContextMemory}: publishes
|
||||
* `context.spliced` to the Agent-scope {@link IEventBus} so collaborators
|
||||
* (e.g. `AgentContextInjectorService`) react to splices exactly as they do
|
||||
* against the real `AgentContextMemoryService`.
|
||||
*/
|
||||
class StubContextMemoryService implements IAgentContextMemoryService {
|
||||
declare readonly _serviceBrand: undefined;
|
||||
private readonly impl: StubContextMemory;
|
||||
constructor(@IEventBus eventBus: IEventBus) {
|
||||
this.impl = stubContextMemory(eventBus);
|
||||
}
|
||||
get messages(): readonly ContextMessage[] {
|
||||
return this.impl.messages;
|
||||
}
|
||||
get(): readonly ContextMessage[] {
|
||||
return this.impl.get();
|
||||
}
|
||||
append(...messages: readonly ContextMessage[]): void {
|
||||
this.impl.append(...messages);
|
||||
}
|
||||
clear(): void {
|
||||
this.impl.clear();
|
||||
}
|
||||
undo(count: number): UndoCut {
|
||||
return this.impl.undo(count);
|
||||
}
|
||||
applyCompaction(input: ContextCompactionInput): ContextCompactionResult {
|
||||
return this.impl.applyCompaction(input);
|
||||
}
|
||||
splice(
|
||||
start: number,
|
||||
deleteCount: number,
|
||||
messages: readonly ContextMessage[],
|
||||
tokens?: number,
|
||||
): void {
|
||||
this.impl.splice(start, deleteCount, messages, tokens);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the default collaborators consumed by `AgentContextMemoryService`
|
||||
* (`IAgentWireRecordService`) and an in-memory `IAgentContextMemoryService`.
|
||||
|
|
@ -127,5 +169,6 @@ export function stubContextMemory(): StubContextMemory {
|
|||
*/
|
||||
export function registerContextMemoryServices(reg: ServiceRegistration): void {
|
||||
reg.defineInstance(IAgentWireRecordService, stubWireRecord());
|
||||
reg.defineInstance(IAgentContextMemoryService, stubContextMemory());
|
||||
reg.define(IEventBus, EventBusService);
|
||||
reg.define(IAgentContextMemoryService, StubContextMemoryService);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ import { IAgentToolRegistryService } from '#/agent/toolRegistry/toolRegistry';
|
|||
import { IAgentTurnService, type Turn } from '#/agent/turn/turn';
|
||||
import { IAgentWireRecordService } from '#/agent/wireRecord/wireRecord';
|
||||
|
||||
import { IAgentEventSinkService } from '#/agent/eventSink';
|
||||
import { stubBootstrap } from '../bootstrap/stubs';
|
||||
import { stubWireRecord } from '../contextMemory/stubs';
|
||||
import { stubLog } from '../log/stubs';
|
||||
|
|
@ -87,7 +86,6 @@ describe('SessionCronService', () => {
|
|||
undo: () => 0,
|
||||
clear: () => {},
|
||||
});
|
||||
ix.stub(IAgentEventSinkService, { emit: () => {}, on: () => ({ dispose: () => {} }) });
|
||||
ix.stub(IAgentWireRecordService, stubWireRecord());
|
||||
ix.stub(IAgentTurnService, turnService);
|
||||
ix.stub(ITelemetryService, { track: () => {} });
|
||||
|
|
|
|||
|
|
@ -310,12 +310,12 @@ describe('CronCreateTool', () => {
|
|||
harness.store.add({ cron: '*/5 * * * *', prompt: `seed-${i}`, recurring: true }, harness.now());
|
||||
}
|
||||
|
||||
const first = await tool.resolveExecution({
|
||||
const first = tool.resolveExecution({
|
||||
cron: '*/5 * * * *',
|
||||
prompt: 'first',
|
||||
recurring: true,
|
||||
});
|
||||
const second = await tool.resolveExecution({
|
||||
const second = tool.resolveExecution({
|
||||
cron: '*/5 * * * *',
|
||||
prompt: 'second',
|
||||
recurring: true,
|
||||
|
|
@ -375,7 +375,7 @@ describe('CronCreateTool', () => {
|
|||
it('uses the execution-time clock for createdAt', async () => {
|
||||
const harness = createToolHarness();
|
||||
const tool = new CronCreateTool(false, harness.cron);
|
||||
const execution = await tool.resolveExecution({
|
||||
const execution = tool.resolveExecution({
|
||||
cron: '*/5 * * * *',
|
||||
prompt: 'delayed approval',
|
||||
recurring: true,
|
||||
|
|
@ -396,17 +396,17 @@ describe('CronCreateTool', () => {
|
|||
const harness = createToolHarness();
|
||||
const tool = new CronCreateTool(false, harness.cron);
|
||||
|
||||
const a = await tool.resolveExecution({
|
||||
const a = tool.resolveExecution({
|
||||
cron: '*/5\n* * * *',
|
||||
prompt: 'same',
|
||||
recurring: true,
|
||||
});
|
||||
const b = await tool.resolveExecution({
|
||||
const b = tool.resolveExecution({
|
||||
cron: '0 9 * * *',
|
||||
prompt: 'same',
|
||||
recurring: true,
|
||||
});
|
||||
const c = await tool.resolveExecution({
|
||||
const c = tool.resolveExecution({
|
||||
cron: '*/5 * * * *',
|
||||
prompt: 'different',
|
||||
recurring: true,
|
||||
|
|
|
|||
|
|
@ -30,17 +30,14 @@ class Service2 implements IService2 {
|
|||
}
|
||||
|
||||
class Service1Consumer {
|
||||
constructor(@IService1 service1: IService1) {
|
||||
expect(service1).toBeInstanceOf(Service1);
|
||||
expect(service1.c).toBe(1);
|
||||
}
|
||||
constructor(@IService1 readonly service1: IService1) {}
|
||||
}
|
||||
|
||||
class Target2Dep {
|
||||
constructor(@IService1 service1: IService1, @IService2 service2: IService2) {
|
||||
expect(service1).toBeInstanceOf(Service1);
|
||||
expect(service2).toBeInstanceOf(Service2);
|
||||
}
|
||||
constructor(
|
||||
@IService1 readonly service1: IService1,
|
||||
@IService2 readonly service2: IService2,
|
||||
) {}
|
||||
}
|
||||
|
||||
describe('ServiceCollection', () => {
|
||||
|
|
@ -71,12 +68,16 @@ describe('ServiceCollection', () => {
|
|||
collection.set(IService1, new Service1());
|
||||
|
||||
const service = new InstantiationService(collection);
|
||||
service.createInstance(Service1Consumer);
|
||||
const consumer = service.createInstance(Service1Consumer);
|
||||
expect(consumer.service1).toBeInstanceOf(Service1);
|
||||
expect(consumer.service1.c).toBe(1);
|
||||
|
||||
// add IService2 AFTER the InstantiationService was built
|
||||
collection.set(IService2, new Service2());
|
||||
|
||||
service.createInstance(Target2Dep);
|
||||
const target2 = service.createInstance(Target2Dep);
|
||||
expect(target2.service1).toBeInstanceOf(Service1);
|
||||
expect(target2.service2).toBeInstanceOf(Service2);
|
||||
service.invokeFunction((a) => {
|
||||
expect(a.get(IService1)).toBeInstanceOf(Service1);
|
||||
expect(a.get(IService2)).toBeInstanceOf(Service2);
|
||||
|
|
|
|||
|
|
@ -1,14 +1,15 @@
|
|||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import { IAgentContextMemoryService } from '#/agent/contextMemory/contextMemory';
|
||||
import { IAgentEventSinkService } from '#/agent/eventSink';
|
||||
import { USER_PROMPT_ORIGIN } from '#/agent/contextMemory/types';
|
||||
import { IAgentGoalService } from '#/agent/goal/goal';
|
||||
import { type AgentGoalService } from '#/agent/goal/goalService';
|
||||
import { IAgentLoopService, type AfterStepContext } from '#/agent/loop/loop';
|
||||
import { IAgentTurnService, type Turn, type TurnResult } from '#/agent/turn/turn';
|
||||
import type { PersistedWireRecord, WireRecord } from '#/agent/wireRecord/wireRecord';
|
||||
import { type DomainEvent, IEventBus } from '#/app/event/eventBus';
|
||||
import type { TokenUsage } from '#/app/llmProtocol/usage';
|
||||
import { ErrorCodes } from '#/errors';
|
||||
import { ErrorCodes, toKimiErrorPayload } from '#/errors';
|
||||
|
||||
import {
|
||||
InMemoryWireRecordPersistence,
|
||||
|
|
@ -23,10 +24,8 @@ import { stubLoopWithHooks, stubTurn, type StubTurn } from '../turn/stubs';
|
|||
|
||||
type GoalServiceTestManager = IAgentGoalService & AgentGoalService;
|
||||
type GoalRecord = Extract<PersistedWireRecord, { type: `goal.${string}` }>;
|
||||
type AgentEvent = Parameters<IAgentEventSinkService['emit']>[0];
|
||||
type AgentEvent = DomainEvent;
|
||||
type GoalUpdatedEvent = Extract<AgentEvent, { type: 'goal.updated' }>;
|
||||
type GoalSnapshot = NonNullable<ReturnType<IAgentGoalService['getGoal']>['goal']>;
|
||||
type GoalChange = GoalUpdatedEvent['change'];
|
||||
|
||||
const zeroUsage: TokenUsage = {
|
||||
inputCacheRead: 0,
|
||||
|
|
@ -94,12 +93,19 @@ async function runStepUsageHooks(
|
|||
return goals.getGoal().goal?.budget.overBudget === true;
|
||||
}
|
||||
|
||||
async function endTurn(
|
||||
turnService: IAgentTurnService,
|
||||
function endTurn(
|
||||
eventBus: IEventBus,
|
||||
turn: Turn,
|
||||
result: TurnResult = { reason: 'completed' },
|
||||
): Promise<void> {
|
||||
await turnService.hooks.onEnded.run({ turn, result });
|
||||
): void {
|
||||
const error = result.error !== undefined ? toKimiErrorPayload(result.error) : undefined;
|
||||
eventBus.publish({
|
||||
type: 'turn.ended',
|
||||
turnId: turn.id,
|
||||
reason: result.reason,
|
||||
error,
|
||||
durationMs: 0,
|
||||
});
|
||||
}
|
||||
|
||||
describe('AgentGoalService', () => {
|
||||
|
|
@ -107,11 +113,7 @@ describe('AgentGoalService', () => {
|
|||
let context: IAgentContextMemoryService;
|
||||
let goals: GoalServiceTestManager;
|
||||
let records: PersistedWireRecord[];
|
||||
let events: Array<{
|
||||
readonly type: string;
|
||||
readonly snapshot?: GoalSnapshot | null;
|
||||
readonly change?: GoalChange;
|
||||
}>;
|
||||
let events: GoalUpdatedEvent[];
|
||||
let telemetry: TelemetryRecord[];
|
||||
|
||||
beforeEach(() => {
|
||||
|
|
@ -125,8 +127,8 @@ describe('AgentGoalService', () => {
|
|||
context = ctx.get(IAgentContextMemoryService);
|
||||
goals = ctx.get(IAgentGoalService) as GoalServiceTestManager;
|
||||
records = persistence.records;
|
||||
const eventSink = ctx.get(IAgentEventSinkService);
|
||||
eventSink.on((event) => {
|
||||
const eventBus = ctx.get(IEventBus);
|
||||
eventBus.subscribe((event) => {
|
||||
if (event.type === 'goal.updated') events.push(event);
|
||||
});
|
||||
});
|
||||
|
|
@ -537,7 +539,7 @@ describe('AgentGoalService core workflow hooks', () => {
|
|||
let goals: IAgentGoalService;
|
||||
let turnService: StubTurn;
|
||||
let loopService: IAgentLoopService;
|
||||
let eventSink: IAgentEventSinkService;
|
||||
let eventBus: IEventBus;
|
||||
|
||||
beforeEach(() => {
|
||||
turnService = stubTurn({ hasActiveTurn: true });
|
||||
|
|
@ -548,7 +550,7 @@ describe('AgentGoalService core workflow hooks', () => {
|
|||
);
|
||||
context = ctx.get(IAgentContextMemoryService);
|
||||
goals = ctx.get(IAgentGoalService);
|
||||
eventSink = ctx.get(IAgentEventSinkService);
|
||||
eventBus = ctx.get(IEventBus);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
|
|
@ -559,9 +561,9 @@ describe('AgentGoalService core workflow hooks', () => {
|
|||
await goals.createGoal({ objective: 'finish the task' });
|
||||
|
||||
const turn = makeTurn(1);
|
||||
await turnService.hooks.onLaunched.run({ turn });
|
||||
eventBus.publish({ type: 'turn.started', turnId: turn.id, origin: USER_PROMPT_ORIGIN });
|
||||
await runGoalStep(loopService, turn);
|
||||
await endTurn(turnService, turn);
|
||||
endTurn(eventBus, turn);
|
||||
|
||||
expect(goals.getGoal().goal).toMatchObject({
|
||||
status: 'active',
|
||||
|
|
@ -580,9 +582,9 @@ describe('AgentGoalService core workflow hooks', () => {
|
|||
await goals.setBudgetLimits({ budgetLimits: { turnBudget: 1 } }, 'model');
|
||||
|
||||
const turn = makeTurn(11);
|
||||
await turnService.hooks.onLaunched.run({ turn });
|
||||
eventBus.publish({ type: 'turn.started', turnId: turn.id, origin: USER_PROMPT_ORIGIN });
|
||||
await runGoalStep(loopService, turn);
|
||||
await endTurn(turnService, turn);
|
||||
endTurn(eventBus, turn);
|
||||
|
||||
expect(goals.getGoal().goal).toMatchObject({
|
||||
status: 'blocked',
|
||||
|
|
@ -597,7 +599,7 @@ describe('AgentGoalService core workflow hooks', () => {
|
|||
await goals.setBudgetLimits({ budgetLimits: { tokenBudget: 7 } }, 'model');
|
||||
|
||||
const turn = turnService.launch();
|
||||
await turnService.hooks.onLaunched.run({ turn });
|
||||
eventBus.publish({ type: 'turn.started', turnId: turn.id, origin: USER_PROMPT_ORIGIN });
|
||||
|
||||
expect(
|
||||
await runStepUsageHooks(loopService, goals, turn, {
|
||||
|
|
@ -643,11 +645,11 @@ describe('AgentGoalService core workflow hooks', () => {
|
|||
|
||||
it('continues after creating a goal mid-turn without counting the starter turn', async () => {
|
||||
const turn = makeTurn(2);
|
||||
await turnService.hooks.onLaunched.run({ turn });
|
||||
eventBus.publish({ type: 'turn.started', turnId: turn.id, origin: USER_PROMPT_ORIGIN });
|
||||
await runGoalStep(loopService, turn);
|
||||
|
||||
await goals.createGoal({ objective: 'finish the task' }, 'model');
|
||||
await endTurn(turnService, turn);
|
||||
endTurn(eventBus, turn);
|
||||
|
||||
expect(goals.getGoal().goal).toMatchObject({
|
||||
status: 'active',
|
||||
|
|
@ -660,7 +662,7 @@ describe('AgentGoalService core workflow hooks', () => {
|
|||
await goals.createGoal({ objective: 'finish the task' });
|
||||
|
||||
const turn = makeTurn(3);
|
||||
await turnService.hooks.onLaunched.run({ turn });
|
||||
eventBus.publish({ type: 'turn.started', turnId: turn.id, origin: USER_PROMPT_ORIGIN });
|
||||
const step = {
|
||||
turnId: turn.id,
|
||||
step: 1,
|
||||
|
|
@ -678,7 +680,7 @@ describe('AgentGoalService core workflow hooks', () => {
|
|||
|
||||
await goals.markComplete({}, 'model');
|
||||
await loopService.hooks.afterStep.run(afterStep);
|
||||
await endTurn(turnService, turn);
|
||||
endTurn(eventBus, turn);
|
||||
|
||||
expect(afterStep.continue).toBe(true);
|
||||
expect(goals.getGoal().goal).toBeNull();
|
||||
|
|
@ -693,8 +695,8 @@ describe('AgentGoalService core workflow hooks', () => {
|
|||
await goals.createGoal({ objective: 'finish the task' });
|
||||
|
||||
const turn = makeTurn(4);
|
||||
await turnService.hooks.onLaunched.run({ turn });
|
||||
await endTurn(turnService, turn, { reason: 'failed', error: new Error('boom') });
|
||||
eventBus.publish({ type: 'turn.started', turnId: turn.id, origin: USER_PROMPT_ORIGIN });
|
||||
endTurn(eventBus, turn, { reason: 'failed', error: new Error('boom') });
|
||||
|
||||
expect(goals.getGoal().goal).toMatchObject({
|
||||
status: 'paused',
|
||||
|
|
@ -707,8 +709,8 @@ describe('AgentGoalService core workflow hooks', () => {
|
|||
await goals.createGoal({ objective: 'finish the task' });
|
||||
|
||||
const turn = makeTurn(5);
|
||||
await turnService.hooks.onLaunched.run({ turn });
|
||||
await endTurn(turnService, turn, { reason: 'blocked' });
|
||||
eventBus.publish({ type: 'turn.started', turnId: turn.id, origin: USER_PROMPT_ORIGIN });
|
||||
endTurn(eventBus, turn, { reason: 'blocked' });
|
||||
|
||||
expect(goals.getGoal().goal).toMatchObject({
|
||||
status: 'blocked',
|
||||
|
|
|
|||
|
|
@ -265,7 +265,7 @@ interface ResumeStateSnapshot {
|
|||
readonly config: {
|
||||
readonly cwd: string;
|
||||
readonly activeToolNames: readonly string[] | undefined;
|
||||
readonly provider: ProviderConfig | undefined;
|
||||
readonly provider: ReturnType<IProviderService['get']>;
|
||||
readonly profileName: string | undefined;
|
||||
readonly thinkingLevel: string;
|
||||
readonly systemPrompt: string;
|
||||
|
|
@ -477,6 +477,31 @@ export function homeDirServices(homeDir: string | undefined): TestAgentServiceOv
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Override the App-scope `IHostEnvironment` with a fully-populated POSIX
|
||||
* snapshot whose `homeDir` points at a hermetic test directory. Used by tests
|
||||
* that render system prompts / resolve user-level config so a developer's real
|
||||
* `~/.kimi-code` / `~/.agents` files never leak into the assertions.
|
||||
*/
|
||||
export function hostEnvironmentServices(homeDir: string): TestAgentServiceOverride {
|
||||
return appServices((reg) => {
|
||||
reg.defineInstance(
|
||||
IHostEnvironment,
|
||||
{
|
||||
_serviceBrand: undefined,
|
||||
osKind: 'Linux',
|
||||
osArch: 'x64',
|
||||
osVersion: 'test',
|
||||
shellName: 'bash',
|
||||
shellPath: '/bin/bash',
|
||||
pathClass: 'posix',
|
||||
homeDir,
|
||||
ready: Promise.resolve(),
|
||||
} satisfies IHostEnvironment,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export function additionalDirServices(additionalDirs: readonly string[]): TestAgentServiceOverride {
|
||||
return sessionServices((reg) => {
|
||||
reg.defineInstance(
|
||||
|
|
@ -1963,10 +1988,13 @@ function isSystemReminderMessage(message: ContextMessage): boolean {
|
|||
function configStateSnapshot(ctx: AgentTestContext): ResumeStateSnapshot['config'] {
|
||||
const profile = ctx.get(IAgentProfileService);
|
||||
const data = profile.data();
|
||||
const model = profile.resolveModel();
|
||||
const providerConfig =
|
||||
model === undefined ? undefined : ctx.get(IProviderService).get(model.providerName);
|
||||
return {
|
||||
cwd: data.cwd,
|
||||
activeToolNames: data.activeToolNames,
|
||||
provider: data.provider,
|
||||
provider: providerConfig,
|
||||
profileName: data.profileName,
|
||||
thinkingLevel: data.thinkingLevel,
|
||||
systemPrompt: data.systemPrompt,
|
||||
|
|
@ -2220,7 +2248,7 @@ function createGenerateBackedProtocolRegistry(generate: GenerateFn): IProtocolAd
|
|||
baseUrl: input.baseUrl,
|
||||
apiKey: input.apiKey,
|
||||
defaultHeaders: input.defaultHeaders as Record<string, string> | undefined,
|
||||
...(input.providerOptions ?? {}),
|
||||
...input.providerOptions,
|
||||
} as ProviderConfig;
|
||||
return input.protocol === 'kimi'
|
||||
? new GenerateBackedKimiChatProvider(
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ export {
|
|||
execEnvServices,
|
||||
externalHookServices,
|
||||
homeDirServices,
|
||||
hostEnvironmentServices,
|
||||
InMemoryWireRecordPersistence,
|
||||
llmGenerateServices,
|
||||
logServices,
|
||||
|
|
|
|||
|
|
@ -1,20 +1,23 @@
|
|||
import { SyncDescriptor } from '#/_base/di/descriptors';
|
||||
import { DisposableStore } from '#/_base/di/lifecycle';
|
||||
import { TestInstantiationService } from '#/_base/di/test';
|
||||
import { IAgentContextMemoryService } from '#/agent/contextMemory';
|
||||
import { IAgentContextMemoryService } from '#/agent/contextMemory/contextMemory';
|
||||
import type { ContextMessage } from '#/agent/contextMemory/types';
|
||||
import { IAgentContextProjectorService } from '#/agent/contextProjector';
|
||||
import { IAgentContextProjectorService } from '#/agent/contextProjector/contextProjector';
|
||||
import { AgentLLMRequesterService } from '#/agent/llmRequester/llmRequesterService';
|
||||
import { IAgentLLMRequesterService } from '#/agent/llmRequester/llmRequester';
|
||||
import { IAgentContextSizeService } from '#/agent/contextSize';
|
||||
import { IAgentProfileService } from '#/agent/profile';
|
||||
import { IAgentToolRegistryService } from '#/agent/toolRegistry';
|
||||
import { IAgentUsageService } from '#/agent/usage';
|
||||
import { IConfigService } from '#/app/config';
|
||||
import { APIStatusError, emptyUsage, type Message, type ModelCapability } from '#/app/llmProtocol';
|
||||
import type { Model } from '#/app/model';
|
||||
import { ITelemetryService } from '#/app/telemetry';
|
||||
import { ILogService } from '#/_base/log';
|
||||
import { IAgentContextSizeService } from '#/agent/contextSize/contextSize';
|
||||
import { IAgentProfileService } from '#/agent/profile/profile';
|
||||
import { IAgentToolRegistryService } from '#/agent/toolRegistry/toolRegistry';
|
||||
import { IAgentUsageService } from '#/agent/usage/usage';
|
||||
import { IConfigService } from '#/app/config/config';
|
||||
import { APIStatusError } from '#/app/llmProtocol/errors';
|
||||
import { emptyUsage } from '#/app/llmProtocol/usage';
|
||||
import type { Message } from '#/app/llmProtocol/message';
|
||||
import type { ModelCapability } from '#/app/llmProtocol/capability';
|
||||
import type { LLMEvent, Model } from '#/app/model/modelInstance';
|
||||
import { ITelemetryService } from '#/app/telemetry/telemetry';
|
||||
import { ILogService } from '#/_base/log/log';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
const capabilities: ModelCapability = {
|
||||
|
|
@ -156,6 +159,8 @@ describe('AgentLLMRequesterService strict resend', () => {
|
|||
const model = createModel({ value: 0 });
|
||||
Object.defineProperty(model, 'request', {
|
||||
value: async function* () {
|
||||
const events: LLMEvent[] = [];
|
||||
for (const event of events) yield event;
|
||||
throw new APIStatusError(401, 'unauthorized');
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import type { AgentEvent } from '@moonshot-ai/protocol';
|
||||
import type { ContentPart } from '#/app/llmProtocol/message';
|
||||
import type { Tool as KosongTool } from '#/app/llmProtocol/tool';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
|
|
@ -6,7 +5,7 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|||
import { SyncDescriptor } from '#/_base/di/descriptors';
|
||||
import { DisposableStore, toDisposable } from '#/_base/di/lifecycle';
|
||||
import { TestInstantiationService } from '#/_base/di/test';
|
||||
import { IAgentEventSinkService } from '#/agent/eventSink';
|
||||
import { type DomainEvent, IEventBus } from '#/app/event/eventBus';
|
||||
import type { McpConnectionManager, McpServerEntry } from '#/agent/mcp/connection-manager';
|
||||
import { IAgentMcpService } from '#/agent/mcp/mcp';
|
||||
import { AgentMcpService } from '#/agent/mcp/mcpService';
|
||||
|
|
@ -128,17 +127,17 @@ class FakeMcpManager {
|
|||
describe('AgentMcpService', () => {
|
||||
let disposables: DisposableStore;
|
||||
let ix: TestInstantiationService;
|
||||
let events: AgentEvent[];
|
||||
let events: DomainEvent[];
|
||||
|
||||
beforeEach(() => {
|
||||
disposables = new DisposableStore();
|
||||
ix = disposables.add(new TestInstantiationService());
|
||||
events = [];
|
||||
ix.stub(IAgentEventSinkService, {
|
||||
emit: (event) => {
|
||||
ix.stub(IEventBus, {
|
||||
publish: (event) => {
|
||||
events.push(event);
|
||||
},
|
||||
on: () => toDisposable(() => {}),
|
||||
subscribe: () => toDisposable(() => {}),
|
||||
});
|
||||
ix.set(IAgentToolRegistryService, new SyncDescriptor(AgentToolRegistryService));
|
||||
ix.set(IAgentToolExecutorService, new SyncDescriptor(AgentToolExecutorService));
|
||||
|
|
|
|||
|
|
@ -9,12 +9,17 @@ import { describe, expect, test } from 'vitest';
|
|||
import { convertMCPContentBlock, mcpResultToExecutableOutput } from '#/agent/mcp/output';
|
||||
import { createMcpTool } from '#/agent/mcp/tools/mcp';
|
||||
import type { MCPClient, MCPContentBlock, MCPToolResult } from '#/agent/mcp/types';
|
||||
import type { ToolExecution } from '#/agent/tool/toolContract';
|
||||
import { sniffImageDimensions } from '#/_base/tools/support/file-type';
|
||||
|
||||
const MCP_OUTPUT_TRUNCATED_TEXT =
|
||||
'\n\n[Output truncated: exceeded 100000 character limit. ' +
|
||||
'Use pagination or more specific queries to get remaining content.]';
|
||||
|
||||
function isPromiseLike(value: ToolExecution | Promise<ToolExecution>): value is Promise<ToolExecution> {
|
||||
return typeof (value as Promise<ToolExecution>).then === 'function';
|
||||
}
|
||||
|
||||
function assertValidMcpBlock<T extends MCPContentBlock>(block: T): T {
|
||||
const parsed = ContentBlockSchema.safeParse(block);
|
||||
if (!parsed.success) {
|
||||
|
|
@ -477,7 +482,8 @@ describe('createMcpTool', () => {
|
|||
{ name: 'tool', description: 'Tool', parameters: {} },
|
||||
client,
|
||||
);
|
||||
const execution = tool.resolveExecution({});
|
||||
const resolved = tool.resolveExecution({});
|
||||
const execution = isPromiseLike(resolved) ? await resolved : resolved;
|
||||
if (execution.isError === true) throw new Error('expected executable tool call');
|
||||
|
||||
const result = await execution.execute({
|
||||
|
|
|
|||
|
|
@ -35,6 +35,8 @@ import { IAgentToolExecutorService } from '#/agent/toolExecutor/toolExecutor';
|
|||
import { IAgentTurnService } from '#/agent/turn/turn';
|
||||
import { ISessionWorkspaceContext } from '#/session/workspaceContext/workspaceContext';
|
||||
import type { ToolCall } from '#/app/llmProtocol/message';
|
||||
import { IEventBus } from '#/app/event/eventBus';
|
||||
import { EventBusService } from '#/app/event/eventBusService';
|
||||
import type { ToolInputDisplay } from '@moonshot-ai/protocol';
|
||||
|
||||
import { stubApprovalService } from '../approval/stubs';
|
||||
|
|
@ -126,6 +128,7 @@ describe('AgentPermissionGate', () => {
|
|||
});
|
||||
reg.defineInstance(IAgentTurnService, stubTurnWithHooks());
|
||||
reg.defineInstance(IAgentToolExecutorService, stubToolExecutor());
|
||||
reg.define(IEventBus, EventBusService);
|
||||
reg.defineInstance(IAgentScopeContext, {
|
||||
_serviceBrand: undefined,
|
||||
agentId: 'main',
|
||||
|
|
@ -397,13 +400,14 @@ describe('AgentPermissionGate', () => {
|
|||
approvalResponse = { decision: 'approved', selectedLabel: 'Approve once' };
|
||||
const request = setApprovalRequest(async () => approvalResponse);
|
||||
const svc = make();
|
||||
const eventBus = ix.get(IEventBus);
|
||||
disposables.add(
|
||||
svc.hooks.onDidRequestApproval.register('test', (ctx) => {
|
||||
eventBus.subscribe('permission.approval.requested', (ctx) => {
|
||||
permissionRequest(ctx);
|
||||
}),
|
||||
);
|
||||
disposables.add(
|
||||
svc.hooks.onDidResolveApproval.register('test', (ctx) => {
|
||||
eventBus.subscribe('permission.approval.resolved', (ctx) => {
|
||||
permissionResult(ctx);
|
||||
}),
|
||||
);
|
||||
|
|
@ -413,6 +417,7 @@ describe('AgentPermissionGate', () => {
|
|||
|
||||
expect(request).toHaveBeenCalledTimes(1);
|
||||
expect(permissionRequest).toHaveBeenCalledWith({
|
||||
type: 'permission.approval.requested',
|
||||
sessionId: 'test-session',
|
||||
agentId: 'main',
|
||||
turnId: 1,
|
||||
|
|
@ -427,6 +432,7 @@ describe('AgentPermissionGate', () => {
|
|||
},
|
||||
});
|
||||
expect(permissionResult).toHaveBeenCalledWith({
|
||||
type: 'permission.approval.resolved',
|
||||
sessionId: 'test-session',
|
||||
agentId: 'main',
|
||||
turnId: 1,
|
||||
|
|
|
|||
|
|
@ -135,7 +135,7 @@ describe('AgentPermissionModeService (wire-backed)', () => {
|
|||
const log2 = ix2.get(IAppendLogStore);
|
||||
const fresh = ix2.get(IAgentWireService);
|
||||
|
||||
fresh.replay({ type: 'permission.set_mode', mode: 'auto' });
|
||||
void fresh.replay({ type: 'permission.set_mode', mode: 'auto' });
|
||||
|
||||
expect(fresh.getModel(PermissionModeModel)).toBe('auto');
|
||||
|
||||
|
|
|
|||
|
|
@ -149,7 +149,7 @@ describe('AgentPermissionRulesService (wire-backed)', () => {
|
|||
let changes = 0;
|
||||
disposables.add(fresh.subscribe(PermissionRulesModel, () => (changes += 1)));
|
||||
|
||||
fresh.replay(...records);
|
||||
void fresh.replay(...records);
|
||||
|
||||
expect(fresh.getModel(PermissionRulesModel)).toEqual({
|
||||
rules: [allowRule, denyRule],
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { Emitter } from '#/_base/event';
|
|||
import { IAgentPluginService } from '#/agent/plugin/agentPlugin';
|
||||
import { AgentPluginService } from '#/agent/plugin/agentPluginService';
|
||||
import { IAgentContextInjectorService } from '#/agent/contextInjector/contextInjector';
|
||||
import { USER_PROMPT_ORIGIN } from '#/agent/contextMemory/types';
|
||||
import { IEventBus } from '#/app/event/eventBus';
|
||||
import { IPluginService } from '#/app/plugin/plugin';
|
||||
import type { EnabledPluginSessionStart, ReloadSummary } from '#/app/plugin/types';
|
||||
|
|
@ -129,6 +130,7 @@ describe('AgentPluginService plugin session-start wiring', () => {
|
|||
ctx.get(IEventBus).publish({
|
||||
type: 'turn.started',
|
||||
turnId: 2,
|
||||
origin: USER_PROMPT_ORIGIN,
|
||||
});
|
||||
await injectRegistered(ctx);
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|||
import { HostFileSystem } from '#/os/backends/node-local/hostFsService';
|
||||
import { IAgentProfileService, type ResolvedAgentProfile } from '#/agent/profile/profile';
|
||||
|
||||
import { createTestAgent, execEnvServices, type TestAgentContext } from '../harness';
|
||||
import { createTestAgent, execEnvServices, hostEnvironmentServices, type TestAgentContext } from '../harness';
|
||||
|
||||
const profile: ResolvedAgentProfile = {
|
||||
name: 'agents-profile',
|
||||
|
|
@ -38,10 +38,8 @@ describe('AgentProfileService.applyProfile', () => {
|
|||
// never leak into the assertions.
|
||||
const fs = new HostFileSystem();
|
||||
ctx = createTestAgent(
|
||||
execEnvServices({
|
||||
hostEnvironment: { homeDir },
|
||||
hostFs: fs,
|
||||
}),
|
||||
execEnvServices({ hostFs: fs }),
|
||||
hostEnvironmentServices(homeDir),
|
||||
{ cwd: workDir },
|
||||
);
|
||||
return { ctx, profile: ctx.get(IAgentProfileService) };
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|||
import { DEFAULT_AGENT_PROFILE_NAME, IAgentProfileCatalogService } from '#/app/agentProfileCatalog/agentProfileCatalog';
|
||||
import { IAgentProfileService } from '#/agent/profile/profile';
|
||||
|
||||
import { createTestAgent, execEnvServices, type TestAgentContext } from '../harness';
|
||||
import { createTestAgent, hostEnvironmentServices, type TestAgentContext } from '../harness';
|
||||
|
||||
const MOCK_MODEL = 'mock-model';
|
||||
|
||||
|
|
@ -27,7 +27,7 @@ describe('AgentProfileService.bind', () => {
|
|||
function buildContext(): { ctx: TestAgentContext; profile: IAgentProfileService } {
|
||||
// Hermetic home dir so a developer's real ~/.kimi-code / ~/.agents files
|
||||
// never leak into the rendered system prompt.
|
||||
ctx = createTestAgent(execEnvServices({ hostEnvironment: { homeDir } }));
|
||||
ctx = createTestAgent(hostEnvironmentServices(homeDir));
|
||||
return { ctx, profile: ctx.get(IAgentProfileService) };
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ describe('ConfigState model capabilities', () => {
|
|||
profile.update({ modelAlias: 'kimi-code/kimi-for-coding' });
|
||||
|
||||
expect(profile.getModel()).toBe('kimi-code/kimi-for-coding');
|
||||
expect(profile.data().provider?.model).toBe('kimi-for-coding');
|
||||
expect(profile.resolveModel()?.name).toBe('kimi-for-coding');
|
||||
expect(profile.getModelCapabilities()).toMatchObject({
|
||||
image_in: true,
|
||||
video_in: true,
|
||||
|
|
@ -187,12 +187,11 @@ describe('ConfigState prompt cache hint', () => {
|
|||
it('uses session id as a provider prompt cache hint without storing it on Agent', () => {
|
||||
profile.update({ modelAlias: 'kimi-code' });
|
||||
|
||||
expect(profile.data().provider).toMatchObject({
|
||||
type: 'kimi',
|
||||
generationKwargs: {
|
||||
prompt_cache_key: 'session-test',
|
||||
},
|
||||
});
|
||||
// The session id is now applied to the resolved `Model`'s generation kwargs
|
||||
// (`prompt_cache_key`) by `AgentProfileService.resolveModel` for kimi
|
||||
// models; the `Model` god-object no longer exposes the raw provider config,
|
||||
// so we assert the resolved protocol and the "not stored on Agent" invariant.
|
||||
expect(profile.resolveModel()?.protocol).toBe('kimi');
|
||||
expect('sessionId' in ctx).toBe(false);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import { IBootstrapService } from '#/app/bootstrap/bootstrap';
|
|||
import { IConfigService } from '#/app/config/config';
|
||||
import type { GenerationKwargs } from '#/app/llmProtocol/kimiOptions';
|
||||
import type { ThinkingEffort } from '#/app/llmProtocol/thinkingEffort';
|
||||
import { type Model } from '#/app/model/modelInstance';
|
||||
import { type LLMEvent, type Model } from '#/app/model/modelInstance';
|
||||
import { IModelResolver } from '#/app/model/modelResolver';
|
||||
import { ITelemetryService } from '#/app/telemetry/telemetry';
|
||||
import { IHostEnvironment } from '#/os/interface/hostEnvironment';
|
||||
|
|
@ -179,7 +179,8 @@ function createRecordingModel(
|
|||
return build(thinkingEffort);
|
||||
},
|
||||
request: async function* () {
|
||||
return;
|
||||
const events: LLMEvent[] = [];
|
||||
for (const event of events) yield event;
|
||||
},
|
||||
});
|
||||
return build(null);
|
||||
|
|
@ -249,7 +250,7 @@ describe('AgentProfileService (wire-backed config.update)', () => {
|
|||
},
|
||||
});
|
||||
|
||||
host.wire.replay(...records);
|
||||
void host.wire.replay(...records);
|
||||
expect(modelOf(host.wire).cwd).toBe('/work');
|
||||
expect(modelOf(host.wire).profileName).toBe(DEFAULT_AGENT_PROFILE_NAME);
|
||||
expect(replayChdir).toBe(0);
|
||||
|
|
@ -269,7 +270,7 @@ describe('AgentProfileService (wire-backed config.update)', () => {
|
|||
// Fresh host whose config section would resolve differently is irrelevant:
|
||||
// the persisted resolved value ('on') is restored verbatim.
|
||||
const host = buildHost('profile-replay-thinking');
|
||||
host.wire.replay(...records);
|
||||
void host.wire.replay(...records);
|
||||
expect(modelOf(host.wire).thinkingLevel).toBe('on');
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import {
|
|||
} from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
|
||||
import { describe, it } from 'vitest';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { SyncDescriptor } from '#/_base/di/descriptors';
|
||||
import { TestInstantiationService } from '#/_base/di/test';
|
||||
|
|
@ -103,14 +103,16 @@ describe('resume round-trip diff diagnosis', () => {
|
|||
await wire2.restore();
|
||||
const r2 = wire2.getRecords();
|
||||
|
||||
expect(r2.length).toBe(r1.length);
|
||||
|
||||
const a = r1[index] as Record<string, unknown> | undefined;
|
||||
const b = r2[index] as Record<string, unknown> | undefined;
|
||||
if (a === undefined || b === undefined) {
|
||||
console.log(`r1[${index}] type=${a?.type}, r2[${index}] type=${b?.type} (one side undefined)`);
|
||||
console.log(`r1[${index}] type=${String(a?.['type'])}, r2[${index}] type=${String(b?.['type'])} (one side undefined)`);
|
||||
continue;
|
||||
}
|
||||
const diff = fieldDiff(a, b);
|
||||
console.log(`r1[${index}].type=${a.type}, r2[${index}].type=${b.type}`);
|
||||
console.log(`r1[${index}].type=${String(a['type'])}, r2[${index}].type=${String(b['type'])}`);
|
||||
console.log(`changed fields: ${Object.keys(diff).join(', ') || '(none)'}`);
|
||||
for (const [key, value] of Object.entries(diff)) {
|
||||
console.log(`--- field "${key}" ---`);
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ import {
|
|||
} from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
|
||||
import { describe, it } from 'vitest';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { SyncDescriptor } from '#/_base/di/descriptors';
|
||||
import { TestInstantiationService } from '#/_base/di/test';
|
||||
|
|
@ -183,7 +183,7 @@ async function mapPool<T, R>(
|
|||
fn: (item: T, index: number) => Promise<R>,
|
||||
onProgress?: (done: number) => void,
|
||||
): Promise<R[]> {
|
||||
const results: R[] = new Array(items.length);
|
||||
const results = Array.from<R>({ length: items.length });
|
||||
let next = 0;
|
||||
let done = 0;
|
||||
async function worker(): Promise<void> {
|
||||
|
|
@ -262,5 +262,8 @@ describe('resume round-trip over real ~/.kimi-code wire logs', () => {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
expect(restoreErrors).toEqual([]);
|
||||
expect(mismatches).toEqual([]);
|
||||
}, 60 * 60 * 1000);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -665,7 +665,7 @@ function context(
|
|||
signal = new AbortController().signal,
|
||||
onForegroundTaskStart?: (taskId: string) => void,
|
||||
) {
|
||||
return { turnId: '0', toolCallId: 'call_bash', args, signal, onForegroundTaskStart };
|
||||
return { turnId: 0, toolCallId: 'call_bash', args, signal, onForegroundTaskStart };
|
||||
}
|
||||
|
||||
function isPromiseLike(value: ToolExecution | Promise<ToolExecution>): value is Promise<ToolExecution> {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import type { ToolCall } from '#/app/llmProtocol/message';
|
|||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { IAgentContextMemoryService } from '#/agent/contextMemory/contextMemory';
|
||||
import { IAgentEventSinkService } from '#/agent/eventSink';
|
||||
import { IEventBus } from '#/app/event/eventBus';
|
||||
import { IAgentProfileService } from '#/agent/profile/profile';
|
||||
import { InMemorySkillCatalog } from '#/app/skillCatalog/registry';
|
||||
import { type SkillCatalog, type SkillDefinition } from '#/app/skillCatalog/types';
|
||||
|
|
@ -295,8 +295,8 @@ describe('ToolManager SkillTool restore behavior', () => {
|
|||
telemetryServices(telemetry),
|
||||
);
|
||||
context = ctx.get(IAgentContextMemoryService);
|
||||
const events = ctx.get(IAgentEventSinkService);
|
||||
emit = vi.spyOn(events, 'emit');
|
||||
const events = ctx.get(IEventBus);
|
||||
emit = vi.spyOn(events, 'publish');
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ describe('JsonAtomicDocumentStore', () => {
|
|||
});
|
||||
});
|
||||
await config.set<State>('session', 'state.json', { title: 'x' });
|
||||
await fired;
|
||||
await expect(fired).resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -139,6 +139,6 @@ describe('TomlAtomicDocumentStore', () => {
|
|||
});
|
||||
});
|
||||
await config.set<State>('session', 'config.toml', { title: 'x' });
|
||||
await fired;
|
||||
await expect(fired).resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ function storageServiceSuite(
|
|||
});
|
||||
await settle();
|
||||
await service.write('s', 'k', enc.encode('v'));
|
||||
await fired;
|
||||
await expect(fired).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('watch does not fire for an unrelated key', async ({ skip }) => {
|
||||
|
|
@ -127,7 +127,7 @@ function storageServiceSuite(
|
|||
});
|
||||
await settle();
|
||||
await service.delete('s', 'k');
|
||||
await fired;
|
||||
await expect(fired).resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -123,7 +123,7 @@ describe('AgentSwarmService', () => {
|
|||
new SyncDescriptor(WireService, [{ logScope: 'wire', logKey: 'swarm-replay' }]),
|
||||
);
|
||||
const fresh = ix2.get(IAgentWireService);
|
||||
fresh.replay(...records);
|
||||
void fresh.replay(...records);
|
||||
expect(fresh.getModel(SwarmModel)).toBe('manual');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import {
|
|||
IAgentTaskService,
|
||||
type AgentTaskInfo,
|
||||
} from '#/agent/task/task';
|
||||
import { IAgentEventSinkService } from '#/agent/eventSink';
|
||||
import { IEventBus } from '#/app/event/eventBus';
|
||||
import {
|
||||
taskServices,
|
||||
createTestAgent,
|
||||
|
|
@ -63,8 +63,8 @@ describe('Background reconcile — stale ghost detection', () => {
|
|||
ctx = createTestAgent(homeDirServices(sessionDir), taskServices());
|
||||
background = ctx.get(IAgentTaskService) as TaskServiceTestManager;
|
||||
emittedEvents = [];
|
||||
const events = ctx.get(IAgentEventSinkService);
|
||||
events.on((event) => {
|
||||
const events = ctx.get(IEventBus);
|
||||
events.subscribe((event) => {
|
||||
emittedEvents.push(event);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import {
|
|||
IAgentTaskService,
|
||||
type AgentTaskInfo,
|
||||
} from '#/agent/task/task';
|
||||
import { IAgentEventSinkService } from '#/agent/eventSink';
|
||||
import { IEventBus } from '#/app/event/eventBus';
|
||||
import {
|
||||
taskServices,
|
||||
createTestAgent,
|
||||
|
|
@ -91,8 +91,8 @@ describe('AgentTaskService — loadFromDisk + reconcile', () => {
|
|||
ctx = createTestAgent(homeDirServices(sessionDir), taskServices());
|
||||
background = ctx.get(IAgentTaskService) as TaskServiceTestManager;
|
||||
emittedEvents = [];
|
||||
const events = ctx.get(IAgentEventSinkService);
|
||||
events.on((event) => {
|
||||
const events = ctx.get(IEventBus);
|
||||
events.subscribe((event) => {
|
||||
emittedEvents.push(event);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import {
|
|||
} from '#/session/agentLifecycle/tools/subagent-task';
|
||||
import { ProcessTask } from '#/os/backends/node-local/tools/process-task';
|
||||
import { IAgentContextMemoryService } from '#/agent/contextMemory/contextMemory';
|
||||
import { IAgentEventSinkService } from '#/agent/eventSink';
|
||||
import { IEventBus } from '#/app/event/eventBus';
|
||||
import type { IExternalHooksRunnerService } from '#/app/externalHooksRunner/externalHooksRunner';
|
||||
import { IAgentPromptService } from '#/agent/prompt/prompt';
|
||||
import { ISessionMetadata } from '#/session/sessionMetadata/sessionMetadata';
|
||||
|
|
@ -200,8 +200,8 @@ function createAgentTaskService(options: {
|
|||
const ctx = createTestAgent(...overrides);
|
||||
|
||||
const emittedEvents: Array<{ type: string; info?: unknown }> = [];
|
||||
const events = ctx.get(IAgentEventSinkService);
|
||||
const disposable = events.on((event) => {
|
||||
const events = ctx.get(IEventBus);
|
||||
const disposable = events.subscribe((event) => {
|
||||
emittedEvents.push(event as { type: string; info?: unknown });
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -108,7 +108,7 @@ class EchoTool implements ExecutableTool<Record<string, unknown>> {
|
|||
constructor(
|
||||
readonly name = 'Echo',
|
||||
private readonly resultFor: (args: Record<string, unknown>) => ExecutableToolResult = (args) => ({
|
||||
output: String(args['text'] ?? ''),
|
||||
output: typeof args['text'] === 'string' ? args['text'] : '',
|
||||
}),
|
||||
) {}
|
||||
|
||||
|
|
|
|||
|
|
@ -338,7 +338,8 @@ describe('AgentToolExecutorService', () => {
|
|||
],
|
||||
{ turnId: 0, signal: new AbortController().signal },
|
||||
)) {
|
||||
yielded.push(String(item.result.output));
|
||||
const output = item.result.output;
|
||||
yielded.push(typeof output === 'string' ? output : JSON.stringify(output));
|
||||
if (yielded.length === 1) firstYielded.resolve();
|
||||
}
|
||||
})();
|
||||
|
|
@ -720,7 +721,9 @@ class TestTool implements ExecutableTool<Record<string, unknown>> {
|
|||
if (this.options.execute !== undefined) {
|
||||
return this.options.execute(ctx, args);
|
||||
}
|
||||
return this.options.result ?? { output: String(args['text'] ?? `${this.name} result`) };
|
||||
return this.options.result ?? {
|
||||
output: typeof args['text'] === 'string' ? args['text'] : `${this.name} result`,
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -368,7 +368,7 @@ describe('AgentTurnService wire state', () => {
|
|||
const log2 = ix2.get(IAppendLogStore);
|
||||
const fresh = ix2.get(IAgentWireService);
|
||||
|
||||
fresh.replay(...records);
|
||||
void fresh.replay(...records);
|
||||
|
||||
// nextTurnId advances past the replayed turnId (0 -> 1).
|
||||
expect(fresh.getModel(TurnModel)).toEqual({ nextTurnId: 1 });
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ import { setTimeout as delay } from 'node:timers/promises';
|
|||
import { type ModelCapability } from '#/app/llmProtocol/capability';
|
||||
import { APIConnectionError, APIEmptyResponseError, APIStatusError, APITimeoutError } from '#/app/llmProtocol/errors';
|
||||
import { type ToolCall } from '#/app/llmProtocol/message';
|
||||
import { type ProviderRequestAuth } from '#/app/llmProtocol/request';
|
||||
import type { ChatProvider } from '#/app/llmProtocol/provider';
|
||||
import { IProtocolAdapterRegistry } from '#/app/protocol/protocol';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { abortError, abortable } from '#/_base/utils/abort';
|
||||
|
|
@ -22,8 +22,7 @@ import { makeHookRunner } from '../externalHooks/runner-stub';
|
|||
import type { ILogger as Logger, LogPayload } from '#/_base/log/log';
|
||||
import { IAgentMcpService } from '#/agent/mcp/mcp';
|
||||
import { McpConnectionManager } from '#/agent/mcp/connection-manager';
|
||||
import { registerMediaTools } from '#/agent/media/registerMediaTools';
|
||||
import { type VideoUploader } from '#/agent/media/tools/read-media';
|
||||
import { createVideoUploader, registerMediaTools } from '#/agent/media/registerMediaTools';
|
||||
import { IAgentPermissionGate } from '#/agent/permissionGate/permissionGate';
|
||||
import { IAgentProfileService } from '#/agent/profile/profile';
|
||||
import { IAgentSwarmService } from '#/agent/swarm/swarm';
|
||||
|
|
@ -1753,10 +1752,27 @@ describe('Agent turn flow', () => {
|
|||
throw new APIStatusError(401, 'Unauthorized', 'req-upload-401');
|
||||
}),
|
||||
} as unknown as ChatProvider;
|
||||
const ctx = testAgent(oauthOptions.services, execEnvServices({ hostFs: createVideoHostFs() }), {
|
||||
initialConfig: oauthOptions.initialConfig,
|
||||
autoConfigure: false,
|
||||
});
|
||||
// The OAuth force-refresh-on-401 behavior now lives inside the `Model`
|
||||
// god-object (`uploadVideo` runs the provider call through the auth-refresh
|
||||
// wrapper). Inject the fake provider via the protocol registry so the
|
||||
// resolved Model uses it, then bind `model.uploadVideo` into the media
|
||||
// tool's `VideoUploader` shape the way the production wiring does.
|
||||
const fakeRegistry = {
|
||||
_serviceBrand: undefined,
|
||||
supportedProtocols: () => ['vertexai'] as const,
|
||||
createChatProvider: () => provider,
|
||||
} satisfies IProtocolAdapterRegistry & { createChatProvider: () => ChatProvider };
|
||||
const ctx = testAgent(
|
||||
oauthOptions.services,
|
||||
appServices((reg) => {
|
||||
reg.defineInstance(IProtocolAdapterRegistry, fakeRegistry);
|
||||
}),
|
||||
execEnvServices({ hostFs: createVideoHostFs() }),
|
||||
{
|
||||
initialConfig: oauthOptions.initialConfig,
|
||||
autoConfigure: false,
|
||||
},
|
||||
);
|
||||
const profile = ctx.get(IAgentProfileService);
|
||||
profile.update({
|
||||
cwd: '/workspace',
|
||||
|
|
@ -1764,14 +1780,8 @@ describe('Agent turn flow', () => {
|
|||
systemPrompt: 'test system prompt',
|
||||
thinkingLevel: 'off',
|
||||
});
|
||||
const withAuth = ctx.modelResolver.resolveAuth?.('kimi-code');
|
||||
if (withAuth === undefined) throw new Error('OAuth model did not resolve auth wrapper');
|
||||
const videoUploader: VideoUploader = (input) =>
|
||||
withAuth((auth: ProviderRequestAuth) => {
|
||||
const uploadVideo = provider.uploadVideo;
|
||||
if (uploadVideo === undefined) throw new Error('Provider did not expose uploadVideo');
|
||||
return uploadVideo.call(provider, input, { auth });
|
||||
});
|
||||
const videoUploader = createVideoUploader(ctx.modelResolver.resolve('kimi-code'));
|
||||
if (videoUploader === undefined) throw new Error('OAuth model did not resolve a video uploader');
|
||||
const registration = registerMediaTools(ctx.get(IAgentToolRegistryService), {
|
||||
fs: ctx.get(IHostFileSystem),
|
||||
env: ctx.get(IHostEnvironment),
|
||||
|
|
|
|||
|
|
@ -160,7 +160,7 @@ describe('AgentUsageService (wire-backed)', () => {
|
|||
const log2 = ix2.get(IAppendLogStore);
|
||||
const fresh = ix2.get(IAgentWireService);
|
||||
|
||||
fresh.replay(...records);
|
||||
void fresh.replay(...records);
|
||||
|
||||
expect(fresh.getModel(UsageModel).byModel).toEqual({
|
||||
'model-a': { inputOther: 11, output: 22, inputCacheRead: 33, inputCacheCreation: 44 },
|
||||
|
|
|
|||
|
|
@ -98,6 +98,7 @@ class MemoryHostFs implements IHostFileSystem {
|
|||
}
|
||||
|
||||
async *readLines(): AsyncGenerator<string> {
|
||||
yield* [];
|
||||
throw new Error('not implemented');
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@
|
|||
* - logout → delegates to facade.logout
|
||||
*/
|
||||
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { assert, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import {
|
||||
DeviceCodeTimeoutError,
|
||||
|
|
@ -129,7 +129,7 @@ describe('OAuthService.startLogin', () => {
|
|||
await mock.loginCalls[0]!.onDeviceCode?.(auth);
|
||||
|
||||
const start = await startPromise;
|
||||
expect(start.status).toBe('pending');
|
||||
assert(start.status === 'pending');
|
||||
expect(start.flow_id).toMatch(/^oauth_/);
|
||||
expect(start.verification_uri).toBe(auth.verificationUri);
|
||||
expect(start.verification_uri_complete).toBe(auth.verificationUriComplete);
|
||||
|
|
@ -147,6 +147,7 @@ describe('OAuthService.startLogin', () => {
|
|||
fakeDeviceAuth({ expiresIn: null }),
|
||||
);
|
||||
const start = await startPromise;
|
||||
assert(start.status === 'pending');
|
||||
expect(start.expires_in).toBe(15 * 60);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -313,15 +313,15 @@ function sendMappedError(
|
|||
}
|
||||
|
||||
function authProviderDetails(err: KimiError): { provider_id: string } | undefined {
|
||||
const providerId = err.details?.provider_id;
|
||||
const providerId = err.details?.['provider_id'];
|
||||
if (typeof providerId !== 'string') return undefined;
|
||||
return { provider_id: providerId };
|
||||
}
|
||||
|
||||
function authModelDetails(err: KimiError): { model_id?: string; provider_id?: string } | null {
|
||||
const details: { model_id?: string; provider_id?: string } = {};
|
||||
const modelId = err.details?.model_id;
|
||||
const providerId = err.details?.provider_id;
|
||||
const modelId = err.details?.['model_id'];
|
||||
const providerId = err.details?.['provider_id'];
|
||||
if (typeof modelId === 'string') details.model_id = modelId;
|
||||
if (typeof providerId === 'string') details.provider_id = providerId;
|
||||
return Object.keys(details).length === 0 ? null : details;
|
||||
|
|
|
|||
|
|
@ -61,7 +61,6 @@ import {
|
|||
IAuthSummaryService,
|
||||
ISessionBtwService,
|
||||
ISessionActivity,
|
||||
ISessionContext,
|
||||
ISessionIndex,
|
||||
ISessionLifecycleService,
|
||||
ISessionMetadata,
|
||||
|
|
|
|||
|
|
@ -41,7 +41,6 @@ import {
|
|||
} from '@moonshot-ai/agent-core-v2';
|
||||
import type {
|
||||
InFlightTurn,
|
||||
Message,
|
||||
SessionSnapshotResponse,
|
||||
SessionStatus,
|
||||
} from '@moonshot-ai/protocol';
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
*
|
||||
* Only the high-value streams are exposed for now:
|
||||
* Core `events` — process-wide `GlobalEvent` bus (`IEventService`)
|
||||
* Session `interactions` — pending human-in-the-loop requests (`ISessionInteractionService.onDidChange`)
|
||||
* Session `interactions` — pending human-in-the-loop requests (`ISessionInteractionService.onDidChangePending`)
|
||||
* Session `interactions:resolved` — request resolutions (`ISessionInteractionService.onDidResolve`)
|
||||
* Agent `events` — per-agent `AgentEvent` stream (live events via
|
||||
* the per-agent `IEventBus`)
|
||||
|
|
@ -44,7 +44,7 @@ export const eventMap: Record<ScopeKind, Record<string, EventSource>> = {
|
|||
interactions: {
|
||||
subscribe: (scope, listener) => {
|
||||
const interaction = scope.accessor.get(ISessionInteractionService);
|
||||
return interaction.onDidChange(() => listener(interaction.listPending()));
|
||||
return interaction.onDidChangePending(() => listener(interaction.listPending()));
|
||||
},
|
||||
},
|
||||
// Pushes `{ id, response }` whenever a pending request is responded to.
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ export class WsClient {
|
|||
this.ws.on('open', () => {
|
||||
this.ws.send(JSON.stringify({ type: 'hello', token: opts.token }));
|
||||
});
|
||||
this.ws.on('message', (data) => this.onMessage(data.toString()));
|
||||
this.ws.on('message', (data) => this.onMessage((data as Buffer).toString()));
|
||||
this.ws.on('error', (err) => {
|
||||
this.rejectReady(err);
|
||||
for (const p of this.pending.values()) p.reject(err);
|
||||
|
|
|
|||
|
|
@ -143,7 +143,7 @@ export class WsConnection {
|
|||
this.cancel(msg.id);
|
||||
return;
|
||||
case 'listen':
|
||||
this.onListen(msg);
|
||||
void this.onListen(msg);
|
||||
return;
|
||||
case 'unlisten':
|
||||
this.cancel(msg.id);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
|
||||
import { modelResolverSeed, SingleModelResolver } from '@moonshot-ai/agent-core-v2';
|
||||
import { IModelResolver } from '@moonshot-ai/agent-core-v2';
|
||||
import { ErrorCode } from '@moonshot-ai/protocol';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
|
|
@ -38,17 +38,19 @@ describe('server-v2 /api/v1/sessions/{sid}/fs:*', () => {
|
|||
beforeEach(async () => {
|
||||
home = await mkdtemp(join(tmpdir(), 'kimi-server-v2-fs-home-'));
|
||||
work = await mkdtemp(join(tmpdir(), 'kimi-server-v2-fs-work-'));
|
||||
const modelResolver = new SingleModelResolver({
|
||||
type: 'openai',
|
||||
model: 'stub',
|
||||
apiKey: 'stub',
|
||||
});
|
||||
const modelResolver: IModelResolver = {
|
||||
_serviceBrand: undefined,
|
||||
resolve: () => {
|
||||
throw new Error('modelResolver.resolve not exercised in this test');
|
||||
},
|
||||
findByName: () => [],
|
||||
};
|
||||
server = await startServer({
|
||||
host: '127.0.0.1',
|
||||
port: 0,
|
||||
homeDir: home,
|
||||
logLevel: 'silent',
|
||||
seeds: modelResolverSeed(modelResolver),
|
||||
seeds: [[IModelResolver, modelResolver]],
|
||||
});
|
||||
base = `http://127.0.0.1:${server.port}`;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -7,8 +7,7 @@ import {
|
|||
IAgentLifecycleService,
|
||||
IAgentWireRecordService,
|
||||
ISessionLifecycleService,
|
||||
modelResolverSeed,
|
||||
SingleModelResolver,
|
||||
IModelResolver,
|
||||
type ScopeSeed,
|
||||
} from '@moonshot-ai/agent-core-v2';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
|
|
@ -50,12 +49,14 @@ describe('server-v2 /api/v1/sessions/{sid}/messages', () => {
|
|||
home = await mkdtemp(join(tmpdir(), 'kimi-server-v2-messages-'));
|
||||
// Seed a stub ISessionModelResolver so the agent scope can instantiate if a
|
||||
// transitive service needs it; IContextMemory itself does not.
|
||||
const modelResolver = new SingleModelResolver({
|
||||
type: 'openai',
|
||||
model: 'stub',
|
||||
apiKey: 'stub',
|
||||
});
|
||||
seeds = modelResolverSeed(modelResolver);
|
||||
const modelResolver: IModelResolver = {
|
||||
_serviceBrand: undefined,
|
||||
resolve: () => {
|
||||
throw new Error('modelResolver.resolve not exercised in this test');
|
||||
},
|
||||
findByName: () => [],
|
||||
};
|
||||
seeds = [[IModelResolver, modelResolver]];
|
||||
await boot();
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -8,8 +8,9 @@ import { tmpdir } from 'node:os';
|
|||
import { join } from 'node:path';
|
||||
|
||||
import {
|
||||
type DomainEvent,
|
||||
IAgentContextMemoryService,
|
||||
IAgentEventSinkService,
|
||||
IEventBus,
|
||||
IAgentLifecycleService,
|
||||
IAgentPromptLegacyService,
|
||||
ILogService,
|
||||
|
|
@ -20,7 +21,7 @@ import {
|
|||
ISessionMetadata,
|
||||
IWorkspaceRegistry,
|
||||
} from '@moonshot-ai/agent-core-v2';
|
||||
import { sessionSnapshotResponseSchema, type AgentEvent } from '@moonshot-ai/protocol';
|
||||
import { sessionSnapshotResponseSchema } from '@moonshot-ai/protocol';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import { registerSnapshotRoutes } from '../src/routes/snapshot';
|
||||
|
|
@ -260,10 +261,10 @@ describe('server-v2 GET /api/v1/sessions/:id/snapshot', () => {
|
|||
if (agents.getHandle('main') === undefined) await agents.create({ agentId: 'main' });
|
||||
}
|
||||
|
||||
function emit(sessionId: string, event: AgentEvent): void {
|
||||
function emit(sessionId: string, event: DomainEvent): void {
|
||||
const session = server!.core.accessor.get(ISessionLifecycleService).get(sessionId);
|
||||
const main = session!.accessor.get(IAgentLifecycleService).getHandle('main');
|
||||
main!.accessor.get(IAgentEventSinkService).emit(event);
|
||||
main!.accessor.get(IEventBus).publish(event);
|
||||
}
|
||||
|
||||
async function snapshot(sid: string) {
|
||||
|
|
@ -296,8 +297,8 @@ describe('server-v2 GET /api/v1/sessions/:id/snapshot', () => {
|
|||
emit(sid, {
|
||||
type: 'turn.started',
|
||||
turnId: 1,
|
||||
} as unknown as AgentEvent); // durable → seq 1
|
||||
emit(sid, { type: 'assistant.delta', turnId: 1, delta: 'Hello' } as unknown as AgentEvent); // volatile
|
||||
} as unknown as DomainEvent); // durable → seq 1
|
||||
emit(sid, { type: 'assistant.delta', turnId: 1, delta: 'Hello' } as unknown as DomainEvent); // volatile
|
||||
|
||||
const snap = await snapshot(sid);
|
||||
expect(snap.as_of_seq).toBe(1);
|
||||
|
|
|
|||
|
|
@ -6,8 +6,7 @@ import {
|
|||
IAgentLifecycleService,
|
||||
IAgentTaskService,
|
||||
ISessionLifecycleService,
|
||||
modelResolverSeed,
|
||||
SingleModelResolver,
|
||||
IModelResolver,
|
||||
type AgentTask,
|
||||
} from '@moonshot-ai/agent-core-v2';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
|
|
@ -50,17 +49,19 @@ describe('server-v2 /api/v1/sessions/{sid}/tasks', () => {
|
|||
home = await mkdtemp(join(tmpdir(), 'kimi-server-v2-tasks-'));
|
||||
// Seed a stub ISessionModelResolver so the agent scope can instantiate if a
|
||||
// transitive service needs it; IAgentTaskService itself does not.
|
||||
const modelResolver = new SingleModelResolver({
|
||||
type: 'openai',
|
||||
model: 'stub',
|
||||
apiKey: 'stub',
|
||||
});
|
||||
const modelResolver: IModelResolver = {
|
||||
_serviceBrand: undefined,
|
||||
resolve: () => {
|
||||
throw new Error('modelResolver.resolve not exercised in this test');
|
||||
},
|
||||
findByName: () => [],
|
||||
};
|
||||
server = await startServer({
|
||||
host: '127.0.0.1',
|
||||
port: 0,
|
||||
homeDir: home,
|
||||
logLevel: 'silent',
|
||||
seeds: modelResolverSeed(modelResolver),
|
||||
seeds: [[IModelResolver, modelResolver]],
|
||||
});
|
||||
base = `http://127.0.0.1:${server.port}`;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -21,8 +21,7 @@ import {
|
|||
IAgentLifecycleService,
|
||||
IAgentToolRegistryService,
|
||||
ISessionLifecycleService,
|
||||
modelResolverSeed,
|
||||
SingleModelResolver,
|
||||
IModelResolver,
|
||||
type ExecutableTool,
|
||||
} from '@moonshot-ai/agent-core-v2';
|
||||
import {
|
||||
|
|
@ -56,17 +55,19 @@ describe('server-v2 /api/v1 tools + mcp', () => {
|
|||
|
||||
beforeEach(async () => {
|
||||
home = await mkdtemp(join(tmpdir(), 'kimi-server-v2-tools-'));
|
||||
const modelResolver = new SingleModelResolver({
|
||||
type: 'openai',
|
||||
model: 'stub',
|
||||
apiKey: 'stub',
|
||||
});
|
||||
const modelResolver: IModelResolver = {
|
||||
_serviceBrand: undefined,
|
||||
resolve: () => {
|
||||
throw new Error('modelResolver.resolve not exercised in this test');
|
||||
},
|
||||
findByName: () => [],
|
||||
};
|
||||
server = await startServer({
|
||||
host: '127.0.0.1',
|
||||
port: 0,
|
||||
homeDir: home,
|
||||
logLevel: 'silent',
|
||||
seeds: modelResolverSeed(modelResolver),
|
||||
seeds: [[IModelResolver, modelResolver]],
|
||||
});
|
||||
base = `http://127.0.0.1:${server.port}`;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -9,11 +9,11 @@ import { tmpdir } from 'node:os';
|
|||
import { join } from 'node:path';
|
||||
|
||||
import {
|
||||
IAgentEventSinkService,
|
||||
type DomainEvent,
|
||||
IEventBus,
|
||||
IAgentLifecycleService,
|
||||
ISessionLifecycleService,
|
||||
} from '@moonshot-ai/agent-core-v2';
|
||||
import type { AgentEvent } from '@moonshot-ai/protocol';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
import { WebSocket } from 'ws';
|
||||
|
||||
|
|
@ -48,7 +48,7 @@ function openConn(url: string, token: string): Promise<Conn> {
|
|||
ws.on('message', (data) => {
|
||||
let frame: Frame;
|
||||
try {
|
||||
frame = JSON.parse(data.toString()) as Frame;
|
||||
frame = JSON.parse((data as Buffer).toString()) as Frame;
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
|
@ -139,13 +139,13 @@ describe('server-v2 /api/v1/ws resync', () => {
|
|||
return { ...payload, token: server!.authTokenService.getToken() };
|
||||
}
|
||||
|
||||
function emitAgentEvent(sessionId: string, event: AgentEvent): void {
|
||||
function emitAgentEvent(sessionId: string, event: DomainEvent): void {
|
||||
const session = server!.core.accessor.get(ISessionLifecycleService).get(sessionId);
|
||||
expect(session).toBeDefined();
|
||||
const agents = session!.accessor.get(IAgentLifecycleService);
|
||||
const main = agents.getHandle('main');
|
||||
expect(main).toBeDefined();
|
||||
main!.accessor.get(IAgentEventSinkService).emit(event);
|
||||
main!.accessor.get(IEventBus).publish(event);
|
||||
}
|
||||
|
||||
it('server_hello then client_hello ack with accepted subscription', async () => {
|
||||
|
|
@ -175,7 +175,7 @@ describe('server-v2 /api/v1/ws resync', () => {
|
|||
c.send({ type: 'client_hello', id: 'h1', payload: withToken({ client_id: 'cli', subscriptions: [sid] }) });
|
||||
await c.next((f) => f.type === 'ack' && f.id === 'h1');
|
||||
|
||||
emitAgentEvent(sid, { type: 'turn.started', turnId: 1 } as unknown as AgentEvent);
|
||||
emitAgentEvent(sid, { type: 'turn.started', turnId: 1 } as unknown as DomainEvent);
|
||||
|
||||
const ev = await c.next((f) => f.type === 'turn.started');
|
||||
expect(ev.seq).toBe(1);
|
||||
|
|
@ -195,8 +195,8 @@ describe('server-v2 /api/v1/ws resync', () => {
|
|||
await c1.next((f) => f.type === 'server_hello');
|
||||
c1.send({ type: 'client_hello', id: 'h1', payload: withToken({ client_id: 'cli', subscriptions: [sid] }) });
|
||||
await c1.next((f) => f.type === 'ack' && f.id === 'h1');
|
||||
emitAgentEvent(sid, { type: 'turn.started', turnId: 1 } as unknown as AgentEvent);
|
||||
emitAgentEvent(sid, { type: 'turn.ended', turnId: 1 } as unknown as AgentEvent);
|
||||
emitAgentEvent(sid, { type: 'turn.started', turnId: 1 } as unknown as DomainEvent);
|
||||
emitAgentEvent(sid, { type: 'turn.ended', turnId: 1 } as unknown as DomainEvent);
|
||||
await c1.next((f) => f.type === 'turn.ended');
|
||||
c1.ws.close();
|
||||
await c1.closed;
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ async function tmpDir() {
|
|||
return fs.mkdtemp(path.join(os.tmpdir(), 'baseline-bench-'));
|
||||
}
|
||||
|
||||
async function bench(label: string, fn: () => Promise<unknown> | unknown) {
|
||||
async function bench(label: string, fn: () => unknown) {
|
||||
if (global.gc) global.gc();
|
||||
const t0 = performance.now();
|
||||
await fn();
|
||||
|
|
|
|||
|
|
@ -28,7 +28,6 @@ const gc = (): void => {
|
|||
}
|
||||
};
|
||||
const mib = (n: number): string => (n / 1048576).toFixed(1) + ' MiB';
|
||||
const kb = (n: number): string => (n / 1024).toFixed(1) + ' KiB';
|
||||
const fmt = (n: number): string => n.toLocaleString('en-US', { maximumFractionDigits: 0 });
|
||||
|
||||
interface Snap {
|
||||
|
|
|
|||
|
|
@ -86,11 +86,10 @@ function loadAllMessages(): Msg[] {
|
|||
function scaleTo(real: Msg[], n: number): Msg[] {
|
||||
const now = Date.now();
|
||||
const span = WINDOW_DAYS * 864e5;
|
||||
const out: Msg[] = new Array(n);
|
||||
for (let i = 0; i < n; i++) {
|
||||
const out: Msg[] = Array.from({ length: n }, (_, i) => {
|
||||
const r = real[i % real.length]!;
|
||||
out[i] = { id: `m${i}`, ts: now - Math.floor((i / n) * span), role: r.role, text: r.text };
|
||||
}
|
||||
return { id: `m${i}`, ts: now - Math.floor((i / n) * span), role: r.role, text: r.text };
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -79,16 +79,15 @@ function loadRealMessages(): Msg[] {
|
|||
function scaleTo(real: Msg[], n: number): Msg[] {
|
||||
const now = Date.now();
|
||||
const span = WINDOW_DAYS * 864e5;
|
||||
const out: Msg[] = new Array(n);
|
||||
for (let i = 0; i < n; i++) {
|
||||
const out: Msg[] = Array.from({ length: n }, (_, i) => {
|
||||
const r = real[i % real.length]!;
|
||||
out[i] = {
|
||||
return {
|
||||
id: `m${i}`,
|
||||
ts: now - Math.floor((i / n) * span),
|
||||
role: r.role,
|
||||
text: r.text,
|
||||
};
|
||||
}
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ function extractWireText(wirePath: string, full: boolean): string {
|
|||
function tokenize(s: string): string[] {
|
||||
return s
|
||||
.toLowerCase()
|
||||
.split(/[\s,,。.!?!?、;;::()\[\]{}"'<>/\\|@#%^&*+=~`\-_]+/)
|
||||
.split(/[\s,,。.!?!?、;;::()[\]{}"'<>/\\|@#%^&*+=~`\-_]+/)
|
||||
.filter((t) => t.length > 0);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -134,11 +134,11 @@ async function main() {
|
|||
value: {
|
||||
title: `session ${n.id}`,
|
||||
workspaceId: 'ws_bench',
|
||||
metadata: {
|
||||
// Only real children carry parent_session_id (+ kind). Roots do
|
||||
// Only real children carry parent_session_id (+ kind). Roots do
|
||||
// not, so they stay out of the byParent index entirely.
|
||||
...(n.parentId ? { parent_session_id: n.parentId, child_session_kind: 'child' } : {}),
|
||||
},
|
||||
metadata: n.parentId
|
||||
? { parent_session_id: n.parentId, child_session_kind: 'child' }
|
||||
: {},
|
||||
},
|
||||
dt: { updatedAt: n.updatedAt },
|
||||
})),
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ interface CompoundEntry {
|
|||
}
|
||||
|
||||
function getPath(doc: unknown, path: string): unknown {
|
||||
return path.split('.').reduce<unknown>((o, k) => (o == null ? undefined : (o as Record<string, unknown>)[k]), doc);
|
||||
return path.split('.').reduce<unknown>((o, k) => (o === null || o === undefined ? undefined : (o as Record<string, unknown>)[k]), doc);
|
||||
}
|
||||
|
||||
export class CompoundIndexManager {
|
||||
|
|
@ -113,7 +113,7 @@ export class CompoundIndexManager {
|
|||
}
|
||||
}
|
||||
|
||||
remove(pk: string, doc?: unknown, dt?: Record<string, number> | null): void {
|
||||
remove(pk: string, _doc?: unknown, _dt?: Record<string, number> | null): void {
|
||||
for (const entry of this.indexes.values()) {
|
||||
const prev = entry.byPk.get(pk);
|
||||
if (prev) {
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ export class UniqueViolationError extends Error {
|
|||
}
|
||||
|
||||
function getField(doc: unknown, path: string): unknown {
|
||||
return path.split('.').reduce<unknown>((o, k) => (o == null ? undefined : (o as Record<string, unknown>)[k]), doc);
|
||||
return path.split('.').reduce<unknown>((o, k) => (o === null || o === undefined ? undefined : (o as Record<string, unknown>)[k]), doc);
|
||||
}
|
||||
|
||||
function stableStringify(v: unknown): string {
|
||||
|
|
@ -66,7 +66,7 @@ function stableStringify(v: unknown): string {
|
|||
|
||||
function scalarKey(v: unknown): string {
|
||||
const t = typeof v;
|
||||
if (t === 'string' || t === 'number' || t === 'boolean') return `${t}:${v}`;
|
||||
if (t === 'string' || t === 'number' || t === 'boolean') return `${t}:${String(v)}`;
|
||||
// Canonicalize property order so {a:1,b:2} and {b:2,a:1} hash to the same
|
||||
// key; JSON.stringify alone preserves insertion order and would split them.
|
||||
return `json:${stableStringify(v)}`;
|
||||
|
|
@ -290,8 +290,14 @@ export class IndexManager {
|
|||
const idx = this.get(name);
|
||||
if (idx.type !== 'range') throw new Error(`index "${name}" is not a range index`);
|
||||
const r: RangeOptions<number> = {};
|
||||
if (opts.min !== undefined) (opts.minExclusive ? (r.gt = opts.min) : (r.gte = opts.min));
|
||||
if (opts.max !== undefined) (opts.maxExclusive ? (r.lt = opts.max) : (r.lte = opts.max));
|
||||
if (opts.min !== undefined) {
|
||||
if (opts.minExclusive) r.gt = opts.min;
|
||||
else r.gte = opts.min;
|
||||
}
|
||||
if (opts.max !== undefined) {
|
||||
if (opts.maxExclusive) r.lt = opts.max;
|
||||
else r.lte = opts.max;
|
||||
}
|
||||
if (opts.offset) r.offset = opts.offset;
|
||||
if (opts.count !== undefined) r.count = opts.count;
|
||||
if (opts.reverse) r.reverse = true;
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ function tokenizePath(path: Path): (string | number)[] {
|
|||
export function getPath(doc: Doc, path: Path): unknown {
|
||||
let cur: unknown = doc;
|
||||
for (const t of tokenizePath(path)) {
|
||||
if (cur == null) return undefined;
|
||||
if (cur === null || cur === undefined) return undefined;
|
||||
cur = (cur as Record<string | number, unknown>)[t];
|
||||
}
|
||||
return cur;
|
||||
|
|
@ -43,7 +43,7 @@ export function setPath(obj: Doc, path: Path, value: unknown): Doc {
|
|||
let cur = obj as Record<string | number, unknown>;
|
||||
for (let i = 0; i < tokens.length - 1; i++) {
|
||||
const t = tokens[i]!;
|
||||
if (cur[t] == null || typeof cur[t] !== 'object') {
|
||||
if (cur[t] === null || cur[t] === undefined || typeof cur[t] !== 'object') {
|
||||
cur[t] = typeof tokens[i + 1] === 'number' ? [] : {};
|
||||
}
|
||||
cur = cur[t] as Record<string | number, unknown>;
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ const reply = {
|
|||
// utf8).
|
||||
bulk: (v: unknown): Buffer => {
|
||||
if (v === undefined || v === null) return Buffer.from(NIL);
|
||||
const b = Buffer.isBuffer(v) ? v : Buffer.from(String(v));
|
||||
const b = Buffer.isBuffer(v) ? v : Buffer.from(String(v as string));
|
||||
return Buffer.concat([Buffer.from(`$${b.length}${CRLF}`), b, Buffer.from(CRLF)]);
|
||||
},
|
||||
array: (items: unknown[]): Buffer => {
|
||||
|
|
@ -165,7 +165,7 @@ export async function startServer({ dir, port = 6379, host = '127.0.0.1', fsyncP
|
|||
const server = net.createServer((socket: Socket) => {
|
||||
const parser = new RespParser();
|
||||
socket.on('data', (chunk: Buffer) => {
|
||||
(async () => {
|
||||
void (async () => {
|
||||
try {
|
||||
for (const args of parser.feed(chunk)) {
|
||||
const res = await handle(db, args);
|
||||
|
|
|
|||
|
|
@ -76,8 +76,8 @@ export class SkipList<K = number, V = string> {
|
|||
}
|
||||
|
||||
insert(key: K, val: V): SkipNode<K, V> {
|
||||
const update = new Array<SkipNode<K, V>>(MAX_LEVEL);
|
||||
const rank = new Array<number>(MAX_LEVEL).fill(0);
|
||||
const update = Array.from<SkipNode<K, V>>({ length: MAX_LEVEL });
|
||||
const rank = Array.from({ length: MAX_LEVEL }, () => 0);
|
||||
let x: SkipNode<K, V> = this.header;
|
||||
const target = { key, val };
|
||||
|
||||
|
|
@ -134,7 +134,7 @@ export class SkipList<K = number, V = string> {
|
|||
}
|
||||
|
||||
delete(key: K, val: V): boolean {
|
||||
const update = new Array<SkipNode<K, V>>(MAX_LEVEL);
|
||||
const update = Array.from<SkipNode<K, V>>({ length: MAX_LEVEL });
|
||||
let x: SkipNode<K, V> = this.header;
|
||||
const target = { key, val };
|
||||
for (let i = this.level - 1; i >= 0; i--) {
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ export function tokenize(str: unknown): string[] {
|
|||
}
|
||||
|
||||
function stringLeaves(obj: unknown, acc: string[] = []): string[] {
|
||||
if (obj == null) return acc;
|
||||
if (obj === null || obj === undefined) return acc;
|
||||
if (typeof obj === 'string') {
|
||||
acc.push(obj);
|
||||
return acc;
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ export function encodePostingList(entries: readonly (readonly [number, number])[
|
|||
export function decodePostingList(buf: Buffer): [number, number][] {
|
||||
const cur = { i: 0 };
|
||||
const count = decodeVarint(buf, cur);
|
||||
const out: [number, number][] = new Array(count);
|
||||
const out = Array.from<[number, number]>({ length: count });
|
||||
let prev = 0;
|
||||
for (let k = 0; k < count; k++) {
|
||||
const d = decodeVarint(buf, cur);
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ export class WAL {
|
|||
this.queuedBytes += frame.length;
|
||||
if (!this.flushing && !this.scheduled) {
|
||||
this.scheduled = true;
|
||||
setImmediate(() => this.flushBatch());
|
||||
setImmediate(() => { void this.flushBatch(); });
|
||||
}
|
||||
});
|
||||
return { offset, done };
|
||||
|
|
@ -142,7 +142,7 @@ export class WAL {
|
|||
this.inflight = null;
|
||||
if (this.queue.length > 0 && !this.closed) {
|
||||
this.scheduled = true;
|
||||
setImmediate(() => this.flushBatch());
|
||||
setImmediate(() => { void this.flushBatch(); });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -78,8 +78,8 @@ test('crc mismatch throws CorruptFrameError with offset', () => {
|
|||
test('finish() reports a torn trailing partial frame at the valid-data offset', () => {
|
||||
const p = new FrameParser();
|
||||
const good = encodeFrame({ type: TYPE_SET, key: B('ok'), value: B('v') });
|
||||
[...p.feed(good)];
|
||||
[...p.feed(Buffer.from([0x4d, 0x44, 0x01]))]; // magic + type: incomplete header
|
||||
void [...p.feed(good)];
|
||||
void [...p.feed(Buffer.from([0x4d, 0x44, 0x01]))]; // magic + type: incomplete header
|
||||
assert.throws(
|
||||
() => p.finish(),
|
||||
(e) => e instanceof CorruptFrameError && e.offset === good.length,
|
||||
|
|
@ -90,7 +90,7 @@ test('finish() returns the clean EOF offset when there is no leftover', () => {
|
|||
const p = new FrameParser();
|
||||
const a = encodeFrame({ type: TYPE_SET, key: B('a'), value: B('1') });
|
||||
const b = encodeFrame({ type: TYPE_SET, key: B('b'), value: B('2') });
|
||||
[...p.feed(Buffer.concat([a, b]))];
|
||||
void [...p.feed(Buffer.concat([a, b]))];
|
||||
assert.equal(p.finish(), a.length + b.length);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
// fresh import of compaction.ts picks up that test's mocked fs.
|
||||
|
||||
import assert from 'node:assert/strict';
|
||||
import { afterEach, test, vi } from 'vitest';
|
||||
import { afterEach, expect, test, vi } from 'vitest';
|
||||
|
||||
interface MockHandle {
|
||||
read?: (...args: unknown[]) => Promise<{ bytesRead: number }>;
|
||||
|
|
@ -64,7 +64,7 @@ test('copyFileRange tolerates a source close() failure (best-effort close)', asy
|
|||
});
|
||||
const { copyFileRange } = await import('../src/compaction.js');
|
||||
// The source close failure is swallowed; the copy itself succeeds.
|
||||
await copyFileRange('/tmp/src', '/tmp/dst', 0, 1);
|
||||
await expect(copyFileRange('/tmp/src', '/tmp/dst', 0, 1)).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
test('copyFileRange tolerates a destination close() failure (best-effort close)', async () => {
|
||||
|
|
@ -83,7 +83,7 @@ test('copyFileRange tolerates a destination close() failure (best-effort close)'
|
|||
const { copyFileRange } = await import('../src/compaction.js');
|
||||
// Empty range (start===end) → no writes, only a dst.sync() then a failing
|
||||
// dst.close() that must be swallowed.
|
||||
await copyFileRange('/tmp/src', '/tmp/dst', 0, 0);
|
||||
await expect(copyFileRange('/tmp/src', '/tmp/dst', 0, 0)).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
test('fsyncDir swallows a sync() failure and still closes the handle', async () => {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
// their dynamic imports + query-string cache busting do not distort the
|
||||
// coverage report of this file (Node's coverage keys scripts by URL).
|
||||
|
||||
import { test } from 'vitest';
|
||||
import { expect, test } from 'vitest';
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs/promises';
|
||||
import os from 'node:os';
|
||||
|
|
@ -142,7 +142,7 @@ test('copyFileRange rejects end < start', async () => {
|
|||
test('fsyncDir syncs an existing directory without throwing', async () => {
|
||||
const dir = await tmpDir();
|
||||
try {
|
||||
await fsyncDir(dir); // should not throw
|
||||
await expect(fsyncDir(dir)).resolves.toBeUndefined();
|
||||
} finally {
|
||||
await fs.rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
|
|
@ -152,5 +152,5 @@ test('fsyncDir swallows errors when the directory cannot be opened', async () =>
|
|||
// A non-existent path makes fs.open reject → the catch branch runs and the
|
||||
// finally closes nothing (fh is null). Best-effort semantics.
|
||||
const bogus = path.join(os.tmpdir(), `minidb-no-such-dir-${Date.now()}`);
|
||||
await fsyncDir(bogus); // must not throw
|
||||
await expect(fsyncDir(bogus)).resolves.toBeUndefined();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
// Fault-injection-only branches (writev short-write, fsync failure, >64MB
|
||||
// RESP payload, cross-user EPERM, process-exit hook) are intentionally not
|
||||
// covered here — see the coverage summary in the commit message.
|
||||
import { test } from 'vitest';
|
||||
import { expect, test } from 'vitest';
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs/promises';
|
||||
import os from 'node:os';
|
||||
|
|
@ -106,10 +106,10 @@ test('WAL append after close rejects', async () => {
|
|||
test('WAL open and close are idempotent', async () => {
|
||||
const dir = await tmpDir();
|
||||
const wal = new WAL(path.join(dir, 'db.wal'), { fsyncPolicy: 'everysec' });
|
||||
await wal.open();
|
||||
await wal.open(); // second open is a no-op
|
||||
await wal.close();
|
||||
await wal.close(); // second close is a no-op
|
||||
await expect(wal.open()).resolves.toBeUndefined();
|
||||
await expect(wal.open()).resolves.toBeUndefined(); // second open is a no-op
|
||||
await expect(wal.close()).resolves.toBeUndefined();
|
||||
await expect(wal.close()).resolves.toBeUndefined(); // second close is a no-op
|
||||
await fs.rm(dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
// reference model, and assert they always agree. Seeded so any failure is
|
||||
// reproducible by re-running with the same seed.
|
||||
|
||||
import { test } from 'vitest';
|
||||
import { expect, test } from 'vitest';
|
||||
import assert from 'node:assert/strict';
|
||||
import { MiniDb } from '../../src/index.js';
|
||||
import { Model } from './helpers/model.js';
|
||||
|
|
@ -61,6 +61,6 @@ async function runSeed(seed, steps) {
|
|||
test('fuzz-model: random op sequences match a reference model (many seeds)', async () => {
|
||||
const seeds = [1, 2, 3, 42, 12345, 99999, 0xdeadbeef, 0xc0ffee, 777, 20240625];
|
||||
for (const seed of seeds) {
|
||||
await runSeed(seed, 500);
|
||||
await expect(runSeed(seed, 500)).resolves.toBeUndefined();
|
||||
}
|
||||
}, 60_000);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// Covers the RESP commands and parser paths not exercised by server.test.ts.
|
||||
import { test } from 'vitest';
|
||||
import { expect, test } from 'vitest';
|
||||
import assert from 'node:assert/strict';
|
||||
import net from 'node:net';
|
||||
import fs from 'node:fs/promises';
|
||||
|
|
@ -116,7 +116,7 @@ test('RESP: QUIT closes the connection', async () => {
|
|||
const srv = await startServer({ dir, port: 0, fsyncPolicy: 'no' });
|
||||
try {
|
||||
const sock = await connect(srv.port);
|
||||
await sendUntilClose(sock, encode('QUIT'));
|
||||
await expect(sendUntilClose(sock, encode('QUIT'))).resolves.toBeUndefined();
|
||||
} finally {
|
||||
await srv.close();
|
||||
await fs.rm(dir, { recursive: true, force: true });
|
||||
|
|
|
|||
|
|
@ -52,11 +52,11 @@ describe('Session.listBackgroundTasks / getBackgroundTaskOutput', () => {
|
|||
const session = await harness.createSession({ id: 'ses_bg_empty_id', workDir });
|
||||
await expect(session.getBackgroundTaskOutput('')).rejects.toMatchObject({
|
||||
name: 'KimiError',
|
||||
code: 'background.task_id_empty',
|
||||
code: 'task.task_id_empty',
|
||||
} satisfies Partial<KimiError>);
|
||||
await expect(session.stopBackgroundTask('')).rejects.toMatchObject({
|
||||
name: 'KimiError',
|
||||
code: 'background.task_id_empty',
|
||||
code: 'task.task_id_empty',
|
||||
} satisfies Partial<KimiError>);
|
||||
} finally {
|
||||
await harness.close();
|
||||
|
|
|
|||
|
|
@ -106,6 +106,8 @@ describe('Event public types', () => {
|
|||
case 'compaction.blocked':
|
||||
case 'compaction.cancelled':
|
||||
case 'compaction.completed':
|
||||
case 'task.started':
|
||||
case 'task.terminated':
|
||||
case 'background.task.started':
|
||||
case 'background.task.terminated':
|
||||
case 'cron.fired':
|
||||
|
|
|
|||
|
|
@ -41,7 +41,6 @@ export type {
|
|||
QuestionsResource,
|
||||
InteractionsResource,
|
||||
SessionWorkspaceResource,
|
||||
AgentFsResource,
|
||||
SessionFsResource,
|
||||
} from './resources/session.js';
|
||||
export type {
|
||||
|
|
|
|||
|
|
@ -39,16 +39,16 @@ export async function resolveKimiHome(homeDir?: string): Promise<string> {
|
|||
typeof process !== 'undefined' ? process.env?.['KIMI_CODE_HOME'] : undefined;
|
||||
if (envHome !== undefined && envHome.length > 0) return envHome;
|
||||
if (!isNode()) return `.${'kimi-code'}`;
|
||||
const [{ homedir }, { join }] = await Promise.all([import('node:os'), import('node:path')]);
|
||||
return join(homedir(), `.${'kimi-code'}`);
|
||||
const [{ homedir }, path] = await Promise.all([import('node:os'), import('node:path')]);
|
||||
return path.join(homedir(), `.${'kimi-code'}`);
|
||||
}
|
||||
|
||||
/** Absolute path of the persistent token file for a given home dir. */
|
||||
export async function serverTokenPath(homeDir?: string): Promise<string> {
|
||||
const home = await resolveKimiHome(homeDir);
|
||||
if (!isNode()) return `${home}/${SERVER_TOKEN_FILE}`;
|
||||
const { join } = await import('node:path');
|
||||
return join(home, SERVER_TOKEN_FILE);
|
||||
const path = await import('node:path');
|
||||
return path.join(home, SERVER_TOKEN_FILE);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import {
|
|||
loadLocalServerToken,
|
||||
resolveKimiHome,
|
||||
serverTokenPath,
|
||||
} from '../../src/v2/index.js';
|
||||
} from '../../src/v2/token.js';
|
||||
|
||||
describe('local token discovery', () => {
|
||||
let home: string | undefined;
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ export function registerMetaRoute(app: RouteHost, opts: MetaRouteOptions): void
|
|||
file_upload: true as const,
|
||||
fs_query: true as const,
|
||||
mcp: true as const,
|
||||
background_tasks: true as const,
|
||||
tasks: true as const,
|
||||
terminal: true as const,
|
||||
}),
|
||||
server_id: opts.serverId,
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@ describe('GET /api/v1/meta — envelope + metaResponseSchema', () => {
|
|||
file_upload: true,
|
||||
fs_query: true,
|
||||
mcp: true,
|
||||
background_tasks: true,
|
||||
tasks: true,
|
||||
terminal: true,
|
||||
});
|
||||
expect(ulidRegex.test(parsed.server_id)).toBe(true);
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ import { tmpdir } from 'node:os';
|
|||
import { join } from 'node:path';
|
||||
|
||||
import { pino } from 'pino';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
import { afterEach, assert, beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
oauthFlowSnapshotSchema,
|
||||
|
|
@ -187,6 +187,7 @@ describe('POST /api/v1/oauth/login (P2.7)', () => {
|
|||
expect(env.code).toBe(0);
|
||||
const data = oauthFlowStartSchema.parse(env.data);
|
||||
expect(data.flow_id).toBe('oauth_01ABCDEFGH');
|
||||
assert(data.status === 'pending');
|
||||
expect(data.verification_uri_complete).toBe(
|
||||
'https://example.com/device?user_code=KIMI-1234',
|
||||
);
|
||||
|
|
|
|||
14
pnpm-lock.yaml
generated
14
pnpm-lock.yaml
generated
|
|
@ -75,6 +75,9 @@ importers:
|
|||
'@moonshot-ai/acp-adapter':
|
||||
specifier: workspace:^
|
||||
version: link:../../packages/acp-adapter
|
||||
'@moonshot-ai/agent-core-v2':
|
||||
specifier: workspace:^
|
||||
version: link:../../packages/agent-core-v2
|
||||
'@moonshot-ai/kap-server':
|
||||
specifier: workspace:^
|
||||
version: link:../../packages/kap-server
|
||||
|
|
@ -643,7 +646,7 @@ importers:
|
|||
specifier: workspace:^
|
||||
version: link:../protocol
|
||||
bcryptjs:
|
||||
specifier: 2.4.3
|
||||
specifier: ^2.4.3
|
||||
version: 2.4.3
|
||||
fastify:
|
||||
specifier: ^5.1.0
|
||||
|
|
@ -651,24 +654,21 @@ importers:
|
|||
pino:
|
||||
specifier: ^9.5.0
|
||||
version: 9.14.0
|
||||
pino-pretty:
|
||||
specifier: ^13.0.0
|
||||
version: 13.1.3
|
||||
smol-toml:
|
||||
specifier: 1.6.1
|
||||
specifier: ^1.6.1
|
||||
version: 1.6.1
|
||||
ulid:
|
||||
specifier: ^3.0.1
|
||||
version: 3.0.2
|
||||
ws:
|
||||
specifier: ^8.20.0
|
||||
specifier: ^8.18.0
|
||||
version: 8.20.0
|
||||
zod:
|
||||
specifier: 'catalog:'
|
||||
version: 4.3.6
|
||||
devDependencies:
|
||||
'@types/bcryptjs':
|
||||
specifier: 2.4.6
|
||||
specifier: ^2.4.6
|
||||
version: 2.4.6
|
||||
'@types/ws':
|
||||
specifier: ^8.18.0
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue