138 lines
8.1 KiB
Go
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
|
|
}
|