From 79ffb7b34748d306ab6eec9f167a55abee109f7b Mon Sep 17 00:00:00 2001 From: "haozhe.yang" Date: Tue, 7 Jul 2026 22:51:27 +0800 Subject: [PATCH] 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 --- .../src/tui/reverse-rpc/approval/adapter.ts | 4 +- packages/agent-core-v2/scripts/debarrel.mjs | 10 +-- packages/agent-core-v2/scripts/dep-graph.mjs | 2 - .../scripts/dep-graph/analyzer/analyze.ts | 2 +- .../scripts/dep-graph/web/src/App.tsx | 2 +- .../scripts/gen-contract-types.mjs | 2 +- packages/agent-core-v2/src/_base/event.ts | 2 +- .../fullCompaction/fullCompactionService.ts | 9 -- .../agent/llmRequester/llmRequesterService.ts | 6 +- .../agent-core-v2/src/agent/rpc/rpcService.ts | 2 +- .../src/agent/task/taskService.ts | 8 +- .../src/agent/wireRecord/wireRecordService.ts | 2 +- .../agent-core-v2/src/app/model/envOverlay.ts | 3 - .../agent-core-v2/src/app/model/modelImpl.ts | 4 +- .../app/modelCatalog/modelCatalogService.ts | 5 +- .../app/web/providers/moonshot-fetch-url.ts | 2 +- .../src/persistence/interface/queryStore.ts | 2 +- .../src/session/sessionFs/fsService.ts | 2 +- .../agent-core-v2/src/wire/wireServiceImpl.ts | 2 +- .../agentLifecycle/agentLifecycle.test.ts | 1 - .../test/app/web/fetch-url.test.ts | 8 +- packages/agent-core-v2/test/auth/auth.test.ts | 26 +++--- .../agent-core-v2/test/contextMemory/stubs.ts | 83 ++++++++++++++----- packages/agent-core-v2/test/cron/cron.test.ts | 2 - .../agent-core-v2/test/cron/tools.test.ts | 12 +-- .../agent-core-v2/test/di/invocation.test.ts | 21 ++--- packages/agent-core-v2/test/goal/goal.test.ts | 64 +++++++------- packages/agent-core-v2/test/harness/agent.ts | 34 +++++++- packages/agent-core-v2/test/harness/index.ts | 1 + .../test/llmRequester/strict-resend.test.ts | 27 +++--- packages/agent-core-v2/test/mcp/mcp.test.ts | 11 ++- .../agent-core-v2/test/mcp/output.test.ts | 8 +- .../test/permission/permissionGate.test.ts | 10 ++- .../permissionMode/permissionMode.test.ts | 2 +- .../permissionRules/permissionRules.test.ts | 2 +- .../test/plugin/plugin-session-start.test.ts | 2 + .../test/profile/apply-profile.test.ts | 8 +- .../test/profile/binding.test.ts | 4 +- .../test/profile/config-state.test.ts | 13 ++- .../test/profile/profile-wire.test.ts | 9 +- .../agent-core-v2/test/resume-debug.test.ts | 8 +- .../test/resume-roundtrip.test.ts | 7 +- .../test/shellTools/bash.test.ts | 2 +- .../test/skill/skill-tool-manager.test.ts | 6 +- .../test/storage/atomicDocumentStore.test.ts | 4 +- .../test/storage/storageService.test.ts | 4 +- .../agent-core-v2/test/swarm/swarm.test.ts | 2 +- .../test/task/heartbeat-stale.test.ts | 6 +- .../agent-core-v2/test/task/reconcile.test.ts | 6 +- .../test/task/rpc-events.test.ts | 6 +- .../test/toolDedupe/tool-dedupe.test.ts | 2 +- .../test/toolExecutor/tool-executor.test.ts | 7 +- .../test/turn/turn-ready.test.ts | 2 +- packages/agent-core-v2/test/turn/turn.test.ts | 40 +++++---- .../agent-core-v2/test/usage/usage.test.ts | 2 +- .../workspace-command.test.ts | 1 + .../test/services/oauth-service.test.ts | 5 +- packages/kap-server/src/routes/prompts.ts | 6 +- packages/kap-server/src/routes/sessions.ts | 1 - .../src/services/snapshot/snapshotReader.ts | 1 - .../kap-server/src/transport/ws/eventMap.ts | 4 +- .../kap-server/src/transport/ws/wsClient.ts | 2 +- .../src/transport/ws/wsConnection.ts | 2 +- packages/kap-server/test/fs.test.ts | 16 ++-- packages/kap-server/test/messages.test.ts | 17 ++-- packages/kap-server/test/snapshot.test.ts | 13 +-- packages/kap-server/test/tasks.test.ts | 17 ++-- packages/kap-server/test/tools.test.ts | 17 ++-- packages/kap-server/test/wsV1Resync.test.ts | 16 ++-- packages/minidb/bench/baseline.ts | 2 +- .../minidb/bench/measure-session-memory.ts | 1 - packages/minidb/bench/message-composed.ts | 7 +- packages/minidb/bench/message-range.ts | 7 +- packages/minidb/bench/search-baseline.ts | 2 +- packages/minidb/bench/session-children.ts | 8 +- packages/minidb/src/compound-index.ts | 4 +- packages/minidb/src/index-manager.ts | 14 +++- packages/minidb/src/query.ts | 4 +- packages/minidb/src/server.ts | 4 +- packages/minidb/src/skiplist.ts | 6 +- packages/minidb/src/text-index.ts | 2 +- packages/minidb/src/text-postings.ts | 2 +- packages/minidb/src/wal.ts | 4 +- packages/minidb/test/codec.test.ts | 6 +- packages/minidb/test/compaction-fault.test.ts | 6 +- .../minidb/test/compaction-internal.test.ts | 6 +- packages/minidb/test/defense.test.ts | 10 +-- packages/minidb/test/e2e/fuzz-model.test.ts | 4 +- packages/minidb/test/server-extra.test.ts | 4 +- .../test/session-background-tasks.test.ts | 4 +- .../node-sdk/test/session-event-types.test.ts | 2 + packages/server-e2e/src/v2/index.ts | 1 - packages/server-e2e/src/v2/token.ts | 8 +- packages/server-e2e/test/v2/token.test.ts | 2 +- packages/server/src/routes/meta.ts | 2 +- packages/server/test/meta.e2e.test.ts | 2 +- packages/server/test/oauth.e2e.test.ts | 3 +- pnpm-lock.yaml | 14 ++-- 98 files changed, 446 insertions(+), 336 deletions(-) diff --git a/apps/kimi-code/src/tui/reverse-rpc/approval/adapter.ts b/apps/kimi-code/src/tui/reverse-rpc/approval/adapter.ts index 8112534a0..373690592 100644 --- a/apps/kimi-code/src/tui/reverse-rpc/approval/adapter.ts +++ b/apps/kimi-code/src/tui/reverse-rpc/approval/adapter.ts @@ -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 []; diff --git a/packages/agent-core-v2/scripts/debarrel.mjs b/packages/agent-core-v2/scripts/debarrel.mjs index 5d03460ca..d6a2a96a0 100644 --- a/packages/agent-core-v2/scripts/debarrel.mjs +++ b/packages/agent-core-v2/scripts/debarrel.mjs @@ -10,7 +10,7 @@ * (default) rewrite all consumer files (src + test) EXCEPT src/index.ts * --only= 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; diff --git a/packages/agent-core-v2/scripts/dep-graph.mjs b/packages/agent-core-v2/scripts/dep-graph.mjs index bbe42cc72..1c34a21b2 100644 --- a/packages/agent-core-v2/scripts/dep-graph.mjs +++ b/packages/agent-core-v2/scripts/dep-graph.mjs @@ -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. */ diff --git a/packages/agent-core-v2/scripts/dep-graph/analyzer/analyze.ts b/packages/agent-core-v2/scripts/dep-graph/analyzer/analyze.ts index 2f2271baf..783350b39 100644 --- a/packages/agent-core-v2/scripts/dep-graph/analyzer/analyze.ts +++ b/packages/agent-core-v2/scripts/dep-graph/analyzer/analyze.ts @@ -1039,7 +1039,7 @@ export function summarize(graph: Graph): string { const byKind = new Map(); 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}`; diff --git a/packages/agent-core-v2/scripts/dep-graph/web/src/App.tsx b/packages/agent-core-v2/scripts/dep-graph/web/src/App.tsx index fcc77ebc8..2913a4e0f 100644 --- a/packages/agent-core-v2/scripts/dep-graph/web/src/App.tsx +++ b/packages/agent-core-v2/scripts/dep-graph/web/src/App.tsx @@ -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)), [], ); diff --git a/packages/agent-core-v2/scripts/gen-contract-types.mjs b/packages/agent-core-v2/scripts/gen-contract-types.mjs index 5b7fdd6ae..2cd8307b0 100644 --- a/packages/agent-core-v2/scripts/gen-contract-types.mjs +++ b/packages/agent-core-v2/scripts/gen-contract-types.mjs @@ -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). diff --git a/packages/agent-core-v2/src/_base/event.ts b/packages/agent-core-v2/src/_base/event.ts index 89b6d5aec..394688761 100644 --- a/packages/agent-core-v2/src/_base/event.ts +++ b/packages/agent-core-v2/src/_base/event.ts @@ -138,7 +138,7 @@ export class AsyncEmitter extends Emitter { continue; } - Object.freeze(thenables); + void Object.freeze(thenables); const settled = await Promise.allSettled(thenables); for (const result of settled) { if (result.status === 'rejected') { diff --git a/packages/agent-core-v2/src/agent/fullCompaction/fullCompactionService.ts b/packages/agent-core-v2/src/agent/fullCompaction/fullCompactionService.ts index bb1621cfb..643d00b01 100644 --- a/packages/agent-core-v2/src/agent/fullCompaction/fullCompactionService.ts +++ b/packages/agent-core-v2/src/agent/fullCompaction/fullCompactionService.ts @@ -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)` diff --git a/packages/agent-core-v2/src/agent/llmRequester/llmRequesterService.ts b/packages/agent-core-v2/src/agent/llmRequester/llmRequesterService.ts index 75e6efde3..9996ff2d1 100644 --- a/packages/agent-core-v2/src/agent/llmRequester/llmRequesterService.ts +++ b/packages/agent-core-v2/src/agent/llmRequester/llmRequesterService.ts @@ -112,7 +112,7 @@ export class AgentLLMRequesterService implements IAgentLLMRequesterService { signal?: AbortSignal, ): Promise { 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); } } diff --git a/packages/agent-core-v2/src/agent/rpc/rpcService.ts b/packages/agent-core-v2/src/agent/rpc/rpcService.ts index 6951feb99..4eb77de5d 100644 --- a/packages/agent-core-v2/src/agent/rpc/rpcService.ts +++ b/packages/agent-core-v2/src/agent/rpc/rpcService.ts @@ -300,7 +300,7 @@ export class AgentRPCService implements IAgentRPCService { } async activateSkill(payload: ActivateSkillPayload): Promise { - this.skills.activate(payload); + void this.skills.activate(payload); await this.updatePromptMetadata(promptMetadataTextFromSkill(payload)); } diff --git a/packages/agent-core-v2/src/agent/task/taskService.ts b/packages/agent-core-v2/src/agent/task/taskService.ts index 20c31b4c8..f3101dcf9 100644 --- a/packages/agent-core-v2/src/agent/task/taskService.ts +++ b/packages/agent-core-v2/src/agent/task/taskService.ts @@ -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 */ diff --git a/packages/agent-core-v2/src/agent/wireRecord/wireRecordService.ts b/packages/agent-core-v2/src/agent/wireRecord/wireRecordService.ts index 7b659e60e..323e273b1 100644 --- a/packages/agent-core-v2/src/agent/wireRecord/wireRecordService.ts +++ b/packages/agent-core-v2/src/agent/wireRecord/wireRecordService.ts @@ -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) { diff --git a/packages/agent-core-v2/src/app/model/envOverlay.ts b/packages/agent-core-v2/src/app/model/envOverlay.ts index 1d5de5e1d..39b2b3742 100644 --- a/packages/agent-core-v2/src/app/model/envOverlay.ts +++ b/packages/agent-core-v2/src/app/model/envOverlay.ts @@ -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; diff --git a/packages/agent-core-v2/src/app/model/modelImpl.ts b/packages/agent-core-v2/src/app/model/modelImpl.ts index acfa8f858..75b494a4a 100644 --- a/packages/agent-core-v2/src/app/model/modelImpl.ts +++ b/packages/agent-core-v2/src/app/model/modelImpl.ts @@ -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 }), ); } diff --git a/packages/agent-core-v2/src/app/modelCatalog/modelCatalogService.ts b/packages/agent-core-v2/src/app/modelCatalog/modelCatalogService.ts index e5330b0aa..8a89e9602 100644 --- a/packages/agent-core-v2/src/app/modelCatalog/modelCatalogService.ts +++ b/packages/agent-core-v2/src/app/modelCatalog/modelCatalogService.ts @@ -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(); } diff --git a/packages/agent-core-v2/src/app/web/providers/moonshot-fetch-url.ts b/packages/agent-core-v2/src/app/web/providers/moonshot-fetch-url.ts index 63d6bf5a0..35e421d2a 100644 --- a/packages/agent-core-v2/src/app/web/providers/moonshot-fetch-url.ts +++ b/packages/agent-core-v2/src/app/web/providers/moonshot-fetch-url.ts @@ -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; } diff --git a/packages/agent-core-v2/src/persistence/interface/queryStore.ts b/packages/agent-core-v2/src/persistence/interface/queryStore.ts index a32aa1f96..b90f2fa5e 100644 --- a/packages/agent-core-v2/src/persistence/interface/queryStore.ts +++ b/packages/agent-core-v2/src/persistence/interface/queryStore.ts @@ -38,7 +38,7 @@ export interface ComparisonOp { } export type QueryFilter = { - readonly [field: string]: unknown | ComparisonOp; + readonly [field: string]: unknown; }; export interface IQuery { diff --git a/packages/agent-core-v2/src/session/sessionFs/fsService.ts b/packages/agent-core-v2/src/session/sessionFs/fsService.ts index f64dbcfd3..7aeaa2b9e 100644 --- a/packages/agent-core-v2/src/session/sessionFs/fsService.ts +++ b/packages/agent-core-v2/src/session/sessionFs/fsService.ts @@ -759,7 +759,7 @@ function parseRgJsonOutput( } } - for (const p of [...fileBuf.keys()]) { + for (const p of fileBuf.keys()) { finalize(p); } diff --git a/packages/agent-core-v2/src/wire/wireServiceImpl.ts b/packages/agent-core-v2/src/wire/wireServiceImpl.ts index 2b409a632..2b17391e0 100644 --- a/packages/agent-core-v2/src/wire/wireServiceImpl.ts +++ b/packages/agent-core-v2/src/wire/wireServiceImpl.ts @@ -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[], diff --git a/packages/agent-core-v2/test/agentLifecycle/agentLifecycle.test.ts b/packages/agent-core-v2/test/agentLifecycle/agentLifecycle.test.ts index 369cf6225..17a3936d7 100644 --- a/packages/agent-core-v2/test/agentLifecycle/agentLifecycle.test.ts +++ b/packages/agent-core-v2/test/agentLifecycle/agentLifecycle.test.ts @@ -182,7 +182,6 @@ describe('AgentLifecycleService', () => { ix.stub(ILogService, noopLog); ix.stub(IAgentPluginService, { _serviceBrand: undefined, - appendFreshSessionStartReminder: async () => {}, }); ix.stub(IAgentToolRegistryService, { _serviceBrand: undefined, diff --git a/packages/agent-core-v2/test/app/web/fetch-url.test.ts b/packages/agent-core-v2/test/app/web/fetch-url.test.ts index 621b62c72..9d85d2b81 100644 --- a/packages/agent-core-v2/test/app/web/fetch-url.test.ts +++ b/packages/agent-core-v2/test/app/web/fetch-url.test.ts @@ -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): value is Promise { return typeof (value as Promise).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'); }); }); diff --git a/packages/agent-core-v2/test/auth/auth.test.ts b/packages/agent-core-v2/test/auth/auth.test.ts index 2ff22ad57..d432f0e2a 100644 --- a/packages/agent-core-v2/test/auth/auth.test.ts +++ b/packages/agent-core-v2/test/auth/auth.test.ts @@ -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 => new Promise((resolve) => setTimeout(resolve, 0)); interface FakeToolkit { - readonly login: ReturnType; + readonly login: Mock<(...args: any[]) => any>; readonly logout: ReturnType; readonly getCachedAccessToken: ReturnType; readonly tokenProvider: ReturnType; @@ -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 }; }); diff --git a/packages/agent-core-v2/test/contextMemory/stubs.ts b/packages/agent-core-v2/test/contextMemory/stubs.ts index 7dba588c1..69179d3bb 100644 --- a/packages/agent-core-v2/test/contextMemory/stubs.ts +++ b/packages/agent-core-v2/test/contextMemory/stubs.ts @@ -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); } diff --git a/packages/agent-core-v2/test/cron/cron.test.ts b/packages/agent-core-v2/test/cron/cron.test.ts index db0ad3820..c9e9f64e6 100644 --- a/packages/agent-core-v2/test/cron/cron.test.ts +++ b/packages/agent-core-v2/test/cron/cron.test.ts @@ -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: () => {} }); diff --git a/packages/agent-core-v2/test/cron/tools.test.ts b/packages/agent-core-v2/test/cron/tools.test.ts index e1998daec..2be1bb44c 100644 --- a/packages/agent-core-v2/test/cron/tools.test.ts +++ b/packages/agent-core-v2/test/cron/tools.test.ts @@ -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, diff --git a/packages/agent-core-v2/test/di/invocation.test.ts b/packages/agent-core-v2/test/di/invocation.test.ts index eeb9c296f..f420a6af7 100644 --- a/packages/agent-core-v2/test/di/invocation.test.ts +++ b/packages/agent-core-v2/test/di/invocation.test.ts @@ -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); diff --git a/packages/agent-core-v2/test/goal/goal.test.ts b/packages/agent-core-v2/test/goal/goal.test.ts index 9a8200410..269c95dae 100644 --- a/packages/agent-core-v2/test/goal/goal.test.ts +++ b/packages/agent-core-v2/test/goal/goal.test.ts @@ -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; -type AgentEvent = Parameters[0]; +type AgentEvent = DomainEvent; type GoalUpdatedEvent = Extract; -type GoalSnapshot = NonNullable['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 { - 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', diff --git a/packages/agent-core-v2/test/harness/agent.ts b/packages/agent-core-v2/test/harness/agent.ts index 10ece3c23..de1504a3e 100644 --- a/packages/agent-core-v2/test/harness/agent.ts +++ b/packages/agent-core-v2/test/harness/agent.ts @@ -265,7 +265,7 @@ interface ResumeStateSnapshot { readonly config: { readonly cwd: string; readonly activeToolNames: readonly string[] | undefined; - readonly provider: ProviderConfig | undefined; + readonly provider: ReturnType; 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 | undefined, - ...(input.providerOptions ?? {}), + ...input.providerOptions, } as ProviderConfig; return input.protocol === 'kimi' ? new GenerateBackedKimiChatProvider( diff --git a/packages/agent-core-v2/test/harness/index.ts b/packages/agent-core-v2/test/harness/index.ts index b44b425ef..3419936c5 100644 --- a/packages/agent-core-v2/test/harness/index.ts +++ b/packages/agent-core-v2/test/harness/index.ts @@ -12,6 +12,7 @@ export { execEnvServices, externalHookServices, homeDirServices, + hostEnvironmentServices, InMemoryWireRecordPersistence, llmGenerateServices, logServices, diff --git a/packages/agent-core-v2/test/llmRequester/strict-resend.test.ts b/packages/agent-core-v2/test/llmRequester/strict-resend.test.ts index dd1120ec7..509509fa3 100644 --- a/packages/agent-core-v2/test/llmRequester/strict-resend.test.ts +++ b/packages/agent-core-v2/test/llmRequester/strict-resend.test.ts @@ -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'); }, }); diff --git a/packages/agent-core-v2/test/mcp/mcp.test.ts b/packages/agent-core-v2/test/mcp/mcp.test.ts index 348547e28..73021ecd8 100644 --- a/packages/agent-core-v2/test/mcp/mcp.test.ts +++ b/packages/agent-core-v2/test/mcp/mcp.test.ts @@ -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)); diff --git a/packages/agent-core-v2/test/mcp/output.test.ts b/packages/agent-core-v2/test/mcp/output.test.ts index e18c77739..41eba2b1b 100644 --- a/packages/agent-core-v2/test/mcp/output.test.ts +++ b/packages/agent-core-v2/test/mcp/output.test.ts @@ -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): value is Promise { + return typeof (value as Promise).then === 'function'; +} + function assertValidMcpBlock(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({ diff --git a/packages/agent-core-v2/test/permission/permissionGate.test.ts b/packages/agent-core-v2/test/permission/permissionGate.test.ts index 71fab68cd..0a27ec5d4 100644 --- a/packages/agent-core-v2/test/permission/permissionGate.test.ts +++ b/packages/agent-core-v2/test/permission/permissionGate.test.ts @@ -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, diff --git a/packages/agent-core-v2/test/permissionMode/permissionMode.test.ts b/packages/agent-core-v2/test/permissionMode/permissionMode.test.ts index 24c7afc09..c3af036a2 100644 --- a/packages/agent-core-v2/test/permissionMode/permissionMode.test.ts +++ b/packages/agent-core-v2/test/permissionMode/permissionMode.test.ts @@ -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'); diff --git a/packages/agent-core-v2/test/permissionRules/permissionRules.test.ts b/packages/agent-core-v2/test/permissionRules/permissionRules.test.ts index 5dcd6ed6f..ea46b94a0 100644 --- a/packages/agent-core-v2/test/permissionRules/permissionRules.test.ts +++ b/packages/agent-core-v2/test/permissionRules/permissionRules.test.ts @@ -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], diff --git a/packages/agent-core-v2/test/plugin/plugin-session-start.test.ts b/packages/agent-core-v2/test/plugin/plugin-session-start.test.ts index 7f2a0c68e..a452b3d53 100644 --- a/packages/agent-core-v2/test/plugin/plugin-session-start.test.ts +++ b/packages/agent-core-v2/test/plugin/plugin-session-start.test.ts @@ -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); diff --git a/packages/agent-core-v2/test/profile/apply-profile.test.ts b/packages/agent-core-v2/test/profile/apply-profile.test.ts index d45e04812..7cd4c5975 100644 --- a/packages/agent-core-v2/test/profile/apply-profile.test.ts +++ b/packages/agent-core-v2/test/profile/apply-profile.test.ts @@ -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) }; diff --git a/packages/agent-core-v2/test/profile/binding.test.ts b/packages/agent-core-v2/test/profile/binding.test.ts index eb8e6094e..12ae70992 100644 --- a/packages/agent-core-v2/test/profile/binding.test.ts +++ b/packages/agent-core-v2/test/profile/binding.test.ts @@ -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) }; } diff --git a/packages/agent-core-v2/test/profile/config-state.test.ts b/packages/agent-core-v2/test/profile/config-state.test.ts index 1c3918b08..3b832e06a 100644 --- a/packages/agent-core-v2/test/profile/config-state.test.ts +++ b/packages/agent-core-v2/test/profile/config-state.test.ts @@ -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); }); }); diff --git a/packages/agent-core-v2/test/profile/profile-wire.test.ts b/packages/agent-core-v2/test/profile/profile-wire.test.ts index 613dc895d..186447b92 100644 --- a/packages/agent-core-v2/test/profile/profile-wire.test.ts +++ b/packages/agent-core-v2/test/profile/profile-wire.test.ts @@ -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'); }); diff --git a/packages/agent-core-v2/test/resume-debug.test.ts b/packages/agent-core-v2/test/resume-debug.test.ts index f03cfc6a7..09befdc62 100644 --- a/packages/agent-core-v2/test/resume-debug.test.ts +++ b/packages/agent-core-v2/test/resume-debug.test.ts @@ -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 | undefined; const b = r2[index] as Record | 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}" ---`); diff --git a/packages/agent-core-v2/test/resume-roundtrip.test.ts b/packages/agent-core-v2/test/resume-roundtrip.test.ts index 458d41beb..00b278915 100644 --- a/packages/agent-core-v2/test/resume-roundtrip.test.ts +++ b/packages/agent-core-v2/test/resume-roundtrip.test.ts @@ -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( fn: (item: T, index: number) => Promise, onProgress?: (done: number) => void, ): Promise { - const results: R[] = new Array(items.length); + const results = Array.from({ length: items.length }); let next = 0; let done = 0; async function worker(): Promise { @@ -262,5 +262,8 @@ describe('resume round-trip over real ~/.kimi-code wire logs', () => { ); } } + + expect(restoreErrors).toEqual([]); + expect(mismatches).toEqual([]); }, 60 * 60 * 1000); }); diff --git a/packages/agent-core-v2/test/shellTools/bash.test.ts b/packages/agent-core-v2/test/shellTools/bash.test.ts index 6d06a5d99..1c9433162 100644 --- a/packages/agent-core-v2/test/shellTools/bash.test.ts +++ b/packages/agent-core-v2/test/shellTools/bash.test.ts @@ -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): value is Promise { diff --git a/packages/agent-core-v2/test/skill/skill-tool-manager.test.ts b/packages/agent-core-v2/test/skill/skill-tool-manager.test.ts index 9e2bb3a75..3297424d7 100644 --- a/packages/agent-core-v2/test/skill/skill-tool-manager.test.ts +++ b/packages/agent-core-v2/test/skill/skill-tool-manager.test.ts @@ -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 () => { diff --git a/packages/agent-core-v2/test/storage/atomicDocumentStore.test.ts b/packages/agent-core-v2/test/storage/atomicDocumentStore.test.ts index 7206e3187..94fcf6706 100644 --- a/packages/agent-core-v2/test/storage/atomicDocumentStore.test.ts +++ b/packages/agent-core-v2/test/storage/atomicDocumentStore.test.ts @@ -88,7 +88,7 @@ describe('JsonAtomicDocumentStore', () => { }); }); await config.set('session', 'state.json', { title: 'x' }); - await fired; + await expect(fired).resolves.toBeUndefined(); }); }); @@ -139,6 +139,6 @@ describe('TomlAtomicDocumentStore', () => { }); }); await config.set('session', 'config.toml', { title: 'x' }); - await fired; + await expect(fired).resolves.toBeUndefined(); }); }); diff --git a/packages/agent-core-v2/test/storage/storageService.test.ts b/packages/agent-core-v2/test/storage/storageService.test.ts index 1122434dd..3d453ff1c 100644 --- a/packages/agent-core-v2/test/storage/storageService.test.ts +++ b/packages/agent-core-v2/test/storage/storageService.test.ts @@ -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(); }); }); } diff --git a/packages/agent-core-v2/test/swarm/swarm.test.ts b/packages/agent-core-v2/test/swarm/swarm.test.ts index 565a9608d..7d59f901a 100644 --- a/packages/agent-core-v2/test/swarm/swarm.test.ts +++ b/packages/agent-core-v2/test/swarm/swarm.test.ts @@ -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'); }); }); diff --git a/packages/agent-core-v2/test/task/heartbeat-stale.test.ts b/packages/agent-core-v2/test/task/heartbeat-stale.test.ts index 748cbce6a..8cb28f4b1 100644 --- a/packages/agent-core-v2/test/task/heartbeat-stale.test.ts +++ b/packages/agent-core-v2/test/task/heartbeat-stale.test.ts @@ -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); }); }); diff --git a/packages/agent-core-v2/test/task/reconcile.test.ts b/packages/agent-core-v2/test/task/reconcile.test.ts index 6034f6bbb..3f2ed9c4c 100644 --- a/packages/agent-core-v2/test/task/reconcile.test.ts +++ b/packages/agent-core-v2/test/task/reconcile.test.ts @@ -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); }); }); diff --git a/packages/agent-core-v2/test/task/rpc-events.test.ts b/packages/agent-core-v2/test/task/rpc-events.test.ts index a3e4bd378..25aa7d9da 100644 --- a/packages/agent-core-v2/test/task/rpc-events.test.ts +++ b/packages/agent-core-v2/test/task/rpc-events.test.ts @@ -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 }); }); diff --git a/packages/agent-core-v2/test/toolDedupe/tool-dedupe.test.ts b/packages/agent-core-v2/test/toolDedupe/tool-dedupe.test.ts index af853adbf..a20b59dbf 100644 --- a/packages/agent-core-v2/test/toolDedupe/tool-dedupe.test.ts +++ b/packages/agent-core-v2/test/toolDedupe/tool-dedupe.test.ts @@ -108,7 +108,7 @@ class EchoTool implements ExecutableTool> { constructor( readonly name = 'Echo', private readonly resultFor: (args: Record) => ExecutableToolResult = (args) => ({ - output: String(args['text'] ?? ''), + output: typeof args['text'] === 'string' ? args['text'] : '', }), ) {} diff --git a/packages/agent-core-v2/test/toolExecutor/tool-executor.test.ts b/packages/agent-core-v2/test/toolExecutor/tool-executor.test.ts index 02eb647fc..557c0f17d 100644 --- a/packages/agent-core-v2/test/toolExecutor/tool-executor.test.ts +++ b/packages/agent-core-v2/test/toolExecutor/tool-executor.test.ts @@ -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> { 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`, + }; }, }; } diff --git a/packages/agent-core-v2/test/turn/turn-ready.test.ts b/packages/agent-core-v2/test/turn/turn-ready.test.ts index 2ba6e8a93..de3ce2bf3 100644 --- a/packages/agent-core-v2/test/turn/turn-ready.test.ts +++ b/packages/agent-core-v2/test/turn/turn-ready.test.ts @@ -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 }); diff --git a/packages/agent-core-v2/test/turn/turn.test.ts b/packages/agent-core-v2/test/turn/turn.test.ts index e13abc1d7..a31e78023 100644 --- a/packages/agent-core-v2/test/turn/turn.test.ts +++ b/packages/agent-core-v2/test/turn/turn.test.ts @@ -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), diff --git a/packages/agent-core-v2/test/usage/usage.test.ts b/packages/agent-core-v2/test/usage/usage.test.ts index 1c2dc5a9f..823c6becd 100644 --- a/packages/agent-core-v2/test/usage/usage.test.ts +++ b/packages/agent-core-v2/test/usage/usage.test.ts @@ -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 }, diff --git a/packages/agent-core-v2/test/workspaceCommand/workspace-command.test.ts b/packages/agent-core-v2/test/workspaceCommand/workspace-command.test.ts index 3b0a37fb6..6bd9bc974 100644 --- a/packages/agent-core-v2/test/workspaceCommand/workspace-command.test.ts +++ b/packages/agent-core-v2/test/workspaceCommand/workspace-command.test.ts @@ -98,6 +98,7 @@ class MemoryHostFs implements IHostFileSystem { } async *readLines(): AsyncGenerator { + yield* []; throw new Error('not implemented'); } diff --git a/packages/agent-core/test/services/oauth-service.test.ts b/packages/agent-core/test/services/oauth-service.test.ts index 3c4e49b24..d8cb87b4a 100644 --- a/packages/agent-core/test/services/oauth-service.test.ts +++ b/packages/agent-core/test/services/oauth-service.test.ts @@ -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); }); }); diff --git a/packages/kap-server/src/routes/prompts.ts b/packages/kap-server/src/routes/prompts.ts index 8bd2a435d..1fe0a7b25 100644 --- a/packages/kap-server/src/routes/prompts.ts +++ b/packages/kap-server/src/routes/prompts.ts @@ -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; diff --git a/packages/kap-server/src/routes/sessions.ts b/packages/kap-server/src/routes/sessions.ts index 2c5d90bf4..1615de535 100644 --- a/packages/kap-server/src/routes/sessions.ts +++ b/packages/kap-server/src/routes/sessions.ts @@ -61,7 +61,6 @@ import { IAuthSummaryService, ISessionBtwService, ISessionActivity, - ISessionContext, ISessionIndex, ISessionLifecycleService, ISessionMetadata, diff --git a/packages/kap-server/src/services/snapshot/snapshotReader.ts b/packages/kap-server/src/services/snapshot/snapshotReader.ts index 5868961d7..24b8fce77 100644 --- a/packages/kap-server/src/services/snapshot/snapshotReader.ts +++ b/packages/kap-server/src/services/snapshot/snapshotReader.ts @@ -41,7 +41,6 @@ import { } from '@moonshot-ai/agent-core-v2'; import type { InFlightTurn, - Message, SessionSnapshotResponse, SessionStatus, } from '@moonshot-ai/protocol'; diff --git a/packages/kap-server/src/transport/ws/eventMap.ts b/packages/kap-server/src/transport/ws/eventMap.ts index 8acf6a10c..d9259a63f 100644 --- a/packages/kap-server/src/transport/ws/eventMap.ts +++ b/packages/kap-server/src/transport/ws/eventMap.ts @@ -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> = { 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. diff --git a/packages/kap-server/src/transport/ws/wsClient.ts b/packages/kap-server/src/transport/ws/wsClient.ts index 8fdf54e05..cdbf2063d 100644 --- a/packages/kap-server/src/transport/ws/wsClient.ts +++ b/packages/kap-server/src/transport/ws/wsClient.ts @@ -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); diff --git a/packages/kap-server/src/transport/ws/wsConnection.ts b/packages/kap-server/src/transport/ws/wsConnection.ts index 8991a1c2a..abfee1f59 100644 --- a/packages/kap-server/src/transport/ws/wsConnection.ts +++ b/packages/kap-server/src/transport/ws/wsConnection.ts @@ -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); diff --git a/packages/kap-server/test/fs.test.ts b/packages/kap-server/test/fs.test.ts index 4d0c1c7c4..14b2771b5 100644 --- a/packages/kap-server/test/fs.test.ts +++ b/packages/kap-server/test/fs.test.ts @@ -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}`; }); diff --git a/packages/kap-server/test/messages.test.ts b/packages/kap-server/test/messages.test.ts index 3739b7304..83df8a1b7 100644 --- a/packages/kap-server/test/messages.test.ts +++ b/packages/kap-server/test/messages.test.ts @@ -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(); }); diff --git a/packages/kap-server/test/snapshot.test.ts b/packages/kap-server/test/snapshot.test.ts index 1b690db14..c2d912550 100644 --- a/packages/kap-server/test/snapshot.test.ts +++ b/packages/kap-server/test/snapshot.test.ts @@ -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); diff --git a/packages/kap-server/test/tasks.test.ts b/packages/kap-server/test/tasks.test.ts index 37b6a735c..e59f58f18 100644 --- a/packages/kap-server/test/tasks.test.ts +++ b/packages/kap-server/test/tasks.test.ts @@ -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}`; }); diff --git a/packages/kap-server/test/tools.test.ts b/packages/kap-server/test/tools.test.ts index a7a52ef47..502d014be 100644 --- a/packages/kap-server/test/tools.test.ts +++ b/packages/kap-server/test/tools.test.ts @@ -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}`; }); diff --git a/packages/kap-server/test/wsV1Resync.test.ts b/packages/kap-server/test/wsV1Resync.test.ts index f180b3efb..f49ada8de 100644 --- a/packages/kap-server/test/wsV1Resync.test.ts +++ b/packages/kap-server/test/wsV1Resync.test.ts @@ -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 { 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; diff --git a/packages/minidb/bench/baseline.ts b/packages/minidb/bench/baseline.ts index b760c6313..5033314eb 100644 --- a/packages/minidb/bench/baseline.ts +++ b/packages/minidb/bench/baseline.ts @@ -25,7 +25,7 @@ async function tmpDir() { return fs.mkdtemp(path.join(os.tmpdir(), 'baseline-bench-')); } -async function bench(label: string, fn: () => Promise | unknown) { +async function bench(label: string, fn: () => unknown) { if (global.gc) global.gc(); const t0 = performance.now(); await fn(); diff --git a/packages/minidb/bench/measure-session-memory.ts b/packages/minidb/bench/measure-session-memory.ts index 7729c4fc1..10e82192e 100644 --- a/packages/minidb/bench/measure-session-memory.ts +++ b/packages/minidb/bench/measure-session-memory.ts @@ -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 { diff --git a/packages/minidb/bench/message-composed.ts b/packages/minidb/bench/message-composed.ts index 6e8efa6f6..053828d76 100644 --- a/packages/minidb/bench/message-composed.ts +++ b/packages/minidb/bench/message-composed.ts @@ -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; } diff --git a/packages/minidb/bench/message-range.ts b/packages/minidb/bench/message-range.ts index 8903f7ded..7e8f6e921 100644 --- a/packages/minidb/bench/message-range.ts +++ b/packages/minidb/bench/message-range.ts @@ -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; } diff --git a/packages/minidb/bench/search-baseline.ts b/packages/minidb/bench/search-baseline.ts index 800dc1542..5fdfe1551 100644 --- a/packages/minidb/bench/search-baseline.ts +++ b/packages/minidb/bench/search-baseline.ts @@ -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); } diff --git a/packages/minidb/bench/session-children.ts b/packages/minidb/bench/session-children.ts index 9ded55ff7..7f7d81244 100644 --- a/packages/minidb/bench/session-children.ts +++ b/packages/minidb/bench/session-children.ts @@ -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 }, })), diff --git a/packages/minidb/src/compound-index.ts b/packages/minidb/src/compound-index.ts index 6e3edbfae..d6c9303ac 100644 --- a/packages/minidb/src/compound-index.ts +++ b/packages/minidb/src/compound-index.ts @@ -36,7 +36,7 @@ interface CompoundEntry { } function getPath(doc: unknown, path: string): unknown { - return path.split('.').reduce((o, k) => (o == null ? undefined : (o as Record)[k]), doc); + return path.split('.').reduce((o, k) => (o === null || o === undefined ? undefined : (o as Record)[k]), doc); } export class CompoundIndexManager { @@ -113,7 +113,7 @@ export class CompoundIndexManager { } } - remove(pk: string, doc?: unknown, dt?: Record | null): void { + remove(pk: string, _doc?: unknown, _dt?: Record | null): void { for (const entry of this.indexes.values()) { const prev = entry.byPk.get(pk); if (prev) { diff --git a/packages/minidb/src/index-manager.ts b/packages/minidb/src/index-manager.ts index 6c005d11c..d426db689 100644 --- a/packages/minidb/src/index-manager.ts +++ b/packages/minidb/src/index-manager.ts @@ -54,7 +54,7 @@ export class UniqueViolationError extends Error { } function getField(doc: unknown, path: string): unknown { - return path.split('.').reduce((o, k) => (o == null ? undefined : (o as Record)[k]), doc); + return path.split('.').reduce((o, k) => (o === null || o === undefined ? undefined : (o as Record)[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 = {}; - 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; diff --git a/packages/minidb/src/query.ts b/packages/minidb/src/query.ts index 1bd20f1fc..9cf70937b 100644 --- a/packages/minidb/src/query.ts +++ b/packages/minidb/src/query.ts @@ -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)[t]; } return cur; @@ -43,7 +43,7 @@ export function setPath(obj: Doc, path: Path, value: unknown): Doc { let cur = obj as Record; 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; diff --git a/packages/minidb/src/server.ts b/packages/minidb/src/server.ts index 7ac24e53b..5f00a8ed0 100644 --- a/packages/minidb/src/server.ts +++ b/packages/minidb/src/server.ts @@ -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); diff --git a/packages/minidb/src/skiplist.ts b/packages/minidb/src/skiplist.ts index 5fd4426db..9feaac4b1 100644 --- a/packages/minidb/src/skiplist.ts +++ b/packages/minidb/src/skiplist.ts @@ -76,8 +76,8 @@ export class SkipList { } insert(key: K, val: V): SkipNode { - const update = new Array>(MAX_LEVEL); - const rank = new Array(MAX_LEVEL).fill(0); + const update = Array.from>({ length: MAX_LEVEL }); + const rank = Array.from({ length: MAX_LEVEL }, () => 0); let x: SkipNode = this.header; const target = { key, val }; @@ -134,7 +134,7 @@ export class SkipList { } delete(key: K, val: V): boolean { - const update = new Array>(MAX_LEVEL); + const update = Array.from>({ length: MAX_LEVEL }); let x: SkipNode = this.header; const target = { key, val }; for (let i = this.level - 1; i >= 0; i--) { diff --git a/packages/minidb/src/text-index.ts b/packages/minidb/src/text-index.ts index dcb4b9de6..d49867f41 100644 --- a/packages/minidb/src/text-index.ts +++ b/packages/minidb/src/text-index.ts @@ -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; diff --git a/packages/minidb/src/text-postings.ts b/packages/minidb/src/text-postings.ts index ab1ff8707..4687d3db5 100644 --- a/packages/minidb/src/text-postings.ts +++ b/packages/minidb/src/text-postings.ts @@ -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); diff --git a/packages/minidb/src/wal.ts b/packages/minidb/src/wal.ts index d5b9f0ecb..bc54f1a7b 100644 --- a/packages/minidb/src/wal.ts +++ b/packages/minidb/src/wal.ts @@ -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(); }); } } }; diff --git a/packages/minidb/test/codec.test.ts b/packages/minidb/test/codec.test.ts index c930abef1..0482aeb01 100644 --- a/packages/minidb/test/codec.test.ts +++ b/packages/minidb/test/codec.test.ts @@ -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); }); diff --git a/packages/minidb/test/compaction-fault.test.ts b/packages/minidb/test/compaction-fault.test.ts index 79a57ca17..1ddc0315d 100644 --- a/packages/minidb/test/compaction-fault.test.ts +++ b/packages/minidb/test/compaction-fault.test.ts @@ -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 () => { diff --git a/packages/minidb/test/compaction-internal.test.ts b/packages/minidb/test/compaction-internal.test.ts index 7a8426458..52afc2228 100644 --- a/packages/minidb/test/compaction-internal.test.ts +++ b/packages/minidb/test/compaction-internal.test.ts @@ -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(); }); diff --git a/packages/minidb/test/defense.test.ts b/packages/minidb/test/defense.test.ts index db2fb9373..69712fdcc 100644 --- a/packages/minidb/test/defense.test.ts +++ b/packages/minidb/test/defense.test.ts @@ -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 }); }); diff --git a/packages/minidb/test/e2e/fuzz-model.test.ts b/packages/minidb/test/e2e/fuzz-model.test.ts index 0c12b9128..cc4e72a15 100644 --- a/packages/minidb/test/e2e/fuzz-model.test.ts +++ b/packages/minidb/test/e2e/fuzz-model.test.ts @@ -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); diff --git a/packages/minidb/test/server-extra.test.ts b/packages/minidb/test/server-extra.test.ts index d391723d9..2b9298e3b 100644 --- a/packages/minidb/test/server-extra.test.ts +++ b/packages/minidb/test/server-extra.test.ts @@ -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 }); diff --git a/packages/node-sdk/test/session-background-tasks.test.ts b/packages/node-sdk/test/session-background-tasks.test.ts index bc124a6d7..0c65b9f3f 100644 --- a/packages/node-sdk/test/session-background-tasks.test.ts +++ b/packages/node-sdk/test/session-background-tasks.test.ts @@ -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); await expect(session.stopBackgroundTask('')).rejects.toMatchObject({ name: 'KimiError', - code: 'background.task_id_empty', + code: 'task.task_id_empty', } satisfies Partial); } finally { await harness.close(); diff --git a/packages/node-sdk/test/session-event-types.test.ts b/packages/node-sdk/test/session-event-types.test.ts index dd1fa5918..8f5dd940f 100644 --- a/packages/node-sdk/test/session-event-types.test.ts +++ b/packages/node-sdk/test/session-event-types.test.ts @@ -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': diff --git a/packages/server-e2e/src/v2/index.ts b/packages/server-e2e/src/v2/index.ts index 5b43d8151..7c606fb39 100644 --- a/packages/server-e2e/src/v2/index.ts +++ b/packages/server-e2e/src/v2/index.ts @@ -41,7 +41,6 @@ export type { QuestionsResource, InteractionsResource, SessionWorkspaceResource, - AgentFsResource, SessionFsResource, } from './resources/session.js'; export type { diff --git a/packages/server-e2e/src/v2/token.ts b/packages/server-e2e/src/v2/token.ts index 7a58e0e19..4d03de4b6 100644 --- a/packages/server-e2e/src/v2/token.ts +++ b/packages/server-e2e/src/v2/token.ts @@ -39,16 +39,16 @@ export async function resolveKimiHome(homeDir?: string): Promise { 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 { 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); } /** diff --git a/packages/server-e2e/test/v2/token.test.ts b/packages/server-e2e/test/v2/token.test.ts index 6a28c85a6..4be56daa9 100644 --- a/packages/server-e2e/test/v2/token.test.ts +++ b/packages/server-e2e/test/v2/token.test.ts @@ -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; diff --git a/packages/server/src/routes/meta.ts b/packages/server/src/routes/meta.ts index da0b0365d..330367176 100644 --- a/packages/server/src/routes/meta.ts +++ b/packages/server/src/routes/meta.ts @@ -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, diff --git a/packages/server/test/meta.e2e.test.ts b/packages/server/test/meta.e2e.test.ts index 428d0e25b..751572aa1 100644 --- a/packages/server/test/meta.e2e.test.ts +++ b/packages/server/test/meta.e2e.test.ts @@ -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); diff --git a/packages/server/test/oauth.e2e.test.ts b/packages/server/test/oauth.e2e.test.ts index 560c57c92..5f6d1d2a5 100644 --- a/packages/server/test/oauth.e2e.test.ts +++ b/packages/server/test/oauth.e2e.test.ts @@ -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', ); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3ee68c7e5..e71a63c20 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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