iop/packages/go/config/stream_evidence_gate_config_test.go
toki 04879f2b43 feat(openai): 실제 provider별 사용량 귀속을 기록한다
요청 종료 계수와 실제 provider 시도 사용량을 분리하고, 직접·pool·retry 경로의 attribution을 보존한다. 관련 계약·스펙과 완료된 task archive 정리도 함께 반영한다.
2026-07-31 20:22:23 +09:00

496 lines
16 KiB
Go

package config_test
import (
"os"
"path/filepath"
"testing"
"iop/packages/go/config"
)
func TestStreamEvidenceGate_DirectStructZeroValueDefaults(t *testing.T) {
var cfg config.EdgeOpenAIConf
if got := cfg.StreamEvidenceGate.EffectiveMaxRequestFaultRecovery(); got != 3 {
t.Errorf("EffectiveMaxRequestFaultRecovery() = %d, want 3", got)
}
if got := cfg.StreamEvidenceGate.EffectiveMaxStrategyFaultRecovery(); got != 3 {
t.Errorf("EffectiveMaxStrategyFaultRecovery() = %d, want 3", got)
}
if got := cfg.StreamEvidenceGate.EffectiveMaxIngressSnapshotBytes(); got != 16777216 {
t.Errorf("EffectiveMaxIngressSnapshotBytes() = %d, want 16777216 (16 MiB)", got)
}
if err := cfg.StreamEvidenceGate.Validate(); err != nil {
t.Errorf("Validate() unexpectedly failed on zero-value struct: %v", err)
}
}
func TestStreamEvidenceGate_YAMLOmittedDefaults(t *testing.T) {
yamlContent := `
openai:
enabled: true
provider_id: "test-provider"
`
tmpDir := t.TempDir()
tmpFile := filepath.Join(tmpDir, "edge.yaml")
if err := os.WriteFile(tmpFile, []byte(yamlContent), 0644); err != nil {
t.Fatalf("failed to write temp yaml: %v", err)
}
cfg, err := config.LoadEdge(tmpFile)
if err != nil {
t.Fatalf("LoadEdge failed: %v", err)
}
gate := cfg.OpenAI.StreamEvidenceGate
if got := gate.EffectiveMaxRequestFaultRecovery(); got != 3 {
t.Errorf("EffectiveMaxRequestFaultRecovery() = %d, want 3", got)
}
if got := gate.EffectiveMaxStrategyFaultRecovery(); got != 3 {
t.Errorf("EffectiveMaxStrategyFaultRecovery() = %d, want 3", got)
}
if got := gate.EffectiveMaxIngressSnapshotBytes(); got != 16777216 {
t.Errorf("EffectiveMaxIngressSnapshotBytes() = %d, want 16777216", got)
}
}
func TestStreamEvidenceGate_EnabledField_Load(t *testing.T) {
tests := []struct {
name string
gateYAML string
wantEnabled bool
}{
{
name: "omitted defaults to disabled",
gateYAML: "",
wantEnabled: false,
},
{
name: "explicit true enables the request runtime",
gateYAML: `
stream_evidence_gate:
enabled: true`,
wantEnabled: true,
},
{
name: "explicit false stays disabled",
gateYAML: `
stream_evidence_gate:
enabled: false`,
wantEnabled: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
yamlContent := "openai:\n enabled: true\n provider_id: \"test-provider\"" + tt.gateYAML + "\n"
tmpDir := t.TempDir()
tmpFile := filepath.Join(tmpDir, "edge.yaml")
if err := os.WriteFile(tmpFile, []byte(yamlContent), 0644); err != nil {
t.Fatalf("failed to write temp yaml: %v", err)
}
cfg, err := config.LoadEdge(tmpFile)
if err != nil {
t.Fatalf("LoadEdge failed: %v", err)
}
if got := cfg.OpenAI.StreamEvidenceGate.Enabled; got != tt.wantEnabled {
t.Errorf("StreamEvidenceGate.Enabled = %v, want %v", got, tt.wantEnabled)
}
})
}
}
func TestStreamEvidenceGate_RequestFaultRecovery_Table(t *testing.T) {
tests := []struct {
name string
totalYAML string
expectErr bool
wantTotal int
wantStrategy int
}{
{
name: "explicit 0 disables fault recovery",
totalYAML: "max_request_fault_recovery: 0",
expectErr: false,
wantTotal: 0,
wantStrategy: 0,
},
{
name: "explicit 1",
totalYAML: "max_request_fault_recovery: 1",
expectErr: false,
wantTotal: 1,
wantStrategy: 1,
},
{
name: "explicit 3",
totalYAML: "max_request_fault_recovery: 3",
expectErr: false,
wantTotal: 3,
wantStrategy: 3,
},
{
name: "explicit 4 rejection",
totalYAML: "max_request_fault_recovery: 4",
expectErr: true,
},
{
name: "negative total rejection",
totalYAML: "max_request_fault_recovery: -1",
expectErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
yamlContent := `
openai:
enabled: true
provider_id: "test-provider"
stream_evidence_gate:
` + tt.totalYAML + `
`
tmpDir := t.TempDir()
tmpFile := filepath.Join(tmpDir, "edge.yaml")
if err := os.WriteFile(tmpFile, []byte(yamlContent), 0644); err != nil {
t.Fatalf("failed to write temp yaml: %v", err)
}
cfg, err := config.LoadEdge(tmpFile)
if tt.expectErr {
if err == nil {
t.Errorf("LoadEdge succeeded unexpectedly, wanted error for %s", tt.totalYAML)
}
return
}
if err != nil {
t.Fatalf("LoadEdge failed: %v", err)
}
gate := cfg.OpenAI.StreamEvidenceGate
if got := gate.EffectiveMaxRequestFaultRecovery(); got != tt.wantTotal {
t.Errorf("EffectiveMaxRequestFaultRecovery() = %d, want %d", got, tt.wantTotal)
}
if got := gate.EffectiveMaxStrategyFaultRecovery(); got != tt.wantStrategy {
t.Errorf("EffectiveMaxStrategyFaultRecovery() = %d, want %d", got, tt.wantStrategy)
}
})
}
}
func TestStreamEvidenceGate_StrategyFaultRecovery_Table(t *testing.T) {
tests := []struct {
name string
gateYAML string
expectErr bool
wantTotal int
wantStrategy int
}{
{
name: "total 3, strategy 0",
gateYAML: `
max_request_fault_recovery: 3
max_strategy_fault_recovery: 0`,
expectErr: false,
wantTotal: 3,
wantStrategy: 0,
},
{
name: "total 3, strategy 2",
gateYAML: `
max_request_fault_recovery: 3
max_strategy_fault_recovery: 2`,
expectErr: false,
wantTotal: 3,
wantStrategy: 2,
},
{
name: "total 3, strategy 3",
gateYAML: `
max_request_fault_recovery: 3
max_strategy_fault_recovery: 3`,
expectErr: false,
wantTotal: 3,
wantStrategy: 3,
},
{
name: "total 3, strategy 4 rejection (strategy > total)",
gateYAML: `
max_request_fault_recovery: 3
max_strategy_fault_recovery: 4`,
expectErr: true,
},
{
name: "total 1, strategy 2 rejection (strategy > total)",
gateYAML: `
max_request_fault_recovery: 1
max_strategy_fault_recovery: 2`,
expectErr: true,
},
{
name: "total 0, strategy 1 rejection (strategy > total)",
gateYAML: `
max_request_fault_recovery: 0
max_strategy_fault_recovery: 1`,
expectErr: true,
},
{
name: "strategy negative rejection",
gateYAML: `
max_request_fault_recovery: 3
max_strategy_fault_recovery: -1`,
expectErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
yamlContent := `
openai:
enabled: true
provider_id: "test-provider"
stream_evidence_gate:` + tt.gateYAML + `
`
tmpDir := t.TempDir()
tmpFile := filepath.Join(tmpDir, "edge.yaml")
if err := os.WriteFile(tmpFile, []byte(yamlContent), 0644); err != nil {
t.Fatalf("failed to write temp yaml: %v", err)
}
cfg, err := config.LoadEdge(tmpFile)
if tt.expectErr {
if err == nil {
t.Errorf("LoadEdge succeeded unexpectedly, wanted error for %s", tt.gateYAML)
}
return
}
if err != nil {
t.Fatalf("LoadEdge failed: %v", err)
}
gate := cfg.OpenAI.StreamEvidenceGate
if got := gate.EffectiveMaxRequestFaultRecovery(); got != tt.wantTotal {
t.Errorf("EffectiveMaxRequestFaultRecovery() = %d, want %d", got, tt.wantTotal)
}
if got := gate.EffectiveMaxStrategyFaultRecovery(); got != tt.wantStrategy {
t.Errorf("EffectiveMaxStrategyFaultRecovery() = %d, want %d", got, tt.wantStrategy)
}
})
}
}
// TestStreamGateFilterPolicy_Defaults verifies the per-filter effective
// defaults (enabled=true, enforcement=blocking, capability=output.<kind>,
// hold=500, timeout=5000) that the runtime relies on when fields are omitted.
func TestStreamGateFilterPolicy_Defaults(t *testing.T) {
f := config.StreamGateFilterPolicyConf{Filter: config.StreamGateFilterRepeatGuard}
if !f.EffectiveEnabled() {
t.Errorf("EffectiveEnabled() = false, want true by default")
}
if got := f.EffectiveEnforcement(); got != config.StreamGateFilterEnforcementBlocking {
t.Errorf("EffectiveEnforcement() = %q, want blocking", got)
}
if got := f.EffectiveCapability(); got != "output.repeat_guard" {
t.Errorf("EffectiveCapability() = %q, want output.repeat_guard", got)
}
if got := f.EffectiveHoldEvidenceRunes(); got != config.DefaultStreamGateFilterHoldEvidenceRunes {
t.Errorf("EffectiveHoldEvidenceRunes() = %d, want %d", got, config.DefaultStreamGateFilterHoldEvidenceRunes)
}
if got := f.EffectiveTimeoutMS(); got != config.DefaultStreamGateFilterTimeoutMS {
t.Errorf("EffectiveTimeoutMS() = %d, want %d", got, config.DefaultStreamGateFilterTimeoutMS)
}
if err := f.Validate(); err != nil {
t.Errorf("Validate() unexpectedly failed on defaulted filter: %v", err)
}
}
// TestStreamGateFilterPolicy_ValidateTable covers the config validation contract
// for the semantic output filters: known kinds, enforcement values, selector
// types/keys, and numeric bounds (S01/S02 config).
func TestStreamGateFilterPolicy_ValidateTable(t *testing.T) {
boolPtr := func(b bool) *bool { return &b }
tests := []struct {
name string
filter config.StreamGateFilterPolicyConf
expectErr bool
}{
{name: "valid repeat guard", filter: config.StreamGateFilterPolicyConf{Filter: config.StreamGateFilterRepeatGuard}},
{name: "valid schema gate observe_only", filter: config.StreamGateFilterPolicyConf{Filter: config.StreamGateFilterSchemaGate, Enforcement: config.StreamGateFilterEnforcementObserveOnly}},
{name: "valid provider error with selectors", filter: config.StreamGateFilterPolicyConf{
Filter: config.StreamGateFilterProviderError,
Priority: 20,
Selectors: []config.StreamGateFilterSelectorConf{
{Type: config.StreamGateFilterSelectorProvider, Key: "prov-a", Enabled: boolPtr(false)},
{Type: config.StreamGateFilterSelectorModel, Key: "ornith:35b", Enforcement: config.StreamGateFilterEnforcementObserveOnly},
},
}},
{name: "unknown kind", filter: config.StreamGateFilterPolicyConf{Filter: "malformed_guard"}, expectErr: true},
{name: "unknown enforcement", filter: config.StreamGateFilterPolicyConf{Filter: config.StreamGateFilterRepeatGuard, Enforcement: "warn"}, expectErr: true},
{name: "hold runes over max", filter: config.StreamGateFilterPolicyConf{Filter: config.StreamGateFilterRepeatGuard, HoldEvidenceRunes: config.MaxStreamGateFilterHoldEvidenceRunes + 1}, expectErr: true},
{name: "negative hold runes", filter: config.StreamGateFilterPolicyConf{Filter: config.StreamGateFilterRepeatGuard, HoldEvidenceRunes: -1}, expectErr: true},
{name: "timeout over max", filter: config.StreamGateFilterPolicyConf{Filter: config.StreamGateFilterRepeatGuard, TimeoutMS: config.MaxStreamGateFilterTimeoutMS + 1}, expectErr: true},
{name: "negative priority", filter: config.StreamGateFilterPolicyConf{Filter: config.StreamGateFilterRepeatGuard, Priority: -1}, expectErr: true},
{name: "unknown selector type", filter: config.StreamGateFilterPolicyConf{Filter: config.StreamGateFilterRepeatGuard, Selectors: []config.StreamGateFilterSelectorConf{{Type: "region", Key: "kr"}}}, expectErr: true},
{name: "empty selector key", filter: config.StreamGateFilterPolicyConf{Filter: config.StreamGateFilterRepeatGuard, Selectors: []config.StreamGateFilterSelectorConf{{Type: config.StreamGateFilterSelectorModel, Key: " "}}}, expectErr: true},
{name: "unknown selector enforcement", filter: config.StreamGateFilterPolicyConf{Filter: config.StreamGateFilterRepeatGuard, Selectors: []config.StreamGateFilterSelectorConf{{Type: config.StreamGateFilterSelectorModel, Key: "m", Enforcement: "warn"}}}, expectErr: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.filter.Validate()
if tt.expectErr && err == nil {
t.Fatalf("Validate() succeeded, want error")
}
if !tt.expectErr && err != nil {
t.Fatalf("Validate() failed: %v", err)
}
})
}
}
// TestStreamGateFilterPolicy_LoadAndDuplicateRejection loads a full filter policy
// from YAML and confirms a duplicate filter kind is rejected at load time.
func TestStreamGateFilterPolicy_LoadAndDuplicateRejection(t *testing.T) {
valid := `
openai:
enabled: true
provider_id: "test-provider"
stream_evidence_gate:
enabled: true
filters:
- filter: repeat_guard
enforcement: observe_only
capability: output.repeat_guard
- filter: provider_error
priority: 20
selectors:
- type: provider
key: prov-a
enabled: false
`
tmpDir := t.TempDir()
tmpFile := filepath.Join(tmpDir, "edge.yaml")
if err := os.WriteFile(tmpFile, []byte(valid), 0644); err != nil {
t.Fatalf("write yaml: %v", err)
}
cfg, err := config.LoadEdge(tmpFile)
if err != nil {
t.Fatalf("LoadEdge valid filters failed: %v", err)
}
filters := cfg.OpenAI.StreamEvidenceGate.Filters
if len(filters) != 2 {
t.Fatalf("loaded %d filters, want 2", len(filters))
}
if filters[0].Filter != config.StreamGateFilterRepeatGuard || filters[0].EffectiveEnforcement() != config.StreamGateFilterEnforcementObserveOnly {
t.Errorf("filter[0] = %+v", filters[0])
}
if len(filters[1].Selectors) != 1 || filters[1].Selectors[0].Type != config.StreamGateFilterSelectorProvider {
t.Errorf("filter[1] selectors = %+v", filters[1].Selectors)
}
dup := `
openai:
enabled: true
provider_id: "test-provider"
stream_evidence_gate:
enabled: true
filters:
- filter: repeat_guard
- filter: repeat_guard
`
dupFile := filepath.Join(tmpDir, "dup.yaml")
if err := os.WriteFile(dupFile, []byte(dup), 0644); err != nil {
t.Fatalf("write dup yaml: %v", err)
}
if _, err := config.LoadEdge(dupFile); err == nil {
t.Fatalf("LoadEdge with duplicate filter kind succeeded, want error")
}
}
func TestStreamEvidenceGate_IngressSnapshotBytes_TableFixture(t *testing.T) {
maxAllowed := 16 * 1024 * 1024 // 16777216
tests := []struct {
name string
ingressYAML string
expectErr bool
wantBytes int
}{
{
name: "ingress limit - 1 (16777215)",
ingressYAML: "max_ingress_snapshot_bytes: 16777215",
expectErr: false,
wantBytes: 16777215,
},
{
name: "ingress limit (16777216)",
ingressYAML: "max_ingress_snapshot_bytes: 16777216",
expectErr: false,
wantBytes: maxAllowed,
},
{
name: "ingress limit + 1 (16777217) rejection",
ingressYAML: "max_ingress_snapshot_bytes: 16777217",
expectErr: true,
},
{
name: "ingress 1 byte boundary",
ingressYAML: "max_ingress_snapshot_bytes: 1",
expectErr: false,
wantBytes: 1,
},
{
name: "ingress negative rejection",
ingressYAML: "max_ingress_snapshot_bytes: -1",
expectErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
yamlContent := `
openai:
enabled: true
provider_id: "test-provider"
stream_evidence_gate:
` + tt.ingressYAML + `
`
tmpDir := t.TempDir()
tmpFile := filepath.Join(tmpDir, "edge.yaml")
if err := os.WriteFile(tmpFile, []byte(yamlContent), 0644); err != nil {
t.Fatalf("failed to write temp yaml: %v", err)
}
cfg, err := config.LoadEdge(tmpFile)
if tt.expectErr {
if err == nil {
t.Errorf("LoadEdge succeeded unexpectedly, wanted error for %s", tt.ingressYAML)
}
return
}
if err != nil {
t.Fatalf("LoadEdge failed: %v", err)
}
gate := cfg.OpenAI.StreamEvidenceGate
if got := gate.EffectiveMaxIngressSnapshotBytes(); got != tt.wantBytes {
t.Errorf("EffectiveMaxIngressSnapshotBytes() = %d, want %d", got, tt.wantBytes)
}
})
}
}
func TestStreamGateFilterPolicy_Environment(t *testing.T) {
var defaults config.StreamEvidenceGateConf
if got := defaults.EffectiveEnvironment(); got != config.StreamGateEnvironmentDev {
t.Fatalf("default environment=%q, want dev", got)
}
for _, environment := range []string{config.StreamGateEnvironmentDev, config.StreamGateEnvironmentDevCorp} {
cfg := config.StreamEvidenceGateConf{Environment: environment}
if err := cfg.Validate(); err != nil {
t.Errorf("Validate(%q): %v", environment, err)
}
}
if err := (config.StreamEvidenceGateConf{Environment: "production"}).Validate(); err == nil {
t.Fatal("Validate(production) succeeded, want closed environment error")
}
}