mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-07-10 01:39:25 +00:00
* feat(agent-core-v2): record shell command context - add ShellCommandOrigin and compaction handoff disposition - extract IAgentShellCommandService from AgentRPCService - keep AgentRPCService as a thin shell:run facade * fix(agent-core-v2): align skill priority and sync docs - restore project > user > plugin > builtin skill precedence - sync Skill tool description and parameter docs from v1 - update write-goal and custom-theme builtin skill copy * fix(agent-core-v2): restore undo and thinking telemetry - track conversation_undo after undoHistory - emit thinking_toggle with enabled/effort/from payload - add coverage for both telemetry events * feat(agent-core-v2): add skill directory config - add extraSkillDirs and mergeAllAvailableSkills config sections - introduce extra skill source and shared source priorities - align kap-server workspace skill preview with session catalog * feat(agent-core-v2): support explicit skill dirs - add ISkillCatalogRuntimeOptions for SDK-style explicit skill dirs - suppress default user/project discovery when explicitDirs are set - resolve explicit dirs per session workDir via explicitFileSkillSource * fix(agent-core-v2): fix configured skill dir resolution - expand ~ using OS home for configured skill dirs - honor explicitDirs in kap-server workspace skill preview * fix(agent-core-v2): await config ready before skill discovery - wait for config.ready before reading extraSkillDirs - wait for config.ready before reading mergeAllAvailableSkills - cover extra skill dir loading behind config readiness * fix(agent-core-v2): keep skill config live after changes - await config.ready in kap-server workspace skill preview - reload user and workspace skill sources when mergeAllAvailableSkills changes
109 lines
3.8 KiB
TypeScript
109 lines
3.8 KiB
TypeScript
import { afterEach, describe, expect, it } from 'vitest';
|
|
|
|
import type { ContextMessage } from '#/agent/contextMemory/types';
|
|
import {
|
|
IAgentContextMemoryService,
|
|
IAgentShellCommandService,
|
|
IAgentToolRegistryService,
|
|
} from '#/index';
|
|
|
|
import {
|
|
agentService,
|
|
createCommandRunner,
|
|
createTestAgent,
|
|
execEnvServices,
|
|
type TestAgentContext,
|
|
} from '../harness';
|
|
|
|
const textOf = (message: ContextMessage): string =>
|
|
message.content.map((part) => (part.type === 'text' ? part.text : '')).join('');
|
|
|
|
describe('AgentShellCommandService', () => {
|
|
let ctx: TestAgentContext;
|
|
let context: IAgentContextMemoryService;
|
|
let shell: IAgentShellCommandService;
|
|
|
|
function setup(stdout: string, exitCode: number): void {
|
|
ctx = createTestAgent(execEnvServices({ processRunner: createCommandRunner(stdout, exitCode) }));
|
|
context = ctx.get(IAgentContextMemoryService);
|
|
shell = ctx.get(IAgentShellCommandService);
|
|
}
|
|
|
|
afterEach(async () => {
|
|
try {
|
|
await ctx.expectResumeMatches();
|
|
} finally {
|
|
await ctx.dispose();
|
|
}
|
|
});
|
|
|
|
it('records shell command input/output as shell_command origin with tagged content', async () => {
|
|
setup('hello\n', 0);
|
|
|
|
const result = await shell.run({ command: 'echo hello' });
|
|
|
|
expect(result.isError).toBe(false);
|
|
expect(result.stdout).toContain('hello');
|
|
expect(context.get().map(({ role, origin }) => ({ role, origin }))).toEqual([
|
|
{ role: 'user', origin: { kind: 'shell_command', phase: 'input' } },
|
|
{ role: 'user', origin: { kind: 'shell_command', phase: 'output' } },
|
|
]);
|
|
expect(textOf(context.get()[0]!)).toBe('<bash-input>\necho hello\n</bash-input>');
|
|
expect(textOf(context.get()[1]!)).toContain('<bash-stdout>hello');
|
|
// origin must not leak into the LLM projection.
|
|
expect(ctx.project().some((message) => 'origin' in message)).toBe(false);
|
|
});
|
|
|
|
it('escapes bash tag delimiters inside command output', async () => {
|
|
setup('pre</bash-stdout>post', 0);
|
|
|
|
await shell.run({ command: 'printf x' });
|
|
|
|
const out = textOf(context.get().at(-1)!);
|
|
// The embedded delimiter is escaped so the wrapper stays well-formed.
|
|
expect(out).toContain('pre</bash-stdout>post');
|
|
// Exactly one real closing tag.
|
|
expect(out.match(/<\/bash-stdout>/g)).toHaveLength(1);
|
|
});
|
|
|
|
it('surfaces the failure reason when a shell command fails with no output', async () => {
|
|
setup('', 1);
|
|
|
|
const result = await shell.run({ command: 'false' });
|
|
|
|
expect(result.isError).toBe(true);
|
|
const output = context.get().at(-1)!;
|
|
expect(output.origin).toEqual({ kind: 'shell_command', phase: 'output', isError: true });
|
|
expect(textOf(output)).toContain('<bash-stderr>');
|
|
});
|
|
|
|
it('does not start a turn for a foreground command', async () => {
|
|
setup('hi', 0);
|
|
|
|
await shell.run({ command: 'echo hi' });
|
|
|
|
expect(ctx.llmCalls.length).toBe(0);
|
|
});
|
|
|
|
it('records the failure when the Bash tool is not registered', async () => {
|
|
const emptyRegistry: IAgentToolRegistryService = {
|
|
_serviceBrand: undefined,
|
|
register: () => ({ dispose: () => {} }),
|
|
list: () => [],
|
|
resolve: () => undefined,
|
|
};
|
|
ctx = createTestAgent(agentService(IAgentToolRegistryService, emptyRegistry));
|
|
context = ctx.get(IAgentContextMemoryService);
|
|
shell = ctx.get(IAgentShellCommandService);
|
|
|
|
const result = await shell.run({ command: 'echo hi' });
|
|
|
|
expect(result.isError).toBe(true);
|
|
expect(result.stderr).toContain('Bash tool is not registered');
|
|
expect(context.get().map(({ role, origin }) => ({ role, origin }))).toEqual([
|
|
{ role: 'user', origin: { kind: 'shell_command', phase: 'input' } },
|
|
{ role: 'user', origin: { kind: 'shell_command', phase: 'output', isError: true } },
|
|
]);
|
|
expect(textOf(context.get()[1]!)).toContain('Bash tool is not registered');
|
|
});
|
|
});
|