From 5a6fd7c6863de333e481edf7eef7b35ec9d9da27 Mon Sep 17 00:00:00 2001 From: Amir Deris Date: Thu, 11 Sep 2025 19:12:58 -0700 Subject: [PATCH 1/6] used AI response in the json format --- gromit.go | 96 ++++++++++++++++++++++++++++++++----------------------- types.go | 6 ++++ 2 files changed, 62 insertions(+), 40 deletions(-) diff --git a/gromit.go b/gromit.go index 6bd426b..c26756d 100644 --- a/gromit.go +++ b/gromit.go @@ -3,12 +3,12 @@ package main import ( "bufio" "context" + "encoding/json" "errors" "fmt" "io" "os" "os/exec" - "regexp" "runtime" "sort" "strings" @@ -16,12 +16,27 @@ import ( "github.com/urfave/cli/v3" ) -const systemPrompt = `You are an assistant providing terminal commands based on user's questions. - You will be given a question about how to do something in the CLI environment. - You will then find out what command to execute and provide the command. - Make sure to enclose the actual command inside *** marker. - For example, if question is about listing all files in a directory for linux, respond with "***ls***". - If no question is asked by user, continue the conversation. If they want to exit, respond with "***exit***".` +const systemPrompt = `You are an assistant providing terminal commands based on user's questions. + Also you should keep up the conversation with the user in case there is no terminal command to execute. + You will be given a question about how to do something in the CLI environment and then find out what command to execute and provide the command. + Provide your response in the following json format: + { + "command": "the command to execute, can be empty if a response is provided", + "response": "the response to the user, can be empty if a command is provided" + "exit": "true if the user wants to exit, false otherwise" + } + For example, if question is about listing all files in a directory for linux, respond with + { + "command": "ls", + "response": "", + "exit": false + } + If no question is asked by user, continue the conversation. If they want to exit, respond with + { + "command": "", + "response": "", + "exit": true + }` type Gromit struct { cli.Command @@ -145,62 +160,49 @@ func (g *Gromit) actionGromit(ctx context.Context, command *cli.Command) error { model: g.String("model"), systemPrompt: prompt, } - terminalCommand, err := g.extractCommandForQuery(ctx, query) - if err != nil { - return err - } - if terminalCommand != "" { - err = g.handleTerminalCommand(ctx, terminalCommand) + + for ctx.Err() == nil { + response, err := g.extractResponseForQuery(ctx, query) if err != nil { return err } - } - for ctx.Err() == nil { - //read the user input, pass it to AI - reader := bufio.NewReader(g.Reader) - query, err := reader.ReadString('\n') + err = g.handleAiResponse(ctx, response) if err != nil { return err } - terminalCommand, err := g.extractCommandForQuery(ctx, query) + reader := bufio.NewReader(g.Reader) + query, err = reader.ReadString('\n') if err != nil { return err } - if terminalCommand == "exit" { - break - } - if terminalCommand != "" { - err = g.handleTerminalCommand(ctx, terminalCommand) - if err != nil { - return err - } - } } return nil } -func (g *Gromit) extractCommandForQuery(ctx context.Context, query string) (string, error) { +func (g *Gromit) extractResponseForQuery(ctx context.Context, query string) (AiResponse, error) { + var result AiResponse assister, err := g.AssisterCreator.GetAssister(g.configuration.AiParameters) if err != nil { - return "", err + return result, err } if query == "" { query = "Can you please introduce yourself or continue the conversation?" } response, err := assister.GetTerminalCommand(ctx, query) if err != nil { - return "", err + return result, err } - //command is enclosed in *** marker - regexp := regexp.MustCompile(`\*\*\*(.*?)\*\*\*`) - commands := regexp.FindStringSubmatch(response) - var command string - if len(commands) > 0 { - command = commands[1] - } else { - g.print(response) + for _, s := range []string{"json", "```"} { + response = strings.ReplaceAll(response, s, "") } - return command, nil + strings.ReplaceAll(response, "json", "") + if !json.Valid([]byte(response)) { + return result, fmt.Errorf("received invalid json response: %s", response) + } + if err = json.Unmarshal([]byte(response), &result); err != nil { + return result, fmt.Errorf("failed to unmarshal json response: \n %s \n error: %s", response, err.Error()) + } + return result, nil } func (g *Gromit) handleTerminalCommand(ctx context.Context, terminalCommand string) error { @@ -222,6 +224,20 @@ func (g *Gromit) handleTerminalCommand(ctx context.Context, terminalCommand stri return nil } +func (g *Gromit) handleAiResponse(ctx context.Context, aiResponse AiResponse) error { + if aiResponse.Command != "" { + err := g.handleTerminalCommand(ctx, aiResponse.Command) + if err != nil { + return err + } + } else if aiResponse.Response != "" { + g.print(aiResponse.Response) + } else if aiResponse.Exit { + os.Exit(0) + } + return nil +} + // adds environment info such as OS, available shells, etc to the system prompt for the AI func addEnvironmentInfo(systemInfo systemInfo, systemPrompt string) string { result := fmt.Sprintf("%s. User's operating system is %s", systemPrompt, systemInfo.operatingSystem) diff --git a/types.go b/types.go index 80dd9f9..c64e700 100644 --- a/types.go +++ b/types.go @@ -37,3 +37,9 @@ type AiParameters struct { apiKey string maxTokens int64 } + +type AiResponse struct { + Command string `json:"command"` + Response string `json:"response"` + Exit bool `json:"exit"` +} From 6c0fa43579773ce08a53e0b972afc6faa1f72ef2 Mon Sep 17 00:00:00 2001 From: Amir Deris Date: Sat, 13 Sep 2025 16:54:32 -0700 Subject: [PATCH 2/6] recording continuous conversation into its own type --- gromit.go | 30 +++++++++++++++++++++--------- types.go | 13 +++++++++++++ 2 files changed, 34 insertions(+), 9 deletions(-) diff --git a/gromit.go b/gromit.go index c26756d..f7669eb 100644 --- a/gromit.go +++ b/gromit.go @@ -145,14 +145,26 @@ func WithAskForConfirmation(confirm bool) ConfigurationModifier { } } +var conversations []Conversation + func (g *Gromit) actionGromit(ctx context.Context, command *cli.Command) error { commandArgs := command.Args().Slice() query := strings.Join(commandArgs, " ") + if query == "" { + query = "Can you please introduce yourself or continue the conversation?" + } prompt := g.String("systemPrompt") if prompt == "" { prompt = systemPrompt } prompt = addEnvironmentInfo(g.configuration.systemInfo, prompt) + conversations = append(conversations, Conversation{ + Role: SystemRole, + Message: prompt, + }, Conversation{ + Role: UserRole, + Message: query, + }) g.configuration.AiParameters = AiParameters{ maxTokens: g.Int64("maxTokens"), apiKey: g.String("apiKey"), @@ -162,7 +174,7 @@ func (g *Gromit) actionGromit(ctx context.Context, command *cli.Command) error { } for ctx.Err() == nil { - response, err := g.extractResponseForQuery(ctx, query) + response, err := g.extractResponseForQuery(ctx, &conversations) if err != nil { return err } @@ -179,16 +191,14 @@ func (g *Gromit) actionGromit(ctx context.Context, command *cli.Command) error { return nil } -func (g *Gromit) extractResponseForQuery(ctx context.Context, query string) (AiResponse, error) { +func (g *Gromit) extractResponseForQuery(ctx context.Context, conversations *[]Conversation) (AiResponse, error) { var result AiResponse assister, err := g.AssisterCreator.GetAssister(g.configuration.AiParameters) if err != nil { return result, err } - if query == "" { - query = "Can you please introduce yourself or continue the conversation?" - } - response, err := assister.GetTerminalCommand(ctx, query) + + response, err := assister.GetTerminalCommand(ctx, conversations) if err != nil { return result, err } @@ -225,14 +235,16 @@ func (g *Gromit) handleTerminalCommand(ctx context.Context, terminalCommand stri } func (g *Gromit) handleAiResponse(ctx context.Context, aiResponse AiResponse) error { + if aiResponse.Response != "" { + g.print(aiResponse.Response) + } if aiResponse.Command != "" { err := g.handleTerminalCommand(ctx, aiResponse.Command) if err != nil { return err } - } else if aiResponse.Response != "" { - g.print(aiResponse.Response) - } else if aiResponse.Exit { + } + if aiResponse.Exit { os.Exit(0) } return nil diff --git a/types.go b/types.go index c64e700..14e2486 100644 --- a/types.go +++ b/types.go @@ -43,3 +43,16 @@ type AiResponse struct { Response string `json:"response"` Exit bool `json:"exit"` } + +type Conversation struct { + Role Role + Message string +} + +type Role int + +const ( + SystemRole Role = iota + UserRole + AssistantRole +) From 34ad5c4a9b3478daa7686289435cb8d36d3b8344 Mon Sep 17 00:00:00 2001 From: Amir Deris Date: Sat, 13 Sep 2025 17:28:43 -0700 Subject: [PATCH 3/6] Made conversation continuous for openai, anthropic and gemini agents --- agent.go | 61 +++++++++++++++++++++++++++++++++++++++++-------------- gromit.go | 24 +++++++++++++++++----- types.go | 4 ++-- 3 files changed, 67 insertions(+), 22 deletions(-) diff --git a/agent.go b/agent.go index 04cf8e3..213e0cd 100644 --- a/agent.go +++ b/agent.go @@ -24,7 +24,7 @@ const ( ) type Assister interface { - GetTerminalCommand(ctx context.Context, userMessage string) (string, error) + GetTerminalCommand(ctx context.Context, conversations *[]Conversation) (string, error) } var _ Assister = (*OpenAIAssister)(nil) @@ -35,7 +35,7 @@ type OpenAIAssister struct { AiParameters } -func (o *OpenAIAssister) GetTerminalCommand(ctx context.Context, userMessage string) (string, error) { +func (o *OpenAIAssister) GetTerminalCommand(ctx context.Context, conversations *[]Conversation) (string, error) { apiKey := o.AiParameters.apiKey var client openai.Client if apiKey != "" { @@ -43,12 +43,19 @@ func (o *OpenAIAssister) GetTerminalCommand(ctx context.Context, userMessage str } else { client = openai.NewClient() } + messages := []openai.ChatCompletionMessageParamUnion{ + openai.SystemMessage(o.AiParameters.systemPrompt), + } + for _, conversation := range *conversations { + if conversation.Role == UserRole { + messages = append(messages, openai.UserMessage(conversation.Text)) + } else if conversation.Role == AssistantRole { + messages = append(messages, openai.AssistantMessage(conversation.Text)) + } + } chatCompletion, err := client.Chat.Completions.New(ctx, openai.ChatCompletionNewParams{ - Messages: []openai.ChatCompletionMessageParamUnion{ - openai.UserMessage(userMessage), - openai.SystemMessage(o.AiParameters.systemPrompt), - }, - Model: o.AiParameters.model, + Messages: messages, + Model: o.AiParameters.model, }) if err != nil { return "", err @@ -60,7 +67,7 @@ type AnthropicAIAssister struct { AiParameters } -func (c *AnthropicAIAssister) GetTerminalCommand(ctx context.Context, userMessage string) (string, error) { +func (c *AnthropicAIAssister) GetTerminalCommand(ctx context.Context, conversations *[]Conversation) (string, error) { apiKey := c.AiParameters.apiKey maxTokens := c.AiParameters.maxTokens if maxTokens == 0 { @@ -72,15 +79,23 @@ func (c *AnthropicAIAssister) GetTerminalCommand(ctx context.Context, userMessag } else { client = anthropic.NewClient() } + + var messages []anthropic.MessageParam + for _, conversation := range *conversations { + if conversation.Role == UserRole { + messages = append(messages, anthropic.NewUserMessage(anthropic.NewTextBlock(conversation.Text))) + } else if conversation.Role == AssistantRole { + messages = append(messages, anthropic.NewAssistantMessage(anthropic.NewTextBlock(conversation.Text))) + } + } + message, err := client.Messages.New(ctx, anthropic.MessageNewParams{ MaxTokens: maxTokens, System: []anthropic.TextBlockParam{ {Text: c.AiParameters.systemPrompt}, }, - Messages: []anthropic.MessageParam{ - anthropic.NewUserMessage(anthropic.NewTextBlock(userMessage)), - }, - Model: anthropic.Model(c.model), + Messages: messages, + Model: anthropic.Model(c.model), }) if err != nil { return "", err @@ -99,7 +114,7 @@ type GeminiAIAssister struct { AiParameters } -func (g *GeminiAIAssister) GetTerminalCommand(ctx context.Context, userMessage string) (string, error) { +func (g *GeminiAIAssister) GetTerminalCommand(ctx context.Context, conversations *[]Conversation) (string, error) { apiKey := g.AiParameters.apiKey client, err := genai.NewClient(ctx, &genai.ClientConfig{ Backend: genai.BackendGeminiAPI, @@ -115,11 +130,27 @@ func (g *GeminiAIAssister) GetTerminalCommand(ctx context.Context, userMessage s }, }, } - chat, err := client.Chats.Create(ctx, g.model, config, nil) + var history []*genai.Content + var lastUserMessage string + for _, conversation := range *conversations { + if conversation.Role == UserRole { + history = append(history, &genai.Content{ + Role: genai.RoleUser, + Parts: []*genai.Part{{Text: conversation.Text}}, + }) + lastUserMessage = conversation.Text + } else if conversation.Role == AssistantRole { + history = append(history, &genai.Content{ + Role: genai.RoleModel, + Parts: []*genai.Part{{Text: conversation.Text}}, + }) + } + } + chat, err := client.Chats.Create(ctx, g.model, config, history) if err != nil { return "", err } - result, err := chat.SendMessage(ctx, genai.Part{Text: userMessage}) + result, err := chat.SendMessage(ctx, genai.Part{Text: lastUserMessage}) if err != nil { return "", err } diff --git a/gromit.go b/gromit.go index f7669eb..303e4e1 100644 --- a/gromit.go +++ b/gromit.go @@ -19,7 +19,7 @@ import ( const systemPrompt = `You are an assistant providing terminal commands based on user's questions. Also you should keep up the conversation with the user in case there is no terminal command to execute. You will be given a question about how to do something in the CLI environment and then find out what command to execute and provide the command. - Provide your response in the following json format: + Always provide your response in the following json format: { "command": "the command to execute, can be empty if a response is provided", "response": "the response to the user, can be empty if a command is provided" @@ -31,6 +31,12 @@ const systemPrompt = `You are an assistant providing terminal commands based on "response": "", "exit": false } + If the question is about telling a joke, respond with: + { + "command": "", + "response": "Some funny joke!", + "exit": false + } If no question is asked by user, continue the conversation. If they want to exit, respond with { "command": "", @@ -159,11 +165,11 @@ func (g *Gromit) actionGromit(ctx context.Context, command *cli.Command) error { } prompt = addEnvironmentInfo(g.configuration.systemInfo, prompt) conversations = append(conversations, Conversation{ - Role: SystemRole, - Message: prompt, + Role: SystemRole, + Text: prompt, }, Conversation{ - Role: UserRole, - Message: query, + Role: UserRole, + Text: query, }) g.configuration.AiParameters = AiParameters{ maxTokens: g.Int64("maxTokens"), @@ -187,6 +193,10 @@ func (g *Gromit) actionGromit(ctx context.Context, command *cli.Command) error { if err != nil { return err } + conversations = append(conversations, Conversation{ + Role: UserRole, + Text: query, + }) } return nil } @@ -212,6 +222,10 @@ func (g *Gromit) extractResponseForQuery(ctx context.Context, conversations *[]C if err = json.Unmarshal([]byte(response), &result); err != nil { return result, fmt.Errorf("failed to unmarshal json response: \n %s \n error: %s", response, err.Error()) } + *conversations = append(*conversations, Conversation{ + Role: AssistantRole, + Text: response, + }) return result, nil } diff --git a/types.go b/types.go index 14e2486..238fbd2 100644 --- a/types.go +++ b/types.go @@ -45,8 +45,8 @@ type AiResponse struct { } type Conversation struct { - Role Role - Message string + Role Role + Text string } type Role int From f23cf51295ddb1e2f5dc301485bfadaf729bb8ed Mon Sep 17 00:00:00 2001 From: Amir Deris Date: Mon, 15 Sep 2025 20:36:19 -0700 Subject: [PATCH 4/6] fixing tests work in progress --- agent_test.go | 6 +++++- gromit.go | 6 +++++- gromit_test.go | 31 +++++++++++++++++++++++-------- 3 files changed, 33 insertions(+), 10 deletions(-) diff --git a/agent_test.go b/agent_test.go index 813d2c8..6fa5161 100644 --- a/agent_test.go +++ b/agent_test.go @@ -195,7 +195,11 @@ func TestGetTerminalCommand(t *testing.T) { }, } } - command, err := assister.GetTerminalCommand(t.Context(), "I want to list all files in current directory") + conversations := &[]Conversation{ + {Text: systemPrompt, Role: SystemRole}, + {Text: "I want to list all files in current directory", Role: UserRole}, + } + command, err := assister.GetTerminalCommand(t.Context(), conversations) require.NoError(t, err) require.Contains(t, command, "ls") }) diff --git a/gromit.go b/gromit.go index 303e4e1..931576d 100644 --- a/gromit.go +++ b/gromit.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" "io" + "log" "os" "os/exec" "runtime" @@ -127,7 +128,10 @@ func getAvailablePathExecutables() []string { } func (m *messagePrinter) print(s string) { - fmt.Fprintf(m.w, "%s %s %s", m.promptPrefix, s, m.delimiter) + _, err := fmt.Fprintf(m.w, "%s %s %s", m.promptPrefix, s, m.delimiter) + if err != nil { + log.Fatal(err) + } } func WithPromptPrefix(prefix string) ConfigurationModifier { diff --git a/gromit_test.go b/gromit_test.go index b2d9934..0feeeb8 100644 --- a/gromit_test.go +++ b/gromit_test.go @@ -60,7 +60,7 @@ func TestWhenAIProviderFailsToCreateAssister(t *testing.T) { } g, err := NewGromit(m) require.NoError(t, err) - _, err = g.extractCommandForQuery(t.Context(), "some query") + _, err = g.extractResponseForQuery(t.Context(), &[]Conversation{}) require.EqualError(t, err, "Unable to create assister") } @@ -77,7 +77,7 @@ func TestWhenAIProviderFailsToFindTheCommand(t *testing.T) { func TestAIAssisterFindingCorrectCommand(t *testing.T) { var buff bytes.Buffer m := &mockAIProvider{ - commandResult: "***ls***", + aiResponse: "json```{\"Command\": \"ls\",\"Response\": \"\",\"Exit\": false}```", } g, err := NewGromit(m, WithWriter(&buff), WithPromptPrefix("🐶"), WithAskForConfirmation(false)) require.NoError(t, err) @@ -98,15 +98,30 @@ func TestAIAssisterFindingCorrectCommand(t *testing.T) { require.Contains(t, m.actualAiParameters.systemPrompt, "User's operating system is") require.Contains(t, m.actualAiParameters.systemPrompt, "User's current shell is") require.Contains(t, m.actualAiParameters.systemPrompt, "User's available path commands are") - require.Contains(t, m.actualUserMessage, "I want to list all files in current directory") + + //_ := []Conversation { + // {Role: SystemRole, Text: "myPrompt"}, + // {Role: UserRole, Text: "hello my ai friend!"}, + // {Role: UserRole, Text: "hello my ai friend!"}, + //} + + for _, c := range *m.actualConversations { + if c.Role == SystemRole { + require.Contains(t, c.Text, "myPrompt") + } else if c.Role == UserRole { + require.Contains(t, c.Text, "I want to list all files in current directory") + } else { + require.Failf(t, "Unknown conversation %s", c.Text) + } + } } type mockAIProvider struct { assisterError error commandError error - commandResult string + aiResponse string - actualUserMessage string + actualConversations *[]Conversation actualAiParameters AiParameters } @@ -119,10 +134,10 @@ func (m *mockAIProvider) GetAssister(p AiParameters) (Assister, error) { return m, nil } -func (m *mockAIProvider) GetTerminalCommand(ctx context.Context, userMessage string) (string, error) { - m.actualUserMessage = userMessage +func (m *mockAIProvider) GetTerminalCommand(ctx context.Context, conversations *[]Conversation) (string, error) { + m.actualConversations = conversations if m.commandError != nil { return "", m.commandError } - return m.commandResult, nil + return m.aiResponse, nil } From 71d6b15419e5f3830c724fb0597bd73c3ec7682f Mon Sep 17 00:00:00 2001 From: Amir Deris Date: Mon, 15 Sep 2025 21:06:51 -0700 Subject: [PATCH 5/6] fixed gromit_test.go --- gromit.go | 14 ++++++++------ gromit_test.go | 45 ++++++++++++++++++++++----------------------- 2 files changed, 30 insertions(+), 29 deletions(-) diff --git a/gromit.go b/gromit.go index 931576d..6ca02d8 100644 --- a/gromit.go +++ b/gromit.go @@ -188,10 +188,13 @@ func (g *Gromit) actionGromit(ctx context.Context, command *cli.Command) error { if err != nil { return err } - err = g.handleAiResponse(ctx, response) + exit, err := g.handleAiResponse(ctx, response) if err != nil { return err } + if exit { + return nil + } reader := bufio.NewReader(g.Reader) query, err = reader.ReadString('\n') if err != nil { @@ -219,7 +222,6 @@ func (g *Gromit) extractResponseForQuery(ctx context.Context, conversations *[]C for _, s := range []string{"json", "```"} { response = strings.ReplaceAll(response, s, "") } - strings.ReplaceAll(response, "json", "") if !json.Valid([]byte(response)) { return result, fmt.Errorf("received invalid json response: %s", response) } @@ -252,20 +254,20 @@ func (g *Gromit) handleTerminalCommand(ctx context.Context, terminalCommand stri return nil } -func (g *Gromit) handleAiResponse(ctx context.Context, aiResponse AiResponse) error { +func (g *Gromit) handleAiResponse(ctx context.Context, aiResponse AiResponse) (shouldExit bool, error error) { if aiResponse.Response != "" { g.print(aiResponse.Response) } if aiResponse.Command != "" { err := g.handleTerminalCommand(ctx, aiResponse.Command) if err != nil { - return err + return false, err } } if aiResponse.Exit { - os.Exit(0) + return true, nil } - return nil + return false, nil } // adds environment info such as OS, available shells, etc to the system prompt for the AI diff --git a/gromit_test.go b/gromit_test.go index 0feeeb8..668c3d3 100644 --- a/gromit_test.go +++ b/gromit_test.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "errors" + "io" "runtime" "strings" "testing" @@ -77,15 +78,26 @@ func TestWhenAIProviderFailsToFindTheCommand(t *testing.T) { func TestAIAssisterFindingCorrectCommand(t *testing.T) { var buff bytes.Buffer m := &mockAIProvider{ - aiResponse: "json```{\"Command\": \"ls\",\"Response\": \"\",\"Exit\": false}```", + aiResponse: []string{ + "json```{\"Command\": \"\",\"Response\": \"Hello to you as well!\",\"Exit\": false}```", + "json```{\"Command\": \"ls\",\"Response\": \"\",\"Exit\": false}```", + "json```{\"Command\": \"\",\"Response\": \"\",\"Exit\": true}```", + }, } g, err := NewGromit(m, WithWriter(&buff), WithPromptPrefix("🐶"), WithAskForConfirmation(false)) require.NoError(t, err) - g.Reader = strings.NewReader("I want to list all files in current directory\n") - g.Run(t.Context(), []string{"gromit", "--model", "myModel", "--agent", "myAgent", + + readers := []io.Reader{ + strings.NewReader("I want to list all files in current directory\n"), + strings.NewReader("Thank you! bye!\n"), + } + g.Reader = io.MultiReader(readers...) + err = g.Run(t.Context(), []string{"gromit", "--model", "myModel", "--agent", "myAgent", "--apiKey=key1234", "--maxTokens=2000", "--systemPrompt", "myPrompt", "hello", "my", "ai", "friend!"}) + require.NoError(t, err) result := buff.String() + require.Contains(t, result, "🐶 Hello to you as well!") require.Contains(t, result, "🐶 In order to do that, you need to run") require.Contains(t, result, "🐶 ls") require.Contains(t, result, "README.md") @@ -98,28 +110,13 @@ func TestAIAssisterFindingCorrectCommand(t *testing.T) { require.Contains(t, m.actualAiParameters.systemPrompt, "User's operating system is") require.Contains(t, m.actualAiParameters.systemPrompt, "User's current shell is") require.Contains(t, m.actualAiParameters.systemPrompt, "User's available path commands are") - - //_ := []Conversation { - // {Role: SystemRole, Text: "myPrompt"}, - // {Role: UserRole, Text: "hello my ai friend!"}, - // {Role: UserRole, Text: "hello my ai friend!"}, - //} - - for _, c := range *m.actualConversations { - if c.Role == SystemRole { - require.Contains(t, c.Text, "myPrompt") - } else if c.Role == UserRole { - require.Contains(t, c.Text, "I want to list all files in current directory") - } else { - require.Failf(t, "Unknown conversation %s", c.Text) - } - } } type mockAIProvider struct { - assisterError error - commandError error - aiResponse string + assisterError error + commandError error + aiResponse []string + aiResponseIndex int actualConversations *[]Conversation @@ -139,5 +136,7 @@ func (m *mockAIProvider) GetTerminalCommand(ctx context.Context, conversations * if m.commandError != nil { return "", m.commandError } - return m.aiResponse, nil + response := m.aiResponse[m.aiResponseIndex] + m.aiResponseIndex = m.aiResponseIndex + 1 + return response, nil } From ee186eb88e8e149881b276daadb742c07cc096bb Mon Sep 17 00:00:00 2001 From: Amir Deris Date: Tue, 16 Sep 2025 20:44:59 -0700 Subject: [PATCH 6/6] added more test coverage for gromit --- gromit.go | 2 +- gromit_test.go | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/gromit.go b/gromit.go index 6ca02d8..0957095 100644 --- a/gromit.go +++ b/gromit.go @@ -235,7 +235,7 @@ func (g *Gromit) extractResponseForQuery(ctx context.Context, conversations *[]C return result, nil } -func (g *Gromit) handleTerminalCommand(ctx context.Context, terminalCommand string) error { +func (g *Gromit) handleTerminalCommand(_ context.Context, terminalCommand string) error { g.print("In order to do that, you need to run:") g.print(terminalCommand) confirmation, err := g.askConfirmation("Would you like to run this command?") diff --git a/gromit_test.go b/gromit_test.go index 668c3d3..95c7ea2 100644 --- a/gromit_test.go +++ b/gromit_test.go @@ -112,6 +112,19 @@ func TestAIAssisterFindingCorrectCommand(t *testing.T) { require.Contains(t, m.actualAiParameters.systemPrompt, "User's available path commands are") } +func TestAIAssisterProvidingInvalidJsonResponse(t *testing.T) { + var buff bytes.Buffer + m := &mockAIProvider{ + aiResponse: []string{ + "json```invalid response```", + }, + } + g, err := NewGromit(m, WithWriter(&buff)) + require.NoError(t, err) + err = g.Run(t.Context(), []string{}) + require.Errorf(t, err, "received invalid json response: invalid response") +} + type mockAIProvider struct { assisterError error commandError error