chore: establish local platform baseline
This commit is contained in:
@@ -0,0 +1,644 @@
|
||||
package releaseevidence
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"data-terminal-cloud/internal/agentmodelsconformance"
|
||||
"data-terminal-cloud/internal/alertreceiverconformance"
|
||||
"data-terminal-cloud/internal/cisupplychainconformance"
|
||||
"data-terminal-cloud/internal/firmwareconformance"
|
||||
"data-terminal-cloud/internal/infrastructureconformance"
|
||||
"data-terminal-cloud/internal/kmsrotationconformance"
|
||||
"data-terminal-cloud/internal/recoveryconformance"
|
||||
"data-terminal-cloud/internal/rtcconformance"
|
||||
"data-terminal-cloud/internal/safariconformance"
|
||||
)
|
||||
|
||||
const fixtureSourceCommit = "1111111111111111111111111111111111111111"
|
||||
|
||||
func TestBuildCreatesHashedBundleAndPreservesPendingGates(t *testing.T) {
|
||||
t.Parallel()
|
||||
root, snapshotPath, indexPath, outputPath := releaseFixture(t, true)
|
||||
result, err := Build(Options{
|
||||
Root: root, CandidateVersion: "0.53.0-rc.local", BuildDigest: "sha256:" + strings.Repeat("a", 64),
|
||||
SourceCommit: fixtureSourceCommit, AcceptanceSnapshotPath: snapshotPath, EvidenceIndexPath: indexPath,
|
||||
OutputPath: outputPath, GeneratedAt: time.Date(2026, 8, 25, 12, 0, 0, 0, time.UTC),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if result.Status != "EXTERNAL_VALIDATION_PENDING" || result.Verified != 1 || result.Pending != len(requiredCodes)-1 || result.Waived != 0 || !strings.HasPrefix(result.SHA256, "sha256:") {
|
||||
t.Fatalf("unexpected result: %+v", result)
|
||||
}
|
||||
reader, err := zip.OpenReader(outputPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer reader.Close()
|
||||
entries := map[string]*zip.File{}
|
||||
for _, entry := range reader.File {
|
||||
entries[entry.Name] = entry
|
||||
}
|
||||
for _, required := range []string{"release-manifest.json", "evidence-index.json", "acceptance/technical-acceptance-snapshot.json", "baseline/openapi/openapi.yaml", "evidence/REMOTE_CI_SUPPLY_CHAIN/01-ci-report.json"} {
|
||||
if entries[required] == nil {
|
||||
t.Fatalf("bundle entry missing: %s", required)
|
||||
}
|
||||
}
|
||||
manifestReader, _ := entries["release-manifest.json"].Open()
|
||||
defer manifestReader.Close()
|
||||
var bundleManifest manifest
|
||||
if err = json.NewDecoder(manifestReader).Decode(&bundleManifest); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bundleManifest.TechnicalGateSatisfied || bundleManifest.PendingCount != len(requiredCodes)-1 || bundleManifest.SBOMCount != 0 || len(bundleManifest.Warnings) != 1 {
|
||||
t.Fatalf("unexpected manifest: %+v", bundleManifest)
|
||||
}
|
||||
if len(bundleManifest.Files) != result.Files-1 {
|
||||
t.Fatalf("manifest file inventory=%d bundle files=%d", len(bundleManifest.Files), result.Files)
|
||||
}
|
||||
indexReader, _ := entries["evidence-index.json"].Open()
|
||||
defer indexReader.Close()
|
||||
var packagedIndex EvidenceIndex
|
||||
if err = json.NewDecoder(indexReader).Decode(&packagedIndex); err != nil || packagedIndex.Items[0].Files[0] != "evidence/REMOTE_CI_SUPPLY_CHAIN/01-ci-report.json" {
|
||||
t.Fatalf("packaged evidence paths were not sanitized: %+v err=%v", packagedIndex.Items[0].Files, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildRejectsUnsatisfiedAcceptanceAndProductionLocalLabel(t *testing.T) {
|
||||
t.Parallel()
|
||||
root, snapshotPath, indexPath, outputPath := releaseFixture(t, false)
|
||||
_, err := Build(Options{Root: root, CandidateVersion: "0.53.0-rc.local", BuildDigest: "sha256:" + strings.Repeat("a", 64), SourceCommit: fixtureSourceCommit, AcceptanceSnapshotPath: snapshotPath, EvidenceIndexPath: indexPath, OutputPath: outputPath})
|
||||
if err == nil || !strings.Contains(err.Error(), "technical gate") {
|
||||
t.Fatalf("expected technical gate rejection, got %v", err)
|
||||
}
|
||||
writeJSONFile(t, snapshotPath, acceptanceSnapshot{
|
||||
ID: "10000000-0000-0000-0000-000000000001", CandidateVersion: "0.53.0-rc.local", BuildDigest: "sha256:" + strings.Repeat("a", 64),
|
||||
Environment: "PRODUCTION", RunType: "PRODUCTION_RELEASE", GeneratedAt: time.Now().UTC(), RedactionPolicy: "ops-acceptance-minimal-v1", OverallGateSatisfied: true,
|
||||
})
|
||||
_, err = Build(Options{Root: root, CandidateVersion: "0.53.0-rc.local", BuildDigest: "sha256:" + strings.Repeat("a", 64), SourceCommit: "local-uncommitted", AcceptanceSnapshotPath: snapshotPath, EvidenceIndexPath: indexPath, OutputPath: outputPath})
|
||||
if err == nil || !strings.Contains(err.Error(), "source commit") {
|
||||
t.Fatalf("expected production source commit rejection, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildRequiresAndPackagesCompletePilotWeek(t *testing.T) {
|
||||
t.Parallel()
|
||||
root, snapshotPath, indexPath, outputPath := releaseFixture(t, true)
|
||||
writeJSONFile(t, snapshotPath, acceptanceSnapshot{
|
||||
ID: "10000000-0000-0000-0000-000000000001", CandidateVersion: "0.53.0-rc.local", BuildDigest: "sha256:" + strings.Repeat("a", 64),
|
||||
Environment: "LAB", RunType: "PILOT", TenantID: "20000000-0000-0000-0000-000000000001", GeneratedAt: time.Now().UTC(), RedactionPolicy: "ops-acceptance-minimal-v1", OverallGateSatisfied: true,
|
||||
})
|
||||
baseOptions := Options{
|
||||
Root: root, CandidateVersion: "0.53.0-rc.local", BuildDigest: "sha256:" + strings.Repeat("a", 64), SourceCommit: fixtureSourceCommit,
|
||||
AcceptanceSnapshotPath: snapshotPath, EvidenceIndexPath: indexPath, OutputPath: outputPath, GeneratedAt: time.Date(2026, 8, 25, 12, 0, 0, 0, time.UTC),
|
||||
}
|
||||
if _, err := Build(baseOptions); err == nil || !strings.Contains(err.Error(), "exactly seven") {
|
||||
t.Fatalf("expected missing PILOT cycle rejection, got %v", err)
|
||||
}
|
||||
location, err := time.LoadLocation("Asia/Shanghai")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
weekStart := time.Date(2026, 8, 10, 0, 0, 0, 0, location)
|
||||
weeklyPath := filepath.Join(root, "pilot-weekly.json")
|
||||
writeJSONFile(t, weeklyPath, pilotWeeklySnapshot{
|
||||
ID: "30000000-0000-0000-0000-000000000001", GeneratedAt: weekStart.AddDate(0, 0, 7).Add(time.Hour).UTC(),
|
||||
TenantID: "20000000-0000-0000-0000-000000000001", CandidateVersion: "0.53.0-rc.local", BuildDigest: "sha256:" + strings.Repeat("a", 64), WeekStart: "2026-08-10", Timezone: "Asia/Shanghai",
|
||||
WindowStart: weekStart.UTC(), WindowEnd: weekStart.AddDate(0, 0, 7).UTC(), IsComplete: true, RedactionPolicy: "ops-pilot-weekly-minimal-v1",
|
||||
ExternalEvidence: []PilotWeeklyExternalEvidence{{Code: "complaints", Status: "REQUIRED"}},
|
||||
})
|
||||
dailyPaths := []string{}
|
||||
for day := 0; day < 7; day++ {
|
||||
date := weekStart.AddDate(0, 0, day)
|
||||
path := filepath.Join(root, "pilot-daily-"+date.Format("2006-01-02")+".json")
|
||||
writeJSONFile(t, path, pilotDailySnapshot{
|
||||
ID: fmt.Sprintf("40000000-0000-0000-0000-%012d", day+1), GeneratedAt: date.Add(12 * time.Hour).UTC(),
|
||||
TenantID: "20000000-0000-0000-0000-000000000001", CandidateVersion: "0.53.0-rc.local", BuildDigest: "sha256:" + strings.Repeat("a", 64), LocalDate: date.Format("2006-01-02"), Timezone: "Asia/Shanghai",
|
||||
WindowStart: date.UTC(), WindowEnd: date.AddDate(0, 0, 1).UTC(), RedactionPolicy: "ops-pilot-daily-minimal-v1",
|
||||
})
|
||||
dailyPaths = append(dailyPaths, path)
|
||||
}
|
||||
baseOptions.PilotDailySnapshotPaths = dailyPaths
|
||||
baseOptions.PilotWeeklySnapshotPath = weeklyPath
|
||||
firstDate := weekStart
|
||||
writeJSONFile(t, dailyPaths[0], pilotDailySnapshot{
|
||||
ID: "40000000-0000-0000-0000-000000000001", GeneratedAt: firstDate.Add(12 * time.Hour).UTC(),
|
||||
TenantID: "20000000-0000-0000-0000-000000000001", CandidateVersion: "different-candidate", BuildDigest: "sha256:" + strings.Repeat("a", 64), LocalDate: firstDate.Format("2006-01-02"), Timezone: "Asia/Shanghai",
|
||||
WindowStart: firstDate.UTC(), WindowEnd: firstDate.AddDate(0, 0, 1).UTC(), RedactionPolicy: "ops-pilot-daily-minimal-v1",
|
||||
})
|
||||
if _, err = Build(baseOptions); err == nil || !strings.Contains(err.Error(), "candidate") {
|
||||
t.Fatalf("expected cross-candidate PILOT evidence rejection, got %v", err)
|
||||
}
|
||||
writeJSONFile(t, dailyPaths[0], pilotDailySnapshot{
|
||||
ID: "40000000-0000-0000-0000-000000000001", GeneratedAt: firstDate.Add(12 * time.Hour).UTC(),
|
||||
TenantID: "20000000-0000-0000-0000-000000000001", CandidateVersion: "0.53.0-rc.local", BuildDigest: "sha256:" + strings.Repeat("a", 64), LocalDate: firstDate.Format("2006-01-02"), Timezone: "Asia/Shanghai",
|
||||
WindowStart: firstDate.UTC(), WindowEnd: firstDate.AddDate(0, 0, 1).UTC(), RedactionPolicy: "ops-pilot-daily-minimal-v1",
|
||||
})
|
||||
result, err := Build(baseOptions)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
reader, err := zip.OpenReader(result.OutputPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer reader.Close()
|
||||
entries := map[string]*zip.File{}
|
||||
for _, entry := range reader.File {
|
||||
entries[entry.Name] = entry
|
||||
}
|
||||
if entries["pilot/weekly/2026-08-10.json"] == nil || entries["pilot/daily/2026-08-16.json"] == nil {
|
||||
t.Fatal("PILOT daily or weekly evidence is missing from bundle")
|
||||
}
|
||||
manifestReader, _ := entries["release-manifest.json"].Open()
|
||||
defer manifestReader.Close()
|
||||
var bundleManifest manifest
|
||||
if err = json.NewDecoder(manifestReader).Decode(&bundleManifest); err != nil || !bundleManifest.PilotEvidenceIncluded || bundleManifest.PilotDailyCount != 7 || bundleManifest.PilotWeekStart != "2026-08-10" || bundleManifest.PilotTimezone != "Asia/Shanghai" {
|
||||
t.Fatalf("unexpected PILOT manifest: %+v err=%v", bundleManifest, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFirmwareHardwareCoverageControlsVerifiedGate(t *testing.T) {
|
||||
t.Parallel()
|
||||
root, snapshotPath, indexPath, outputPath := releaseFixture(t, true)
|
||||
var index EvidenceIndex
|
||||
indexBytes, err := os.ReadFile(indexPath)
|
||||
if err != nil || json.Unmarshal(indexBytes, &index) != nil {
|
||||
t.Fatal("read evidence index", err)
|
||||
}
|
||||
reportPaths := []string{}
|
||||
for device := 1; device <= firmwareDeviceMinimum; device++ {
|
||||
for scenarioPosition, scenario := range requiredFirmwareScenarios {
|
||||
path := filepath.Join(root, fmt.Sprintf("firmware-%d-%s.json", device, strings.ToLower(scenario)))
|
||||
writeJSONFile(t, path, firmwareReportFixture(device, scenarioPosition, scenario, "HARDWARE_CAPTURE"))
|
||||
reportPaths = append(reportPaths, path)
|
||||
}
|
||||
}
|
||||
for position := range index.Items {
|
||||
if index.Items[position].Code == firmwareEvidenceCode {
|
||||
index.Items[position].Status = "VERIFIED"
|
||||
index.Items[position].Owner = "硬件与测试负责人"
|
||||
index.Items[position].Note = "三台候选设备四场景真机一致性报告"
|
||||
index.Items[position].Files = reportPaths
|
||||
}
|
||||
}
|
||||
writeJSONFile(t, indexPath, index)
|
||||
result, err := Build(Options{
|
||||
Root: root, CandidateVersion: "0.53.0-rc.local", BuildDigest: "sha256:" + strings.Repeat("a", 64),
|
||||
SourceCommit: fixtureSourceCommit, AcceptanceSnapshotPath: snapshotPath, EvidenceIndexPath: indexPath,
|
||||
OutputPath: outputPath, GeneratedAt: time.Date(2026, 8, 25, 12, 0, 0, 0, time.UTC),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
reader, err := zip.OpenReader(result.OutputPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer reader.Close()
|
||||
for _, entry := range reader.File {
|
||||
if entry.Name != "release-manifest.json" {
|
||||
continue
|
||||
}
|
||||
manifestReader, _ := entry.Open()
|
||||
defer manifestReader.Close()
|
||||
var bundleManifest manifest
|
||||
if err = json.NewDecoder(manifestReader).Decode(&bundleManifest); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bundleManifest.FirmwareValidation.CoverageComplete || bundleManifest.FirmwareValidation.QualifiedDeviceCount != 3 || bundleManifest.FirmwareValidation.ReportCount != 12 || len(bundleManifest.FirmwareValidation.Gaps) != 0 {
|
||||
t.Fatalf("unexpected firmware coverage: %+v", bundleManifest.FirmwareValidation)
|
||||
}
|
||||
}
|
||||
|
||||
index.Items[2].Files = index.Items[2].Files[:len(index.Items[2].Files)-1]
|
||||
writeJSONFile(t, indexPath, index)
|
||||
_, err = Build(Options{
|
||||
Root: root, CandidateVersion: "0.53.0-rc.local", BuildDigest: "sha256:" + strings.Repeat("a", 64),
|
||||
SourceCommit: fixtureSourceCommit, AcceptanceSnapshotPath: snapshotPath, EvidenceIndexPath: indexPath,
|
||||
OutputPath: filepath.Join(root, "out", "incomplete.zip"),
|
||||
})
|
||||
if err == nil || !strings.Contains(err.Error(), "must remain PENDING") {
|
||||
t.Fatalf("expected incomplete hardware suite rejection, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFirmwareEvidenceRejectsSimulatorReport(t *testing.T) {
|
||||
t.Parallel()
|
||||
root, snapshotPath, indexPath, outputPath := releaseFixture(t, true)
|
||||
path := filepath.Join(root, "simulator-report.json")
|
||||
writeJSONFile(t, path, firmwareReportFixture(1, 0, "AB_SUCCESS", "PROTOCOL_SIMULATION"))
|
||||
var index EvidenceIndex
|
||||
content, _ := os.ReadFile(indexPath)
|
||||
if err := json.Unmarshal(content, &index); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for position := range index.Items {
|
||||
if index.Items[position].Code == firmwareEvidenceCode {
|
||||
index.Items[position].Files = []string{path}
|
||||
}
|
||||
}
|
||||
writeJSONFile(t, indexPath, index)
|
||||
_, err := Build(Options{
|
||||
Root: root, CandidateVersion: "0.53.0-rc.local", BuildDigest: "sha256:" + strings.Repeat("a", 64), SourceCommit: fixtureSourceCommit,
|
||||
AcceptanceSnapshotPath: snapshotPath, EvidenceIndexPath: indexPath, OutputPath: outputPath,
|
||||
})
|
||||
if err == nil || !strings.Contains(err.Error(), "HARDWARE_CAPTURE") {
|
||||
t.Fatalf("expected simulator evidence rejection, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRealAlertReceiverReportControlsEvidenceGate(t *testing.T) {
|
||||
t.Parallel()
|
||||
root, _, indexPath, _ := releaseFixture(t, true)
|
||||
path := filepath.Join(root, "real-alert-receiver-report.json")
|
||||
writeJSONFile(t, path, alertReceiverReportFixture("REAL_RECEIVER", false))
|
||||
var index EvidenceIndex
|
||||
content, _ := os.ReadFile(indexPath)
|
||||
if err := json.Unmarshal(content, &index); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for position := range index.Items {
|
||||
if index.Items[position].Code == alertReceiverEvidenceCode {
|
||||
index.Items[position].Status = "VERIFIED"
|
||||
index.Items[position].Files = []string{path}
|
||||
}
|
||||
}
|
||||
summary, err := validateAlertReceiverEvidence(index, root, "0.53.0-rc.local", "sha256:"+strings.Repeat("a", 64))
|
||||
if err != nil || !summary.CoverageComplete || !summary.FormalDrillVerified || summary.ActiveRouteCount != 1 {
|
||||
t.Fatalf("unexpected real receiver summary=%+v err=%v", summary, err)
|
||||
}
|
||||
writeJSONFile(t, path, alertReceiverReportFixture("MOCK_RECEIVER", true))
|
||||
if _, err = validateAlertReceiverEvidence(index, root, "0.53.0-rc.local", "sha256:"+strings.Repeat("a", 64)); err == nil || !strings.Contains(err.Error(), "REAL_RECEIVER") {
|
||||
t.Fatalf("expected mock receiver rejection, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRealKMSRotationReportControlsEvidenceGate(t *testing.T) {
|
||||
t.Parallel()
|
||||
root, _, indexPath, _ := releaseFixture(t, true)
|
||||
path := filepath.Join(root, "real-kms-rotation-report.json")
|
||||
writeJSONFile(t, path, kmsRotationReportFixture("REAL_KMS_HSM", false))
|
||||
var index EvidenceIndex
|
||||
content, _ := os.ReadFile(indexPath)
|
||||
if err := json.Unmarshal(content, &index); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for position := range index.Items {
|
||||
if index.Items[position].Code == kmsRotationEvidenceCode {
|
||||
index.Items[position].Status = "VERIFIED"
|
||||
index.Items[position].Files = []string{path}
|
||||
}
|
||||
}
|
||||
summary, err := validateKMSRotationEvidence(index, root, "0.53.0-rc.local", "sha256:"+strings.Repeat("a", 64))
|
||||
if err != nil || !summary.CoverageComplete || summary.RotationCount != 2 || len(summary.ConsumptionModes) != 2 {
|
||||
t.Fatalf("unexpected KMS summary=%+v err=%v", summary, err)
|
||||
}
|
||||
writeJSONFile(t, path, kmsRotationReportFixture("LOCAL_SECRET", true))
|
||||
if _, err = validateKMSRotationEvidence(index, root, "0.53.0-rc.local", "sha256:"+strings.Repeat("a", 64)); err == nil || !strings.Contains(err.Error(), "REAL_KMS_HSM") {
|
||||
t.Fatalf("expected local rotation rejection, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProductionRecoveryReportControlsEvidenceGate(t *testing.T) {
|
||||
t.Parallel()
|
||||
root, _, indexPath, _ := releaseFixture(t, true)
|
||||
path := filepath.Join(root, "production-recovery-report.json")
|
||||
writeJSONFile(t, path, productionRecoveryReportFixture("PRODUCTION_EQUIVALENT", false))
|
||||
var index EvidenceIndex
|
||||
content, _ := os.ReadFile(indexPath)
|
||||
if err := json.Unmarshal(content, &index); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for position := range index.Items {
|
||||
if index.Items[position].Code == productionRecoveryEvidenceCode {
|
||||
index.Items[position].Status = "VERIFIED"
|
||||
index.Items[position].Files = []string{path}
|
||||
}
|
||||
}
|
||||
summary, err := validateProductionRecoveryEvidence(index, root, "0.53.0-rc.local", "sha256:"+strings.Repeat("a", 64))
|
||||
if err != nil || !summary.CoverageComplete || summary.DrillCount != 4 || len(summary.CompletedDrillTypes) != 4 {
|
||||
t.Fatalf("unexpected recovery summary=%+v err=%v", summary, err)
|
||||
}
|
||||
writeJSONFile(t, path, productionRecoveryReportFixture("LOCAL_RECOVERY", true))
|
||||
if _, err = validateProductionRecoveryEvidence(index, root, "0.53.0-rc.local", "sha256:"+strings.Repeat("a", 64)); err == nil || !strings.Contains(err.Error(), "PRODUCTION_EQUIVALENT") {
|
||||
t.Fatalf("expected local recovery rejection, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProductionInfrastructureReportControlsEvidenceGate(t *testing.T) {
|
||||
t.Parallel()
|
||||
root, _, indexPath, _ := releaseFixture(t, true)
|
||||
path := filepath.Join(root, "production-infrastructure-report.json")
|
||||
writeJSONFile(t, path, productionInfrastructureReportFixture("PRODUCTION_INFRASTRUCTURE", false))
|
||||
var index EvidenceIndex
|
||||
content, _ := os.ReadFile(indexPath)
|
||||
if err := json.Unmarshal(content, &index); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for position := range index.Items {
|
||||
if index.Items[position].Code == productionInfrastructureEvidenceCode {
|
||||
index.Items[position].Status = "VERIFIED"
|
||||
index.Items[position].Files = []string{path}
|
||||
}
|
||||
}
|
||||
summary, err := validateProductionInfrastructureEvidence(index, root, "0.53.0-rc.local", "sha256:"+strings.Repeat("a", 64))
|
||||
if err != nil || !summary.CoverageComplete || summary.ComponentCount != len(infrastructureconformance.RequiredComponents) || len(summary.VerifiedComponents) != len(infrastructureconformance.RequiredComponents) {
|
||||
t.Fatalf("unexpected infrastructure summary=%+v err=%v", summary, err)
|
||||
}
|
||||
writeJSONFile(t, path, productionInfrastructureReportFixture("LOCAL_INFRASTRUCTURE", true))
|
||||
if _, err = validateProductionInfrastructureEvidence(index, root, "0.53.0-rc.local", "sha256:"+strings.Repeat("a", 64)); err == nil || !strings.Contains(err.Error(), "PRODUCTION_INFRASTRUCTURE") {
|
||||
t.Fatalf("expected local infrastructure rejection, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSafariCompatibilityReportControlsEvidenceGate(t *testing.T) {
|
||||
t.Parallel()
|
||||
root, _, indexPath, _ := releaseFixture(t, true)
|
||||
path := filepath.Join(root, "safari-compatibility-report.json")
|
||||
writeJSONFile(t, path, safariCompatibilityReportFixture("REAL_SAFARI", false))
|
||||
var index EvidenceIndex
|
||||
content, _ := os.ReadFile(indexPath)
|
||||
if err := json.Unmarshal(content, &index); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for position := range index.Items {
|
||||
if index.Items[position].Code == safariCompatibilityEvidenceCode {
|
||||
index.Items[position].Status = "VERIFIED"
|
||||
index.Items[position].Files = []string{path}
|
||||
}
|
||||
}
|
||||
summary, err := validateSafariCompatibilityEvidence(index, root, "0.53.0-rc.local", "sha256:"+strings.Repeat("a", 64))
|
||||
if err != nil || !summary.CoverageComplete || summary.RunCount != 2 || summary.ResultCount != 24 {
|
||||
t.Fatalf("unexpected Safari summary=%+v err=%v", summary, err)
|
||||
}
|
||||
writeJSONFile(t, path, safariCompatibilityReportFixture("NON_SAFARI_TOOLCHAIN", true))
|
||||
if _, err = validateSafariCompatibilityEvidence(index, root, "0.53.0-rc.local", "sha256:"+strings.Repeat("a", 64)); err == nil || !strings.Contains(err.Error(), "REAL_SAFARI") {
|
||||
t.Fatalf("expected non-Safari rejection, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRealDeviceRTCReportControlsEvidenceGate(t *testing.T) {
|
||||
t.Parallel()
|
||||
root, _, indexPath, _ := releaseFixture(t, true)
|
||||
path := filepath.Join(root, "real-device-rtc-report.json")
|
||||
writeJSONFile(t, path, realDeviceRTCReportFixture("REAL_DEVICE_RTC_PROVIDER", false))
|
||||
var index EvidenceIndex
|
||||
content, _ := os.ReadFile(indexPath)
|
||||
if err := json.Unmarshal(content, &index); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for position := range index.Items {
|
||||
if index.Items[position].Code == realDeviceRTCEvidenceCode {
|
||||
index.Items[position].Status = "VERIFIED"
|
||||
index.Items[position].Files = []string{path}
|
||||
}
|
||||
}
|
||||
summary, err := validateRealDeviceRTCEvidence(index, root, "0.53.0-rc.local", "sha256:"+strings.Repeat("a", 64))
|
||||
if err != nil || !summary.CoverageComplete || summary.DeviceCount != 3 || summary.SessionCount != 6 || len(summary.CompletedEngineModes) != 3 || len(summary.CompletedScenarios) != 4 {
|
||||
t.Fatalf("unexpected RTC summary=%+v err=%v", summary, err)
|
||||
}
|
||||
writeJSONFile(t, path, realDeviceRTCReportFixture("PROTOCOL_SIMULATION", true))
|
||||
if _, err = validateRealDeviceRTCEvidence(index, root, "0.53.0-rc.local", "sha256:"+strings.Repeat("a", 64)); err == nil || !strings.Contains(err.Error(), "REAL_DEVICE_RTC_PROVIDER") {
|
||||
t.Fatalf("expected protocol simulation rejection, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRealAgentModelsReportControlsEvidenceGate(t *testing.T) {
|
||||
t.Parallel()
|
||||
root, _, indexPath, _ := releaseFixture(t, true)
|
||||
path := filepath.Join(root, "real-agent-models-report.json")
|
||||
writeJSONFile(t, path, realAgentModelsReportFixture("REAL_AGENT_MODELS", false))
|
||||
var index EvidenceIndex
|
||||
content, _ := os.ReadFile(indexPath)
|
||||
if err := json.Unmarshal(content, &index); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for position := range index.Items {
|
||||
if index.Items[position].Code == realAgentModelsEvidenceCode {
|
||||
index.Items[position].Status = "VERIFIED"
|
||||
index.Items[position].Files = []string{path}
|
||||
}
|
||||
}
|
||||
summary, err := validateRealAgentModelsEvidence(index, root, "0.53.0-rc.local", "sha256:"+strings.Repeat("a", 64))
|
||||
if err != nil || !summary.CoverageComplete || summary.GenerationCaseCount != 10 || len(summary.EmbeddingCategories) != 6 || len(summary.ContentSafetyCategories) != 6 {
|
||||
t.Fatalf("unexpected Agent models summary=%+v err=%v", summary, err)
|
||||
}
|
||||
writeJSONFile(t, path, realAgentModelsReportFixture("LOCAL_AGENT_TOOLCHAIN", true))
|
||||
if _, err = validateRealAgentModelsEvidence(index, root, "0.53.0-rc.local", "sha256:"+strings.Repeat("a", 64)); err == nil || !strings.Contains(err.Error(), "REAL_AGENT_MODELS") {
|
||||
t.Fatalf("expected local Agent models rejection, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func releaseFixture(t *testing.T, gateSatisfied bool) (string, string, string, string) {
|
||||
t.Helper()
|
||||
root := t.TempDir()
|
||||
files := []string{
|
||||
"openapi/openapi.yaml", "compose.yaml", ".github/workflows/ci.yml", "go.mod", "go.sum", "web/package.json", "web/package-lock.json",
|
||||
"docs/development-plan.md", "docs/development-status.md", "docs/requirements-traceability.md", "docs/external-acceptance-checklist.md",
|
||||
"docs/kubernetes-deployment.md", "docs/alert-operations.md", "docs/integration-diagnostics.md", "migrations/000001.up.sql", "deploy/kubernetes/base/api.yaml",
|
||||
"docs/content-safety-provider.md", "docs/remote-ci-supply-chain-evidence.md", "docs/real-device-rtc-evidence.md", "docs/real-agent-models-evidence.md", "docs/production-infrastructure-evidence.md", "docs/safari-compatibility-evidence.md", "docs/pilot-daily-operations.md", "docs/pilot-weekly-review.md",
|
||||
}
|
||||
for _, relative := range files {
|
||||
path := filepath.Join(root, filepath.FromSlash(relative))
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
content := relative + "\n"
|
||||
if relative == "openapi/openapi.yaml" {
|
||||
content = "openapi: 3.1.0\ninfo:\n title: test\n version: 0.54.0\n"
|
||||
}
|
||||
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
snapshotPath := filepath.Join(root, "snapshot.json")
|
||||
writeJSONFile(t, snapshotPath, acceptanceSnapshot{
|
||||
ID: "10000000-0000-0000-0000-000000000001", CandidateVersion: "0.53.0-rc.local", BuildDigest: "sha256:" + strings.Repeat("a", 64),
|
||||
Environment: "LAB", RunType: "REAL_DEVICE_PROVIDER", GeneratedAt: time.Now().UTC(), RedactionPolicy: "ops-acceptance-minimal-v1", OverallGateSatisfied: gateSatisfied,
|
||||
})
|
||||
evidencePath := filepath.Join(root, "ci-report.json")
|
||||
writeJSONFile(t, evidencePath, ciSupplyChainReportFixture())
|
||||
index := EvidenceIndex{SchemaVersion: IndexSchemaVersion, CandidateVersion: "0.53.0-rc.local", BuildDigest: "sha256:" + strings.Repeat("a", 64)}
|
||||
for indexPosition, code := range requiredCodes {
|
||||
item := EvidenceItem{Code: code, Status: "PENDING", Owner: "待外部责任人确认", Note: "外部环境待验证", Files: []string{}}
|
||||
if indexPosition == 0 {
|
||||
item.Status = "VERIFIED"
|
||||
item.Owner = "研发"
|
||||
item.Note = "本地样例证据"
|
||||
item.Files = []string{"ci-report.json"}
|
||||
}
|
||||
index.Items = append(index.Items, item)
|
||||
}
|
||||
indexPath := filepath.Join(root, "evidence-index.json")
|
||||
writeJSONFile(t, indexPath, index)
|
||||
return root, snapshotPath, indexPath, filepath.Join(root, "out", "release.zip")
|
||||
}
|
||||
|
||||
func ciSupplyChainReportFixture() cisupplychainconformance.Report {
|
||||
checks := make([]cisupplychainconformance.Check, 0, len(requiredRemoteCISupplyChainChecks))
|
||||
for _, code := range requiredRemoteCISupplyChainChecks {
|
||||
checks = append(checks, cisupplychainconformance.Check{Code: code, Status: "PASS", Detail: "fixture check passed"})
|
||||
}
|
||||
return cisupplychainconformance.Report{
|
||||
SchemaVersion: cisupplychainconformance.ReportSchema, Result: "PASS", GeneratedAt: time.Date(2026, 8, 25, 9, 30, 0, 0, time.UTC),
|
||||
CandidateVersion: "0.53.0-rc.local", BuildDigest: "sha256:" + strings.Repeat("a", 64), SourceCommit: fixtureSourceCommit,
|
||||
EvidenceScope: "REMOTE_CI_SUPPLY_CHAIN", ExternalValidationRequired: false, Provider: "GITHUB_ACTIONS", RunID: "123456", RunURL: "https://github.com/example/voice/actions/runs/123456",
|
||||
CompletedChecks: slices.Clone(cisupplychainconformance.RequiredChecks),
|
||||
ImageDigests: map[string]string{"BACKEND_IMAGE": "sha256:" + strings.Repeat("b", 64), "WEB_IMAGE": "sha256:" + strings.Repeat("c", 64)},
|
||||
SBOMDigests: map[string]string{"SOURCE": "sha256:" + strings.Repeat("d", 64), "BACKEND_IMAGE": "sha256:" + strings.Repeat("e", 64), "WEB_IMAGE": "sha256:" + strings.Repeat("f", 64)},
|
||||
InputSHA256: map[string]string{"ci_capture": strings.Repeat("9", 64)}, Checks: checks, Disclaimer: "fixture remote CI evidence",
|
||||
}
|
||||
}
|
||||
|
||||
func writeJSONFile(t *testing.T, path string, value any) {
|
||||
t.Helper()
|
||||
data, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = os.WriteFile(path, data, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func firmwareReportFixture(device, scenarioPosition int, scenario, scope string) firmwareconformance.Report {
|
||||
checks := make([]firmwareconformance.Check, 0, len(requiredFirmwareChecks))
|
||||
for _, code := range requiredFirmwareChecks {
|
||||
checks = append(checks, firmwareconformance.Check{Code: code, Status: "PASS", Detail: "fixture check passed"})
|
||||
}
|
||||
return firmwareconformance.Report{
|
||||
SchemaVersion: firmwareconformance.ReportSchema, Result: "PASS", GeneratedAt: time.Date(2026, 8, 25, 10, device, scenarioPosition, 0, time.UTC),
|
||||
CandidateVersion: "0.53.0-rc.local", BuildDigest: "sha256:" + strings.Repeat("a", 64), EvidenceScope: scope, ExternalValidationRequired: true,
|
||||
DeviceID: fmt.Sprintf("10000000-0000-0000-0000-%012d", device), ReleaseID: fmt.Sprintf("20000000-0000-0000-0000-%012d", scenarioPosition+1),
|
||||
RolloutID: fmt.Sprintf("30000000-0000-0000-%04d-%012d", device, scenarioPosition+1), CommandID: fmt.Sprintf("40000000-0000-0000-%04d-%012d", device, scenarioPosition+1),
|
||||
Scenario: scenario, CloudEventCount: 7, DeviceEventCount: 5,
|
||||
InputSHA256: map[string]string{"cloud_rollout": strings.Repeat("b", 64), "device_evidence": strings.Repeat("c", 64)},
|
||||
Checks: checks, Disclaimer: "fixture retains external hardware review",
|
||||
}
|
||||
}
|
||||
|
||||
func alertReceiverReportFixture(scope string, externalRequired bool) alertreceiverconformance.Report {
|
||||
checks := make([]alertreceiverconformance.Check, 0, len(requiredAlertReceiverChecks))
|
||||
for _, code := range requiredAlertReceiverChecks {
|
||||
checks = append(checks, alertreceiverconformance.Check{Code: code, Status: "PASS", Detail: "fixture check passed"})
|
||||
}
|
||||
return alertreceiverconformance.Report{
|
||||
SchemaVersion: alertreceiverconformance.ReportSchema, Result: "PASS", GeneratedAt: time.Date(2026, 8, 25, 12, 0, 0, 0, time.UTC),
|
||||
CandidateVersion: "0.53.0-rc.local", BuildDigest: "sha256:" + strings.Repeat("a", 64), EvidenceScope: scope, ExternalValidationRequired: externalRequired,
|
||||
TenantID: "10000000-0000-0000-0000-000000000001", ReceiverSystemRef: "pager-gateway:production", ActiveRouteCount: 1, MappedGroupCount: 2, TestReceiptCount: 1, FormalDrillVerified: true,
|
||||
RouteCoverage: []alertreceiverconformance.RouteCoverage{{RoutingPolicyID: "20000000-0000-0000-0000-000000000001", ReceiverGroup: "platform-oncall", TestRef: "30000000-0000-0000-0000-000000000001", Status: "PASS"}},
|
||||
InputSHA256: map[string]string{"cloud_routes": strings.Repeat("1", 64), "cloud_tests": strings.Repeat("2", 64), "cloud_incident": strings.Repeat("3", 64), "receiver_capture": strings.Repeat("4", 64)},
|
||||
Checks: checks, Disclaimer: "fixture retains business release approval boundary",
|
||||
}
|
||||
}
|
||||
|
||||
func kmsRotationReportFixture(scope string, externalRequired bool) kmsrotationconformance.Report {
|
||||
checks := make([]kmsrotationconformance.Check, 0, len(requiredKMSRotationChecks))
|
||||
for _, code := range requiredKMSRotationChecks {
|
||||
checks = append(checks, kmsrotationconformance.Check{Code: code, Status: "PASS", Detail: "fixture check passed"})
|
||||
}
|
||||
return kmsrotationconformance.Report{
|
||||
SchemaVersion: kmsrotationconformance.ReportSchema, Result: "PASS", GeneratedAt: time.Date(2026, 8, 25, 12, 0, 0, 0, time.UTC),
|
||||
CandidateVersion: "0.53.0-rc.local", BuildDigest: "sha256:" + strings.Repeat("a", 64), EvidenceScope: scope, ExternalValidationRequired: externalRequired,
|
||||
Environment: "STAGING", ProviderRef: "cloud-secret-manager:voice-platform", RotationCount: 2, ConsumptionModes: []string{"DYNAMIC_READ", "ROLLING_RESTART"},
|
||||
InputSHA256: map[string]string{"rotation_capture": strings.Repeat("5", 64)}, Checks: checks, Disclaimer: "fixture retains business approval boundary",
|
||||
}
|
||||
}
|
||||
|
||||
func productionRecoveryReportFixture(scope string, externalRequired bool) recoveryconformance.Report {
|
||||
checks := make([]recoveryconformance.Check, 0, len(requiredProductionRecoveryChecks))
|
||||
for _, code := range requiredProductionRecoveryChecks {
|
||||
checks = append(checks, recoveryconformance.Check{Code: code, Status: "PASS", Detail: "fixture check passed"})
|
||||
}
|
||||
durations := map[string]int{}
|
||||
for _, drillType := range recoveryconformance.RequiredDrillTypes {
|
||||
durations[drillType] = 120
|
||||
}
|
||||
return recoveryconformance.Report{
|
||||
SchemaVersion: recoveryconformance.ReportSchema, Result: "PASS", GeneratedAt: time.Date(2026, 8, 25, 12, 0, 0, 0, time.UTC),
|
||||
CandidateVersion: "0.53.0-rc.local", BuildDigest: "sha256:" + strings.Repeat("a", 64), EvidenceScope: scope, ExternalValidationRequired: externalRequired,
|
||||
Environment: "STAGING", InfrastructureRef: "cluster:staging-primary", DrillCount: 4, CompletedDrillTypes: slices.Clone(recoveryconformance.RequiredDrillTypes), ObservedRecoverySeconds: durations,
|
||||
InputSHA256: map[string]string{"recovery_capture": strings.Repeat("6", 64)}, Checks: checks, Disclaimer: "fixture retains release approval boundary",
|
||||
}
|
||||
}
|
||||
|
||||
func productionInfrastructureReportFixture(scope string, externalRequired bool) infrastructureconformance.Report {
|
||||
checks := make([]infrastructureconformance.Check, 0, len(requiredProductionInfrastructureChecks))
|
||||
for _, code := range requiredProductionInfrastructureChecks {
|
||||
checks = append(checks, infrastructureconformance.Check{Code: code, Status: "PASS", Detail: "fixture check passed"})
|
||||
}
|
||||
controlCounts := map[string]int{}
|
||||
for _, component := range infrastructureconformance.RequiredComponents {
|
||||
controlCounts[component] = len(infrastructureconformance.RequiredControls(component))
|
||||
}
|
||||
return infrastructureconformance.Report{
|
||||
SchemaVersion: infrastructureconformance.ReportSchema, Result: "PASS", GeneratedAt: time.Date(2026, 8, 25, 12, 0, 0, 0, time.UTC),
|
||||
CandidateVersion: "0.53.0-rc.local", BuildDigest: "sha256:" + strings.Repeat("a", 64), EvidenceScope: scope, ExternalValidationRequired: externalRequired,
|
||||
Environment: "STAGING", RegionRef: "cn-region-a", ClusterRef: "cluster-a", ComponentCount: len(infrastructureconformance.RequiredComponents),
|
||||
VerifiedComponents: slices.Clone(infrastructureconformance.RequiredComponents), ControlCounts: controlCounts,
|
||||
InputSHA256: map[string]string{"infrastructure_capture": strings.Repeat("7", 64)}, Checks: checks, Disclaimer: "fixture retains release approval boundary",
|
||||
}
|
||||
}
|
||||
|
||||
func safariCompatibilityReportFixture(scope string, externalRequired bool) safariconformance.Report {
|
||||
checks := make([]safariconformance.Check, 0, len(requiredSafariCompatibilityChecks))
|
||||
for _, code := range requiredSafariCompatibilityChecks {
|
||||
checks = append(checks, safariconformance.Check{Code: code, Status: "PASS", Detail: "fixture check passed"})
|
||||
}
|
||||
workflows := map[string]int{}
|
||||
for _, workflow := range safariconformance.RequiredWorkflows {
|
||||
workflows[workflow] = len(safariconformance.RequiredReleaseRanks) * len(safariconformance.RequiredViewports)
|
||||
}
|
||||
viewports := map[string]int{}
|
||||
for _, viewport := range safariconformance.RequiredViewports {
|
||||
viewports[viewport] = len(safariconformance.RequiredReleaseRanks) * len(safariconformance.RequiredWorkflows)
|
||||
}
|
||||
return safariconformance.Report{
|
||||
SchemaVersion: safariconformance.ReportSchema, Result: "PASS", GeneratedAt: time.Date(2026, 8, 25, 12, 0, 0, 0, time.UTC),
|
||||
CandidateVersion: "0.53.0-rc.local", BuildDigest: "sha256:" + strings.Repeat("a", 64), EvidenceScope: scope, ExternalValidationRequired: externalRequired,
|
||||
Environment: "STAGING", BrowserVersions: map[string]string{"CURRENT": "safari-current", "PREVIOUS": "safari-previous"}, RunCount: 2, ResultCount: 24,
|
||||
CompletedReleaseRanks: slices.Clone(safariconformance.RequiredReleaseRanks), CompletedWorkflows: workflows, CompletedViewports: viewports,
|
||||
InputSHA256: map[string]string{"safari_capture": strings.Repeat("8", 64)}, Checks: checks, Disclaimer: "fixture retains release approval boundary",
|
||||
}
|
||||
}
|
||||
|
||||
func realDeviceRTCReportFixture(scope string, externalRequired bool) rtcconformance.Report {
|
||||
checks := make([]rtcconformance.Check, 0, len(requiredRealDeviceRTCChecks))
|
||||
for _, code := range requiredRealDeviceRTCChecks {
|
||||
checks = append(checks, rtcconformance.Check{Code: code, Status: "PASS", Detail: "fixture check passed"})
|
||||
}
|
||||
modes := map[string]int{"s2s_pure": 2, "s2s_hybrid": 2, "cascade": 2}
|
||||
scenarios := map[string]int{"NORMAL_COMPLETION": 3, "TOKEN_RENEWAL": 1, "NETWORK_SWITCH_RECOVERY": 1, "CONTROLLED_STOP": 1}
|
||||
return rtcconformance.Report{
|
||||
SchemaVersion: rtcconformance.ReportSchema, Result: "PASS", GeneratedAt: time.Date(2026, 8, 25, 12, 0, 0, 0, time.UTC),
|
||||
CandidateVersion: "0.53.0-rc.local", BuildDigest: "sha256:" + strings.Repeat("a", 64), EvidenceScope: scope, ExternalValidationRequired: externalRequired,
|
||||
Environment: "STAGING", RTCProviderRef: "volc-rtc:test", VoiceProviderRef: "doubao-voice:test", DeviceCount: 3, SessionCount: 6,
|
||||
CompletedEngineModes: slices.Clone(rtcconformance.RequiredEngineModes), CompletedScenarios: slices.Clone(rtcconformance.RequiredScenarios), ModeSessionCounts: modes, ScenarioSessionCounts: scenarios,
|
||||
InputSHA256: map[string]string{"rtc_capture": strings.Repeat("9", 64)}, Checks: checks, Disclaimer: "fixture retains release approval boundary",
|
||||
}
|
||||
}
|
||||
|
||||
func realAgentModelsReportFixture(scope string, externalRequired bool) agentmodelsconformance.Report {
|
||||
checks := make([]agentmodelsconformance.Check, 0, len(requiredRealAgentModelsChecks))
|
||||
for _, code := range requiredRealAgentModelsChecks {
|
||||
checks = append(checks, agentmodelsconformance.Check{Code: code, Status: "PASS", Detail: "fixture check passed"})
|
||||
}
|
||||
return agentmodelsconformance.Report{
|
||||
SchemaVersion: agentmodelsconformance.ReportSchema, Result: "PASS", GeneratedAt: time.Date(2026, 8, 25, 12, 0, 0, 0, time.UTC),
|
||||
CandidateVersion: "0.53.0-rc.local", BuildDigest: "sha256:" + strings.Repeat("a", 64), EvidenceScope: scope, ExternalValidationRequired: externalRequired, Environment: "STAGING",
|
||||
GenerationProviderRef: "ark:generation", AgentVersion: "agent-v1", ModelConfigRef: "runtime-config-version:model-v1", SuiteVersion: "agent-quality-baseline-v1", GenerationCaseCount: 10,
|
||||
EmbeddingProviderRef: "ark:embedding", EmbeddingModelRef: "embedding-v1", EmbeddingQueryCount: 12, EmbeddingCategories: slices.Clone(agentmodelsconformance.RequiredEmbeddingCategories),
|
||||
ContentSafetyProviderRef: "managed:safety", ContentSafetyPolicyRef: "safety-v1", ContentSafetyCaseCount: 12, ContentSafetyCategories: slices.Clone(agentmodelsconformance.RequiredSafetyCategories),
|
||||
InputSHA256: map[string]string{"models_capture": strings.Repeat("1", 64), "agent_suite": strings.Repeat("2", 64), "agent_observations": strings.Repeat("3", 64), "agent_report": strings.Repeat("4", 64)},
|
||||
Checks: checks, Disclaimer: "fixture retains release approval boundary",
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user