Files
story/internal/opsconfig/agent_quality_integration_test.go
T
2026-08-29 21:56:51 +08:00

138 lines
8.1 KiB
Go

package opsconfig
import (
"context"
"errors"
"os"
"strings"
"testing"
"data-terminal-cloud/internal/audit"
"data-terminal-cloud/internal/identity"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgxpool"
)
func TestAgentBundleRequiresStandardQualityEvidence(t *testing.T) {
databaseURL := os.Getenv("TEST_DATABASE_URL")
if databaseURL == "" {
t.Skip("TEST_DATABASE_URL is not set")
}
ctx := context.Background()
pool, err := pgxpool.New(ctx, databaseURL)
if err != nil {
t.Fatal(err)
}
defer pool.Close()
service, err := NewService(pool, audit.NewRepository())
if err != nil {
t.Fatal(err)
}
admin := identity.Principal{Issuer: "http://localhost:58180/realms/voice-local", Subject: "00000000-0000-0000-0000-000000000104"}
approver := identity.Principal{Issuer: admin.Issuer, Subject: "00000000-0000-0000-0000-000000000105"}
tenantID := "10000000-0000-0000-0000-000000000010"
configKey := "integration.agent.bundle." + uuid.NewString()
invalidConfigKey := "integration.agent.bundle.invalid." + uuid.NewString()
componentPrefix := "integration.agent.component." + uuid.NewString() + "."
keys := []string{"agent-bundle-create-" + uuid.NewString(), "agent-bundle-generic-" + uuid.NewString(), "agent-bundle-standard-" + uuid.NewString(), "agent-bundle-invalid-" + uuid.NewString(), "agent-bundle-mismatched-evidence-" + uuid.NewString()}
defer func() {
_, _ = pool.Exec(ctx, `DELETE FROM audit_event WHERE request_id LIKE 'agent-bundle-%'`)
_, _ = pool.Exec(ctx, `DELETE FROM idempotency_record WHERE idempotency_key=ANY($1::text[])`, keys)
_, _ = pool.Exec(ctx, `DELETE FROM runtime_config_rollout WHERE config_key=$1`, configKey)
_, _ = pool.Exec(ctx, `DELETE FROM ops_config_evaluation WHERE config_version_id IN (SELECT id FROM runtime_config_version WHERE config_key=$1 OR config_key=$2)`, configKey, invalidConfigKey)
_, _ = pool.Exec(ctx, `DELETE FROM runtime_config_version WHERE config_key=$1 OR config_key=$2 OR config_key LIKE $3`, configKey, invalidConfigKey, componentPrefix+"%")
}()
componentIDs := seedPublishedAgentComponents(t, ctx, pool, tenantID, admin.Issuer, admin.Subject, componentPrefix)
invalidInput := CreateInput{
TenantID: tenantID, ConfigKey: invalidConfigKey, DisplayName: "Agent组件类型错误验证", ConfigKind: "AGENT_BUNDLE",
Payload: map[string]any{
"persona_version_id": componentIDs["PERSONA"], "voice_version_id": componentIDs["PERSONA"],
"model_version_id": componentIDs["MODEL"], "routing_version_id": componentIDs["ROUTING"],
}, SecretRefs: map[string]string{}, ChangeSummary: "验证错误类型的组件版本不能提交",
}
invalidVersion, _, err := service.Create(ctx, admin, keys[3], "agent-bundle-invalid-create", invalidInput)
if err != nil {
t.Fatal(err)
}
if _, err = service.Submit(ctx, admin, tenantID, invalidVersion.ID, "agent-bundle-invalid-submit", invalidVersion.RowVersion); !errors.Is(err, ErrInvalid) {
t.Fatalf("wrong component kind submit error = %v", err)
}
input := CreateInput{
TenantID: tenantID, ConfigKey: configKey, DisplayName: "Agent质量门禁集成配置", ConfigKind: "AGENT_BUNDLE",
Payload: map[string]any{
"persona_version_id": componentIDs["PERSONA"], "voice_version_id": componentIDs["VOICE"],
"model_version_id": componentIDs["MODEL"], "routing_version_id": componentIDs["ROUTING"],
}, SecretRefs: map[string]string{}, ChangeSummary: "验证标准Agent质量证据门禁",
}
version, _, err := service.Create(ctx, admin, keys[0], "agent-bundle-create", input)
if err != nil {
t.Fatal(err)
}
version, err = service.Submit(ctx, admin, tenantID, version.ID, "agent-bundle-submit", version.RowVersion)
if err != nil {
t.Fatal(err)
}
rollout := RolloutInput{ScopeType: "TENANT", RolloutPercent: 100, MaxConcurrentCalls: 10}
if _, _, err = service.CreateEvaluation(ctx, admin, tenantID, version.ID, keys[1], "agent-bundle-generic", EvidenceTypeGeneric, "generic-v1", "PASSED", "artifact://generic.json", map[string]any{"passed": true}); err != nil {
t.Fatal(err)
}
if _, err = service.Publish(ctx, approver, tenantID, version.ID, "agent-bundle-publish-generic", PublishInput{Reason: "通用证据不得放行", Rollout: rollout}, version.RowVersion); !errors.Is(err, ErrPublishGate) {
t.Fatalf("generic evidence publish error = %v", err)
}
metrics := map[string]any{
"report_schema": "agent-eval-report-v1", "suite_sha256": "b5cd3b798a01a48ddf0081f2fcc2a917f93681a3ee2fa55bbf4190466de3bef8",
"agent_version": "agent-v1", "model_config_ref": "runtime-config-version:" + componentIDs["MODEL"], "config_version_id": version.ID, "environment": "mock", "executed_at": "2026-08-24T10:00:00Z",
"cases_total": 10, "cases_passed": 10, "failed_check_count": 0, "conversation_success_rate": 1.0,
"first_response_p95_ms": 1200, "interruption_recovery_rate": 1.0, "average_response_chars": 36.5,
"salutation_accuracy": 1.0, "memory_accuracy": 1.0, "fabricated_facts": 0,
"story_source_completeness": 1.0, "sensitive_information_handling_rate": 1.0,
}
mismatchedMetrics := make(map[string]any, len(metrics))
for key, value := range metrics {
mismatchedMetrics[key] = value
}
mismatchedMetrics["model_config_ref"] = "runtime-config-version:" + componentIDs["VOICE"]
if _, _, err = service.CreateEvaluation(ctx, admin, tenantID, version.ID, keys[4], "agent-bundle-mismatched-evidence", EvidenceTypeAgentQualityV1, "agent-quality-mismatched-v1", "PASSED", "artifact://agent-quality-mismatched.json", mismatchedMetrics); !errors.Is(err, ErrInvalid) {
t.Fatalf("mismatched model evidence error = %v", err)
}
evaluation, _, err := service.CreateEvaluation(ctx, admin, tenantID, version.ID, keys[2], "agent-bundle-standard", EvidenceTypeAgentQualityV1, "agent-quality-v1", "PASSED", "artifact://agent-quality.json", metrics)
if err != nil || evaluation.EvidenceType != EvidenceTypeAgentQualityV1 {
t.Fatalf("standard evaluation = %+v, error = %v", evaluation, err)
}
version, err = service.Publish(ctx, approver, tenantID, version.ID, "agent-bundle-publish-standard", PublishInput{Reason: "标准质量证据通过", Rollout: rollout}, version.RowVersion)
if err != nil || version.Status != "PUBLISHED" {
t.Fatalf("published version = %+v, error = %v", version, err)
}
}
func seedPublishedAgentComponents(t *testing.T, ctx context.Context, pool *pgxpool.Pool, tenantPublicID, oidcIssuer, oidcSubject, keyPrefix string) map[string]string {
t.Helper()
components := map[string]struct {
payload map[string]any
refs map[string]string
}{
"PERSONA": {payload: map[string]any{"system_prompt": "集成验证人设", "tone": "warm"}, refs: map[string]string{}},
"VOICE": {payload: map[string]any{"provider": "local", "voice_resource_ref": "voice://integration/approved"}, refs: map[string]string{"credential_ref": "kms://integration/voice"}},
"MODEL": {payload: map[string]any{"provider": "local", "model_id": "model://integration/approved"}, refs: map[string]string{"credential_ref": "kms://integration/model"}},
"ROUTING": {payload: map[string]any{"default_engine_mode": "s2s_hybrid", "routes": []any{map[string]any{"goal": "open_conversation", "engine_mode": "s2s_hybrid"}}}, refs: map[string]string{}},
}
ids := make(map[string]string, len(components))
for kind, component := range components {
publicID := uuid.NewString()
tag, err := pool.Exec(ctx, `
INSERT INTO runtime_config_version(public_id,tenant_id,config_key,display_name,config_kind,version_no,status,payload,secret_refs,change_summary,validation_result,created_by,submitted_by,submitted_at,approved_by,approved_at,published_by,published_at)
SELECT $1::uuid,t.id,$2,$3,$4,1,'PUBLISHED',$5::jsonb,$6::jsonb,'Agent组合组件引用集成夹具','{"valid":true}'::jsonb,u.id,u.id,now(),u.id,now(),u.id,now()
FROM tenant t JOIN user_account u ON u.tenant_id=t.id WHERE t.public_id=$7::uuid AND u.oidc_issuer=$8 AND u.oidc_subject=$9`,
publicID, keyPrefix+strings.ToLower(kind), "Agent组件"+kind, kind, mustJSON(component.payload), mustJSON(component.refs), tenantPublicID, oidcIssuer, oidcSubject)
if err != nil {
t.Fatal(err)
}
if tag.RowsAffected() != 1 {
t.Fatalf("seed %s component affected %d rows", kind, tag.RowsAffected())
}
ids[kind] = publicID
}
return ids
}