From: Anderson A. Giacomolli Date: Mon, 9 Nov 2026 21:00:39 +0000 (-0300) Subject: Check operation context on returned errors X-Git-Url: https://git.giacomolli.uk/onechat/commitdiff_plain/d9fddc0f506eb4ead7049f5638674b2b8773fcaa?ds=inline;hp=087d725b5ab41ad34ffd24938482d45a8b99a080 Check operation context on returned errors Wrapcheck requires operation context on errors from package and interface calls. Returned errors keep their causes for errors.Is and errors.As. Context cancellation and the listed functions with one error path keep their direct returns, as the Go rules allow. --- diff --git a/.golangci.yml b/.golangci.yml index 58a78b4..bba6f25 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -7,3 +7,30 @@ linters: - gocritic - testifylint - revive + - wrapcheck + settings: + wrapcheck: + # go-code: Return ctx.Err() directly. + extra-ignore-sigs: + - "(context.Context).Err() error" + exclusions: + rules: + # go-code: A function with one error path can return it directly. + - linters: [wrapcheck] + path: ^records\.go$ + source: 'return r\.file\.Close\(\)' + - linters: [wrapcheck] + path: ^log_test\.go$ + source: 'return os\.Truncate\(' + - linters: [wrapcheck] + path: ^sync_buffer_helper_test\.go$ + source: 'return b\.buffer\.Write\(' + - linters: [wrapcheck] + path: ^probe\.go$ + source: 'return s\.inner\.Summarize\(' + - linters: [wrapcheck] + path: ^view\.go$ + source: 'return false, err' + - linters: [wrapcheck] + path: ^whatsapp\.go$ + text: 'sqlstore\.Container\)\.Close\(' diff --git a/chain.go b/chain.go index 3ab9fe1..0211787 100644 --- a/chain.go +++ b/chain.go @@ -43,7 +43,7 @@ func (c *Chain) Run(ctx context.Context, input []any) (string, error) { request.Input = input response, err := c.client.Create(ctx, request) if err != nil { - return "", err + return "", fmt.Errorf("create model response: %w", err) } var text string diff --git a/chatgpt.go b/chatgpt.go index 302e143..7a5afc2 100644 --- a/chatgpt.go +++ b/chatgpt.go @@ -145,7 +145,7 @@ func (a *ChatGPTAuth) Login(ctx context.Context, out io.Writer) error { _, err = fmt.Fprintf(out, "Open %s/codex/device on any device and enter this code:\n\n %s\n\nWaiting for the approval...\n", a.issuer, userCode) if err != nil { - return err + return fmt.Errorf("write device code: %w", err) } grant, err := a.poll(ctx, code.DeviceAuthID, userCode, chatgptPollInterval(code.Interval)) @@ -164,8 +164,11 @@ func (a *ChatGPTAuth) Login(ctx context.Context, out io.Writer) error { } _, err = fmt.Fprintln(out, "Logged in.") + if err != nil { + return fmt.Errorf("write login result: %w", err) + } - return err + return nil } // read reads the token file. diff --git a/log.go b/log.go index 24feb3b..75080f9 100644 --- a/log.go +++ b/log.go @@ -208,7 +208,7 @@ func (l *Log) catchUp(ctx context.Context) error { var message Message err := json.Unmarshal(line, &message) if err != nil { - return err + return fmt.Errorf("decode log message: %w", err) } if message.ID != l.index.count { diff --git a/main.go b/main.go index ac4afe4..e25e7f3 100644 --- a/main.go +++ b/main.go @@ -42,7 +42,7 @@ func run() error { case len(args) == 1 && (args[0] == "help" || args[0] == "--help" || args[0] == "-h"): _, err := fmt.Print(usage) if err != nil { - return err + return fmt.Errorf("write command help: %w", err) } case len(args) == 1 && args[0] == "login": err := chatgptLogin(ctx, os.Stdout) diff --git a/subagent_run_test.go b/subagent_run_test.go index 0341ba6..b76c18d 100644 --- a/subagent_run_test.go +++ b/subagent_run_test.go @@ -185,11 +185,11 @@ func TestFullContextGivesAFailedReport(t *testing.T) { name := test.events.Last("subagent") require.Equal(t, []string{ - "work: [" + name + "]\nfailed: model call: HTTP 400: " + subagentTestFullContext + "\n\nFull chat: zoom(\"" + name + "\")", + "work: [" + name + "]\nfailed: create model response: model call: HTTP 400: " + subagentTestFullContext + "\n\nFull chat: zoom(\"" + name + "\")", "reply: It failed.", }, test.Log(t)) require.Equal(t, subagentFailed, test.events.LastEvent("work").Status) - require.Equal(t, "subagent "+name+" started\nsubagent "+name+" failed after 0s: model call: HTTP 400: "+subagentTestFullContext+"\n", test.logs.String()) + require.Equal(t, "subagent "+name+" started\nsubagent "+name+" failed after 0s: create model response: model call: HTTP 400: "+subagentTestFullContext+"\n", test.logs.String()) } func TestSubagentHasItsOwnEffortAndNoSpawn(t *testing.T) { diff --git a/terminal.go b/terminal.go index 876986a..eff009c 100644 --- a/terminal.go +++ b/terminal.go @@ -71,7 +71,11 @@ func terminalSession(ctx context.Context, conn net.Conn, in io.Reader, out io.Wr <-shown } - return err + if err != nil { + return fmt.Errorf("run terminal client: %w", err) + } + + return nil } // terminalSendLines sends each line of in as a request until the input ends. @@ -113,7 +117,12 @@ func terminalSendLines(ctx context.Context, conn net.Conn, in io.Reader) error { } } - return scanner.Err() + err := scanner.Err() + if err != nil { + return fmt.Errorf("read terminal input: %w", err) + } + + return nil } // terminalShowEvents writes each event from the daemon to out until the @@ -135,11 +144,16 @@ func terminalShowEvents(conn net.Conn, out io.Writer) error { _, err = io.WriteString(out, text+"\n") if err != nil { - return err + return fmt.Errorf("write terminal event: %w", err) } } - return scanner.Err() + err := scanner.Err() + if err != nil { + return fmt.Errorf("read daemon events: %w", err) + } + + return nil } // terminalCmpError returns err, or fallback when err is nil. diff --git a/terminal_client_helper_test.go b/terminal_client_helper_test.go index 077d90c..0fdd93b 100644 --- a/terminal_client_helper_test.go +++ b/terminal_client_helper_test.go @@ -1,6 +1,7 @@ package main import ( + "fmt" "io" "net" "strings" @@ -44,7 +45,7 @@ func (c *TerminalTestClient) WaitFor(t *testing.T, text string) { func (c *TerminalTestClient) Close() error { err := c.input.Close() if err != nil { - return err + return fmt.Errorf("close terminal input: %w", err) } return <-c.done diff --git a/tools.go b/tools.go index 009e331..1cb098a 100644 --- a/tools.go +++ b/tools.go @@ -71,12 +71,12 @@ type Toolbox struct { func (b *Toolbox) Call(ctx context.Context, name, arguments string) (string, error) { _, err := b.log.Append("tool", toolCallText(name, arguments)) if err != nil { - return "", err + return "", fmt.Errorf("append tool call: %w", err) } err = b.log.Flush(ctx) if err != nil && ctx.Err() == nil { - return "", err + return "", fmt.Errorf("flush tool call: %w", err) } output := "not run: the turn was interrupted" @@ -86,7 +86,7 @@ func (b *Toolbox) Call(ctx context.Context, name, arguments string) (string, err _, err = b.log.Append("echo", output) if err != nil { - return "", err + return "", fmt.Errorf("append tool result: %w", err) } return output, nil @@ -234,7 +234,7 @@ func (b *Toolbox) zoom(id, n int) (string, error) { if n == 1 { message, err := b.chat.Message(id) if err != nil { - return "", err + return "", fmt.Errorf("read zoom message: %w", err) } return leafSource(message), nil @@ -245,7 +245,7 @@ func (b *Toolbox) zoom(id, n int) (string, error) { for _, index := range []int{2 * id / n, 2*id/n + 1} { node, built, err := b.chat.Node(level, index) if err != nil { - return "", err + return "", fmt.Errorf("read zoom node: %w", err) } text := viewPending @@ -267,12 +267,12 @@ func (b *Toolbox) date(id int) (string, error) { message, err := b.chat.Message(id) if err != nil { - return "", err + return "", fmt.Errorf("read dated message: %w", err) } date, err := time.Parse(time.RFC3339Nano, message.Date) if err != nil { - return "", err + return "", fmt.Errorf("parse message date: %w", err) } return date.Local().Format("Monday 2006-01-02 15:04:05 -07:00"), nil @@ -317,7 +317,7 @@ func (b *Toolbox) shell(ctx context.Context, command string) string { func (b *Toolbox) read(path string) (text string, err error) { file, err := os.Open(b.path(path)) if err != nil { - return "", err + return "", fmt.Errorf("open tool file: %w", err) } defer func() { @@ -327,7 +327,7 @@ func (b *Toolbox) read(path string) (text string, err error) { output := &ToolHeadTail{} _, err = io.Copy(output, file) if err != nil { - return "", err + return "", fmt.Errorf("read tool file: %w", err) } return output.String(), nil @@ -339,12 +339,12 @@ func (b *Toolbox) write(path, text string) (string, error) { path = b.path(path) err := os.MkdirAll(filepath.Dir(path), 0o755) if err != nil { - return "", err + return "", fmt.Errorf("create tool file directory: %w", err) } err = os.WriteFile(path, []byte(text), 0o644) if err != nil { - return "", err + return "", fmt.Errorf("write tool file: %w", err) } return fmt.Sprintf("wrote %d bytes to %s", len(text), path), nil @@ -356,7 +356,7 @@ func (b *Toolbox) edit(path, old, replacement string) (string, error) { path = b.path(path) data, err := os.ReadFile(path) if err != nil { - return "", err + return "", fmt.Errorf("read file to edit: %w", err) } if old == "" { @@ -370,12 +370,12 @@ func (b *Toolbox) edit(path, old, replacement string) (string, error) { info, err := os.Stat(path) if err != nil { - return "", err + return "", fmt.Errorf("read file mode: %w", err) } err = os.WriteFile(path, []byte(strings.Replace(string(data), old, replacement, 1)), info.Mode().Perm()) if err != nil { - return "", err + return "", fmt.Errorf("write edited file: %w", err) } return "edited " + path, nil diff --git a/tools_test.go b/tools_test.go index 45cd4a2..ed58438 100644 --- a/tools_test.go +++ b/tools_test.go @@ -107,7 +107,7 @@ func TestFileTools(t *testing.T) { require.Equal(t, "error: old occurs 2 times in "+path+", not once", box.run(t.Context(), "edit", `{"path":"notes/a.txt","old":"two","new":"2"}`)) require.Equal(t, "edited "+path, box.run(t.Context(), "edit", `{"path":"notes/a.txt","old":"one","new":"1"}`)) require.Equal(t, "1 two two\n", box.run(t.Context(), "read", `{"path":"notes/a.txt"}`)) - require.Contains(t, box.run(t.Context(), "read", `{"path":"missing.txt"}`), "error: open "+filepath.Join(dir, "missing.txt")) + require.Contains(t, box.run(t.Context(), "read", `{"path":"missing.txt"}`), "error: open tool file: open "+filepath.Join(dir, "missing.txt")) require.Equal(t, `error: arguments: json: unknown field "file"`, box.run(t.Context(), "read", `{"file":"a.txt"}`)) require.Equal(t, `error: unknown tool "grep"`, box.run(t.Context(), "grep", `{}`)) } diff --git a/tree.go b/tree.go index af164ae..83ecda3 100644 --- a/tree.go +++ b/tree.go @@ -247,7 +247,7 @@ func (t *Tree) open(ctx context.Context, total int) error { var node Node err := json.Unmarshal(line, &node) if err != nil { - return err + return fmt.Errorf("decode tree node: %w", err) } err = t.check(node) diff --git a/view.go b/view.go index 233e089..2124d3d 100644 --- a/view.go +++ b/view.go @@ -316,7 +316,7 @@ func (v *View) mostDue(tree ViewNodeReader) (int, error) { built, err := tree.Built(left.level+1, left.index/2) if err != nil { - return -1, err + return -1, fmt.Errorf("check parent node: %w", err) } if built {