rdev/internal/domain/work_test.go
jordan cfba724f8a feat: add work task error classification and user-facing error codes
- Add WorkErrorCode type with RATE_LIMITED, AUTH_FAILED, TIMEOUT, STALE_WORKER, AGENT_ERROR, INVALID_SPEC
- Add ClassifyAgentError function to detect error patterns from stderr
- Add error_code column to work_queue table (migration 016)
- Add FailWithCode method to WorkQueue interface and implementations
- Update RequeueStaleWithIDs to mark permanently failed tasks with STALE_WORKER
- Add ErrorCode to BuildResult for API responses
- Update work executor to classify errors before failing tasks

This enables users to see actual failure reasons (e.g., "RATE_LIMITED") instead of
builds stuck in "running" state forever when Claude hits rate limits.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 00:07:34 -07:00

104 lines
2.5 KiB
Go

package domain
import "testing"
func TestClassifyAgentError(t *testing.T) {
tests := []struct {
name string
errMsg string
stderr string
expected WorkErrorCode
}{
{
name: "rate limit in stderr",
errMsg: "command failed",
stderr: "You've hit your limit · resets 7am (UTC)",
expected: WorkErrorCodeRateLimited,
},
{
name: "rate limit in error message",
errMsg: "rate limit exceeded, try again later",
stderr: "",
expected: WorkErrorCodeRateLimited,
},
{
name: "quota exceeded",
errMsg: "Quota exceeded for today",
stderr: "",
expected: WorkErrorCodeRateLimited,
},
{
name: "auth failed - not authenticated",
errMsg: "not authenticated, please log in",
stderr: "",
expected: WorkErrorCodeAuthFailed,
},
{
name: "auth failed - invalid api key",
errMsg: "Invalid API key provided",
stderr: "",
expected: WorkErrorCodeAuthFailed,
},
{
name: "auth failed - claude login hint",
errMsg: "",
stderr: "Run claude login to authenticate",
expected: WorkErrorCodeAuthFailed,
},
{
name: "context timeout",
errMsg: "context deadline exceeded",
stderr: "",
expected: WorkErrorCodeTimeout,
},
{
name: "operation timed out",
errMsg: "operation timed out after 10 minutes",
stderr: "",
expected: WorkErrorCodeTimeout,
},
{
name: "generic error",
errMsg: "something went wrong",
stderr: "error: file not found",
expected: WorkErrorCodeAgentError,
},
{
name: "empty error",
errMsg: "",
stderr: "",
expected: WorkErrorCodeAgentError,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := ClassifyAgentError(tt.errMsg, tt.stderr)
if got != tt.expected {
t.Errorf("ClassifyAgentError(%q, %q) = %q, want %q",
tt.errMsg, tt.stderr, got, tt.expected)
}
})
}
}
func TestWorkErrorCode_Constants(t *testing.T) {
// Ensure constants are defined with expected values
codes := map[WorkErrorCode]string{
WorkErrorCodeNone: "",
WorkErrorCodeRateLimited: "RATE_LIMITED",
WorkErrorCodeAuthFailed: "AUTH_FAILED",
WorkErrorCodeTimeout: "TIMEOUT",
WorkErrorCodeStaleWorker: "STALE_WORKER",
WorkErrorCodeAgentError: "AGENT_ERROR",
WorkErrorCodeInvalidSpec: "INVALID_SPEC",
}
for code, expected := range codes {
if string(code) != expected {
t.Errorf("WorkErrorCode constant %q has value %q, want %q",
expected, string(code), expected)
}
}
}