4c0d92154b
Co-authored-by: nova28 <17953305+nova28@users.noreply.github.com> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> Co-authored-by: Carlos Florencio <carlosmflorencio@gmail.com>
116 lines
4.5 KiB
Go
116 lines
4.5 KiB
Go
package usage
|
|
|
|
import (
|
|
"math"
|
|
"testing"
|
|
)
|
|
|
|
// TestValidateStage_AllChecksPass_ReturnsEmptyReason pins the happy path:
|
|
// a fully-formed payload passes validate with no drop reason.
|
|
func TestValidateStage_AllChecksPass_ReturnsEmptyReason(t *testing.T) {
|
|
p := &usageEventPayload{
|
|
UsageEventID: "evt-1",
|
|
TaskID: "task-1",
|
|
Usage: &promptUsagePayload{InputTokens: 100},
|
|
}
|
|
if reason := validateStage(p); reason != "" {
|
|
t.Errorf("validateStage = %q, want empty (valid)", reason)
|
|
}
|
|
}
|
|
|
|
// TestValidateStage_MissingUsageEventID_ReturnsInvalid pins sub-check (1):
|
|
// checked first, regardless of what else is wrong with the payload.
|
|
func TestValidateStage_MissingUsageEventID_ReturnsInvalid(t *testing.T) {
|
|
p := &usageEventPayload{
|
|
UsageEventID: "",
|
|
TaskID: "",
|
|
Usage: &promptUsagePayload{InputTokens: -1},
|
|
}
|
|
if reason := validateStage(p); reason != dropReasonInvalid {
|
|
t.Errorf("validateStage = %q, want %q (usage_event_id missing wins over every other failure)", reason, dropReasonInvalid)
|
|
}
|
|
}
|
|
|
|
// TestValidateStage_MissingTaskID_ReturnsUnattributable pins sub-check (2).
|
|
func TestValidateStage_MissingTaskID_ReturnsUnattributable(t *testing.T) {
|
|
p := &usageEventPayload{
|
|
UsageEventID: "evt-1",
|
|
TaskID: "",
|
|
Usage: &promptUsagePayload{InputTokens: 10},
|
|
}
|
|
if reason := validateStage(p); reason != dropReasonUnattributable {
|
|
t.Errorf("validateStage = %q, want %q", reason, dropReasonUnattributable)
|
|
}
|
|
}
|
|
|
|
// TestValidateStage_MissingTaskIDAndNegativeValue_ReturnsUnattributable pins
|
|
// the specific (2)-vs-(3) ordering AC-27 calls out by name: a payload
|
|
// missing task_id AND carrying a negative token value counts
|
|
// unattributable, never reaching the negative-value check.
|
|
func TestValidateStage_MissingTaskIDAndNegativeValue_ReturnsUnattributable(t *testing.T) {
|
|
p := &usageEventPayload{
|
|
UsageEventID: "evt-1",
|
|
TaskID: "",
|
|
Usage: &promptUsagePayload{InputTokens: -5},
|
|
}
|
|
if reason := validateStage(p); reason != dropReasonUnattributable {
|
|
t.Errorf("validateStage = %q, want %q (task_id-missing must win over negative-value)", reason, dropReasonUnattributable)
|
|
}
|
|
}
|
|
|
|
// TestValidateStage_NegativeValue_ReturnsInvalid pins sub-check (3) across
|
|
// every field it covers, including the provider-reported cost.
|
|
func TestValidateStage_NegativeValue_ReturnsInvalid(t *testing.T) {
|
|
tests := map[string]*promptUsagePayload{
|
|
"negative input tokens": {InputTokens: -1},
|
|
"negative output tokens": {OutputTokens: -1},
|
|
"negative cached read tokens": {CachedReadTokens: -1},
|
|
"negative cached write tokens": {CachedWriteTokens: -1},
|
|
"negative thought tokens": {ThoughtTokens: -1},
|
|
"negative provider-reported cost": {ProviderReportedCostPresent: true, ProviderReportedCostSubcents: -1},
|
|
}
|
|
for name, usage := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
p := &usageEventPayload{UsageEventID: "evt-1", TaskID: "task-1", Usage: usage}
|
|
if reason := validateStage(p); reason != dropReasonInvalid {
|
|
t.Errorf("validateStage = %q, want %q", reason, dropReasonInvalid)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestValidateStage_NegativeProviderCostButNotPresent_PassesValidation pins
|
|
// that an unset provider-reported cost is never inspected for sign, since
|
|
// ProviderReportedCostPresent=false means the field simply wasn't sent.
|
|
func TestValidateStage_NegativeProviderCostButNotPresent_PassesValidation(t *testing.T) {
|
|
p := &usageEventPayload{
|
|
UsageEventID: "evt-1",
|
|
TaskID: "task-1",
|
|
Usage: &promptUsagePayload{ProviderReportedCostPresent: false, ProviderReportedCostSubcents: -1},
|
|
}
|
|
if reason := validateStage(p); reason != "" {
|
|
t.Errorf("validateStage = %q, want empty (unset provider cost is not sign-checked)", reason)
|
|
}
|
|
}
|
|
|
|
// TestValidateStage_NilUsage_PassesValidation pins the defensive-only nil
|
|
// guard: publishPromptUsage never publishes a nil Usage (AC-24), but the
|
|
// decode stage doesn't reject one either, so validate must not panic.
|
|
func TestValidateStage_NilUsage_PassesValidation(t *testing.T) {
|
|
p := &usageEventPayload{UsageEventID: "evt-1", TaskID: "task-1", Usage: nil}
|
|
if reason := validateStage(p); reason != "" {
|
|
t.Errorf("validateStage = %q, want empty", reason)
|
|
}
|
|
}
|
|
|
|
func TestValidateStage_TokenTotalOverflow_ReturnsOverflow(t *testing.T) {
|
|
p := &usageEventPayload{
|
|
UsageEventID: "evt-1",
|
|
TaskID: "task-1",
|
|
Usage: &promptUsagePayload{InputTokens: math.MaxInt64, CachedReadTokens: 1},
|
|
}
|
|
if reason := validateStage(p); reason != dropReasonOverflow {
|
|
t.Errorf("validateStage = %q, want %q for an overflowing token total", reason, dropReasonOverflow)
|
|
}
|
|
}
|