kimi-code / apps /vscode /test /event-adapter.test.ts
SaylorTwift's picture
SaylorTwift HF Staff
Add files using upload-large-folder tool
6a2bc3b verified
Raw History Blame Contribute Delete
15.2 kB
/**
* Scenario: public Node SDK events are projected into the released VS Code Webview protocol.
* Responsibilities: verify legacy shapes, routing state, and terminal metadata one event at a time.
* Wiring: the pure adapter and real protocol types are used directly; there are no stubs.
* Run: pnpm exec vitest run --config apps/vscode/vitest.config.ts apps/vscode/test/event-adapter.test.ts
*/
import { describe, expect, it } from 'vitest';
import { isPreflightError } from '../shared/errors';
import {
adaptSdkEvent,
createEventAdapterState,
} from '../src/runtime/event-adapter';
describe('event adapter (projects SDK events into the legacy Webview contract)', () => {
it('emits the pending input when a main-agent turn starts', () => {
const result = adaptSdkEvent(
createEventAdapterState(),
{
type: 'turn.started',
sessionId: 'session-1',
agentId: 'main',
turnId: 7,
origin: { kind: 'user' },
},
{ pendingInput: 'Fix the failing test' },
);
expect(result.event).toEqual({
type: 'TurnBegin',
payload: { user_input: 'Fix the failing test' },
_sessionId: 'session-1',
});
});
it('emits text content when the assistant streams a delta', () => {
const result = adaptSdkEvent(createEventAdapterState(), {
type: 'assistant.delta',
sessionId: 'session-1',
agentId: 'main',
turnId: 7,
delta: 'Done',
});
expect(result.event).toEqual({
type: 'ContentPart',
payload: { type: 'text', text: 'Done' },
_sessionId: 'session-1',
});
});
it('emits thinking content when the model streams a thinking delta', () => {
const result = adaptSdkEvent(createEventAdapterState(), {
type: 'thinking.delta',
sessionId: 'session-1',
agentId: 'main',
turnId: 7,
delta: 'Checking the types',
});
expect(result.event).toEqual({
type: 'ContentPart',
payload: { type: 'think', think: 'Checking the types' },
_sessionId: 'session-1',
});
});
it('emits a numbered legacy step when an SDK step starts', () => {
const result = adaptSdkEvent(createEventAdapterState(), {
type: 'turn.step.started',
sessionId: 'session-1',
agentId: 'main',
turnId: 7,
step: 2,
stepId: 'step-2',
});
expect(result.event).toEqual({
type: 'StepBegin',
payload: { n: 2 },
_sessionId: 'session-1',
});
});
it.each([
['Bash', 'Shell'],
['Read', 'ReadFile'],
['Write', 'WriteFile'],
['Edit', 'StrReplaceFile'],
['TodoList', 'SetTodoList'],
['Glob', 'Glob'],
] as const)('maps the %s tool name to %s when a tool starts', (sdkName, legacyName) => {
const result = adaptSdkEvent(createEventAdapterState(), {
type: 'tool.call.started',
sessionId: 'session-1',
agentId: 'main',
turnId: 7,
toolCallId: 'tool-1',
name: sdkName,
args: { path: 'src/index.ts' },
});
expect(result.event).toEqual({
type: 'ToolCall',
payload: {
type: 'function',
id: 'tool-1',
function: {
name: legacyName,
arguments: '{"path":"src/index.ts"}',
},
},
_sessionId: 'session-1',
});
});
it('preserves each tool-call ID when argument deltas are interleaved', () => {
const state = createEventAdapterState();
const first = adaptSdkEvent(state, {
type: 'tool.call.delta',
sessionId: 'session-1',
agentId: 'main',
turnId: 7,
toolCallId: 'tool-a',
name: 'Read',
argumentsPart: '{"path":"a',
});
const second = adaptSdkEvent(first.state, {
type: 'tool.call.delta',
sessionId: 'session-1',
agentId: 'main',
turnId: 7,
toolCallId: 'tool-b',
name: 'Read',
argumentsPart: '{"path":"b',
});
expect([first.event, second.event]).toEqual([
{
type: 'ToolCallPart',
payload: { tool_call_id: 'tool-a', arguments_part: '{"path":"a' },
_sessionId: 'session-1',
},
{
type: 'ToolCallPart',
payload: { tool_call_id: 'tool-b', arguments_part: '{"path":"b' },
_sessionId: 'session-1',
},
]);
});
it('emits a legacy result when an SDK tool call finishes', () => {
const result = adaptSdkEvent(createEventAdapterState(), {
type: 'tool.result',
sessionId: 'session-1',
agentId: 'main',
turnId: 7,
toolCallId: 'tool-1',
output: { exitCode: 0 },
isError: false,
});
expect(result.event).toEqual({
type: 'ToolResult',
payload: {
tool_call_id: 'tool-1',
return_value: {
is_error: false,
output: '{\n "exitCode": 0\n}',
message: '',
display: [],
},
},
_sessionId: 'session-1',
});
});
it('carries a file diff display from tool start into its matching result', () => {
const started = adaptSdkEvent(createEventAdapterState(), {
type: 'tool.call.started',
sessionId: 'session-1',
agentId: 'main',
turnId: 7,
toolCallId: 'tool-1',
name: 'Edit',
args: { path: 'src/index.ts' },
display: {
kind: 'diff',
path: 'src/index.ts',
before: 'old',
after: 'new',
},
});
const finished = adaptSdkEvent(started.state, {
type: 'tool.result',
sessionId: 'session-1',
agentId: 'main',
turnId: 7,
toolCallId: 'tool-1',
output: 'updated',
});
expect(finished.event).toMatchObject({
type: 'ToolResult',
payload: {
return_value: {
display: [{
type: 'diff',
path: 'src/index.ts',
old_text: 'old',
new_text: 'new',
}],
},
},
});
});
it('carries a todo display only to the result with the same tool-call ID', () => {
const started = adaptSdkEvent(createEventAdapterState(), {
type: 'tool.call.started',
sessionId: 'session-1',
agentId: 'main',
turnId: 7,
toolCallId: 'todo-1',
name: 'TodoList',
args: {},
display: {
kind: 'todo_list',
items: [{ title: 'Ship it', status: 'done' }],
},
});
const unrelated = adaptSdkEvent(started.state, {
type: 'tool.result',
sessionId: 'session-1',
agentId: 'main',
turnId: 7,
toolCallId: 'other',
output: 'other result',
});
expect(unrelated.event).toMatchObject({
payload: { return_value: { display: [] } },
});
expect(unrelated.state.toolDisplays['todo-1']).toEqual([
{ type: 'todo', items: [{ title: 'Ship it', status: 'done' }] },
]);
const matching = adaptSdkEvent(unrelated.state, {
type: 'tool.result',
sessionId: 'session-1',
agentId: 'main',
turnId: 7,
toolCallId: 'todo-1',
output: 'updated',
});
expect(matching.event).toMatchObject({
payload: {
return_value: {
display: [{ type: 'todo', items: [{ title: 'Ship it', status: 'done' }] }],
},
},
});
});
it('emits snake-case status fields when agent status changes', () => {
const result = adaptSdkEvent(createEventAdapterState(), {
type: 'agent.status.updated',
sessionId: 'session-1',
agentId: 'main',
contextUsage: 0.25,
planMode: true,
usage: {
currentTurn: {
inputOther: 10,
output: 4,
inputCacheRead: 3,
inputCacheCreation: 2,
},
},
});
expect(result.event).toEqual({
type: 'StatusUpdate',
payload: {
context_usage: 0.25,
plan_mode: true,
token_usage: {
input_other: 10,
output: 4,
input_cache_read: 3,
input_cache_creation: 2,
},
},
_sessionId: 'session-1',
});
});
it('derives context_usage from the v2 context token pair', () => {
const result = adaptSdkEvent(createEventAdapterState(), {
type: 'agent.status.updated',
sessionId: 'session-1',
agentId: 'main',
contextTokens: 25_600,
maxContextTokens: 256_000,
});
expect(result.event).toEqual({
type: 'StatusUpdate',
payload: { context_usage: 0.1 },
_sessionId: 'session-1',
});
});
it('preserves an explicit contextUsage field over the context token pair', () => {
const result = adaptSdkEvent(createEventAdapterState(), {
type: 'agent.status.updated',
sessionId: 'session-1',
agentId: 'main',
contextUsage: 0,
contextTokens: 25_600,
maxContextTokens: 256_000,
});
expect(result.event).toMatchObject({
type: 'StatusUpdate',
payload: { context_usage: 0 },
});
});
it('does not derive a ratio from an invalid context capacity', () => {
const result = adaptSdkEvent(createEventAdapterState(), {
type: 'agent.status.updated',
sessionId: 'session-1',
agentId: 'main',
contextTokens: 25_600,
maxContextTokens: 0,
});
expect(result.event).toBeUndefined();
});
it('emits only new token usage when SDK status carries cumulative turn usage', () => {
const first = adaptSdkEvent(createEventAdapterState(), {
type: 'agent.status.updated',
sessionId: 'session-1',
agentId: 'main',
usage: {
currentTurn: {
inputOther: 10,
output: 4,
inputCacheRead: 3,
inputCacheCreation: 2,
},
},
});
const second = adaptSdkEvent(first.state, {
type: 'agent.status.updated',
sessionId: 'session-1',
agentId: 'main',
usage: {
currentTurn: {
inputOther: 14,
output: 7,
inputCacheRead: 8,
inputCacheCreation: 2,
},
},
});
expect(second.event).toEqual({
type: 'StatusUpdate',
payload: {
token_usage: {
input_other: 4,
output: 3,
input_cache_read: 5,
input_cache_creation: 0,
},
},
_sessionId: 'session-1',
});
});
it('routes a child-agent event through its parent tool after spawn', () => {
const spawned = adaptSdkEvent(createEventAdapterState(), {
type: 'subagent.spawned',
sessionId: 'session-1',
agentId: 'main',
subagentId: 'child-1',
subagentName: 'coder',
parentToolCallId: 'agent-call-1',
parentAgentId: 'main',
runInBackground: false,
});
const childEvent = adaptSdkEvent(spawned.state, {
type: 'assistant.delta',
sessionId: 'session-1',
agentId: 'child-1',
turnId: 1,
delta: 'Child result',
});
expect(childEvent.event).toEqual({
type: 'SubagentEvent',
payload: {
parent_tool_call_id: 'agent-call-1',
event: {
type: 'ContentPart',
payload: { type: 'text', text: 'Child result' },
},
},
_sessionId: 'session-1',
});
});
it('scopes a child tool-call ID when the child starts a tool', () => {
const spawned = adaptSdkEvent(createEventAdapterState(), {
type: 'subagent.spawned',
sessionId: 'session-1',
agentId: 'main',
subagentId: 'child-1',
subagentName: 'coder',
parentToolCallId: 'agent-call-1',
parentAgentId: 'main',
runInBackground: false,
});
const childEvent = adaptSdkEvent(spawned.state, {
type: 'tool.call.started',
sessionId: 'session-1',
agentId: 'child-1',
turnId: 1,
toolCallId: 'tool-1',
name: 'Read',
args: { path: 'README.md' },
});
expect(childEvent.event).toEqual({
type: 'SubagentEvent',
payload: {
parent_tool_call_id: 'agent-call-1',
event: {
type: 'ToolCall',
payload: {
type: 'function',
id: 'child-1:tool-1',
function: {
name: 'ReadFile',
arguments: '{"path":"README.md"}',
},
},
},
},
_sessionId: 'session-1',
});
});
it('emits compaction begin when SDK compaction starts', () => {
const result = adaptSdkEvent(createEventAdapterState(), {
type: 'compaction.started',
sessionId: 'session-1',
agentId: 'main',
trigger: 'manual',
});
expect(result.event).toEqual({
type: 'CompactionBegin',
payload: {},
_sessionId: 'session-1',
});
});
it.each([
{
type: 'compaction.completed' as const,
result: { summary: 'Summary', compactedCount: 3, tokensBefore: 100, tokensAfter: 30 },
},
{ type: 'compaction.cancelled' as const },
{ type: 'compaction.blocked' as const, turnId: 7 },
])('emits compaction end when SDK reports $type', (event) => {
const result = adaptSdkEvent(createEventAdapterState(), {
...event,
sessionId: 'session-1',
agentId: 'main',
});
expect(result.event).toEqual({
type: 'CompactionEnd',
payload: {},
_sessionId: 'session-1',
});
});
it('returns terminal metadata when the main turn completes', () => {
const result = adaptSdkEvent(createEventAdapterState(), {
type: 'turn.ended',
sessionId: 'session-1',
agentId: 'main',
turnId: 7,
reason: 'completed',
durationMs: 50,
});
expect(result.event).toBeUndefined();
expect(result.terminal).toEqual({
key: 'session-1:main:7',
sessionId: 'session-1',
agentId: 'main',
turnId: 7,
reason: 'completed',
error: undefined,
});
});
it('preserves the SDK error when a main turn fails', () => {
const result = adaptSdkEvent(createEventAdapterState(), {
type: 'turn.ended',
sessionId: 'session-1',
agentId: 'main',
turnId: 7,
reason: 'failed',
error: {
code: 'internal',
message: 'Provider failed',
retryable: false,
},
});
expect(result.terminal).toEqual({
key: 'session-1:main:7',
sessionId: 'session-1',
agentId: 'main',
turnId: 7,
reason: 'failed',
error: {
code: 'internal',
message: 'Provider failed',
retryable: false,
},
});
});
it('emits a bridge error with the caller-selected phase when the SDK reports an error', () => {
const result = adaptSdkEvent(
createEventAdapterState(),
{
type: 'error',
sessionId: 'session-1',
agentId: 'main',
code: 'internal',
message: 'Configuration failed',
details: { path: 'config.toml' },
retryable: false,
},
{ errorPhase: 'preflight' },
);
expect(result.event).toEqual({
type: 'error',
code: 'internal',
message: 'Configuration failed',
detail: '{\n "path": "config.toml"\n}',
phase: 'preflight',
_sessionId: 'session-1',
});
});
it('classifies a missing Windows Git Bash runtime as a preflight error', () => {
expect(isPreflightError('shell.git_bash_not_found')).toBe(true);
});
});