Files
kandev/apps/backend/internal/task/handlers/errors_test.go
T

86 lines
3.2 KiB
Go

package handlers
import (
"context"
"errors"
"fmt"
"net"
"testing"
"github.com/kandev/kandev/internal/agent/runtime/lifecycle"
taskrepo "github.com/kandev/kandev/internal/task/repository/sqlite"
"github.com/kandev/kandev/internal/task/service"
)
// TestErrorsAreClassifiable verifies that the package's error-classification
// helpers detect typed sentinels through any depth of wrapping, so HTTP
// status mapping survives downstream wrap/unwrap changes.
func TestErrorsAreClassifiable(t *testing.T) {
notFoundSentinels := map[string]error{
"taskrepo.ErrTaskNotFound": taskrepo.ErrTaskNotFound,
"taskrepo.ErrNoPrimarySession": taskrepo.ErrNoPrimarySession,
"service.ErrDocumentNotFound": service.ErrDocumentNotFound,
"service.ErrTaskPlanNotFound": service.ErrTaskPlanNotFound,
"service.ErrRevisionNotFound": service.ErrRevisionNotFound,
}
for name, sentinel := range notFoundSentinels {
t.Run("isNotFound recognizes "+name, func(t *testing.T) {
wrapped := fmt.Errorf("look up failed: %w", sentinel)
if !isNotFound(wrapped) {
t.Errorf("expected wrapped %s to classify as not-found", name)
}
})
}
t.Run("isAgentReportedError uses lifecycle.ErrAgentReported", func(t *testing.T) {
wrapped := fmt.Errorf("waitForPromptDone: %w", lifecycle.ErrAgentReported)
if !isAgentReportedError(wrapped) {
t.Errorf("expected wrapped ErrAgentReported to classify")
}
if isAgentReportedError(errors.New("agent error: not the sentinel")) {
t.Errorf("untyped lookalike must no longer classify")
}
})
}
// TestIsTimeoutError pins the UX-classification contract for the prompt
// error-message renderer. The pre-refactor substring check on "timeout"
// covered three classes of producer; the typed-sentinel rewrite must keep
// all three classified or the user-facing "Request timed out…" message
// silently downgrades to the generic "Failed to send message to agent".
func TestIsTimeoutError(t *testing.T) {
cases := []struct {
name string
err error
want bool
}{
{"nil", nil, false},
{"unrelated", errors.New("something else"), false},
// net.Error with Timeout()==true (typed) — e.g. http.Client deadline.
{"net error timeout", &timeoutNetErr{}, true},
// Plain fmt.Errorf("timeout …") from waitForSessionReady, agentctl
// health waits, agent-stream connect waits. No typed Timeout() method.
{"session-ready timeout (substring fallback)", errors.New("timeout waiting for session to become ready after resume"), true},
{"agent-stream timeout (substring fallback)", errors.New("timeout waiting for agent stream to connect after restart"), true},
// context.DeadlineExceeded is itself a net.Error with Timeout()==true,
// so it matches via the typed path — no separate errors.Is needed in
// createPromptErrorMessage.
{"context.DeadlineExceeded", context.DeadlineExceeded, true},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := isTimeoutError(tc.err); got != tc.want {
t.Errorf("isTimeoutError(%v) = %v, want %v", tc.err, got, tc.want)
}
})
}
}
type timeoutNetErr struct{}
func (timeoutNetErr) Error() string { return "i/o timeout" }
func (timeoutNetErr) Timeout() bool { return true }
func (timeoutNetErr) Temporary() bool { return false }
var _ net.Error = timeoutNetErr{}