독립 호스트에서 안전한 작업 실행과 복구를 제공하기 위해 런타임 설정, 정책, 상태 저장소, 워크스페이스 격리 및 AgentTask 오케스트레이션을 확장한다.
239 lines
8.2 KiB
Go
239 lines
8.2 KiB
Go
package status
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"reflect"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
var quotaCheckedAt = time.Date(2026, 7, 25, 8, 0, 0, 0, time.UTC)
|
|
|
|
func TestNormalizeQuotaSnapshotExhaustedWinsMissing(t *testing.T) {
|
|
snapshot := NormalizeQuotaSnapshot("agy", "Gemini", []string{"overall", "model:missing"}, quotaCheckedAt, &UsageStatus{DailyLimit: "0%"}, nil)
|
|
if got := snapshot.Targets[0].Status; got != "exhausted" {
|
|
t.Fatalf("status = %q, want exhausted", got)
|
|
}
|
|
if snapshot.RequiredCaps[1].Status != "unknown" {
|
|
t.Fatalf("missing model status = %q, want unknown", snapshot.RequiredCaps[1].Status)
|
|
}
|
|
}
|
|
|
|
func TestNormalizeQuotaSnapshotAvailableWhenAllPositive(t *testing.T) {
|
|
snapshot := NormalizeQuotaSnapshot("agy", "Gemini", []string{"overall", "model:Flash"}, quotaCheckedAt, &UsageStatus{
|
|
DailyLimit: "70%",
|
|
Metadata: map[string]string{
|
|
"model_usage_count": "1",
|
|
"model_usage_0_name": "flash",
|
|
"model_usage_0_used_percent": "20%",
|
|
},
|
|
}, nil)
|
|
if got := snapshot.Targets[0].Status; got != "available" {
|
|
t.Fatalf("status = %q, want available", got)
|
|
}
|
|
for _, cap := range snapshot.RequiredCaps {
|
|
if cap.Status != "available" || cap.RemainingPercent == nil || *cap.RemainingPercent <= 0 {
|
|
t.Fatalf("cap = %#v, want positive available evidence", cap)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestNormalizeQuotaSnapshotUnknownForMissingConflictAndParseFailure(t *testing.T) {
|
|
for name, tc := range map[string]struct {
|
|
requiredCaps []string
|
|
usage *UsageStatus
|
|
}{
|
|
"missing": {
|
|
requiredCaps: []string{"overall", "model:missing"},
|
|
usage: &UsageStatus{DailyLimit: "50%"},
|
|
},
|
|
"conflict": {
|
|
requiredCaps: []string{"model:flash"},
|
|
usage: &UsageStatus{Metadata: map[string]string{
|
|
"model_usage_count": "2",
|
|
"model_usage_0_name": "Flash",
|
|
"model_usage_0_used_percent": "10%",
|
|
"model_usage_1_name": "flash",
|
|
"model_usage_1_used_percent": "20%",
|
|
}},
|
|
},
|
|
"parse": {
|
|
requiredCaps: []string{"overall"},
|
|
usage: &UsageStatus{DailyLimit: "not-a-percent"},
|
|
},
|
|
} {
|
|
t.Run(name, func(t *testing.T) {
|
|
snapshot := NormalizeQuotaSnapshot("agy", "Gemini", tc.requiredCaps, quotaCheckedAt, tc.usage, nil)
|
|
if got := snapshot.Targets[0].Status; got != "unknown" {
|
|
t.Fatalf("status = %q, want unknown", got)
|
|
}
|
|
})
|
|
}
|
|
snapshot := NormalizeQuotaSnapshot("agy", "Gemini", []string{"overall"}, quotaCheckedAt, nil, errors.New("provider output must not escape"))
|
|
if got := snapshot.Targets[0].Status; got != "unknown" {
|
|
t.Fatalf("checker error status = %q, want unknown", got)
|
|
}
|
|
if len(snapshot.ReasonCodes) != 1 || snapshot.ReasonCodes[0] != "checker_error" {
|
|
t.Fatalf("reason codes = %#v", snapshot.ReasonCodes)
|
|
}
|
|
}
|
|
|
|
func TestNormalizeQuotaSnapshotUnknownForNonFinitePercent(t *testing.T) {
|
|
for _, value := range []string{"NaN%", "Inf%", "-Inf%"} {
|
|
t.Run("overall_"+value, func(t *testing.T) {
|
|
snapshot := NormalizeQuotaSnapshot("agy", "Gemini", []string{"overall"}, quotaCheckedAt, &UsageStatus{DailyLimit: value}, nil)
|
|
if got := snapshot.Targets[0].Status; got != "unknown" {
|
|
t.Fatalf("status = %q, want unknown", got)
|
|
}
|
|
if _, err := json.Marshal(snapshot); err != nil {
|
|
t.Fatalf("snapshot must be JSON encodable: %v", err)
|
|
}
|
|
})
|
|
t.Run("model_"+value, func(t *testing.T) {
|
|
snapshot := NormalizeQuotaSnapshot("agy", "Gemini", []string{"model:flash"}, quotaCheckedAt, &UsageStatus{Metadata: map[string]string{
|
|
"model_usage_count": "1",
|
|
"model_usage_0_name": "flash",
|
|
"model_usage_0_used_percent": value,
|
|
}}, nil)
|
|
if got := snapshot.Targets[0].Status; got != "unknown" {
|
|
t.Fatalf("status = %q, want unknown", got)
|
|
}
|
|
if _, err := json.Marshal(snapshot); err != nil {
|
|
t.Fatalf("snapshot must be JSON encodable: %v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNormalizeQuotaSnapshotUsesAntigravityModelMetadata(t *testing.T) {
|
|
usage, err := ParseStatusOutput("Auto (Antigravity) quota\n0% used (Limit resets in 24h)\nModel usage\nFlash ▬▬▬ 2% Resets: tomorrow\n(Press Esc to close)\n")
|
|
if err != nil {
|
|
t.Fatalf("parse status: %v", err)
|
|
}
|
|
snapshot := NormalizeQuotaSnapshot("agy", "Gemini", []string{"overall", "model:FLASH"}, quotaCheckedAt, usage, nil)
|
|
if got := snapshot.Targets[0].Status; got != "available" {
|
|
t.Fatalf("status = %q, want available", got)
|
|
}
|
|
}
|
|
|
|
func TestNormalizeQuotaSnapshotNotApplicable(t *testing.T) {
|
|
snapshot := NormalizeQuotaSnapshot(
|
|
"provider",
|
|
"profile",
|
|
nil,
|
|
quotaCheckedAt,
|
|
nil,
|
|
errors.New("diagnostic must not escape"),
|
|
)
|
|
if got := snapshot.Targets[0].Status; got != "not_applicable" {
|
|
t.Fatalf("status = %q, want not_applicable", got)
|
|
}
|
|
if len(snapshot.RequiredCaps) != 0 {
|
|
t.Fatalf("required caps = %#v, want empty", snapshot.RequiredCaps)
|
|
}
|
|
if !reflect.DeepEqual(snapshot.ReasonCodes, []string{"quota_not_applicable"}) {
|
|
t.Fatalf("reason codes = %#v", snapshot.ReasonCodes)
|
|
}
|
|
if err := ValidateQuotaSnapshot(snapshot); err != nil {
|
|
t.Fatalf("ValidateQuotaSnapshot: %v", err)
|
|
}
|
|
encoded, err := json.Marshal(snapshot)
|
|
if err != nil {
|
|
t.Fatalf("Marshal: %v", err)
|
|
}
|
|
if string(encoded) == "" || containsAny(string(encoded), "diagnostic", "escape") {
|
|
t.Fatalf("snapshot retained checker diagnostics: %s", encoded)
|
|
}
|
|
}
|
|
|
|
func TestValidateQuotaSnapshotRejectsTampering(t *testing.T) {
|
|
base := NormalizeQuotaSnapshot(
|
|
"provider",
|
|
"profile",
|
|
[]string{"overall", "model:flash"},
|
|
quotaCheckedAt,
|
|
&UsageStatus{
|
|
DailyLimit: "80%",
|
|
Metadata: map[string]string{
|
|
"model_usage_count": "1",
|
|
"model_usage_0_name": "flash",
|
|
"model_usage_0_used_percent": "25%",
|
|
},
|
|
},
|
|
nil,
|
|
)
|
|
if err := ValidateQuotaSnapshot(base); err != nil {
|
|
t.Fatalf("base snapshot is invalid: %v", err)
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
mutate func(*QuotaSnapshot)
|
|
}{
|
|
{"schema", func(snapshot *QuotaSnapshot) { snapshot.SchemaVersion = "2.0" }},
|
|
{"source", func(snapshot *QuotaSnapshot) { snapshot.Source = "other" }},
|
|
{"checked time", func(snapshot *QuotaSnapshot) {
|
|
snapshot.CheckedAt = quotaCheckedAt.Add(time.Second).Format(time.RFC3339Nano)
|
|
}},
|
|
{"adapter", func(snapshot *QuotaSnapshot) { snapshot.Targets[0].Adapter = "other" }},
|
|
{"target", func(snapshot *QuotaSnapshot) { snapshot.Targets[0].Target = "other" }},
|
|
{"target status", func(snapshot *QuotaSnapshot) { snapshot.Targets[0].Status = "exhausted" }},
|
|
{"cap name", func(snapshot *QuotaSnapshot) { snapshot.RequiredCaps[0].Name = "other" }},
|
|
{"cap status", func(snapshot *QuotaSnapshot) { snapshot.RequiredCaps[0].Status = "unknown" }},
|
|
{"cap remaining", func(snapshot *QuotaSnapshot) {
|
|
remaining := 79.0
|
|
snapshot.RequiredCaps[0].RemainingPercent = &remaining
|
|
}},
|
|
{"allowlisted reason", func(snapshot *QuotaSnapshot) {
|
|
snapshot.ReasonCodes = []string{"cap_evidence_unknown"}
|
|
}},
|
|
{"unsafe reason", func(snapshot *QuotaSnapshot) {
|
|
snapshot.ReasonCodes = []string{"token=provider-secret"}
|
|
}},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
snapshot := cloneQuotaSnapshot(base)
|
|
test.mutate(&snapshot)
|
|
if err := ValidateQuotaSnapshot(snapshot); err == nil {
|
|
t.Fatalf("ValidateQuotaSnapshot accepted tampered snapshot: %#v", snapshot)
|
|
}
|
|
})
|
|
}
|
|
|
|
reordered := cloneQuotaSnapshot(base)
|
|
reordered.RequiredCaps[0], reordered.RequiredCaps[1] =
|
|
reordered.RequiredCaps[1], reordered.RequiredCaps[0]
|
|
if err := ValidateQuotaSnapshot(reordered); err != nil {
|
|
t.Fatalf("normalized cap order should not affect identity: %v", err)
|
|
}
|
|
}
|
|
|
|
func cloneQuotaSnapshot(snapshot QuotaSnapshot) QuotaSnapshot {
|
|
out := snapshot
|
|
out.Targets = append([]QuotaTargetView(nil), snapshot.Targets...)
|
|
out.RequiredCaps = make([]QuotaCapView, len(snapshot.RequiredCaps))
|
|
for index, cap := range snapshot.RequiredCaps {
|
|
out.RequiredCaps[index] = cap
|
|
if cap.RemainingPercent != nil {
|
|
remaining := *cap.RemainingPercent
|
|
out.RequiredCaps[index].RemainingPercent = &remaining
|
|
}
|
|
}
|
|
out.ReasonCodes = append([]string(nil), snapshot.ReasonCodes...)
|
|
return out
|
|
}
|
|
|
|
func containsAny(value string, candidates ...string) bool {
|
|
for _, candidate := range candidates {
|
|
if len(candidate) > 0 && len(value) >= len(candidate) {
|
|
for index := 0; index+len(candidate) <= len(value); index++ {
|
|
if value[index:index+len(candidate)] == candidate {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|