- 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>
104 lines
2.5 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|