chore: establish local platform baseline

This commit is contained in:
Codex Local
2026-08-26 06:41:54 +08:00
commit 29bdf4900f
575 changed files with 95644 additions and 0 deletions
@@ -0,0 +1,137 @@
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-000000000101"}
approver := identity.Principal{Issuer: admin.Issuer, Subject: "00000000-0000-0000-0000-000000000102"}
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
}
+37
View File
@@ -0,0 +1,37 @@
package opsconfig
import "testing"
func TestAgentQualityEvaluationMetrics(t *testing.T) {
metrics := map[string]any{
"report_schema": "agent-eval-report-v1", "suite_sha256": "b5cd3b798a01a48ddf0081f2fcc2a917f93681a3ee2fa55bbf4190466de3bef8",
"agent_version": "agent-v1", "model_config_ref": "runtime-config://voice/agent-v1",
"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,
}
if !validEvaluationMetrics(EvidenceTypeAgentQualityV1, "PASSED", metrics) {
t.Fatal("valid standard Agent quality evidence was rejected")
}
metrics["cases_passed"] = 9
if validEvaluationMetrics(EvidenceTypeAgentQualityV1, "PASSED", metrics) {
t.Fatal("incomplete PASSED evidence was accepted")
}
metrics["failed_check_count"] = 1
if !validEvaluationMetrics(EvidenceTypeAgentQualityV1, "FAILED", metrics) {
t.Fatal("well-formed FAILED evidence was rejected")
}
delete(metrics, "memory_accuracy")
if validEvaluationMetrics(EvidenceTypeAgentQualityV1, "FAILED", metrics) {
t.Fatal("standard evidence missing a required metric was accepted")
}
if !validEvaluationMetrics(EvidenceTypeGeneric, "PASSED", map[string]any{"legacy": true}) {
t.Fatal("generic evaluation compatibility was broken")
}
if validEvaluationMetrics("UNKNOWN", "PASSED", metrics) {
t.Fatal("unknown evidence type was accepted")
}
}
+733
View File
@@ -0,0 +1,733 @@
package opsconfig
import (
"context"
"encoding/json"
"errors"
"fmt"
"regexp"
"strings"
"time"
"data-terminal-cloud/internal/audit"
"data-terminal-cloud/internal/idempotency"
"data-terminal-cloud/internal/identity"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
)
var ErrPublishGate = errors.New("runtime configuration publish gate failed")
const (
EvidenceTypeGeneric = "GENERIC"
EvidenceTypeAgentQualityV1 = "AGENT_QUALITY_V1"
)
type RolloutInput struct {
ScopeType string `json:"scope_type"`
ScopeID string `json:"scope_id,omitempty"`
RolloutPercent int `json:"rollout_percent"`
MaxConcurrentCalls int `json:"max_concurrent_calls"`
}
type PublishInput struct {
Reason string `json:"reason"`
Rollout RolloutInput `json:"rollout"`
}
type ResourceReference struct {
ID string `json:"id"`
TenantID string `json:"tenant_id"`
ResourceType string `json:"resource_type"`
ReferenceName string `json:"reference_name"`
DisplayName string `json:"display_name"`
Status string `json:"status"`
VerifiedAt time.Time `json:"verified_at"`
LastRotatedAt *time.Time `json:"last_rotated_at,omitempty"`
ExpiresAt *time.Time `json:"expires_at,omitempty"`
CreatedBy string `json:"created_by"`
RowVersion int64 `json:"row_version"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type Evaluation struct {
ID string `json:"id"`
ConfigVersionID string `json:"config_version_id"`
EvidenceType string `json:"evidence_type"`
SuiteVersion string `json:"suite_version"`
Result string `json:"result"`
Metrics map[string]any `json:"metrics"`
ReportRef string `json:"report_ref"`
EvaluatedBy string `json:"evaluated_by"`
EvaluatedAt time.Time `json:"evaluated_at"`
}
type DeviceGroup struct {
ID string `json:"id"`
TenantID string `json:"tenant_id"`
Name string `json:"name"`
Selector map[string]any `json:"selector"`
Status string `json:"status"`
DeviceIDs []string `json:"device_ids"`
CreatedBy string `json:"created_by"`
RowVersion int64 `json:"row_version"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type Rollout struct {
ID string `json:"id"`
ConfigVersionID string `json:"config_version_id"`
ConfigKey string `json:"config_key"`
ScopeType string `json:"scope_type"`
ScopeID *string `json:"scope_id,omitempty"`
RolloutPercent int `json:"rollout_percent"`
MaxConcurrentCalls int `json:"max_concurrent_calls"`
Status string `json:"status"`
Reason string `json:"reason"`
ActivatedBy string `json:"activated_by"`
ActivatedAt time.Time `json:"activated_at"`
PausedAt *time.Time `json:"paused_at,omitempty"`
EndedAt *time.Time `json:"ended_at,omitempty"`
RowVersion int64 `json:"row_version"`
}
func (s *Service) CreateResourceReference(ctx context.Context, principal identity.Principal, tenantPublicID, key, requestID, resourceType, referenceName, displayName string, verifiedAt time.Time, expiresAt *time.Time) (ResourceReference, bool, error) {
resourceType = strings.ToUpper(strings.TrimSpace(resourceType))
referenceName = strings.TrimSpace(referenceName)
displayName = strings.TrimSpace(displayName)
if !referencePattern.MatchString(referenceName) || displayName == "" || verifiedAt.IsZero() || verifiedAt.After(time.Now().UTC().Add(5*time.Minute)) {
return ResourceReference{}, false, ErrInvalid
}
if resourceType != "PROVIDER_CREDENTIAL" && resourceType != "VOICE_RESOURCE" && resourceType != "MODEL_RESOURCE" && resourceType != "CERTIFICATE" {
return ResourceReference{}, false, ErrInvalid
}
if (resourceType == "CERTIFICATE") != (expiresAt != nil) || (expiresAt != nil && !expiresAt.After(verifiedAt)) {
return ResourceReference{}, false, ErrInvalid
}
currentActor, err := s.resolveActor(ctx, principal, tenantPublicID, "ops_operator", "ops_approver", "ops_admin")
if err != nil {
return ResourceReference{}, false, err
}
requestBody, _ := json.Marshal(map[string]any{"resource_type": resourceType, "reference_name": referenceName, "display_name": displayName, "verified_at": verifiedAt.UTC(), "expires_at": expiresAt})
result, err := s.idem.Execute(ctx, idempotency.Request{TenantID: &currentActor.tenantID, ActorKey: "ops:" + currentActor.userPublicID, Scope: "POST /api/v1/ops/resource-references", Key: key, RequestHash: idempotency.Hash(requestBody), ExpiresAfter: 24 * time.Hour}, func(operationContext context.Context, tx pgx.Tx) (idempotency.Result, error) {
publicID := uuid.NewString()
var internalID string
err := tx.QueryRow(operationContext, `INSERT INTO ops_resource_reference(public_id,tenant_id,resource_type,reference_name,display_name,verified_at,expires_at,created_by) VALUES($1::uuid,$2::uuid,$3,$4,$5,$6,$7,$8::uuid) RETURNING id::text`, publicID, currentActor.tenantID, resourceType, referenceName, displayName, verifiedAt.UTC(), expiresAt, currentActor.userID).Scan(&internalID)
if err != nil {
return idempotency.Result{}, err
}
if err = s.audit.Record(operationContext, tx, audit.Event{TenantID: &currentActor.tenantID, ActorType: "user", ActorPublicID: &currentActor.userPublicID, Action: "ops.resource_reference.create", ResourceType: "ops_resource_reference", ResourceID: &internalID, RequestID: requestID, Purpose: "register verified resource reference", Result: "success", Detail: map[string]any{"resource_type": resourceType, "reference_name": referenceName}}); err != nil {
return idempotency.Result{}, err
}
item, err := getResourceReferenceTx(operationContext, tx, currentActor.tenantID, publicID)
if err != nil {
return idempotency.Result{}, err
}
body, _ := json.Marshal(item)
return idempotency.Result{StatusCode: 201, Body: body, ResourceID: &internalID}, nil
})
if err != nil {
return ResourceReference{}, false, err
}
var item ResourceReference
if err = json.Unmarshal(result.Body, &item); err != nil {
return ResourceReference{}, false, err
}
return item, !result.Replayed, nil
}
func (s *Service) ListResourceReferences(ctx context.Context, principal identity.Principal, tenantPublicID string) ([]ResourceReference, error) {
currentActor, err := s.resolveActor(ctx, principal, tenantPublicID, "ops_viewer", "ops_operator", "ops_approver", "ops_admin")
if err != nil {
return nil, err
}
rows, err := s.pool.Query(ctx, resourceReferenceSelect+` WHERE rr.tenant_id=$1::uuid ORDER BY rr.resource_type,rr.reference_name LIMIT 500`, currentActor.tenantID)
if err != nil {
return nil, err
}
defer rows.Close()
items := []ResourceReference{}
for rows.Next() {
item, scanErr := scanResourceReference(rows)
if scanErr != nil {
return nil, scanErr
}
items = append(items, item)
}
return items, rows.Err()
}
func (s *Service) RevokeResourceReference(ctx context.Context, principal identity.Principal, tenantPublicID, publicID, requestID, reason string, expectedVersion int64) (ResourceReference, error) {
reason = strings.TrimSpace(reason)
if reason == "" || len([]rune(reason)) > 500 {
return ResourceReference{}, ErrInvalid
}
currentActor, err := s.resolveActor(ctx, principal, tenantPublicID, "ops_operator", "ops_approver", "ops_admin")
if err != nil {
return ResourceReference{}, err
}
tx, err := s.pool.BeginTx(ctx, pgx.TxOptions{})
if err != nil {
return ResourceReference{}, err
}
defer func() { _ = tx.Rollback(context.Background()) }()
var internalID, status string
var rowVersion int64
err = tx.QueryRow(ctx, `SELECT id::text,status,row_version FROM ops_resource_reference WHERE tenant_id=$1::uuid AND public_id=$2::uuid FOR UPDATE`, currentActor.tenantID, publicID).Scan(&internalID, &status, &rowVersion)
if errors.Is(err, pgx.ErrNoRows) {
return ResourceReference{}, ErrNotFound
}
if err != nil {
return ResourceReference{}, err
}
if rowVersion != expectedVersion {
return ResourceReference{}, ErrVersionConflict
}
if status != "ACTIVE" {
return ResourceReference{}, ErrStateConflict
}
if _, err = tx.Exec(ctx, `UPDATE ops_resource_reference SET status='REVOKED',row_version=row_version+1,updated_at=now() WHERE id=$1::uuid`, internalID); err != nil {
return ResourceReference{}, err
}
if err = s.audit.Record(ctx, tx, audit.Event{TenantID: &currentActor.tenantID, ActorType: "user", ActorPublicID: &currentActor.userPublicID, Action: "ops.resource_reference.revoke", ResourceType: "ops_resource_reference", ResourceID: &internalID, RequestID: requestID, Purpose: reason, Result: "success"}); err != nil {
return ResourceReference{}, err
}
item, err := getResourceReferenceTx(ctx, tx, currentActor.tenantID, publicID)
if err == nil {
err = tx.Commit(ctx)
}
return item, err
}
func (s *Service) CreateEvaluation(ctx context.Context, principal identity.Principal, tenantPublicID, configPublicID, key, requestID, evidenceType, suiteVersion, result, reportRef string, metrics map[string]any) (Evaluation, bool, error) {
evidenceType = strings.ToUpper(strings.TrimSpace(evidenceType))
if evidenceType == "" {
evidenceType = EvidenceTypeGeneric
}
suiteVersion = strings.TrimSpace(suiteVersion)
result = strings.ToUpper(strings.TrimSpace(result))
reportRef = strings.TrimSpace(reportRef)
encodedMetrics, _ := json.Marshal(metrics)
if suiteVersion == "" || len([]rune(suiteVersion)) > 100 || (result != "PASSED" && result != "FAILED") || reportRef == "" || len([]rune(reportRef)) > 300 || metrics == nil || len(encodedMetrics) > 32768 || !validEvaluationMetrics(evidenceType, result, metrics) {
return Evaluation{}, false, ErrInvalid
}
currentActor, err := s.resolveActor(ctx, principal, tenantPublicID, "ops_operator", "ops_approver", "ops_admin")
if err != nil {
return Evaluation{}, false, err
}
requestBody, _ := json.Marshal(map[string]any{"config_id": configPublicID, "evidence_type": evidenceType, "suite_version": suiteVersion, "result": result, "report_ref": reportRef, "metrics": metrics})
idemResult, err := s.idem.Execute(ctx, idempotency.Request{TenantID: &currentActor.tenantID, ActorKey: "ops:" + currentActor.userPublicID, Scope: "POST /api/v1/ops/config-versions/evaluations", Key: key, RequestHash: idempotency.Hash(requestBody), ExpiresAfter: 24 * time.Hour}, func(operationContext context.Context, tx pgx.Tx) (idempotency.Result, error) {
var configID, status, configKind string
var configPayloadJSON []byte
if queryErr := tx.QueryRow(operationContext, `SELECT id::text,status,config_kind,payload FROM runtime_config_version WHERE tenant_id=$1::uuid AND public_id=$2::uuid FOR UPDATE`, currentActor.tenantID, configPublicID).Scan(&configID, &status, &configKind, &configPayloadJSON); errors.Is(queryErr, pgx.ErrNoRows) {
return idempotency.Result{}, ErrNotFound
} else if queryErr != nil {
return idempotency.Result{}, queryErr
}
if status != "DRAFT" && status != "PENDING_APPROVAL" {
return idempotency.Result{}, ErrStateConflict
}
if evidenceType == EvidenceTypeAgentQualityV1 && configKind != "AGENT_BUNDLE" {
return idempotency.Result{}, ErrInvalid
}
if evidenceType == EvidenceTypeAgentQualityV1 {
boundConfigID, bound := metrics["config_version_id"].(string)
if !bound || strings.TrimSpace(boundConfigID) != configPublicID {
return idempotency.Result{}, ErrInvalid
}
var configPayload map[string]any
if queryErr := json.Unmarshal(configPayloadJSON, &configPayload); queryErr != nil {
return idempotency.Result{}, queryErr
}
modelVersionID, modelExists := configPayload["model_version_id"].(string)
modelConfigRef, refExists := metrics["model_config_ref"].(string)
if !modelExists || !refExists || modelConfigRef != "runtime-config-version:"+modelVersionID {
return idempotency.Result{}, ErrInvalid
}
}
publicID := uuid.NewString()
var internalID string
if queryErr := tx.QueryRow(operationContext, `INSERT INTO ops_config_evaluation(public_id,tenant_id,config_version_id,evidence_type,suite_version,result,metrics,report_ref,evaluated_by) VALUES($1::uuid,$2::uuid,$3::uuid,$4,$5,$6,$7::jsonb,$8,$9::uuid) RETURNING id::text`, publicID, currentActor.tenantID, configID, evidenceType, suiteVersion, result, encodedMetrics, reportRef, currentActor.userID).Scan(&internalID); queryErr != nil {
return idempotency.Result{}, queryErr
}
if queryErr := s.audit.Record(operationContext, tx, audit.Event{TenantID: &currentActor.tenantID, ActorType: "user", ActorPublicID: &currentActor.userPublicID, Action: "ops.config.evaluate", ResourceType: "ops_config_evaluation", ResourceID: &internalID, RequestID: requestID, Purpose: suiteVersion, Result: "success", Detail: map[string]any{"config_version_id": configPublicID, "evidence_type": evidenceType, "evaluation_result": result, "report_ref": reportRef}}); queryErr != nil {
return idempotency.Result{}, queryErr
}
item, queryErr := getEvaluationTx(operationContext, tx, currentActor.tenantID, publicID)
if queryErr != nil {
return idempotency.Result{}, queryErr
}
body, _ := json.Marshal(item)
return idempotency.Result{StatusCode: 201, Body: body, ResourceID: &internalID}, nil
})
if err != nil {
return Evaluation{}, false, err
}
var item Evaluation
if err = json.Unmarshal(idemResult.Body, &item); err != nil {
return Evaluation{}, false, err
}
return item, !idemResult.Replayed, nil
}
func validEvaluationMetrics(evidenceType, result string, metrics map[string]any) bool {
if evidenceType == EvidenceTypeGeneric {
return true
}
if evidenceType != EvidenceTypeAgentQualityV1 {
return false
}
reportSchema, schemaOK := metrics["report_schema"].(string)
suiteDigest, digestOK := metrics["suite_sha256"].(string)
agentVersion, agentOK := metrics["agent_version"].(string)
modelConfigRef, configOK := metrics["model_config_ref"].(string)
environment, environmentOK := metrics["environment"].(string)
executedAt, executedOK := metrics["executed_at"].(string)
if !schemaOK || reportSchema != "agent-eval-report-v1" || !digestOK || !regexp.MustCompile(`^[0-9a-f]{64}$`).MatchString(suiteDigest) || !agentOK || strings.TrimSpace(agentVersion) == "" || !configOK || strings.TrimSpace(modelConfigRef) == "" || !environmentOK || (environment != "mock" && environment != "real") || !executedOK {
return false
}
if _, err := time.Parse(time.RFC3339, executedAt); err != nil {
return false
}
casesTotal, totalOK := integerMetric(metrics, "cases_total")
casesPassed, passedOK := integerMetric(metrics, "cases_passed")
failedChecks, failedOK := integerMetric(metrics, "failed_check_count")
fabricatedFacts, fabricatedOK := integerMetric(metrics, "fabricated_facts")
firstResponseP95, latencyOK := numberMetric(metrics, "first_response_p95_ms")
averageResponseChars, lengthOK := numberMetric(metrics, "average_response_chars")
if !totalOK || !passedOK || !failedOK || !fabricatedOK || !latencyOK || !lengthOK || casesTotal < 10 || casesPassed < 0 || casesPassed > casesTotal || failedChecks < 0 || fabricatedFacts < 0 || firstResponseP95 < 0 || averageResponseChars < 0 {
return false
}
for _, key := range []string{"conversation_success_rate", "interruption_recovery_rate", "salutation_accuracy", "memory_accuracy", "story_source_completeness", "sensitive_information_handling_rate"} {
value, ok := numberMetric(metrics, key)
if !ok || value < 0 || value > 1 {
return false
}
}
if result == "PASSED" && (casesPassed != casesTotal || failedChecks != 0) {
return false
}
if result == "FAILED" && failedChecks == 0 {
return false
}
return true
}
func numberMetric(metrics map[string]any, key string) (float64, bool) {
switch value := metrics[key].(type) {
case float64:
return value, true
case float32:
return float64(value), true
case int:
return float64(value), true
case int64:
return float64(value), true
case json.Number:
parsed, err := value.Float64()
return parsed, err == nil
default:
return 0, false
}
}
func integerMetric(metrics map[string]any, key string) (int64, bool) {
value, ok := numberMetric(metrics, key)
if !ok || value != float64(int64(value)) {
return 0, false
}
return int64(value), true
}
func (s *Service) ListEvaluations(ctx context.Context, principal identity.Principal, tenantPublicID, configPublicID string) ([]Evaluation, error) {
currentActor, err := s.resolveActor(ctx, principal, tenantPublicID, "ops_viewer", "ops_operator", "ops_approver", "ops_admin")
if err != nil {
return nil, err
}
rows, err := s.pool.Query(ctx, `
SELECT e.public_id::text,rcv.public_id::text,e.evidence_type,e.suite_version,e.result,e.metrics,
e.report_ref,ua.public_id::text,e.evaluated_at
FROM ops_config_evaluation e
JOIN runtime_config_version rcv ON rcv.id=e.config_version_id AND rcv.tenant_id=e.tenant_id
JOIN user_account ua ON ua.id=e.evaluated_by AND ua.tenant_id=e.tenant_id
WHERE e.tenant_id=$1::uuid AND rcv.public_id=$2::uuid
ORDER BY e.evaluated_at DESC,e.id DESC
LIMIT 500`, currentActor.tenantID, configPublicID)
if err != nil {
return nil, err
}
defer rows.Close()
items := []Evaluation{}
for rows.Next() {
var item Evaluation
var metrics []byte
if err = rows.Scan(&item.ID, &item.ConfigVersionID, &item.EvidenceType, &item.SuiteVersion, &item.Result, &metrics, &item.ReportRef, &item.EvaluatedBy, &item.EvaluatedAt); err != nil {
return nil, err
}
if err = json.Unmarshal(metrics, &item.Metrics); err != nil {
return nil, err
}
items = append(items, item)
}
return items, rows.Err()
}
func (s *Service) CreateDeviceGroup(ctx context.Context, principal identity.Principal, tenantPublicID, key, requestID, name string, selector map[string]any, devicePublicIDs []string) (DeviceGroup, bool, error) {
name = strings.TrimSpace(name)
selectorJSON, _ := json.Marshal(selector)
if name == "" || len([]rune(name)) > 200 || selector == nil || len(selectorJSON) > 8192 || len(devicePublicIDs) == 0 || len(devicePublicIDs) > 1000 {
return DeviceGroup{}, false, ErrInvalid
}
seen := map[string]struct{}{}
for _, id := range devicePublicIDs {
if _, err := uuid.Parse(id); err != nil {
return DeviceGroup{}, false, ErrInvalid
}
seen[id] = struct{}{}
}
devicePublicIDs = devicePublicIDs[:0]
for id := range seen {
devicePublicIDs = append(devicePublicIDs, id)
}
currentActor, err := s.resolveActor(ctx, principal, tenantPublicID, "ops_operator", "ops_approver", "ops_admin")
if err != nil {
return DeviceGroup{}, false, err
}
requestBody, _ := json.Marshal(map[string]any{"name": name, "selector": selector, "device_ids": devicePublicIDs})
idemResult, err := s.idem.Execute(ctx, idempotency.Request{TenantID: &currentActor.tenantID, ActorKey: "ops:" + currentActor.userPublicID, Scope: "POST /api/v1/ops/device-groups", Key: key, RequestHash: idempotency.Hash(requestBody), ExpiresAfter: 24 * time.Hour}, func(operationContext context.Context, tx pgx.Tx) (idempotency.Result, error) {
publicID := uuid.NewString()
var internalID string
if queryErr := tx.QueryRow(operationContext, `INSERT INTO ops_device_group(public_id,tenant_id,name,selector,created_by) VALUES($1::uuid,$2::uuid,$3,$4::jsonb,$5::uuid) RETURNING id::text`, publicID, currentActor.tenantID, name, selectorJSON, currentActor.userID).Scan(&internalID); queryErr != nil {
return idempotency.Result{}, queryErr
}
tag, queryErr := tx.Exec(operationContext, `INSERT INTO ops_device_group_member(group_id,tenant_id,device_id) SELECT $1::uuid,$2::uuid,d.id FROM device d WHERE d.tenant_id=$2::uuid AND d.public_id=ANY($3::uuid[])`, internalID, currentActor.tenantID, devicePublicIDs)
if queryErr != nil {
return idempotency.Result{}, queryErr
}
if tag.RowsAffected() != int64(len(devicePublicIDs)) {
return idempotency.Result{}, ErrAccessDenied
}
if queryErr = s.audit.Record(operationContext, tx, audit.Event{TenantID: &currentActor.tenantID, ActorType: "user", ActorPublicID: &currentActor.userPublicID, Action: "ops.device_group.create", ResourceType: "ops_device_group", ResourceID: &internalID, RequestID: requestID, Purpose: name, Result: "success", Detail: map[string]any{"device_count": len(devicePublicIDs)}}); queryErr != nil {
return idempotency.Result{}, queryErr
}
item, queryErr := getDeviceGroupTx(operationContext, tx, currentActor.tenantID, publicID)
if queryErr != nil {
return idempotency.Result{}, queryErr
}
body, _ := json.Marshal(item)
return idempotency.Result{StatusCode: 201, Body: body, ResourceID: &internalID}, nil
})
if err != nil {
return DeviceGroup{}, false, err
}
var item DeviceGroup
if err = json.Unmarshal(idemResult.Body, &item); err != nil {
return DeviceGroup{}, false, err
}
return item, !idemResult.Replayed, nil
}
func (s *Service) ListDeviceGroups(ctx context.Context, principal identity.Principal, tenantPublicID string) ([]DeviceGroup, error) {
currentActor, err := s.resolveActor(ctx, principal, tenantPublicID, "ops_viewer", "ops_operator", "ops_approver", "ops_admin")
if err != nil {
return nil, err
}
rows, err := s.pool.Query(ctx, deviceGroupSelect+` WHERE dg.tenant_id=$1::uuid GROUP BY dg.id,t.id,ua.id ORDER BY dg.name LIMIT 500`, currentActor.tenantID)
if err != nil {
return nil, err
}
defer rows.Close()
items := []DeviceGroup{}
for rows.Next() {
item, scanErr := scanDeviceGroup(rows)
if scanErr != nil {
return nil, scanErr
}
items = append(items, item)
}
return items, rows.Err()
}
func (s *Service) ListRollouts(ctx context.Context, principal identity.Principal, tenantPublicID, configKey string) ([]Rollout, error) {
currentActor, err := s.resolveActor(ctx, principal, tenantPublicID, "ops_viewer", "ops_operator", "ops_approver", "ops_admin")
if err != nil {
return nil, err
}
configKey = strings.TrimSpace(configKey)
if configKey != "" && !regexp.MustCompile(`^[a-z][a-z0-9_.-]{2,99}$`).MatchString(configKey) {
return nil, ErrInvalid
}
rows, err := s.pool.Query(ctx, rolloutSelect+`
WHERE rr.tenant_id=$1::uuid AND ($2='' OR rr.config_key=$2)
ORDER BY CASE rr.status WHEN 'ACTIVE' THEN 1 WHEN 'PAUSED' THEN 2 ELSE 3 END,rr.activated_at DESC,rr.id DESC
LIMIT 1000`, currentActor.tenantID, configKey)
if err != nil {
return nil, err
}
defer rows.Close()
items := []Rollout{}
for rows.Next() {
item, scanErr := scanRollout(rows)
if scanErr != nil {
return nil, scanErr
}
items = append(items, item)
}
return items, rows.Err()
}
func (s *Service) PauseRollout(ctx context.Context, principal identity.Principal, tenantPublicID, publicID, requestID, reason string, expectedVersion int64) (Rollout, error) {
return s.transitionRollout(ctx, principal, tenantPublicID, publicID, requestID, reason, expectedVersion, "ACTIVE", "PAUSED")
}
func (s *Service) ResumeRollout(ctx context.Context, principal identity.Principal, tenantPublicID, publicID, requestID, reason string, expectedVersion int64) (Rollout, error) {
return s.transitionRollout(ctx, principal, tenantPublicID, publicID, requestID, reason, expectedVersion, "PAUSED", "ACTIVE")
}
func (s *Service) transitionRollout(ctx context.Context, principal identity.Principal, tenantPublicID, publicID, requestID, reason string, expectedVersion int64, fromStatus, toStatus string) (Rollout, error) {
reason = strings.TrimSpace(reason)
if _, err := uuid.Parse(publicID); err != nil || reason == "" || len([]rune(reason)) > 500 || expectedVersion < 1 {
return Rollout{}, ErrInvalid
}
currentActor, err := s.resolveActor(ctx, principal, tenantPublicID, "ops_approver", "ops_admin")
if err != nil {
return Rollout{}, err
}
tx, err := s.pool.Begin(ctx)
if err != nil {
return Rollout{}, err
}
defer tx.Rollback(ctx)
var internalID, status, configStatus string
var rowVersion int64
err = tx.QueryRow(ctx, `SELECT rr.id::text,rr.status,rr.row_version,rcv.status FROM runtime_config_rollout rr JOIN runtime_config_version rcv ON rcv.id=rr.config_version_id AND rcv.tenant_id=rr.tenant_id WHERE rr.tenant_id=$1::uuid AND rr.public_id=$2::uuid FOR UPDATE OF rr`, currentActor.tenantID, publicID).Scan(&internalID, &status, &rowVersion, &configStatus)
if errors.Is(err, pgx.ErrNoRows) {
return Rollout{}, ErrNotFound
}
if err != nil {
return Rollout{}, err
}
if rowVersion != expectedVersion {
return Rollout{}, ErrVersionConflict
}
if status != fromStatus || (toStatus == "ACTIVE" && configStatus != "PUBLISHED") {
return Rollout{}, ErrStateConflict
}
var commandTag pgconn.CommandTag
if toStatus == "PAUSED" {
commandTag, err = tx.Exec(ctx, `UPDATE runtime_config_rollout SET status='PAUSED',paused_at=now(),row_version=row_version+1,updated_at=now() WHERE id=$1::uuid`, internalID)
} else {
commandTag, err = tx.Exec(ctx, `UPDATE runtime_config_rollout SET status='ACTIVE',paused_at=NULL,row_version=row_version+1,updated_at=now() WHERE id=$1::uuid AND NOT EXISTS(SELECT 1 FROM runtime_config_rollout active WHERE active.tenant_id=runtime_config_rollout.tenant_id AND active.config_key=runtime_config_rollout.config_key AND active.status='ACTIVE' AND active.scope_type=runtime_config_rollout.scope_type AND ((active.scope_type='TENANT') OR (active.scope_type='SUBJECT' AND active.subject_id=runtime_config_rollout.subject_id) OR (active.scope_type='DEVICE_GROUP' AND active.device_group_id=runtime_config_rollout.device_group_id)))`, internalID)
}
if err != nil {
var pgErr *pgconn.PgError
if errors.As(err, &pgErr) && pgErr.Code == "23505" {
return Rollout{}, ErrStateConflict
}
return Rollout{}, err
}
if commandTag.RowsAffected() != 1 {
return Rollout{}, ErrStateConflict
}
item, err := getRolloutTx(ctx, tx, currentActor.tenantID, publicID)
if err != nil {
return Rollout{}, err
}
if item.Status != toStatus {
return Rollout{}, ErrStateConflict
}
action := "ops.config_rollout.pause"
if toStatus == "ACTIVE" {
action = "ops.config_rollout.resume"
}
resourceID := internalID
if err = s.audit.Record(ctx, tx, audit.Event{TenantID: &currentActor.tenantID, ActorType: "user", ActorPublicID: &currentActor.userPublicID, Action: action, ResourceType: "runtime_config_rollout", ResourceID: &resourceID, RequestID: requestID, Purpose: reason, Result: "success", Detail: map[string]any{"config_key": item.ConfigKey, "scope_type": item.ScopeType, "row_version": item.RowVersion}}); err != nil {
return Rollout{}, err
}
if err = tx.Commit(ctx); err != nil {
return Rollout{}, err
}
return item, nil
}
func validateRollout(input RolloutInput) error {
input.ScopeType = strings.ToUpper(strings.TrimSpace(input.ScopeType))
if input.RolloutPercent < 1 || input.RolloutPercent > 100 || input.MaxConcurrentCalls < 1 || input.MaxConcurrentCalls > 100000 {
return ErrInvalid
}
switch input.ScopeType {
case "TENANT":
if strings.TrimSpace(input.ScopeID) != "" {
return ErrInvalid
}
case "SUBJECT":
if input.RolloutPercent != 100 {
return ErrInvalid
}
fallthrough
case "DEVICE_GROUP":
if _, err := uuid.Parse(input.ScopeID); err != nil {
return ErrInvalid
}
default:
return ErrInvalid
}
return nil
}
func (s *Service) checkPublishGatesTx(ctx context.Context, tx pgx.Tx, tenantID, configInternalID string, item Version) error {
if item.ConfigKind == "AGENT_BUNDLE" {
if _, err := validateAgentBundleComponentsTx(ctx, tx, tenantID, item); err != nil {
return ErrPublishGate
}
}
var passed bool
if err := tx.QueryRow(ctx, `SELECT EXISTS(SELECT 1 FROM ops_config_evaluation WHERE tenant_id=$1::uuid AND config_version_id=$2::uuid AND result='PASSED' AND ($3<>'AGENT_BUNDLE' OR evidence_type='AGENT_QUALITY_V1'))`, tenantID, configInternalID, item.ConfigKind).Scan(&passed); err != nil {
return err
}
if !passed {
return ErrPublishGate
}
refs := make(map[string]string, len(item.SecretRefs)+1)
for _, ref := range item.SecretRefs {
refs[ref] = "PROVIDER_CREDENTIAL"
}
if item.ConfigKind == "VOICE" {
if ref, ok := item.Payload["voice_resource_ref"].(string); ok {
refs[ref] = "VOICE_RESOURCE"
}
}
if item.ConfigKind == "MODEL" {
if ref, ok := item.Payload["model_id"].(string); ok {
refs[ref] = "MODEL_RESOURCE"
}
}
for ref, resourceType := range refs {
var active bool
if err := tx.QueryRow(ctx, `SELECT EXISTS(SELECT 1 FROM ops_resource_reference WHERE tenant_id=$1::uuid AND reference_name=$2 AND resource_type=$3 AND status='ACTIVE' AND verified_at>now()-interval '180 days')`, tenantID, ref, resourceType).Scan(&active); err != nil {
return err
}
if !active {
return ErrPublishGate
}
}
return nil
}
func (s *Service) activateRolloutTx(ctx context.Context, tx pgx.Tx, currentActor actor, configInternalID string, item Version, rollout RolloutInput, reason string) (string, *string, error) {
rollout.ScopeType = strings.ToUpper(strings.TrimSpace(rollout.ScopeType))
var subjectID, groupID *string
switch rollout.ScopeType {
case "SUBJECT":
var id string
if err := tx.QueryRow(ctx, `SELECT id::text FROM subject_profile WHERE tenant_id=$1::uuid AND public_id=$2::uuid AND status='active'`, currentActor.tenantID, rollout.ScopeID).Scan(&id); errors.Is(err, pgx.ErrNoRows) {
return "", nil, ErrAccessDenied
} else if err != nil {
return "", nil, err
}
subjectID = &id
case "DEVICE_GROUP":
var id string
if err := tx.QueryRow(ctx, `SELECT id::text FROM ops_device_group WHERE tenant_id=$1::uuid AND public_id=$2::uuid AND status='ACTIVE'`, currentActor.tenantID, rollout.ScopeID).Scan(&id); errors.Is(err, pgx.ErrNoRows) {
return "", nil, ErrAccessDenied
} else if err != nil {
return "", nil, err
}
groupID = &id
}
endSQL := `UPDATE runtime_config_rollout SET status='ENDED',ended_at=now(),row_version=row_version+1,updated_at=now() WHERE tenant_id=$1::uuid AND config_key=$2 AND status IN ('ACTIVE','PAUSED') AND scope_type=$3 AND (($3='TENANT') OR ($3='SUBJECT' AND subject_id=$4::uuid) OR ($3='DEVICE_GROUP' AND device_group_id=$5::uuid)) RETURNING config_version_id::text`
rows, err := tx.Query(ctx, endSQL, currentActor.tenantID, item.ConfigKey, rollout.ScopeType, subjectID, groupID)
if err != nil {
return "", nil, err
}
endedVersions := []string{}
for rows.Next() {
var id string
if err = rows.Scan(&id); err != nil {
rows.Close()
return "", nil, err
}
endedVersions = append(endedVersions, id)
}
rows.Close()
for _, endedID := range endedVersions {
if _, err = tx.Exec(ctx, `UPDATE runtime_config_version SET status='RETIRED',retired_at=now(),row_version=row_version+1,updated_at=now() WHERE id=$1::uuid AND NOT EXISTS(SELECT 1 FROM runtime_config_rollout WHERE config_version_id=$1::uuid AND status='ACTIVE')`, endedID); err != nil {
return "", nil, err
}
}
publicID := uuid.NewString()
cohortSeed := int(time.Now().UnixNano() & 0x7fffffff)
_, err = tx.Exec(ctx, `INSERT INTO runtime_config_rollout(public_id,tenant_id,config_version_id,config_key,scope_type,subject_id,device_group_id,rollout_percent,max_concurrent_calls,cohort_seed,reason,activated_by) VALUES($1::uuid,$2::uuid,$3::uuid,$4,$5,$6::uuid,$7::uuid,$8,$9,$10,$11,$12::uuid)`, publicID, currentActor.tenantID, configInternalID, item.ConfigKey, rollout.ScopeType, subjectID, groupID, rollout.RolloutPercent, rollout.MaxConcurrentCalls, cohortSeed, reason, currentActor.userID)
if err != nil {
return "", nil, err
}
if len(endedVersions) == 0 {
return publicID, nil, nil
}
return publicID, &endedVersions[0], nil
}
const resourceReferenceSelect = `SELECT rr.public_id::text,t.public_id::text,rr.resource_type,rr.reference_name,rr.display_name,rr.status,rr.verified_at,rr.last_rotated_at,rr.expires_at,ua.public_id::text,rr.row_version,rr.created_at,rr.updated_at FROM ops_resource_reference rr JOIN tenant t ON t.id=rr.tenant_id JOIN user_account ua ON ua.id=rr.created_by`
const rolloutSelect = `SELECT rr.public_id::text,rcv.public_id::text,rr.config_key,rr.scope_type,
CASE rr.scope_type WHEN 'SUBJECT' THEN sp.public_id::text WHEN 'DEVICE_GROUP' THEN dg.public_id::text END,
rr.rollout_percent,rr.max_concurrent_calls,rr.status,rr.reason,ua.public_id::text,
rr.activated_at,rr.paused_at,rr.ended_at,rr.row_version
FROM runtime_config_rollout rr
JOIN runtime_config_version rcv ON rcv.id=rr.config_version_id AND rcv.tenant_id=rr.tenant_id
JOIN user_account ua ON ua.id=rr.activated_by AND ua.tenant_id=rr.tenant_id
LEFT JOIN subject_profile sp ON sp.id=rr.subject_id AND sp.tenant_id=rr.tenant_id
LEFT JOIN ops_device_group dg ON dg.id=rr.device_group_id AND dg.tenant_id=rr.tenant_id`
func scanRollout(row rowScanner) (Rollout, error) {
var item Rollout
err := row.Scan(&item.ID, &item.ConfigVersionID, &item.ConfigKey, &item.ScopeType, &item.ScopeID, &item.RolloutPercent, &item.MaxConcurrentCalls, &item.Status, &item.Reason, &item.ActivatedBy, &item.ActivatedAt, &item.PausedAt, &item.EndedAt, &item.RowVersion)
return item, err
}
func getRolloutTx(ctx context.Context, tx pgx.Tx, tenantID, publicID string) (Rollout, error) {
return scanRollout(tx.QueryRow(ctx, rolloutSelect+` WHERE rr.tenant_id=$1::uuid AND rr.public_id=$2::uuid`, tenantID, publicID))
}
func scanResourceReference(row rowScanner) (ResourceReference, error) {
var item ResourceReference
err := row.Scan(&item.ID, &item.TenantID, &item.ResourceType, &item.ReferenceName, &item.DisplayName, &item.Status, &item.VerifiedAt, &item.LastRotatedAt, &item.ExpiresAt, &item.CreatedBy, &item.RowVersion, &item.CreatedAt, &item.UpdatedAt)
return item, err
}
func getResourceReferenceTx(ctx context.Context, tx pgx.Tx, tenantID, publicID string) (ResourceReference, error) {
item, err := scanResourceReference(tx.QueryRow(ctx, resourceReferenceSelect+` WHERE rr.tenant_id=$1::uuid AND rr.public_id=$2::uuid`, tenantID, publicID))
return item, err
}
func getEvaluationTx(ctx context.Context, tx pgx.Tx, tenantID, publicID string) (Evaluation, error) {
var item Evaluation
var metrics []byte
err := tx.QueryRow(ctx, `SELECT e.public_id::text,rcv.public_id::text,e.evidence_type,e.suite_version,e.result,e.metrics,e.report_ref,ua.public_id::text,e.evaluated_at FROM ops_config_evaluation e JOIN runtime_config_version rcv ON rcv.id=e.config_version_id JOIN user_account ua ON ua.id=e.evaluated_by WHERE e.tenant_id=$1::uuid AND e.public_id=$2::uuid`, tenantID, publicID).Scan(&item.ID, &item.ConfigVersionID, &item.EvidenceType, &item.SuiteVersion, &item.Result, &metrics, &item.ReportRef, &item.EvaluatedBy, &item.EvaluatedAt)
if err == nil {
err = json.Unmarshal(metrics, &item.Metrics)
}
return item, err
}
const deviceGroupSelect = `SELECT dg.public_id::text,t.public_id::text,dg.name,dg.selector,dg.status,COALESCE(array_agg(d.public_id::text ORDER BY d.public_id) FILTER(WHERE d.id IS NOT NULL),'{}'),ua.public_id::text,dg.row_version,dg.created_at,dg.updated_at FROM ops_device_group dg JOIN tenant t ON t.id=dg.tenant_id JOIN user_account ua ON ua.id=dg.created_by LEFT JOIN ops_device_group_member dgm ON dgm.group_id=dg.id LEFT JOIN device d ON d.id=dgm.device_id`
func scanDeviceGroup(row rowScanner) (DeviceGroup, error) {
var item DeviceGroup
var selector []byte
err := row.Scan(&item.ID, &item.TenantID, &item.Name, &selector, &item.Status, &item.DeviceIDs, &item.CreatedBy, &item.RowVersion, &item.CreatedAt, &item.UpdatedAt)
if err == nil {
err = json.Unmarshal(selector, &item.Selector)
}
return item, err
}
func getDeviceGroupTx(ctx context.Context, tx pgx.Tx, tenantID, publicID string) (DeviceGroup, error) {
return scanDeviceGroup(tx.QueryRow(ctx, deviceGroupSelect+` WHERE dg.tenant_id=$1::uuid AND dg.public_id=$2::uuid GROUP BY dg.id,t.id,ua.id`, tenantID, publicID))
}
func (r RolloutInput) String() string {
return fmt.Sprintf("%s:%s:%d%%/%d", r.ScopeType, r.ScopeID, r.RolloutPercent, r.MaxConcurrentCalls)
}
@@ -0,0 +1,123 @@
package opsconfig
import (
"context"
"errors"
"os"
"testing"
"time"
"data-terminal-cloud/internal/audit"
"data-terminal-cloud/internal/identity"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgxpool"
)
func TestResourceReferencesAndDeviceGroupsLifecycle(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-000000000101"}
outsider := identity.Principal{Issuer: admin.Issuer, Subject: "00000000-0000-0000-0000-000000000103"}
tenantID := "10000000-0000-0000-0000-000000000010"
deviceID := "10000000-0000-0000-0000-000000000511"
resourceKey := "ops-gates-core-resource-" + uuid.NewString()
groupKey := "ops-gates-core-group-" + uuid.NewString()
deniedKey := "ops-gates-core-denied-" + uuid.NewString()
referenceName := "vault://certificates/core-" + uuid.NewString()
groupName := "核心灰度组-" + uuid.NewString()
requestPrefix := "ops-gates-core-"
defer func() {
_, _ = pool.Exec(ctx, `DELETE FROM audit_event WHERE request_id LIKE $1`, requestPrefix+"%")
_, _ = pool.Exec(ctx, `DELETE FROM idempotency_record WHERE idempotency_key=ANY($1::text[])`, []string{resourceKey, groupKey, deniedKey})
_, _ = pool.Exec(ctx, `DELETE FROM ops_device_group_member WHERE group_id IN (SELECT id FROM ops_device_group WHERE name=$1)`, groupName)
_, _ = pool.Exec(ctx, `DELETE FROM ops_device_group WHERE name=$1`, groupName)
_, _ = pool.Exec(ctx, `DELETE FROM ops_resource_reference WHERE reference_name=$1`, referenceName)
}()
verifiedAt := time.Now().UTC().Add(-time.Hour)
expiresAt := time.Now().UTC().Add(30 * 24 * time.Hour)
if _, _, err := service.CreateResourceReference(ctx, admin, tenantID, "bad", requestPrefix+"invalid-certificate", "CERTIFICATE", referenceName, "缺少到期时间", verifiedAt, nil); !errors.Is(err, ErrInvalid) {
t.Fatalf("invalid certificate error=%v", err)
}
resource, created, err := service.CreateResourceReference(ctx, admin, tenantID, resourceKey, requestPrefix+"resource-create", " certificate ", referenceName, "核心测试证书", verifiedAt, &expiresAt)
if err != nil || !created || resource.Status != "ACTIVE" || resource.RowVersion != 1 || resource.ResourceType != "CERTIFICATE" {
t.Fatalf("resource=%+v created=%t err=%v", resource, created, err)
}
replayed, createdAgain, err := service.CreateResourceReference(ctx, admin, tenantID, resourceKey, requestPrefix+"resource-replay", "CERTIFICATE", referenceName, "核心测试证书", verifiedAt, &expiresAt)
if err != nil || createdAgain || replayed.ID != resource.ID {
t.Fatalf("resource replay=%+v created=%t err=%v", replayed, createdAgain, err)
}
resources, err := service.ListResourceReferences(ctx, admin, tenantID)
if err != nil {
t.Fatal(err)
}
found := false
for _, item := range resources {
if item.ID == resource.ID {
found = true
}
}
if !found {
t.Fatalf("resource %s missing from list", resource.ID)
}
if _, err := service.ListResourceReferences(ctx, outsider, tenantID); !errors.Is(err, ErrAccessDenied) {
t.Fatalf("outsider list resources error=%v", err)
}
if _, err := service.RevokeResourceReference(ctx, admin, tenantID, resource.ID, requestPrefix+"empty-reason", "", 1); !errors.Is(err, ErrInvalid) {
t.Fatalf("empty revoke reason error=%v", err)
}
if _, err := service.RevokeResourceReference(ctx, admin, tenantID, resource.ID, requestPrefix+"wrong-version", "错误版本", 99); !errors.Is(err, ErrVersionConflict) {
t.Fatalf("wrong resource version error=%v", err)
}
revoked, err := service.RevokeResourceReference(ctx, admin, tenantID, resource.ID, requestPrefix+"resource-revoke", "证书轮换完成", 1)
if err != nil || revoked.Status != "REVOKED" || revoked.RowVersion != 2 {
t.Fatalf("revoked=%+v err=%v", revoked, err)
}
if _, err := service.RevokeResourceReference(ctx, admin, tenantID, resource.ID, requestPrefix+"resource-repeat", "重复撤销", 2); !errors.Is(err, ErrStateConflict) {
t.Fatalf("repeat revoke error=%v", err)
}
if _, _, err := service.CreateDeviceGroup(ctx, admin, tenantID, "bad", requestPrefix+"invalid-group", "", nil, nil); !errors.Is(err, ErrInvalid) {
t.Fatalf("invalid group error=%v", err)
}
group, created, err := service.CreateDeviceGroup(ctx, admin, tenantID, groupKey, requestPrefix+"group-create", groupName, map[string]any{"purpose": "core-coverage"}, []string{deviceID, deviceID})
if err != nil || !created || group.Status != "ACTIVE" || group.RowVersion != 1 || len(group.DeviceIDs) != 1 || group.DeviceIDs[0] != deviceID {
t.Fatalf("group=%+v created=%t err=%v", group, created, err)
}
replayedGroup, createdAgain, err := service.CreateDeviceGroup(ctx, admin, tenantID, groupKey, requestPrefix+"group-replay", groupName, map[string]any{"purpose": "core-coverage"}, []string{deviceID, deviceID})
if err != nil || createdAgain || replayedGroup.ID != group.ID {
t.Fatalf("group replay=%+v created=%t err=%v", replayedGroup, createdAgain, err)
}
groups, err := service.ListDeviceGroups(ctx, admin, tenantID)
if err != nil {
t.Fatal(err)
}
found = false
for _, item := range groups {
if item.ID == group.ID && len(item.DeviceIDs) == 1 && item.DeviceIDs[0] == deviceID {
found = true
}
}
if !found {
t.Fatalf("device group %s missing from list", group.ID)
}
if _, _, err := service.CreateDeviceGroup(ctx, admin, tenantID, deniedKey, requestPrefix+"group-denied", groupName+"-denied", map[string]any{}, []string{uuid.NewString()}); !errors.Is(err, ErrAccessDenied) {
t.Fatalf("unknown device group error=%v", err)
}
if _, err := service.ListRollouts(ctx, admin, tenantID, "INVALID KEY"); !errors.Is(err, ErrInvalid) {
t.Fatalf("invalid rollout filter error=%v", err)
}
}
+549
View File
@@ -0,0 +1,549 @@
package opsconfig
import (
"context"
"encoding/json"
"errors"
"fmt"
"regexp"
"strings"
"time"
"data-terminal-cloud/internal/audit"
"data-terminal-cloud/internal/idempotency"
"data-terminal-cloud/internal/identity"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
var (
ErrInvalid = errors.New("invalid runtime configuration")
ErrAccessDenied = errors.New("operations access denied")
ErrNotFound = errors.New("runtime configuration not found")
ErrStateConflict = errors.New("runtime configuration state conflict")
ErrVersionConflict = errors.New("runtime configuration version conflict")
ErrSelfApproval = errors.New("configuration creator cannot approve own change")
)
var referencePattern = regexp.MustCompile(`^[A-Za-z][A-Za-z0-9_.:/-]{2,199}$`)
type Service struct {
pool *pgxpool.Pool
audit *audit.Repository
idem *idempotency.Executor
}
type CreateInput struct {
TenantID string `json:"tenant_id"`
ConfigKey string `json:"config_key"`
DisplayName string `json:"display_name"`
ConfigKind string `json:"config_kind"`
Payload map[string]any `json:"payload"`
SecretRefs map[string]string `json:"secret_refs"`
ChangeSummary string `json:"change_summary"`
}
type Version struct {
ID string `json:"id"`
TenantID string `json:"tenant_id"`
ConfigKey string `json:"config_key"`
DisplayName string `json:"display_name"`
ConfigKind string `json:"config_kind"`
VersionNo int `json:"version_no"`
Status string `json:"status"`
Payload map[string]any `json:"payload"`
SecretRefs map[string]string `json:"secret_refs"`
ChangeSummary string `json:"change_summary"`
ValidationResult map[string]any `json:"validation_result"`
CreatedBy string `json:"created_by"`
SubmittedBy *string `json:"submitted_by,omitempty"`
SubmittedAt *time.Time `json:"submitted_at,omitempty"`
ApprovedBy *string `json:"approved_by,omitempty"`
ApprovedAt *time.Time `json:"approved_at,omitempty"`
PublishedBy *string `json:"published_by,omitempty"`
PublishedAt *time.Time `json:"published_at,omitempty"`
RetiredAt *time.Time `json:"retired_at,omitempty"`
SupersedesID *string `json:"supersedes_id,omitempty"`
RollbackOfID *string `json:"rollback_of_id,omitempty"`
RowVersion int64 `json:"row_version"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type actor struct {
tenantID string
tenantPublicID string
userID string
userPublicID string
role string
}
func NewService(pool *pgxpool.Pool, auditRepository *audit.Repository) (*Service, error) {
if pool == nil || auditRepository == nil {
return nil, errors.New("operations configuration database and audit repository are required")
}
return &Service{pool: pool, audit: auditRepository, idem: idempotency.NewExecutor(pool)}, nil
}
func (s *Service) Create(ctx context.Context, principal identity.Principal, key, requestID string, input CreateInput) (Version, bool, error) {
input.ConfigKey = strings.TrimSpace(strings.ToLower(input.ConfigKey))
input.DisplayName = strings.TrimSpace(input.DisplayName)
input.ConfigKind = strings.TrimSpace(strings.ToUpper(input.ConfigKind))
input.ChangeSummary = strings.TrimSpace(input.ChangeSummary)
if err := validateInput(input); err != nil {
return Version{}, false, err
}
currentActor, err := s.resolveActor(ctx, principal, input.TenantID, "ops_operator", "ops_approver", "ops_admin")
if err != nil {
return Version{}, false, err
}
requestBody, _ := json.Marshal(input)
result, err := s.idem.Execute(ctx, idempotency.Request{
TenantID: &currentActor.tenantID, ActorKey: "ops:" + currentActor.userPublicID,
Scope: "POST /api/v1/ops/config-versions", Key: key,
RequestHash: idempotency.Hash(requestBody), ExpiresAfter: 24 * time.Hour,
}, func(operationContext context.Context, tx pgx.Tx) (idempotency.Result, error) {
if _, lockErr := tx.Exec(operationContext, `SELECT pg_advisory_xact_lock(hashtextextended($1,0))`, currentActor.tenantID+":"+input.ConfigKey); lockErr != nil {
return idempotency.Result{}, lockErr
}
var versionNo int
if queryErr := tx.QueryRow(operationContext, `SELECT COALESCE(MAX(version_no),0)+1 FROM runtime_config_version WHERE tenant_id=$1::uuid AND config_key=$2`, currentActor.tenantID, input.ConfigKey).Scan(&versionNo); queryErr != nil {
return idempotency.Result{}, queryErr
}
payloadJSON, _ := json.Marshal(input.Payload)
refsJSON, _ := json.Marshal(input.SecretRefs)
publicID := uuid.NewString()
var internalID string
if queryErr := tx.QueryRow(operationContext, `
INSERT INTO runtime_config_version(public_id,tenant_id,config_key,display_name,config_kind,version_no,payload,secret_refs,change_summary,created_by)
VALUES($1::uuid,$2::uuid,$3,$4,$5,$6,$7::jsonb,$8::jsonb,$9,$10::uuid) RETURNING id::text`,
publicID, currentActor.tenantID, input.ConfigKey, input.DisplayName, input.ConfigKind, versionNo, payloadJSON, refsJSON, input.ChangeSummary, currentActor.userID).Scan(&internalID); queryErr != nil {
return idempotency.Result{}, queryErr
}
resourceID := internalID
if auditErr := s.audit.Record(operationContext, tx, audit.Event{TenantID: &currentActor.tenantID, ActorType: "user", ActorPublicID: &currentActor.userPublicID, Action: "ops.config.create", ResourceType: "runtime_config_version", ResourceID: &resourceID, RequestID: requestID, Purpose: input.ChangeSummary, Result: "success", Detail: map[string]any{"config_key": input.ConfigKey, "version_no": versionNo, "config_kind": input.ConfigKind}}); auditErr != nil {
return idempotency.Result{}, auditErr
}
item, queryErr := s.getTx(operationContext, tx, currentActor.tenantID, publicID)
if queryErr != nil {
return idempotency.Result{}, queryErr
}
body, _ := json.Marshal(item)
return idempotency.Result{StatusCode: 201, Body: body, ResourceID: &internalID}, nil
})
if err != nil {
return Version{}, false, err
}
var item Version
if err := json.Unmarshal(result.Body, &item); err != nil {
return Version{}, false, err
}
return item, !result.Replayed, nil
}
func (s *Service) List(ctx context.Context, principal identity.Principal, tenantPublicID, configKey string) ([]Version, error) {
currentActor, err := s.resolveActor(ctx, principal, tenantPublicID, "ops_viewer", "ops_operator", "ops_approver", "ops_admin")
if err != nil {
return nil, err
}
configKey = strings.TrimSpace(strings.ToLower(configKey))
rows, err := s.pool.Query(ctx, versionSelect+` WHERE rcv.tenant_id=$1::uuid AND (NULLIF($2,'') IS NULL OR rcv.config_key=$2) ORDER BY rcv.config_key,rcv.version_no DESC LIMIT 500`, currentActor.tenantID, configKey)
if err != nil {
return nil, err
}
defer rows.Close()
items := []Version{}
for rows.Next() {
item, scanErr := scanVersion(rows)
if scanErr != nil {
return nil, scanErr
}
items = append(items, item)
}
return items, rows.Err()
}
func (s *Service) Submit(ctx context.Context, principal identity.Principal, tenantPublicID, publicID, requestID string, expectedVersion int64) (Version, error) {
currentActor, err := s.resolveActor(ctx, principal, tenantPublicID, "ops_operator", "ops_approver", "ops_admin")
if err != nil {
return Version{}, err
}
tx, err := s.pool.BeginTx(ctx, pgx.TxOptions{})
if err != nil {
return Version{}, err
}
defer func() { _ = tx.Rollback(context.Background()) }()
item, internalID, err := s.lockVersion(ctx, tx, currentActor.tenantID, publicID)
if err != nil {
return Version{}, err
}
if item.RowVersion != expectedVersion {
return Version{}, ErrVersionConflict
}
if item.Status != "DRAFT" {
return Version{}, ErrStateConflict
}
validation, err := validateVersionTx(ctx, tx, currentActor.tenantID, item)
if err != nil {
return Version{}, err
}
validationJSON, _ := json.Marshal(validation)
_, err = tx.Exec(ctx, `UPDATE runtime_config_version SET status='PENDING_APPROVAL',validation_result=$2::jsonb,submitted_by=$3::uuid,submitted_at=now(),row_version=row_version+1,updated_at=now() WHERE id=$1::uuid`, internalID, validationJSON, currentActor.userID)
if err != nil {
return Version{}, err
}
if err = s.record(ctx, tx, currentActor, "ops.config.submit", internalID, requestID, item.ChangeSummary, map[string]any{"config_key": item.ConfigKey, "version_no": item.VersionNo, "checks": validation}); err != nil {
return Version{}, err
}
updated, err := s.getTx(ctx, tx, currentActor.tenantID, publicID)
if err == nil {
err = tx.Commit(ctx)
}
return updated, err
}
func (s *Service) Publish(ctx context.Context, principal identity.Principal, tenantPublicID, publicID, requestID string, input PublishInput, expectedVersion int64) (Version, error) {
input.Reason = strings.TrimSpace(input.Reason)
if input.Reason == "" || len([]rune(input.Reason)) > 500 || validateRollout(input.Rollout) != nil {
return Version{}, ErrInvalid
}
currentActor, err := s.resolveActor(ctx, principal, tenantPublicID, "ops_approver", "ops_admin")
if err != nil {
return Version{}, err
}
tx, err := s.pool.BeginTx(ctx, pgx.TxOptions{})
if err != nil {
return Version{}, err
}
defer func() { _ = tx.Rollback(context.Background()) }()
item, internalID, err := s.lockVersion(ctx, tx, currentActor.tenantID, publicID)
if err != nil {
return Version{}, err
}
if item.RowVersion != expectedVersion {
return Version{}, ErrVersionConflict
}
if item.Status != "PENDING_APPROVAL" {
return Version{}, ErrStateConflict
}
var creatorID string
if err = tx.QueryRow(ctx, `SELECT created_by::text FROM runtime_config_version WHERE id=$1::uuid`, internalID).Scan(&creatorID); err != nil {
return Version{}, err
}
if creatorID == currentActor.userID {
return Version{}, ErrSelfApproval
}
if err = s.checkPublishGatesTx(ctx, tx, currentActor.tenantID, internalID, item); err != nil {
return Version{}, err
}
if _, err = tx.Exec(ctx, `SELECT pg_advisory_xact_lock(hashtextextended($1,0))`, currentActor.tenantID+":"+item.ConfigKey); err != nil {
return Version{}, err
}
_, err = tx.Exec(ctx, `UPDATE runtime_config_version SET status='PUBLISHED',approved_by=$2::uuid,approved_at=now(),published_by=$2::uuid,published_at=now(),retired_at=NULL,row_version=row_version+1,updated_at=now() WHERE id=$1::uuid`, internalID, currentActor.userID)
if err != nil {
return Version{}, err
}
rolloutID, supersededID, err := s.activateRolloutTx(ctx, tx, currentActor, internalID, item, input.Rollout, input.Reason)
if err != nil {
return Version{}, err
}
if supersededID != nil {
if _, err = tx.Exec(ctx, `UPDATE runtime_config_version SET supersedes_id=$2::uuid WHERE id=$1::uuid`, internalID, *supersededID); err != nil {
return Version{}, err
}
}
if err = s.record(ctx, tx, currentActor, "ops.config.publish", internalID, requestID, input.Reason, map[string]any{"config_key": item.ConfigKey, "version_no": item.VersionNo, "rollout_id": rolloutID, "rollout": input.Rollout.String()}); err != nil {
return Version{}, err
}
updated, err := s.getTx(ctx, tx, currentActor.tenantID, publicID)
if err == nil {
err = tx.Commit(ctx)
}
return updated, err
}
func (s *Service) Rollback(ctx context.Context, principal identity.Principal, tenantPublicID, targetPublicID, key, requestID string, input PublishInput) (Version, bool, error) {
input.Reason = strings.TrimSpace(input.Reason)
if input.Reason == "" || len([]rune(input.Reason)) > 500 || validateRollout(input.Rollout) != nil {
return Version{}, false, ErrInvalid
}
currentActor, err := s.resolveActor(ctx, principal, tenantPublicID, "ops_approver", "ops_admin")
if err != nil {
return Version{}, false, err
}
requestBody, _ := json.Marshal(map[string]any{"target_id": targetPublicID, "reason": input.Reason, "rollout": input.Rollout})
result, err := s.idem.Execute(ctx, idempotency.Request{
TenantID: &currentActor.tenantID, ActorKey: "ops:" + currentActor.userPublicID,
Scope: "POST /api/v1/ops/config-versions/rollback", Key: key,
RequestHash: idempotency.Hash(requestBody), ExpiresAfter: 24 * time.Hour,
}, func(operationContext context.Context, tx pgx.Tx) (idempotency.Result, error) {
target, targetInternalID, rollbackErr := s.lockVersion(operationContext, tx, currentActor.tenantID, targetPublicID)
if rollbackErr != nil {
return idempotency.Result{}, rollbackErr
}
if target.Status != "RETIRED" && target.Status != "PUBLISHED" {
return idempotency.Result{}, ErrStateConflict
}
if rollbackErr = s.checkPublishGatesTx(operationContext, tx, currentActor.tenantID, targetInternalID, target); rollbackErr != nil {
return idempotency.Result{}, rollbackErr
}
if _, rollbackErr = tx.Exec(operationContext, `SELECT pg_advisory_xact_lock(hashtextextended($1,0))`, currentActor.tenantID+":"+target.ConfigKey); rollbackErr != nil {
return idempotency.Result{}, rollbackErr
}
var nextVersion int
if rollbackErr = tx.QueryRow(operationContext, `SELECT MAX(version_no)+1 FROM runtime_config_version WHERE tenant_id=$1::uuid AND config_key=$2`, currentActor.tenantID, target.ConfigKey).Scan(&nextVersion); rollbackErr != nil {
return idempotency.Result{}, rollbackErr
}
newPublicID := uuid.NewString()
var newInternalID string
if rollbackErr = tx.QueryRow(operationContext, `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,rollback_of_id) VALUES($1::uuid,$2::uuid,$3,$4,$5,$6,'PUBLISHED',$7::jsonb,$8::jsonb,$9,$10::jsonb,$11::uuid,$11::uuid,now(),$11::uuid,now(),$11::uuid,now(),$12::uuid) RETURNING id::text`, newPublicID, currentActor.tenantID, target.ConfigKey, target.DisplayName, target.ConfigKind, nextVersion, mustJSON(target.Payload), mustJSON(target.SecretRefs), input.Reason, mustJSON(target.ValidationResult), currentActor.userID, targetInternalID).Scan(&newInternalID); rollbackErr != nil {
return idempotency.Result{}, rollbackErr
}
rolloutID, supersededID, rollbackErr := s.activateRolloutTx(operationContext, tx, currentActor, newInternalID, target, input.Rollout, input.Reason)
if rollbackErr != nil {
return idempotency.Result{}, rollbackErr
}
if supersededID != nil {
if _, rollbackErr = tx.Exec(operationContext, `UPDATE runtime_config_version SET supersedes_id=$2::uuid WHERE id=$1::uuid`, newInternalID, *supersededID); rollbackErr != nil {
return idempotency.Result{}, rollbackErr
}
}
if rollbackErr = s.record(operationContext, tx, currentActor, "ops.config.rollback", newInternalID, requestID, input.Reason, map[string]any{"config_key": target.ConfigKey, "new_version_no": nextVersion, "rollback_of": targetPublicID, "rollout_id": rolloutID, "rollout": input.Rollout.String()}); rollbackErr != nil {
return idempotency.Result{}, rollbackErr
}
updated, rollbackErr := s.getTx(operationContext, tx, currentActor.tenantID, newPublicID)
if rollbackErr != nil {
return idempotency.Result{}, rollbackErr
}
body, _ := json.Marshal(updated)
return idempotency.Result{StatusCode: 201, Body: body, ResourceID: &newInternalID}, nil
})
if err != nil {
return Version{}, false, err
}
var item Version
if err := json.Unmarshal(result.Body, &item); err != nil {
return Version{}, false, err
}
return item, !result.Replayed, nil
}
func (s *Service) resolveActor(ctx context.Context, principal identity.Principal, tenantPublicID string, roles ...string) (actor, error) {
if _, err := uuid.Parse(tenantPublicID); err != nil {
return actor{}, ErrInvalid
}
var result actor
err := s.pool.QueryRow(ctx, `SELECT t.id::text,t.public_id::text,ua.id::text,ua.public_id::text,ua.platform_role FROM user_account ua JOIN tenant t ON t.id=ua.tenant_id AND t.status='active' WHERE t.public_id=$1::uuid AND ua.oidc_issuer=$2 AND ua.oidc_subject=$3 AND ua.status='active'`, tenantPublicID, principal.Issuer, principal.Subject).Scan(&result.tenantID, &result.tenantPublicID, &result.userID, &result.userPublicID, &result.role)
if errors.Is(err, pgx.ErrNoRows) {
return actor{}, ErrAccessDenied
}
if err != nil {
return actor{}, err
}
for _, role := range roles {
if result.role == role {
return result, nil
}
}
return actor{}, ErrAccessDenied
}
func validateInput(input CreateInput) error {
if !regexp.MustCompile(`^[a-z][a-z0-9_.-]{2,99}$`).MatchString(input.ConfigKey) || input.DisplayName == "" || len([]rune(input.DisplayName)) > 200 || input.ChangeSummary == "" || len([]rune(input.ChangeSummary)) > 500 || input.Payload == nil {
return ErrInvalid
}
switch input.ConfigKind {
case "PERSONA", "VOICE", "MODEL", "ROUTING", "AGENT_BUNDLE":
default:
return ErrInvalid
}
if _, err := validatePayload(input.ConfigKind, input.Payload, input.SecretRefs); err != nil {
return err
}
return nil
}
func validateVersion(item Version) (map[string]any, error) {
return validatePayload(item.ConfigKind, item.Payload, item.SecretRefs)
}
func validateVersionTx(ctx context.Context, tx pgx.Tx, tenantID string, item Version) (map[string]any, error) {
validation, err := validateVersion(item)
if err != nil {
return nil, err
}
if item.ConfigKind != "AGENT_BUNDLE" {
return validation, nil
}
components, err := validateAgentBundleComponentsTx(ctx, tx, tenantID, item)
if err != nil {
return nil, ErrInvalid
}
checks, _ := validation["checks"].([]string)
validation["checks"] = append(checks, "component_versions")
validation["component_versions"] = components
return validation, nil
}
func validateAgentBundleComponentsTx(ctx context.Context, tx pgx.Tx, tenantID string, item Version) ([]map[string]string, error) {
expectedKinds := []struct {
field string
kind string
}{
{field: "persona_version_id", kind: "PERSONA"},
{field: "voice_version_id", kind: "VOICE"},
{field: "model_version_id", kind: "MODEL"},
{field: "routing_version_id", kind: "ROUTING"},
}
components := make([]map[string]string, 0, len(expectedKinds))
for _, expected := range expectedKinds {
publicID, ok := item.Payload[expected.field].(string)
if !ok {
return nil, ErrInvalid
}
publicID = strings.TrimSpace(publicID)
if _, parseErr := uuid.Parse(publicID); parseErr != nil {
return nil, ErrInvalid
}
var kind, status string
err := tx.QueryRow(ctx, `SELECT config_kind,status FROM runtime_config_version WHERE tenant_id=$1::uuid AND public_id=$2::uuid`, tenantID, publicID).Scan(&kind, &status)
if errors.Is(err, pgx.ErrNoRows) {
return nil, ErrInvalid
}
if err != nil {
return nil, err
}
if kind != expected.kind || (status != "PUBLISHED" && status != "RETIRED") {
return nil, ErrInvalid
}
components = append(components, map[string]string{"field": expected.field, "version_id": publicID, "config_kind": kind, "status": status})
}
return components, nil
}
func validatePayload(kind string, payload map[string]any, refs map[string]string) (map[string]any, error) {
encoded, err := json.Marshal(payload)
if err != nil || len(encoded) == 0 || len(encoded) > 65536 || containsSensitiveKey(payload) {
return nil, ErrInvalid
}
for key, value := range refs {
if !referencePattern.MatchString(key) || !referencePattern.MatchString(strings.TrimSpace(value)) {
return nil, ErrInvalid
}
}
requireString := func(key string) bool { value, ok := payload[key].(string); return ok && strings.TrimSpace(value) != "" }
switch kind {
case "PERSONA":
if !requireString("system_prompt") || !requireString("tone") {
return nil, ErrInvalid
}
case "VOICE":
if !requireString("provider") || !requireString("voice_resource_ref") || refs["credential_ref"] == "" {
return nil, ErrInvalid
}
case "MODEL":
if !requireString("provider") || !requireString("model_id") || refs["credential_ref"] == "" {
return nil, ErrInvalid
}
case "ROUTING":
mode, _ := payload["default_engine_mode"].(string)
if mode != "s2s_pure" && mode != "s2s_hybrid" && mode != "cascade" {
return nil, ErrInvalid
}
if _, exists := payload["routes"]; !exists {
return nil, ErrInvalid
}
case "AGENT_BUNDLE":
for _, key := range []string{"persona_version_id", "voice_version_id", "model_version_id", "routing_version_id"} {
value, _ := payload[key].(string)
if !requireString(key) {
return nil, ErrInvalid
}
if _, parseErr := uuid.Parse(strings.TrimSpace(value)); parseErr != nil {
return nil, ErrInvalid
}
}
}
return map[string]any{"valid": true, "schema": "runtime-config-v1", "checked_at": time.Now().UTC().Format(time.RFC3339), "checks": []string{"structure", "required_fields", "secret_references", "sensitive_key_rejection"}}, nil
}
func containsSensitiveKey(value any) bool {
switch typed := value.(type) {
case map[string]any:
for key, child := range typed {
lower := strings.ToLower(key)
for _, forbidden := range []string{"secret", "password", "token", "private_key", "api_key", "credential"} {
if strings.Contains(lower, forbidden) {
return true
}
}
if containsSensitiveKey(child) {
return true
}
}
case []any:
for _, child := range typed {
if containsSensitiveKey(child) {
return true
}
}
}
return false
}
const versionSelect = `SELECT rcv.public_id::text,t.public_id::text,rcv.config_key,rcv.display_name,rcv.config_kind,rcv.version_no,rcv.status,rcv.payload,rcv.secret_refs,rcv.change_summary,rcv.validation_result,creator.public_id::text,submitter.public_id::text,rcv.submitted_at,approver.public_id::text,rcv.approved_at,publisher.public_id::text,rcv.published_at,rcv.retired_at,supersedes.public_id::text,rollback.public_id::text,rcv.row_version,rcv.created_at,rcv.updated_at FROM runtime_config_version rcv JOIN tenant t ON t.id=rcv.tenant_id JOIN user_account creator ON creator.id=rcv.created_by LEFT JOIN user_account submitter ON submitter.id=rcv.submitted_by LEFT JOIN user_account approver ON approver.id=rcv.approved_by LEFT JOIN user_account publisher ON publisher.id=rcv.published_by LEFT JOIN runtime_config_version supersedes ON supersedes.id=rcv.supersedes_id LEFT JOIN runtime_config_version rollback ON rollback.id=rcv.rollback_of_id`
type rowScanner interface{ Scan(...any) error }
func scanVersion(row rowScanner) (Version, error) {
var item Version
var payloadJSON, refsJSON, validationJSON []byte
err := row.Scan(&item.ID, &item.TenantID, &item.ConfigKey, &item.DisplayName, &item.ConfigKind, &item.VersionNo, &item.Status, &payloadJSON, &refsJSON, &item.ChangeSummary, &validationJSON, &item.CreatedBy, &item.SubmittedBy, &item.SubmittedAt, &item.ApprovedBy, &item.ApprovedAt, &item.PublishedBy, &item.PublishedAt, &item.RetiredAt, &item.SupersedesID, &item.RollbackOfID, &item.RowVersion, &item.CreatedAt, &item.UpdatedAt)
if err != nil {
return Version{}, err
}
if err = json.Unmarshal(payloadJSON, &item.Payload); err != nil {
return Version{}, err
}
if err = json.Unmarshal(refsJSON, &item.SecretRefs); err != nil {
return Version{}, err
}
if err = json.Unmarshal(validationJSON, &item.ValidationResult); err != nil {
return Version{}, err
}
return item, nil
}
func (s *Service) getTx(ctx context.Context, tx pgx.Tx, tenantID, publicID string) (Version, error) {
item, err := scanVersion(tx.QueryRow(ctx, versionSelect+` WHERE rcv.tenant_id=$1::uuid AND rcv.public_id=$2::uuid`, tenantID, publicID))
if errors.Is(err, pgx.ErrNoRows) {
return Version{}, ErrNotFound
}
return item, err
}
func (s *Service) lockVersion(ctx context.Context, tx pgx.Tx, tenantID, publicID string) (Version, string, error) {
var internalID string
err := tx.QueryRow(ctx, `SELECT id::text FROM runtime_config_version WHERE tenant_id=$1::uuid AND public_id=$2::uuid FOR UPDATE`, tenantID, publicID).Scan(&internalID)
if errors.Is(err, pgx.ErrNoRows) {
return Version{}, "", ErrNotFound
}
if err != nil {
return Version{}, "", err
}
item, err := s.getTx(ctx, tx, tenantID, publicID)
return item, internalID, err
}
func (s *Service) record(ctx context.Context, tx pgx.Tx, currentActor actor, action, resourceID, requestID, purpose string, detail map[string]any) error {
return s.audit.Record(ctx, tx, audit.Event{TenantID: &currentActor.tenantID, ActorType: "user", ActorPublicID: &currentActor.userPublicID, Action: action, ResourceType: "runtime_config_version", ResourceID: &resourceID, RequestID: requestID, Purpose: purpose, Result: "success", Detail: detail})
}
func mustJSON(value any) []byte {
encoded, _ := json.Marshal(value)
return encoded
}
func (v Version) String() string { return fmt.Sprintf("%s@%d", v.ConfigKey, v.VersionNo) }
@@ -0,0 +1,145 @@
package opsconfig
import (
"context"
"errors"
"os"
"testing"
"data-terminal-cloud/internal/audit"
"data-terminal-cloud/internal/identity"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgxpool"
)
func TestRoutingConfigurationApprovalPublishAndRollback(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()
if _, err := NewService(nil, audit.NewRepository()); err == nil {
t.Fatal("nil database pool was accepted")
}
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-000000000101"}
approver := identity.Principal{Issuer: admin.Issuer, Subject: "00000000-0000-0000-0000-000000000102"}
outsider := identity.Principal{Issuer: admin.Issuer, Subject: "00000000-0000-0000-0000-000000000103"}
tenantID := "10000000-0000-0000-0000-000000000010"
configKey := "integration.routing." + uuid.NewString()
keys := []string{"ops-integration-v1-" + uuid.NewString(), "ops-integration-eval1-" + uuid.NewString(), "ops-integration-v2-" + uuid.NewString(), "ops-integration-eval2-" + uuid.NewString(), "ops-integration-rollback-" + uuid.NewString()}
defer func() {
_, _ = pool.Exec(ctx, `DELETE FROM audit_event WHERE request_id LIKE 'ops-integration-%'`)
_, _ = 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)`, configKey)
_, _ = pool.Exec(ctx, `DELETE FROM runtime_config_version WHERE config_key=$1`, configKey)
}()
input := CreateInput{TenantID: tenantID, ConfigKey: configKey, DisplayName: "集成路由配置", ConfigKind: "ROUTING", Payload: map[string]any{"default_engine_mode": "s2s_hybrid", "routes": []any{}}, SecretRefs: map[string]string{}, ChangeSummary: "建立集成路由基线"}
v1, created, err := service.Create(ctx, admin, keys[0], "ops-integration-create-v1", input)
if err != nil || !created || v1.Status != "DRAFT" || v1.VersionNo != 1 || v1.RowVersion != 1 {
t.Fatalf("create v1=%+v created=%v err=%v", v1, created, err)
}
replayed, created, err := service.Create(ctx, admin, keys[0], "ops-integration-create-v1-replay", input)
if err != nil || created || replayed.ID != v1.ID {
t.Fatalf("replay v1=%+v created=%v err=%v", replayed, created, err)
}
if _, _, err := service.Create(ctx, outsider, "ops-integration-outsider", "ops-integration-outsider", input); !errors.Is(err, ErrAccessDenied) {
t.Fatalf("outsider create error=%v", err)
}
versions, err := service.List(ctx, approver, tenantID, configKey)
if err != nil || len(versions) != 1 || versions[0].ID != v1.ID {
t.Fatalf("versions=%+v err=%v", versions, err)
}
v1, err = service.Submit(ctx, admin, tenantID, v1.ID, "ops-integration-submit-v1", 1)
if err != nil || v1.Status != "PENDING_APPROVAL" || v1.RowVersion != 2 || v1.SubmittedAt == nil {
t.Fatalf("submitted v1=%+v err=%v", v1, err)
}
rollout := RolloutInput{ScopeType: "TENANT", RolloutPercent: 100, MaxConcurrentCalls: 10}
if _, err := service.Publish(ctx, admin, tenantID, v1.ID, "ops-integration-self-publish", PublishInput{Reason: "禁止自批", Rollout: rollout}, 2); !errors.Is(err, ErrSelfApproval) {
t.Fatalf("self approval error=%v", err)
}
if _, err := service.Publish(ctx, approver, tenantID, v1.ID, "ops-integration-no-eval", PublishInput{Reason: "缺少评测", Rollout: rollout}, 2); !errors.Is(err, ErrPublishGate) {
t.Fatalf("missing evaluation gate error=%v", err)
}
evaluation, created, err := service.CreateEvaluation(ctx, admin, tenantID, v1.ID, keys[1], "ops-integration-eval-v1", EvidenceTypeGeneric, "routing-suite-v1", "PASSED", "integration://reports/routing-v1", map[string]any{"cases": 12, "passed": 12})
if err != nil || !created || evaluation.Result != "PASSED" {
t.Fatalf("evaluation=%+v created=%v err=%v", evaluation, created, err)
}
evaluations, err := service.ListEvaluations(ctx, approver, tenantID, v1.ID)
if err != nil || len(evaluations) != 1 || evaluations[0].ID != evaluation.ID {
t.Fatalf("evaluations=%+v err=%v", evaluations, err)
}
v1, err = service.Publish(ctx, approver, tenantID, v1.ID, "ops-integration-publish-v1", PublishInput{Reason: "评测通过后发布", Rollout: rollout}, 2)
if err != nil || v1.Status != "PUBLISHED" || v1.RowVersion != 3 || v1.PublishedAt == nil {
t.Fatalf("published v1=%+v err=%v", v1, err)
}
rollouts, err := service.ListRollouts(ctx, approver, tenantID, configKey)
if err != nil || len(rollouts) != 1 || rollouts[0].Status != "ACTIVE" || rollouts[0].ConfigVersionID != v1.ID || rollouts[0].RowVersion != 1 {
t.Fatalf("v1 rollouts=%+v err=%v", rollouts, err)
}
if _, err = service.PauseRollout(ctx, outsider, tenantID, rollouts[0].ID, "ops-integration-pause-outsider", "无权暂停", rollouts[0].RowVersion); !errors.Is(err, ErrAccessDenied) {
t.Fatalf("outsider pause error=%v", err)
}
paused, err := service.PauseRollout(ctx, approver, tenantID, rollouts[0].ID, "ops-integration-pause-v1", "观察到异常,暂停新通话命中", rollouts[0].RowVersion)
if err != nil || paused.Status != "PAUSED" || paused.PausedAt == nil || paused.RowVersion != 2 {
t.Fatalf("paused rollout=%+v err=%v", paused, err)
}
if _, err = service.ResumeRollout(ctx, approver, tenantID, paused.ID, "ops-integration-resume-stale", "使用过期版本尝试恢复", 1); !errors.Is(err, ErrVersionConflict) {
t.Fatalf("stale rollout resume error=%v", err)
}
resumed, err := service.ResumeRollout(ctx, approver, tenantID, paused.ID, "ops-integration-resume-v1", "观察确认正常,恢复投放", paused.RowVersion)
if err != nil || resumed.Status != "ACTIVE" || resumed.PausedAt != nil || resumed.RowVersion != 3 {
t.Fatalf("resumed rollout=%+v err=%v", resumed, err)
}
paused, err = service.PauseRollout(ctx, approver, tenantID, resumed.ID, "ops-integration-pause-before-v2", "发布第二版前暂停旧投放", resumed.RowVersion)
if err != nil || paused.Status != "PAUSED" || paused.RowVersion != 4 {
t.Fatalf("second pause=%+v err=%v", paused, err)
}
input.Payload = map[string]any{"default_engine_mode": "cascade", "routes": []any{map[string]any{"goal": "open_conversation", "engine_mode": "cascade"}}}
input.ChangeSummary = "切换合成级联路由"
v2, created, err := service.Create(ctx, admin, keys[2], "ops-integration-create-v2", input)
if err != nil || !created || v2.VersionNo != 2 {
t.Fatalf("create v2=%+v created=%v err=%v", v2, created, err)
}
v2, err = service.Submit(ctx, admin, tenantID, v2.ID, "ops-integration-submit-v2", 1)
if err != nil {
t.Fatal(err)
}
if _, _, err := service.CreateEvaluation(ctx, admin, tenantID, v2.ID, keys[3], "ops-integration-eval-v2", EvidenceTypeGeneric, "routing-suite-v1", "PASSED", "integration://reports/routing-v2", map[string]any{"cases": 12, "passed": 12}); err != nil {
t.Fatal(err)
}
v2, err = service.Publish(ctx, approver, tenantID, v2.ID, "ops-integration-publish-v2", PublishInput{Reason: "发布第二版", Rollout: rollout}, v2.RowVersion)
if err != nil || v2.Status != "PUBLISHED" || v2.SupersedesID == nil || *v2.SupersedesID != v1.ID {
t.Fatalf("published v2=%+v err=%v", v2, err)
}
if _, err = service.ResumeRollout(ctx, approver, tenantID, paused.ID, "ops-integration-resume-superseded", "不得恢复已被替代的投放", paused.RowVersion+1); !errors.Is(err, ErrStateConflict) {
t.Fatalf("superseded rollout resume error=%v", err)
}
versions, err = service.List(ctx, approver, tenantID, configKey)
if err != nil || len(versions) != 2 || versions[1].Status != "RETIRED" {
t.Fatalf("post-v2 versions=%+v err=%v", versions, err)
}
rollback, created, err := service.Rollback(ctx, approver, tenantID, v1.ID, keys[4], "ops-integration-rollback", PublishInput{Reason: "回滚到稳定路由", Rollout: rollout})
if err != nil || !created || rollback.VersionNo != 3 || rollback.Status != "PUBLISHED" || rollback.RollbackOfID == nil || *rollback.RollbackOfID != v1.ID {
t.Fatalf("rollback=%+v created=%v err=%v", rollback, created, err)
}
replayedRollback, created, err := service.Rollback(ctx, approver, tenantID, v1.ID, keys[4], "ops-integration-rollback-replay", PublishInput{Reason: "回滚到稳定路由", Rollout: rollout})
if err != nil || created || replayedRollback.ID != rollback.ID {
t.Fatalf("rollback replay=%+v created=%v err=%v", replayedRollback, created, err)
}
rollouts, err = service.ListRollouts(ctx, approver, tenantID, configKey)
if err != nil || len(rollouts) != 3 || rollouts[0].ConfigVersionID != rollback.ID || rollouts[0].Status != "ACTIVE" {
t.Fatalf("final rollouts=%+v err=%v", rollouts, err)
}
}
+83
View File
@@ -0,0 +1,83 @@
package opsconfig
import (
"errors"
"testing"
)
func TestValidatePayloadRoutingAndSensitiveFields(t *testing.T) {
valid := map[string]any{"default_engine_mode": "s2s_hybrid", "routes": []any{map[string]any{"goal": "open_conversation", "engine_mode": "s2s_hybrid"}}}
result, err := validatePayload("ROUTING", valid, map[string]string{})
if err != nil || result["valid"] != true {
t.Fatalf("valid routing config rejected: result=%v err=%v", result, err)
}
for name, payload := range map[string]map[string]any{
"unsupported engine": {"default_engine_mode": "silent-fallback", "routes": []any{}},
"embedded secret": {"default_engine_mode": "s2s_hybrid", "routes": []any{}, "api_secret": "plaintext"},
"nested token": {"default_engine_mode": "s2s_hybrid", "routes": []any{map[string]any{"access_token": "plaintext"}}},
} {
t.Run(name, func(t *testing.T) {
if _, err := validatePayload("ROUTING", payload, map[string]string{}); !errors.Is(err, ErrInvalid) {
t.Fatalf("error = %v, want ErrInvalid", err)
}
})
}
}
func TestValidatePayloadRequiresControlledCredentialReferences(t *testing.T) {
payload := map[string]any{"provider": "volcengine", "model_id": "model://approved/default"}
if _, err := validatePayload("MODEL", payload, map[string]string{}); !errors.Is(err, ErrInvalid) {
t.Fatalf("missing credential reference error = %v", err)
}
if _, err := validatePayload("MODEL", payload, map[string]string{"credential_ref": "kms://voice-platform/provider/prod"}); err != nil {
t.Fatalf("controlled credential reference rejected: %v", err)
}
if _, err := validatePayload("MODEL", payload, map[string]string{"credential_ref": "plain text secret"}); !errors.Is(err, ErrInvalid) {
t.Fatalf("invalid credential reference error = %v", err)
}
}
func TestValidateAgentBundleRequiresVersionUUIDs(t *testing.T) {
payload := map[string]any{
"persona_version_id": "10000000-0000-0000-0000-000000000001",
"voice_version_id": "10000000-0000-0000-0000-000000000002",
"model_version_id": "10000000-0000-0000-0000-000000000003",
"routing_version_id": "10000000-0000-0000-0000-000000000004",
}
if _, err := validatePayload("AGENT_BUNDLE", payload, map[string]string{}); err != nil {
t.Fatalf("valid component UUIDs rejected: %v", err)
}
payload["model_version_id"] = "not-a-version-id"
if _, err := validatePayload("AGENT_BUNDLE", payload, map[string]string{}); !errors.Is(err, ErrInvalid) {
t.Fatalf("invalid component UUID error = %v", err)
}
}
func TestValidateRolloutScopeAndImpactLimits(t *testing.T) {
validID := "10000000-0000-0000-0000-000000000401"
for name, input := range map[string]RolloutInput{
"tenant": {ScopeType: "TENANT", RolloutPercent: 1, MaxConcurrentCalls: 1},
"subject": {ScopeType: "SUBJECT", ScopeID: validID, RolloutPercent: 100, MaxConcurrentCalls: 25},
"device group": {ScopeType: "DEVICE_GROUP", ScopeID: validID, RolloutPercent: 15, MaxConcurrentCalls: 100000},
} {
t.Run(name, func(t *testing.T) {
if err := validateRollout(input); err != nil {
t.Fatalf("valid rollout rejected: %v", err)
}
})
}
for name, input := range map[string]RolloutInput{
"tenant with id": {ScopeType: "TENANT", ScopeID: validID, RolloutPercent: 100, MaxConcurrentCalls: 1},
"subject partial": {ScopeType: "SUBJECT", ScopeID: validID, RolloutPercent: 50, MaxConcurrentCalls: 1},
"group missing id": {ScopeType: "DEVICE_GROUP", RolloutPercent: 50, MaxConcurrentCalls: 1},
"zero percent": {ScopeType: "TENANT", RolloutPercent: 0, MaxConcurrentCalls: 1},
"excess concurrency": {ScopeType: "TENANT", RolloutPercent: 100, MaxConcurrentCalls: 100001},
"unsupported scope type": {ScopeType: "DEVICE", ScopeID: validID, RolloutPercent: 100, MaxConcurrentCalls: 1},
} {
t.Run(name, func(t *testing.T) {
if err := validateRollout(input); !errors.Is(err, ErrInvalid) {
t.Fatalf("error = %v, want ErrInvalid", err)
}
})
}
}