test: cover Codex usage-limit dynamic fallback (#3088)
This commit is contained in:
@@ -93,6 +93,42 @@ func TestEngineAppliesCandidateErrorActions(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Contract coverage: Codex's complete usage-limit envelope must reach the
|
||||
// existing hard-error policy without adding a provider branch to the engine.
|
||||
func TestEngineRoutesCodexUsageLimitThroughHardPolicy(t *testing.T) {
|
||||
failure := routingerr.Classify(routingerr.Input{
|
||||
Phase: routingerr.PhasePromptSend,
|
||||
ProviderID: "codex-acp",
|
||||
Stderr: `{"code":-32603,"message":"Internal error","data":{"codexErrorInfo":"usageLimitExceeded","message":"You've hit your usage limit. Visit https://chatgpt.com/codex/settings/usage to purchase more credits or try again at Sep 1st, 2026 3:14 PM."}}`,
|
||||
})
|
||||
if failure.Code != routingerr.CodeQuotaLimited || failure.Class != routingerr.ClassHard {
|
||||
t.Fatalf("classification = %+v, want hard quota_limited", failure)
|
||||
}
|
||||
|
||||
profile := Profile{
|
||||
ID: "dynamic-1", Version: 1,
|
||||
Candidates: []Candidate{
|
||||
{ID: "codex", Enabled: true, BindingKey: "codex", Policies: routingpolicy.DefaultDocument()},
|
||||
{ID: "fallback", Enabled: true, BindingKey: "fallback", Policies: routingpolicy.DefaultDocument()},
|
||||
},
|
||||
}
|
||||
engine := NewEngine()
|
||||
initial, err := engine.Select("session-1", profile, 0, "")
|
||||
if err != nil {
|
||||
t.Fatalf("Select: %v", err)
|
||||
}
|
||||
|
||||
decision, err := engine.ApplyFailure(
|
||||
"session-1", profile, initial.Generation, initial.ExecutionProfileID, failure,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("ApplyFailure: %v", err)
|
||||
}
|
||||
if decision.ExecutionProfileID != "fallback" || decision.Reason != "policy_skip" {
|
||||
t.Fatalf("decision = %#v, want fallback selected by hard policy", decision)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEngineRetrySameKeepsTheFailedCandidate(t *testing.T) {
|
||||
profile := Profile{
|
||||
ID: "dynamic-1", Version: 1,
|
||||
|
||||
@@ -59,7 +59,12 @@ func TestClassify_ProviderRules(t *testing.T) {
|
||||
{"claude auth", "claude-acp", "you are not authenticated", CodeAuthRequired},
|
||||
{"claude model", "claude-acp", "model claude-foo not found here", CodeModelUnavailable},
|
||||
{"codex quota", "codex-acp", "insufficient_quota for project", CodeQuotaLimited},
|
||||
{"codex usage limit", "codex-acp", `{"codexErrorInfo":"usageLimitExceeded"}`, CodeQuotaLimited},
|
||||
{
|
||||
"codex usage limit",
|
||||
"codex-acp",
|
||||
`{"code":-32603,"message":"Internal error","data":{"codexErrorInfo":"usageLimitExceeded","message":"You've hit your usage limit. Visit https://chatgpt.com/codex/settings/usage to purchase more credits or try again at Sep 1st, 2026 3:14 PM."}}`,
|
||||
CodeQuotaLimited,
|
||||
},
|
||||
{"codex rate", "codex-acp", "rate_limit_exceeded", CodeRateLimited},
|
||||
{"codex apikey", "codex-acp", "invalid api key provided", CodeMissingCredentials},
|
||||
{"opencode auth", "opencode-acp", "Unauthorized request", CodeAuthRequired},
|
||||
|
||||
@@ -6,29 +6,41 @@ import (
|
||||
"github.com/kandev/kandev/internal/orchestrator/watcher"
|
||||
)
|
||||
|
||||
// Contract coverage: a recognized provider failure still cannot override the
|
||||
// dynamic route's pre-result and effect-safety gate.
|
||||
func TestDynamicPreResultRequiresExplicitKnownEvidence(t *testing.T) {
|
||||
if dynamicPreResultSafe(watcher.AgentEventData{DynamicRouteAttempt: true}) {
|
||||
t.Fatal("unknown dynamic attempt was treated as pre-result safe")
|
||||
}
|
||||
if !dynamicPreResultSafe(watcher.AgentEventData{
|
||||
usageLimitFailure := watcher.AgentEventData{
|
||||
AgentID: "codex-acp",
|
||||
ErrorMessage: `{"code":-32603,"message":"Internal error","data":{"codexErrorInfo":"usageLimitExceeded","message":"You've hit your usage limit. Visit https://chatgpt.com/codex/settings/usage to purchase more credits or try again at Sep 1st, 2026 3:14 PM."}}`,
|
||||
DynamicRouteAttempt: true,
|
||||
EvidenceKnown: true,
|
||||
}) {
|
||||
t.Fatal("known no-output/no-effect attempt was not pre-result safe")
|
||||
}
|
||||
if dynamicPreResultSafe(watcher.AgentEventData{
|
||||
DynamicRouteAttempt: true,
|
||||
EvidenceKnown: true,
|
||||
OutputObserved: true,
|
||||
}) {
|
||||
t.Fatal("output-producing attempt was treated as pre-result safe")
|
||||
if !dynamicPreResultSafe(usageLimitFailure) {
|
||||
t.Fatal("pre-result usage-limit failure was not safe to route")
|
||||
}
|
||||
if dynamicPreResultSafe(watcher.AgentEventData{
|
||||
DynamicRouteAttempt: true,
|
||||
EvidenceKnown: true,
|
||||
EffectObserved: true,
|
||||
}) {
|
||||
t.Fatal("effect-producing attempt was treated as pre-result safe")
|
||||
|
||||
unsafeCases := []struct {
|
||||
name string
|
||||
data watcher.AgentEventData
|
||||
}{
|
||||
{name: "unknown evidence", data: watcher.AgentEventData{DynamicRouteAttempt: true}},
|
||||
{name: "assistant output", data: func() watcher.AgentEventData {
|
||||
data := usageLimitFailure
|
||||
data.OutputObserved = true
|
||||
return data
|
||||
}()},
|
||||
{name: "tool effect", data: func() watcher.AgentEventData {
|
||||
data := usageLimitFailure
|
||||
data.EffectObserved = true
|
||||
return data
|
||||
}()},
|
||||
}
|
||||
for _, test := range unsafeCases {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
if dynamicPreResultSafe(test.data) {
|
||||
t.Fatalf("case %q was incorrectly treated as pre-result safe", test.name)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user