include llama.cpp since there were some edits
This commit is contained in:
1 parent
db7f28d087
commit
8c66990853
3918 files changed
+1640170
-1
No files matched your search
@@ -0,0 +1,216 @@
|
||||
// Guards the exec_shell_command title while the call is still being
|
||||
// generated: the command must stream into the terminal title from the
|
||||
// first character (args arrive as partial JSON), with a blinking cursor
|
||||
// while the args are incomplete, and the conversation cwd standing in
|
||||
// for the x-tool-cwd value that only arrives with the result message.
|
||||
|
||||
import ChatMessageToolCallBlockExecShellCommand from '$lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlockExecShellCommand.svelte';
|
||||
import ChatMessageAgenticContent from '$lib/components/app/chat/ChatMessages/ChatMessageAgenticContent.svelte';
|
||||
import { SETTINGS_KEYS } from '$lib/constants';
|
||||
import { AgenticSectionType, BuiltInTool, MessageRole, MessageType } from '$lib/enums';
|
||||
import { conversationsStore, settingsStore } from '$lib/stores';
|
||||
import type { AgenticSection, DatabaseMessage } from '$lib/types';
|
||||
import { tick } from 'svelte';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
import { render } from 'vitest-browser-svelte';
|
||||
|
||||
const CONV_ID = 'conv-1';
|
||||
const TOOL_CALL_ID = 'call-1';
|
||||
const TOOL_NAME = BuiltInTool.SERVER_EXEC_SHELL_COMMAND;
|
||||
const CWD = '/home/user/project';
|
||||
|
||||
function messageWithArgs(args: string): DatabaseMessage {
|
||||
return {
|
||||
children: [],
|
||||
content: '',
|
||||
convId: CONV_ID,
|
||||
id: 'assistant-1',
|
||||
parent: null,
|
||||
role: MessageRole.ASSISTANT,
|
||||
timestamp: 0,
|
||||
toolCalls: JSON.stringify([
|
||||
{ function: { arguments: args, name: TOOL_NAME }, id: TOOL_CALL_ID }
|
||||
]),
|
||||
type: MessageType.TEXT
|
||||
};
|
||||
}
|
||||
|
||||
function toolResultMessage(content: string, toolCwd?: string): DatabaseMessage {
|
||||
return {
|
||||
children: [],
|
||||
content,
|
||||
convId: CONV_ID,
|
||||
id: 'tool-1',
|
||||
parent: null,
|
||||
role: MessageRole.TOOL,
|
||||
timestamp: 0,
|
||||
toolCallId: TOOL_CALL_ID,
|
||||
toolCalls: '',
|
||||
toolCwd,
|
||||
type: MessageType.TEXT
|
||||
};
|
||||
}
|
||||
|
||||
function triggerText(container: Element): string {
|
||||
return container.querySelector('[data-slot="collapsible-trigger"]')?.textContent ?? '';
|
||||
}
|
||||
|
||||
function hasCursor(container: Element): boolean {
|
||||
return container.querySelector('.exec-cursor') !== null;
|
||||
}
|
||||
|
||||
describe('exec_shell_command streaming title', () => {
|
||||
beforeEach(() => {
|
||||
settingsStore.updateMultipleConfig({
|
||||
[SETTINGS_KEYS.ALWAYS_SHOW_TOOL_CALL_CONTENT]: false,
|
||||
[SETTINGS_KEYS.SHOW_TOOL_CALL_IN_PROGRESS]: true
|
||||
});
|
||||
conversationsStore.activeConversation = {
|
||||
currNode: null,
|
||||
cwd: CWD,
|
||||
id: CONV_ID,
|
||||
lastModified: 0,
|
||||
name: 'test'
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
conversationsStore.activeConversation = null;
|
||||
conversationsStore.activeMessages = [];
|
||||
});
|
||||
|
||||
it('streams the partial command into the title with a cursor', async () => {
|
||||
const { container } = render(ChatMessageAgenticContent, {
|
||||
isStreaming: true,
|
||||
message: messageWithArgs('{"command":"git sta'),
|
||||
toolMessages: []
|
||||
});
|
||||
|
||||
await tick();
|
||||
|
||||
const title = triggerText(container);
|
||||
|
||||
expect(title).toContain('git sta');
|
||||
expect(title).toContain('$');
|
||||
expect(title).toContain(CWD);
|
||||
expect(hasCursor(container)).toBe(true);
|
||||
});
|
||||
|
||||
it('drops the cursor once the args JSON is complete', async () => {
|
||||
const screen = render(ChatMessageAgenticContent, {
|
||||
isStreaming: true,
|
||||
message: messageWithArgs('{"command":"git sta'),
|
||||
toolMessages: []
|
||||
});
|
||||
|
||||
await tick();
|
||||
|
||||
expect(hasCursor(screen.container)).toBe(true);
|
||||
|
||||
screen.rerender({
|
||||
isStreaming: true,
|
||||
message: messageWithArgs('{"command":"git status"}'),
|
||||
toolMessages: []
|
||||
});
|
||||
await tick();
|
||||
|
||||
expect(triggerText(screen.container)).toContain('git status');
|
||||
expect(hasCursor(screen.container)).toBe(false);
|
||||
});
|
||||
|
||||
it('switches the prompt from the conversation cwd to the persisted one', async () => {
|
||||
const screen = render(ChatMessageAgenticContent, {
|
||||
isStreaming: true,
|
||||
message: messageWithArgs('{"command":"git status"}'),
|
||||
toolMessages: []
|
||||
});
|
||||
|
||||
await tick();
|
||||
|
||||
expect(triggerText(screen.container)).toContain(CWD);
|
||||
|
||||
// Result message lands: its x-tool-cwd takes over the prompt.
|
||||
screen.rerender({
|
||||
isStreaming: true,
|
||||
message: messageWithArgs('{"command":"git status"}'),
|
||||
toolMessages: [toolResultMessage('', '/other/dir')]
|
||||
});
|
||||
await tick();
|
||||
|
||||
expect(triggerText(screen.container)).toContain('/other/dir');
|
||||
expect(triggerText(screen.container)).not.toContain(CWD);
|
||||
});
|
||||
|
||||
it('shows the executing label only after the command finishes streaming', async () => {
|
||||
const screen = render(ChatMessageAgenticContent, {
|
||||
isStreaming: true,
|
||||
message: messageWithArgs('{"command":"git sta'),
|
||||
toolMessages: []
|
||||
});
|
||||
|
||||
await tick();
|
||||
|
||||
// While the args stream, the title cursor alone signals activity.
|
||||
expect(screen.container.textContent).not.toContain('Executing command...');
|
||||
|
||||
screen.rerender({
|
||||
isStreaming: true,
|
||||
message: messageWithArgs('{"command":"git status"}'),
|
||||
toolMessages: []
|
||||
});
|
||||
await tick();
|
||||
|
||||
expect(screen.container.textContent).toContain('Executing command...');
|
||||
});
|
||||
|
||||
it('shows Executing command... while executing before the first output chunk', async () => {
|
||||
// The result message is created empty right before execution, so
|
||||
// the block must not go blank while the process produces nothing.
|
||||
const section: AgenticSection = {
|
||||
content: '',
|
||||
toolArgs: '{"command":"sleep 1"}',
|
||||
toolCallId: TOOL_CALL_ID,
|
||||
toolName: TOOL_NAME,
|
||||
toolResult: '',
|
||||
type: AgenticSectionType.TOOL_CALL
|
||||
};
|
||||
const { container } = render(ChatMessageToolCallBlockExecShellCommand, {
|
||||
isExecuting: true,
|
||||
isStreaming: true,
|
||||
open: true,
|
||||
section
|
||||
});
|
||||
|
||||
await tick();
|
||||
|
||||
expect(container.textContent).toContain('Executing command...');
|
||||
});
|
||||
|
||||
it('updates the title when the store mutates toolCalls in place, as the stream does', async () => {
|
||||
// The live flow never swaps the message object: onToolCallsStreaming
|
||||
// writes growing toolCalls strings into the same store row per delta.
|
||||
conversationsStore.activeMessages = [messageWithArgs('')];
|
||||
|
||||
const screen = render(ChatMessageAgenticContent, {
|
||||
isStreaming: true,
|
||||
message: conversationsStore.activeMessages[0]!,
|
||||
toolMessages: []
|
||||
});
|
||||
|
||||
await tick();
|
||||
|
||||
const idx = conversationsStore.findMessageIndex('assistant-1');
|
||||
|
||||
for (const args of ['{"command', '{"command":"git', '{"command":"git sta']) {
|
||||
conversationsStore.updateMessageAtIndex(idx, {
|
||||
toolCalls: JSON.stringify([
|
||||
{ function: { arguments: args, name: TOOL_NAME }, id: TOOL_CALL_ID }
|
||||
])
|
||||
});
|
||||
await tick();
|
||||
|
||||
expect(triggerText(screen.container)).toContain(args.slice('{"command":"'.length));
|
||||
expect(hasCursor(screen.container)).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user