From 5a30d5c055a718d55c44568dbf4cd844d05c1401 Mon Sep 17 00:00:00 2001 From: AnnatarHe Date: Sun, 17 May 2026 02:24:59 +0800 Subject: [PATCH] revert(cli): undo PR #273 AI response parsing changes Reverts commit 88967e8 (Merge pull request #273), which introduced sanitizeSuggestedCommand and SSE leading-space handling. The merge turned out to be wrong and is being rolled back. Conflict in commands/query.go was resolved by preserving the getSystemContext(query, ai *model.AIConfig) signature introduced by unrelated PR #274. --- commands/query.go | 39 +------------ commands/query_test.go | 37 +----------- model/ai_service.go | 36 ++++-------- model/ai_service_test.go | 122 --------------------------------------- 4 files changed, 12 insertions(+), 222 deletions(-) diff --git a/commands/query.go b/commands/query.go index ec27c44..8f5dd2c 100644 --- a/commands/query.go +++ b/commands/query.go @@ -102,11 +102,7 @@ func commandQuery(c *cli.Context) error { // Print newline after streaming fmt.Println() - newCommand := sanitizeSuggestedCommand(result.String()) - if newCommand == "" { - color.Red.Println("AI returned an empty response. Try rephrasing your query.") - return fmt.Errorf("empty AI response") - } + newCommand := strings.TrimSpace(result.String()) // Check auto-run configuration if cfg.AI != nil && (cfg.AI.Agent.View || cfg.AI.Agent.Edit || cfg.AI.Agent.Delete) { @@ -192,39 +188,6 @@ func executeCommand(ctx context.Context, command string) error { return nil } -// sanitizeSuggestedCommand normalizes raw AI output into an executable command. -// It strips triple-backtick fences (with optional language tag like bash, sh, -// zsh, fish, pwsh, powershell), strips surrounding single backticks when the -// result is a single line, and trims whitespace. Responses that start with `#` -// are treated as refusal comments and preserved verbatim so the caller can -// surface them to the user without attempting execution. -func sanitizeSuggestedCommand(raw string) string { - s := strings.TrimSpace(raw) - if s == "" { - return "" - } - - if strings.HasPrefix(s, "```") { - s = strings.TrimPrefix(s, "```") - if nl := strings.IndexByte(s, '\n'); nl >= 0 { - switch strings.ToLower(strings.TrimSpace(s[:nl])) { - case "", "bash", "sh", "shell", "zsh", "fish", "pwsh", "powershell": - s = s[nl+1:] - } - } - s = strings.TrimRight(s, " \t\n") - s = strings.TrimSuffix(s, "```") - s = strings.TrimSpace(s) - } - - if !strings.ContainsRune(s, '\n') && len(s) >= 2 && - strings.HasPrefix(s, "`") && strings.HasSuffix(s, "`") { - s = strings.TrimSpace(s[1 : len(s)-1]) - } - - return s -} - func getSystemContext(query string, ai *model.AIConfig) (model.CommandSuggestVariables, error) { // Get shell information shell := os.Getenv("SHELL") diff --git a/commands/query_test.go b/commands/query_test.go index 52bef78..1899bff 100644 --- a/commands/query_test.go +++ b/commands/query_test.go @@ -458,42 +458,7 @@ func (s *queryTestSuite) TestQueryCommandEmptyAIResponse() { } err := s.app.Run(command) - assert.NotNil(s.T(), err) - assert.Contains(s.T(), err.Error(), "empty AI response") -} - -func (s *queryTestSuite) TestSanitizeSuggestedCommand() { - tests := []struct { - name string - in string - want string - }{ - {"plain", "ls -la", "ls -la"}, - {"trims whitespace", " ls -la \n\t", "ls -la"}, - {"fence with bash tag", "```bash\necho hi\n```", "echo hi"}, - {"fence with sh tag", "```sh\necho hi\n```", "echo hi"}, - {"fence with zsh tag", "```zsh\necho hi\n```", "echo hi"}, - {"fence with shell tag", "```shell\necho hi\n```", "echo hi"}, - {"fence with fish tag", "```fish\nset -x FOO bar\n```", "set -x FOO bar"}, - {"fence with powershell tag", "```powershell\nGet-Process\n```", "Get-Process"}, - {"fence with pwsh tag", "```pwsh\nGet-Process\n```", "Get-Process"}, - {"fence no language tag", "```\necho hi\n```", "echo hi"}, - {"fence with trailing newline before closing", "```bash\nls -la\n\n```", "ls -la"}, - {"single backticks around single-line", "`ls -la`", "ls -la"}, - {"single backticks with surrounding space", " `ls -la` ", "ls -la"}, - {"only whitespace", " \n\t ", ""}, - {"empty", "", ""}, - {"comment passthrough preserved", "# refusing: unsafe request", "# refusing: unsafe request"}, - {"multiline without fences kept", "ls\ncat foo", "ls\ncat foo"}, - } - for _, tt := range tests { - s.T().Run(tt.name, func(t *testing.T) { - got := sanitizeSuggestedCommand(tt.in) - if got != tt.want { - t.Errorf("sanitizeSuggestedCommand(%q) = %q, want %q", tt.in, got, tt.want) - } - }) - } + assert.Nil(s.T(), err) } func (s *queryTestSuite) TestQueryCommandDescription() { diff --git a/model/ai_service.go b/model/ai_service.go index a916bf9..51df91a 100644 --- a/model/ai_service.go +++ b/model/ai_service.go @@ -74,26 +74,24 @@ func (s *sseAIService) QueryCommandStream( for scanner.Scan() { line := scanner.Text() - if line == "" { - isError = false + if line == "event: error" { + isError = true continue } - if v, ok := stripSSEField(line, "event:"); ok { - if v == "error" { - isError = true - } - continue - } + if strings.HasPrefix(line, "data:") { + data := line[len("data:"):] - if v, ok := stripSSEField(line, "data:"); ok { if isError { - return fmt.Errorf("server error: %s", v) + return fmt.Errorf("server error: %s", data) } - if v == "[DONE]" { + + if data == "[DONE]" { return nil } - onToken(v) + + onToken(data) + isError = false } } @@ -103,17 +101,3 @@ func (s *sseAIService) QueryCommandStream( return nil } - -// stripSSEField returns the value after prefix, stripping one optional leading -// space per the SSE specification (ยง9.2 "If value starts with a U+0020 SPACE -// character, remove it from value"). -func stripSSEField(line, prefix string) (string, bool) { - if !strings.HasPrefix(line, prefix) { - return "", false - } - v := line[len(prefix):] - if strings.HasPrefix(v, " ") { - v = v[1:] - } - return v, true -} diff --git a/model/ai_service_test.go b/model/ai_service_test.go index 9e252d0..7879557 100644 --- a/model/ai_service_test.go +++ b/model/ai_service_test.go @@ -6,7 +6,6 @@ import ( "fmt" "net/http" "net/http/httptest" - "strings" "testing" ) @@ -91,124 +90,3 @@ func TestQueryCommandStream_ErrorResponseBody(t *testing.T) { }) } } - -func TestQueryCommandStream_SSEParsing(t *testing.T) { - tests := []struct { - name string - body string - wantErr bool - wantErrSubstr string - wantTokens []string - }{ - { - name: "data with space and [DONE] terminates cleanly", - body: "data: [DONE]\n\n", - wantTokens: nil, - }, - { - name: "data without space and [DONE] terminates cleanly", - body: "data:[DONE]\n\n", - wantTokens: nil, - }, - { - name: "single data token with leading space is stripped", - body: "data: hello\n\ndata: [DONE]\n\n", - wantTokens: []string{"hello"}, - }, - { - name: "single data token without leading space passes through", - body: "data:hello\n\ndata:[DONE]\n\n", - wantTokens: []string{"hello"}, - }, - { - name: "multi-token stream concatenates without spurious spaces", - body: "data: ls\n\ndata: -la\n\ndata: [DONE]\n\n", - wantTokens: []string{"ls", " -la"}, - }, - { - name: "event error with space", - body: "event: error\ndata: boom\n\n", - wantErr: true, - wantErrSubstr: "boom", - }, - { - name: "event error without space", - body: "event:error\ndata:boom\n\n", - wantErr: true, - wantErrSubstr: "boom", - }, - { - name: "blank line resets error state between events", - body: "event: error\n\ndata: hello\n\ndata: [DONE]\n\n", - wantTokens: []string{"hello"}, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "text/event-stream") - w.WriteHeader(http.StatusOK) - _, _ = w.Write([]byte(tt.body)) - })) - defer server.Close() - - var got []string - svc := NewAIService() - err := svc.QueryCommandStream( - context.Background(), - CommandSuggestVariables{Shell: "bash", Os: "linux", Query: "test"}, - Endpoint{APIEndpoint: server.URL, Token: "test-token"}, - func(token string) { got = append(got, token) }, - ) - - if tt.wantErr { - if err == nil { - t.Fatalf("expected error, got nil (tokens=%v)", got) - } - if !strings.Contains(err.Error(), tt.wantErrSubstr) { - t.Fatalf("expected error to contain %q, got %q", tt.wantErrSubstr, err.Error()) - } - return - } - - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if len(got) != len(tt.wantTokens) { - t.Fatalf("token count mismatch: want %d %v, got %d %v", len(tt.wantTokens), tt.wantTokens, len(got), got) - } - for i, tok := range tt.wantTokens { - if got[i] != tok { - t.Errorf("token[%d] = %q, want %q", i, got[i], tok) - } - } - }) - } -} - -func TestStripSSEField(t *testing.T) { - tests := []struct { - name string - line string - prefix string - wantVal string - wantOk bool - }{ - {"no match", "foo:bar", "data:", "", false}, - {"match no space", "data:hello", "data:", "hello", true}, - {"match one space stripped", "data: hello", "data:", "hello", true}, - {"match two spaces preserves second", "data: hello", "data:", " hello", true}, - {"empty value no space", "data:", "data:", "", true}, - {"empty value one space", "data: ", "data:", "", true}, - {"event error with space", "event: error", "event:", "error", true}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - v, ok := stripSSEField(tt.line, tt.prefix) - if ok != tt.wantOk || v != tt.wantVal { - t.Errorf("stripSSEField(%q, %q) = (%q, %v), want (%q, %v)", tt.line, tt.prefix, v, ok, tt.wantVal, tt.wantOk) - } - }) - } -}