Pulse/internal/ai/resource_export_test.go
2026-03-19 04:07:47 +00:00

110 lines
3.5 KiB
Go

package ai
import (
"testing"
"time"
unifiedresources "github.com/rcourtman/pulse-go-rewrite/internal/unifiedresources"
)
func TestRecordUnifiedResourceExport_UsesCanonicalPrivacyHelpers(t *testing.T) {
cases := []struct {
name string
counts map[unifiedresources.ResourceSensitivity]int
localOnlyCount int
redactionHints []unifiedresources.ResourceRedactionHint
wantDecision unifiedresources.ExportDecision
wantReason string
wantRedactions []string
}{
{
name: "public",
counts: map[unifiedresources.ResourceSensitivity]int{unifiedresources.ResourceSensitivityPublic: 1},
wantDecision: unifiedresources.ExportAllowed,
wantReason: "public unified resource context",
wantRedactions: nil,
},
{
name: "internal",
counts: map[unifiedresources.ResourceSensitivity]int{unifiedresources.ResourceSensitivityInternal: 1},
wantDecision: unifiedresources.ExportRequiresConsent,
wantReason: "internal unified resource context requires export consent",
wantRedactions: nil,
},
{
name: "redacted",
counts: map[unifiedresources.ResourceSensitivity]int{unifiedresources.ResourceSensitivitySensitive: 1},
localOnlyCount: 1,
redactionHints: []unifiedresources.ResourceRedactionHint{
unifiedresources.ResourceRedactionHostname,
unifiedresources.ResourceRedactionHostname,
unifiedresources.ResourceRedactionPath,
},
wantDecision: unifiedresources.ExportRedacted,
wantReason: "governed unified resource context exported in redacted form",
wantRedactions: []string{"Hostname", "Path"},
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
store := unifiedresources.NewMemoryStore()
svc := &Service{
orgID: "org-1",
resourceExportStore: store,
resourceExportStoreOrgID: "org-1",
}
svc.recordUnifiedResourceExport(
"anthropic:test-model",
"canonical summary",
unifiedresources.ResourceStats{
Total: 1,
ByType: map[unifiedresources.ResourceType]int{
unifiedresources.ResourceTypeAgent: 1,
},
},
tc.counts,
tc.localOnlyCount,
tc.redactionHints,
)
audits, err := store.GetExportAudits(time.Time{}, 10)
if err != nil {
t.Fatalf("GetExportAudits: %v", err)
}
if len(audits) != 1 {
t.Fatalf("expected 1 export audit, got %d", len(audits))
}
if audits[0].Decision != tc.wantDecision {
t.Fatalf("decision = %q, want %q", audits[0].Decision, tc.wantDecision)
}
if audits[0].Destination != "anthropic:test-model" {
t.Fatalf("destination = %q, want anthropic:test-model", audits[0].Destination)
}
if audits[0].EnvelopeHash == "" {
t.Fatal("expected envelope hash to be recorded")
}
if len(audits[0].Redactions) != len(tc.wantRedactions) {
t.Fatalf("redactions = %#v, want %#v", audits[0].Redactions, tc.wantRedactions)
}
for i := range tc.wantRedactions {
if audits[0].Redactions[i] != tc.wantRedactions[i] {
t.Fatalf("redactions = %#v, want %#v", audits[0].Redactions, tc.wantRedactions)
}
}
decision, reason := unifiedresources.ExportDecisionForContext(
unifiedresources.ExportSensitivityFloor(tc.counts),
tc.localOnlyCount,
len(tc.wantRedactions),
)
if decision != tc.wantDecision {
t.Fatalf("shared decision = %q, want %q", decision, tc.wantDecision)
}
if reason != tc.wantReason {
t.Fatalf("shared reason = %q, want %q", reason, tc.wantReason)
}
})
}
}