feat: gate releases on platform health
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
# 开发状态与门禁记录
|
||||
|
||||
> 更新日期:2026-08-26
|
||||
> 当前接口契约版本:0.65.0
|
||||
> 当前接口契约版本:0.66.0
|
||||
> 当前主里程碑:M8目标环境联调、试点与上线验收(外部条件待提供)
|
||||
|
||||
## 阶段状态
|
||||
@@ -250,6 +250,7 @@
|
||||
| 真实Agent生成、Embedding和内容安全质量门禁 | 新增`real-agent-models-capture-v1`、`real-agent-models-conformance-report-v1`和`cmd/agent-models-conformance`。内容生成复用现有固定10类Agent评测,并使用评测集、脱敏观测和报告重新计算防篡改;Embedding按版本化阈值覆盖精确/改写召回、跨长者隔离、撤销/敏感排除和无匹配降级六类;内容安全覆盖验证码、金融账户、提示注入、敏感医疗、正常内容和模型输出六类。真实证据还必须绑定受控提供器、账号/区域、数据条款、额度和审计引用;本地生成器、哈希向量和规则固定为`LOCAL_AGENT_TOOLCHAIN/external_validation_required=true`。v3证据包新增`real_agent_models_validation`,该门禁不可豁免。按低强度策略执行聚合器/证据包定向测试和一条重算现有模拟报告的本地工具链冒烟:既有10类模拟报告重新计算一致,两组六类结构完整,输出`PASS/LOCAL_AGENT_TOOLCHAIN`并继续外部待验;证据包保留真实报告、三提供器和12类别共16项缺口,临时文件已清理。 |
|
||||
| 托管内容安全外部预检与告警 | OpenAPI 0.64.0把`content_safety_tls`纳入外部依赖查询和候选验收快照。生产Worker仅执行DNS、TCP和受信TLS握手,不发送待分类正文;结果复用依赖可用性指标、数据库脱敏状态和运营诊断页面,连续5分钟不可用进入统一CRITICAL告警,记录缺失/失败/过期均可阻断候选技术快照但不驱逐Pod。按低强度策略执行App/探针/诊断定向测试、前端类型生成/类型检查/生产构建和Prometheus规则校验。 |
|
||||
| 云端主机健康状态接入 | OpenAPI 0.65.0新增独立HMAC签名的主机健康回调、30天脱敏快照表和运营只读状态页面。五分钟周期上报覆盖公网入口、容器、备份、证书、磁盘和定时器;无签名、过期、重复检查及综合状态不一致报告被拒绝,页面超过15分钟未更新明确标记过期。该能力不替代整机、Callbacks或数据库失联时必须使用的外部探针。 |
|
||||
| 云端主机健康发布门禁 | OpenAPI 0.66.0把最新主机健康快照纳入发布候选技术验收:无上报、超过15分钟、综合预警或严重异常均阻断,只有当前有效且22项检查全部正常才通过。快照仅归档状态、时间和检查数量,不复制磁盘、证书或备份明细值。 |
|
||||
| 阶段8外部执行轻量预检 | 新增`scripts/stage8-preflight.ps1`,以单一只读入口核对候选Git提交、远程仓库、Compose本地基线、外部配置变量名称和十类证据索引结构/状态;报告明确不读取密钥值,也不把环境变量或本地服务可用性当作验收证据。默认仅报告缺口,`-Strict`可供自动化阻断未就绪交接;当前已建立本地`main`候选提交,本地运行基线就绪且索引结构有效,仍缺远程仓库,样例构建摘要为占位值,十类外部证据均为`PENDING`。 |
|
||||
| 候选构建身份贯通 | Compose新增统一候选构建参数,后端二进制和后端/Web镜像OCI标签共享版本、完整Git提交及UTC构建时间;远程CI从OpenAPI版本和运行编号形成候选版本,构建后重新读取两个镜像标签并与`GITHUB_SHA`和构建时间逐项比对,再把候选版本、提交、时间和双镜像摘要写入构建元数据。修复了此前Dockerfile虽支持注入但Compose/CI未传参、运行版本仍为`dev/unknown`的问题。 |
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# 设备 API 兼容性约定
|
||||
|
||||
> 当前设备接口主版本:`/device/v2`
|
||||
> 当前接口契约:OpenAPI 0.65.0
|
||||
> 当前接口契约:OpenAPI 0.66.0
|
||||
> 适用对象:云端 API、终端固件、模拟终端和设备联调工具
|
||||
|
||||
## 1. 兼容目标
|
||||
@@ -44,6 +44,7 @@ CI在拉取请求中以目标分支OpenAPI为基线执行破坏性变更检查
|
||||
|---|---|---|
|
||||
| 0.64.0 | 运营外部预检增加`content_safety_tls`枚举和候选快照第四类可选必需依赖;未修改设备端点、字段、状态机或终端行为 | v2完全兼容 |
|
||||
| 0.65.0 | 新增主机健康签名回调和运营只读查询;未修改设备端点、字段、状态机或终端行为 | v2完全兼容 |
|
||||
| 0.66.0 | 发布候选技术验收快照增加主机健康当前性门禁;未修改设备端点、字段、状态机或终端行为 | v2完全兼容 |
|
||||
| 0.63.0 | 增加设备固件阶段事件查询及云端证据字段;设备已有命令回执仅记录同一结果的不可变事件,未增加旧终端必填字段 | v2兼容,旧终端行为不变 |
|
||||
| 0.62.0 | 命令结果增加A/B写入、待重启、启动健康和回退可选字段,省略时保持原有单分区结果语义 | v2兼容,旧终端可继续省略 |
|
||||
| 0.61.0 | 新增家庭侧固件版本影响只读接口,未修改设备协议 | v2完全兼容 |
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
| `POST /callbacks/v1/platform-health` | 独立HMAC签名 | 新快照返回201;完全重复返回200 |
|
||||
| `GET /api/v1/ops/diagnostics/platform-health` | OIDC + 运营角色 + 客户组织选择 | 返回全平台最新脱敏快照 |
|
||||
|
||||
接口契约见`openapi/openapi.yaml` 0.65.0。
|
||||
接口契约见`openapi/openapi.yaml` 0.66.0。最新快照同时进入发布候选技术验收门禁:缺失、超过15分钟、warning或critical都会阻断,只有当前healthy快照通过。
|
||||
|
||||
## 5. 单机配置
|
||||
|
||||
|
||||
@@ -143,4 +143,4 @@ go test ./internal/releaseevidence ./cmd/release-evidence
|
||||
.\scripts\smoke-release-evidence.ps1
|
||||
```
|
||||
|
||||
烟测使用真实OIDC生成绑定同一候选版本和API镜像摘要的`PILOT`技术验收快照、上一完整自然周的7份日报和1份周报,构造十项全部`PENDING`的外部索引,生成并解包临时ZIP,核对OpenAPI 0.64.0、v3清单、候选一致性、周期证据、摘要、待验计数和真机固件四场景缺口,随后清理临时文件、审计和幂等记录。该结果只证明打包链路可用,不证明十项外部验收、真机固件或投诉证据已经完成。
|
||||
烟测使用真实OIDC生成绑定同一候选版本和API镜像摘要的`PILOT`技术验收快照、上一完整自然周的7份日报和1份周报,构造十项全部`PENDING`的外部索引,生成并解包临时ZIP,核对OpenAPI 0.66.0、v3清单、候选一致性、周期证据、摘要、待验计数和真机固件四场景缺口,随后清理临时文件、审计和幂等记录。技术验收快照还要求主机健康上报在15分钟内且没有warning/critical。该结果只证明打包链路可用,不证明十项外部验收、真机固件或投诉证据已经完成。
|
||||
|
||||
@@ -87,7 +87,7 @@
|
||||
|
||||
| 来源 | 要求 | 当前处理 |
|
||||
|---|---|---|
|
||||
| P0414 | 首期需求、验收用例、缺陷和风险形成对应关系 | 本文完成首期功能到代码、接口、数据和验证的基线;V30问题清单由V36补充严重级别、根因、修复版本、回归证据及未关闭HIGH/CRITICAL发布阻断汇总;OpenAPI 0.50.0—0.55.0可按候选构建导出六项技术门禁及脱敏阻断告警定位快照,绑定候选版本/构建摘要后按业务时区归档日/周运行证据。`cmd/release-evidence`现使用v3清单,把同一候选的快照、接口、迁移、部署、SBOM和十类外部状态形成逐文件摘要;远程CI供应链、真机固件、真实终端RTC、真实Agent模型、真实告警网关、云KMS/HSM轮换、生产等价恢复、生产基础设施和Safari兼容分别要求结构化报告,并把14项CI检查/镜像/SBOM、设备固件场景、语音方式与恢复场景、生成/Embedding/内容安全质量、路由演练、轮换模式、恢复类型、基础设施控制项及Safari矩阵覆盖与缺口写入清单。OpenAPI 0.64.0、V38及九个离线校验器把云端状态与远程构建、外部设备、RTC/语音供应商、模型提供器、网关、密钥提供器、目标环境和浏览器证据绑定;`content_safety_tls`把策略服务网络/证书状态纳入指标、告警、运营页面和候选快照。缺检查、缺制品、缺场景、缺演练、缺质量类别、缺控制项、`PENDING`和本地模拟证据均不会被误报为外部验收完成,整体发布仍必须基于真实联调、灰度和审批记录确认。 |
|
||||
| P0414 | 首期需求、验收用例、缺陷和风险形成对应关系 | 本文完成首期功能到代码、接口、数据和验证的基线;V30问题清单由V36补充严重级别、根因、修复版本、回归证据及未关闭HIGH/CRITICAL发布阻断汇总;OpenAPI 0.50.0—0.66.0可按候选构建导出七项技术门禁及脱敏阻断告警定位快照,绑定候选版本/构建摘要后按业务时区归档日/周运行证据。第七项门禁要求主机健康快照在15分钟内且全部检查正常。`cmd/release-evidence`现使用v3清单,把同一候选的快照、接口、迁移、部署、SBOM和十类外部状态形成逐文件摘要;远程CI供应链、真机固件、真实终端RTC、真实Agent模型、真实告警网关、云KMS/HSM轮换、生产等价恢复、生产基础设施和Safari兼容分别要求结构化报告,并把14项CI检查/镜像/SBOM、设备固件场景、语音方式与恢复场景、生成/Embedding/内容安全质量、路由演练、轮换模式、恢复类型、基础设施控制项及Safari矩阵覆盖与缺口写入清单。V38及九个离线校验器把云端状态与远程构建、外部设备、RTC/语音供应商、模型提供器、网关、密钥提供器、目标环境和浏览器证据绑定;`content_safety_tls`把策略服务网络/证书状态纳入指标、告警、运营页面和候选快照。缺检查、缺制品、缺场景、缺演练、缺质量类别、缺控制项、`PENDING`和本地模拟证据均不会被误报为外部验收完成,整体发布仍必须基于真实联调、灰度和审批记录确认。 |
|
||||
| P0445 | 需求编号关联代码、测试、接口或配置变更 | 本文逐项建立关联;后续功能变更必须同步更新本矩阵和`development-status.md`。 |
|
||||
|
||||
当前仍不能宣告完成的部分集中为:真实火山RTC与豆包三模式联调、真实方舟内容与Embedding质量、真实听筒的长者交流体验、量产设备/KMS注入、跨故障域生产对象存储与备份、真机异常恢复、真实账单和生产级内容安全。阿里云单机环境已经形成加密全组件归档、数据库隔离恢复和Redis RDB校验,但同机归档不能替代托管PITR、异地副本和生产恢复演练。这些剩余项属于阶段8外部验收,不应通过增加本地低价值测试伪造完成证据。
|
||||
|
||||
@@ -3,6 +3,8 @@ package diagnostics
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
@@ -69,6 +71,17 @@ type AcceptanceAlertState struct {
|
||||
BlockingItemsTruncated bool `json:"blocking_items_truncated"`
|
||||
}
|
||||
|
||||
type AcceptancePlatformHealthState struct {
|
||||
State string `json:"state"`
|
||||
Status string `json:"status,omitempty"`
|
||||
ObservedAt *time.Time `json:"observed_at,omitempty"`
|
||||
ReceivedAt *time.Time `json:"received_at,omitempty"`
|
||||
CheckCount int `json:"check_count"`
|
||||
WarningCount int `json:"warning_count"`
|
||||
CriticalCount int `json:"critical_count"`
|
||||
GateSatisfied bool `json:"gate_satisfied"`
|
||||
}
|
||||
|
||||
type AcceptanceBlockingAlert struct {
|
||||
ID string `json:"id"`
|
||||
RuleCode string `json:"rule_code"`
|
||||
@@ -88,24 +101,25 @@ type AcceptanceGate struct {
|
||||
}
|
||||
|
||||
type AcceptanceSnapshot struct {
|
||||
ID string `json:"id"`
|
||||
GeneratedAt time.Time `json:"generated_at"`
|
||||
TenantID string `json:"tenant_id"`
|
||||
CandidateVersion string `json:"candidate_version"`
|
||||
BuildDigest string `json:"build_digest"`
|
||||
Environment string `json:"environment"`
|
||||
RunType string `json:"run_type"`
|
||||
Scope string `json:"scope"`
|
||||
RedactionPolicy string `json:"redaction_policy"`
|
||||
RuntimeBuild buildinfo.Info `json:"runtime_build"`
|
||||
Database AcceptanceDatabaseState `json:"database"`
|
||||
IntegrationIssues IntegrationIssueSummary `json:"integration_issues"`
|
||||
ExternalDependencies []AcceptanceDependencyState `json:"external_dependencies"`
|
||||
Outbox AcceptanceOutboxState `json:"outbox"`
|
||||
Alerts AcceptanceAlertState `json:"alerts"`
|
||||
Gates []AcceptanceGate `json:"gates"`
|
||||
OverallGateSatisfied bool `json:"overall_gate_satisfied"`
|
||||
Disclaimer string `json:"disclaimer"`
|
||||
ID string `json:"id"`
|
||||
GeneratedAt time.Time `json:"generated_at"`
|
||||
TenantID string `json:"tenant_id"`
|
||||
CandidateVersion string `json:"candidate_version"`
|
||||
BuildDigest string `json:"build_digest"`
|
||||
Environment string `json:"environment"`
|
||||
RunType string `json:"run_type"`
|
||||
Scope string `json:"scope"`
|
||||
RedactionPolicy string `json:"redaction_policy"`
|
||||
RuntimeBuild buildinfo.Info `json:"runtime_build"`
|
||||
Database AcceptanceDatabaseState `json:"database"`
|
||||
IntegrationIssues IntegrationIssueSummary `json:"integration_issues"`
|
||||
ExternalDependencies []AcceptanceDependencyState `json:"external_dependencies"`
|
||||
Outbox AcceptanceOutboxState `json:"outbox"`
|
||||
Alerts AcceptanceAlertState `json:"alerts"`
|
||||
PlatformHealth AcceptancePlatformHealthState `json:"platform_health"`
|
||||
Gates []AcceptanceGate `json:"gates"`
|
||||
OverallGateSatisfied bool `json:"overall_gate_satisfied"`
|
||||
Disclaimer string `json:"disclaimer"`
|
||||
}
|
||||
|
||||
func (s *Service) ExportAcceptanceSnapshot(ctx context.Context, principal identity.Principal, tenantPublicID, key, requestID string, input AcceptanceSnapshotInput) (AcceptanceSnapshot, bool, error) {
|
||||
@@ -275,6 +289,11 @@ func buildAcceptanceSnapshot(ctx context.Context, tx pgx.Tx, tenantPublicID, ten
|
||||
); err != nil {
|
||||
return AcceptanceSnapshot{}, err
|
||||
}
|
||||
platformHealth, err := loadAcceptancePlatformHealth(ctx, tx, now)
|
||||
if err != nil {
|
||||
return AcceptanceSnapshot{}, err
|
||||
}
|
||||
snapshot.PlatformHealth = platformHealth
|
||||
|
||||
runtimeSatisfied := input.Environment == "LAB" || (snapshot.RuntimeBuild.Version == input.CandidateVersion && snapshot.RuntimeBuild.Commit != "unknown" && snapshot.RuntimeBuild.BuildTime != "unknown")
|
||||
dependenciesSatisfied := true
|
||||
@@ -290,6 +309,7 @@ func buildAcceptanceSnapshot(ctx context.Context, tx pgx.Tx, tenantPublicID, ten
|
||||
{Code: "integration_issues_clear", Satisfied: snapshot.IntegrationIssues.ReleaseGateSatisfied, Summary: "不得存在未关闭的高或严重级联调问题"},
|
||||
{Code: "outbox_clear", Satisfied: outboxSatisfied, Summary: "租户及平台级待处理、处理中和死信任务必须清零"},
|
||||
{Code: "critical_alerts_clear", Satisfied: alertsSatisfied, Summary: "不得存在未解决的高或严重级告警"},
|
||||
{Code: "platform_health_current", Satisfied: snapshot.PlatformHealth.GateSatisfied, Summary: acceptancePlatformHealthSummary(snapshot.PlatformHealth)},
|
||||
}
|
||||
snapshot.OverallGateSatisfied = true
|
||||
for _, gate := range snapshot.Gates {
|
||||
@@ -298,6 +318,60 @@ func buildAcceptanceSnapshot(ctx context.Context, tx pgx.Tx, tenantPublicID, ten
|
||||
return snapshot, nil
|
||||
}
|
||||
|
||||
func loadAcceptancePlatformHealth(ctx context.Context, tx pgx.Tx, now time.Time) (AcceptancePlatformHealthState, error) {
|
||||
state := AcceptancePlatformHealthState{State: "MISSING"}
|
||||
var observedAt, receivedAt time.Time
|
||||
var checksJSON []byte
|
||||
err := tx.QueryRow(ctx, `
|
||||
SELECT status,observed_at,received_at,checks
|
||||
FROM platform_health_snapshot ORDER BY observed_at DESC,id DESC LIMIT 1`).Scan(
|
||||
&state.Status, &observedAt, &receivedAt, &checksJSON,
|
||||
)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return state, nil
|
||||
}
|
||||
if err != nil {
|
||||
return AcceptancePlatformHealthState{}, err
|
||||
}
|
||||
var checks []PlatformHealthCheck
|
||||
if err = json.Unmarshal(checksJSON, &checks); err != nil {
|
||||
return AcceptancePlatformHealthState{}, err
|
||||
}
|
||||
return evaluateAcceptancePlatformHealth(state.Status, observedAt, receivedAt, checks, now), nil
|
||||
}
|
||||
|
||||
func evaluateAcceptancePlatformHealth(status string, observedAt, receivedAt time.Time, checks []PlatformHealthCheck, now time.Time) AcceptancePlatformHealthState {
|
||||
state := AcceptancePlatformHealthState{
|
||||
State: "CURRENT", Status: status, ObservedAt: &observedAt, ReceivedAt: &receivedAt, CheckCount: len(checks),
|
||||
}
|
||||
for _, check := range checks {
|
||||
switch check.Status {
|
||||
case "warning":
|
||||
state.WarningCount++
|
||||
case "critical":
|
||||
state.CriticalCount++
|
||||
}
|
||||
}
|
||||
if observedAt.Before(now.Add(-platformHealthMaxAge)) || observedAt.After(now.Add(platformHealthFuture)) {
|
||||
state.State = "STALE"
|
||||
}
|
||||
state.GateSatisfied = state.State == "CURRENT" && state.Status == "healthy" && state.WarningCount == 0 && state.CriticalCount == 0
|
||||
return state
|
||||
}
|
||||
|
||||
func acceptancePlatformHealthSummary(state AcceptancePlatformHealthState) string {
|
||||
switch state.State {
|
||||
case "MISSING":
|
||||
return "尚未收到云端主机健康快照"
|
||||
case "STALE":
|
||||
return "云端主机健康快照必须在15分钟内更新"
|
||||
}
|
||||
if state.Status != "healthy" || state.WarningCount > 0 || state.CriticalCount > 0 {
|
||||
return fmt.Sprintf("云端主机存在%d项预警和%d项严重异常", state.WarningCount, state.CriticalCount)
|
||||
}
|
||||
return fmt.Sprintf("云端主机健康快照当前有效,共%d项检查", state.CheckCount)
|
||||
}
|
||||
|
||||
func acceptanceRuntimeSummary(environment string) string {
|
||||
if environment == "LAB" {
|
||||
return "实验室环境允许开发构建,但候选版本与镜像摘要必须明确"
|
||||
|
||||
@@ -59,7 +59,7 @@ func TestAcceptanceSnapshotIsAuditedIdempotentAndTenantScoped(t *testing.T) {
|
||||
if err != nil || !created {
|
||||
t.Fatalf("first=%+v created=%t err=%v", first, created, err)
|
||||
}
|
||||
if first.ID == "" || first.Database.MigrationVersion < 36 || first.Database.Dirty || first.RedactionPolicy != "ops-acceptance-minimal-v1" || len(first.ExternalDependencies) != 1 || len(first.Gates) != 6 {
|
||||
if first.ID == "" || first.Database.MigrationVersion < 39 || first.Database.Dirty || first.RedactionPolicy != "ops-acceptance-minimal-v1" || len(first.ExternalDependencies) != 1 || len(first.Gates) != 7 {
|
||||
t.Fatalf("unexpected snapshot=%+v", first)
|
||||
}
|
||||
if first.ExternalDependencies[0].Dependency != "oidc_jwks" || first.ExternalDependencies[0].ServiceName != "api" {
|
||||
@@ -74,6 +74,9 @@ func TestAcceptanceSnapshotIsAuditedIdempotentAndTenantScoped(t *testing.T) {
|
||||
if !foundBlockingAlert || first.Alerts.Blocking == 0 || first.Gates[5].Code != "critical_alerts_clear" || first.Gates[5].Satisfied {
|
||||
t.Fatalf("blocking alert evidence=%+v gates=%+v", first.Alerts, first.Gates)
|
||||
}
|
||||
if first.Gates[6].Code != "platform_health_current" || first.PlatformHealth.State == "" {
|
||||
t.Fatalf("platform health=%+v gates=%+v", first.PlatformHealth, first.Gates)
|
||||
}
|
||||
replayed, createdAgain, err := service.ExportAcceptanceSnapshot(ctx, admin, tenantPublicID, key, requestID, input)
|
||||
if err != nil || createdAgain || replayed.ID != first.ID || !replayed.GeneratedAt.Equal(first.GeneratedAt) {
|
||||
t.Fatalf("replayed=%+v created=%t err=%v", replayed, createdAgain, err)
|
||||
|
||||
@@ -3,6 +3,7 @@ package diagnostics
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestValidAcceptanceSnapshotInputLimitsAuditReason(t *testing.T) {
|
||||
@@ -24,6 +25,52 @@ func TestValidAcceptanceSnapshotInputLimitsAuditReason(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluateAcceptancePlatformHealthBlocksStaleAndUnhealthyReports(t *testing.T) {
|
||||
now := time.Date(2026, 8, 26, 14, 0, 0, 0, time.UTC)
|
||||
tests := []struct {
|
||||
name string
|
||||
status string
|
||||
observedAt time.Time
|
||||
checks []PlatformHealthCheck
|
||||
state string
|
||||
satisfied bool
|
||||
}{
|
||||
{
|
||||
name: "current healthy", status: "healthy", observedAt: now.Add(-5 * time.Minute),
|
||||
checks: []PlatformHealthCheck{{Name: "public_https", Status: "healthy", Value: "http_200"}}, state: "CURRENT", satisfied: true,
|
||||
},
|
||||
{
|
||||
name: "stale healthy", status: "healthy", observedAt: now.Add(-16 * time.Minute),
|
||||
checks: []PlatformHealthCheck{{Name: "public_https", Status: "healthy", Value: "http_200"}}, state: "STALE", satisfied: false,
|
||||
},
|
||||
{
|
||||
name: "current warning", status: "warning", observedAt: now.Add(-5 * time.Minute),
|
||||
checks: []PlatformHealthCheck{{Name: "backup", Status: "warning", Value: "late"}}, state: "CURRENT", satisfied: false,
|
||||
},
|
||||
{
|
||||
name: "current critical", status: "critical", observedAt: now.Add(-5 * time.Minute),
|
||||
checks: []PlatformHealthCheck{{Name: "disk", Status: "critical", Value: "full"}}, state: "CURRENT", satisfied: false,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
state := evaluateAcceptancePlatformHealth(test.status, test.observedAt, now, test.checks, now)
|
||||
if state.State != test.state || state.GateSatisfied != test.satisfied {
|
||||
t.Fatalf("state=%+v", state)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAcceptancePlatformHealthSummaryCoversMissingAndCounts(t *testing.T) {
|
||||
if summary := acceptancePlatformHealthSummary(AcceptancePlatformHealthState{State: "MISSING"}); !strings.Contains(summary, "尚未收到") {
|
||||
t.Fatalf("missing summary=%q", summary)
|
||||
}
|
||||
if summary := acceptancePlatformHealthSummary(AcceptancePlatformHealthState{State: "CURRENT", Status: "warning", WarningCount: 2, CriticalCount: 1}); !strings.Contains(summary, "2项预警") || !strings.Contains(summary, "1项严重异常") {
|
||||
t.Fatalf("unhealthy summary=%q", summary)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAcceptanceSnapshotAllowsAllProductionExternalDependencies(t *testing.T) {
|
||||
input := AcceptanceSnapshotInput{
|
||||
CandidateVersion: "0.64.0-rc.1", BuildDigest: "sha256:" + strings.Repeat("b", 64),
|
||||
|
||||
+20
-6
@@ -1,8 +1,8 @@
|
||||
openapi: 3.1.0
|
||||
info:
|
||||
title: 数据采集终端—云端系统 API
|
||||
version: 0.65.0
|
||||
description: 工程框架、SaaS核心业务、设备v2能力协商与智能体启动状态查询、首次通话就绪与最低固件门禁、背景资料准备失败闭环、活动通话授权撤回安全停止、语音内容闭环、终端终稿补传、批量设备遥测、录音上传队列、隐私对账、固件、双轨录音、状态事件、具体资源分享、受控运营配置、Agent组合组件引用、质量证据门禁、运营自定义版本族投放、新通话运行时冻结与供应商启动传递、带并发版本校验的配置投放暂停恢复、带严重度和验收证据门禁的联调问题闭环、含阻断告警定位证据的发布候选技术验收快照、绑定候选版本与构建摘要并按业务时区生成的可审计试点日/周运行证据、可审计且不生成正式告警的接收器联通测试、外部依赖预检台账、证书到期清单,以及带价格版本冻结、供应商用量去重和硬额度控制的费用治理接口。
|
||||
version: 0.66.0
|
||||
description: 工程框架、SaaS核心业务、设备v2能力协商与智能体启动状态查询、首次通话就绪与最低固件门禁、背景资料准备失败闭环、活动通话授权撤回安全停止、语音内容闭环、终端终稿补传、批量设备遥测、录音上传队列、隐私对账、固件、双轨录音、状态事件、具体资源分享、受控运营配置、Agent组合组件引用、质量证据门禁、运营自定义版本族投放、新通话运行时冻结与供应商启动传递、带并发版本校验的配置投放暂停恢复、带严重度和验收证据门禁的联调问题闭环、含阻断告警定位及当前主机健康证据的发布候选技术验收快照、绑定候选版本与构建摘要并按业务时区生成的可审计试点日/周运行证据、可审计且不生成正式告警的接收器联通测试、外部依赖预检台账、证书到期清单,以及带价格版本冻结、供应商用量去重和硬额度控制的费用治理接口。
|
||||
servers:
|
||||
- url: http://localhost:58080
|
||||
description: 本地开发
|
||||
@@ -3271,6 +3271,19 @@ components:
|
||||
properties:
|
||||
migration_version: { type: integer, format: int64, minimum: 0 }
|
||||
dirty: { type: boolean }
|
||||
OpsAcceptancePlatformHealthState:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required: [state, check_count, warning_count, critical_count, gate_satisfied]
|
||||
properties:
|
||||
state: { type: string, enum: [CURRENT, STALE, MISSING] }
|
||||
status: { type: string, enum: [healthy, warning, critical] }
|
||||
observed_at: { type: string, format: date-time }
|
||||
received_at: { type: string, format: date-time }
|
||||
check_count: { type: integer, minimum: 0, maximum: 64 }
|
||||
warning_count: { type: integer, minimum: 0, maximum: 64 }
|
||||
critical_count: { type: integer, minimum: 0, maximum: 64 }
|
||||
gate_satisfied: { type: boolean, description: 仅当快照未超过15分钟且综合状态及全部检查均为healthy时为true。 }
|
||||
OpsAcceptanceDependencyState:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
@@ -3325,13 +3338,13 @@ components:
|
||||
additionalProperties: false
|
||||
required: [code, satisfied, summary]
|
||||
properties:
|
||||
code: { type: string, enum: [candidate_build_identified, database_migration_clean, required_dependencies_available, integration_issues_clear, outbox_clear, critical_alerts_clear] }
|
||||
code: { type: string, enum: [candidate_build_identified, database_migration_clean, required_dependencies_available, integration_issues_clear, outbox_clear, critical_alerts_clear, platform_health_current] }
|
||||
satisfied: { type: boolean }
|
||||
summary: { type: string }
|
||||
OpsAcceptanceSnapshot:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required: [id, generated_at, tenant_id, candidate_version, build_digest, environment, run_type, scope, redaction_policy, runtime_build, database, integration_issues, external_dependencies, outbox, alerts, gates, overall_gate_satisfied, disclaimer]
|
||||
required: [id, generated_at, tenant_id, candidate_version, build_digest, environment, run_type, scope, redaction_policy, runtime_build, database, integration_issues, external_dependencies, outbox, alerts, platform_health, gates, overall_gate_satisfied, disclaimer]
|
||||
properties:
|
||||
id: { type: string, format: uuid }
|
||||
generated_at: { type: string, format: date-time }
|
||||
@@ -3345,10 +3358,11 @@ components:
|
||||
runtime_build: { $ref: '#/components/schemas/BuildInfo' }
|
||||
database: { $ref: '#/components/schemas/OpsAcceptanceDatabaseState' }
|
||||
integration_issues: { $ref: '#/components/schemas/IntegrationIssueSummary' }
|
||||
external_dependencies: { type: array, minItems: 1, maxItems: 3, items: { $ref: '#/components/schemas/OpsAcceptanceDependencyState' } }
|
||||
external_dependencies: { type: array, minItems: 1, maxItems: 4, items: { $ref: '#/components/schemas/OpsAcceptanceDependencyState' } }
|
||||
outbox: { $ref: '#/components/schemas/OpsAcceptanceOutboxState' }
|
||||
alerts: { $ref: '#/components/schemas/OpsAcceptanceAlertState' }
|
||||
gates: { type: array, minItems: 6, maxItems: 6, items: { $ref: '#/components/schemas/OpsAcceptanceGate' } }
|
||||
platform_health: { $ref: '#/components/schemas/OpsAcceptancePlatformHealthState' }
|
||||
gates: { type: array, minItems: 7, maxItems: 7, items: { $ref: '#/components/schemas/OpsAcceptanceGate' } }
|
||||
overall_gate_satisfied: { type: boolean }
|
||||
disclaimer: { type: string }
|
||||
OpsAcceptanceSnapshotResponse:
|
||||
|
||||
@@ -78,7 +78,7 @@ try {
|
||||
if ($result.status -ne 'EXTERNAL_VALIDATION_PENDING' -or $result.pending -ne 10 -or $result.verified -ne 0 -or $result.sha256 -notmatch '^sha256:[a-f0-9]{64}$') { throw '证据包生成结果不正确' }
|
||||
Expand-Archive -LiteralPath $bundlePath -DestinationPath $expandedPath
|
||||
$manifest = Get-Content -Raw -LiteralPath (Join-Path $expandedPath 'release-manifest.json') | ConvertFrom-Json
|
||||
if (-not $manifest.technical_gate_satisfied -or $manifest.evidence_status -ne 'EXTERNAL_VALIDATION_PENDING' -or $manifest.pending_count -ne 10 -or $manifest.openapi_version -ne '0.65.0' -or $manifest.schema_version -ne 'release-candidate-evidence-bundle-v3' -or -not $manifest.pilot_evidence_included -or $manifest.pilot_daily_count -ne 7 -or $manifest.pilot_week_start -ne $weekStartText -or $manifest.pilot_timezone -ne 'Asia/Shanghai') { throw '证据包清单内容不正确' }
|
||||
if (-not $manifest.technical_gate_satisfied -or $manifest.evidence_status -ne 'EXTERNAL_VALIDATION_PENDING' -or $manifest.pending_count -ne 10 -or $manifest.openapi_version -ne '0.66.0' -or $manifest.schema_version -ne 'release-candidate-evidence-bundle-v3' -or -not $manifest.pilot_evidence_included -or $manifest.pilot_daily_count -ne 7 -or $manifest.pilot_week_start -ne $weekStartText -or $manifest.pilot_timezone -ne 'Asia/Shanghai') { throw '证据包清单内容不正确' }
|
||||
if ($manifest.firmware_validation.status -ne 'PENDING' -or $manifest.firmware_validation.coverage_complete -or $manifest.firmware_validation.required_device_count -ne 3 -or @($manifest.firmware_validation.required_scenarios).Count -ne 4 -or @($manifest.firmware_validation.gaps).Count -ne 5) { throw '真机固件场景缺口未在证据包中保留' }
|
||||
if ($manifest.alert_receiver_validation.status -ne 'PENDING' -or $manifest.alert_receiver_validation.coverage_complete -or $manifest.alert_receiver_validation.report_present -or @($manifest.alert_receiver_validation.gaps).Count -ne 2) { throw '真实告警接收器与正式演练缺口未在证据包中保留' }
|
||||
if ($manifest.kms_rotation_validation.status -ne 'PENDING' -or $manifest.kms_rotation_validation.coverage_complete -or $manifest.kms_rotation_validation.report_present -or @($manifest.kms_rotation_validation.gaps).Count -ne 3) { throw '真实KMS/HSM两类轮换缺口未在证据包中保留' }
|
||||
|
||||
Vendored
+17
-1
@@ -2694,6 +2694,21 @@ export interface components {
|
||||
migration_version: number;
|
||||
dirty: boolean;
|
||||
};
|
||||
OpsAcceptancePlatformHealthState: {
|
||||
/** @enum {string} */
|
||||
state: "CURRENT" | "STALE" | "MISSING";
|
||||
/** @enum {string} */
|
||||
status?: "healthy" | "warning" | "critical";
|
||||
/** Format: date-time */
|
||||
observed_at?: string;
|
||||
/** Format: date-time */
|
||||
received_at?: string;
|
||||
check_count: number;
|
||||
warning_count: number;
|
||||
critical_count: number;
|
||||
/** @description 仅当快照未超过15分钟且综合状态及全部检查均为healthy时为true。 */
|
||||
gate_satisfied: boolean;
|
||||
};
|
||||
OpsAcceptanceDependencyState: {
|
||||
/** @enum {string} */
|
||||
service_name: "api" | "worker";
|
||||
@@ -2758,7 +2773,7 @@ export interface components {
|
||||
};
|
||||
OpsAcceptanceGate: {
|
||||
/** @enum {string} */
|
||||
code: "candidate_build_identified" | "database_migration_clean" | "required_dependencies_available" | "integration_issues_clear" | "outbox_clear" | "critical_alerts_clear";
|
||||
code: "candidate_build_identified" | "database_migration_clean" | "required_dependencies_available" | "integration_issues_clear" | "outbox_clear" | "critical_alerts_clear" | "platform_health_current";
|
||||
satisfied: boolean;
|
||||
summary: string;
|
||||
};
|
||||
@@ -2784,6 +2799,7 @@ export interface components {
|
||||
external_dependencies: components["schemas"]["OpsAcceptanceDependencyState"][];
|
||||
outbox: components["schemas"]["OpsAcceptanceOutboxState"];
|
||||
alerts: components["schemas"]["OpsAcceptanceAlertState"];
|
||||
platform_health: components["schemas"]["OpsAcceptancePlatformHealthState"];
|
||||
gates: components["schemas"]["OpsAcceptanceGate"][];
|
||||
overall_gate_satisfied: boolean;
|
||||
disclaimer: string;
|
||||
|
||||
@@ -152,5 +152,5 @@ function platformHealthCheckStatusLabel(value:PlatformHealthSnapshot["checks"][n
|
||||
function platformHealthCheckLabel(value:string){const labels:Record<string,string>={public_origin:"公网地址",public_https:"HTTPS入口",oidc_issuer:"身份签发地址",backup_freshness:"本地备份",offsite_backup:"异地备份",backup_service:"备份服务","platform-backup.timer":"备份定时器","platform-offsite-backup.timer":"异地备份定时器","certbot-ip-renew.timer":"证书续期定时器",certificate:"HTTPS证书",disk_usage:"磁盘空间",container_postgres:"PostgreSQL",container_redis:"Redis",container_minio:"对象存储",container_keycloak:"身份服务",container_api:"业务API",container_callbacks:"回调服务",container_worker:"后台任务",container_web:"前端页面",container_gateway:"HTTPS网关","container_mock-provider":"模拟语音提供器","container_mock-alert-receiver":"模拟告警接收器"};return labels[value]??value}
|
||||
function platformHealthValueLabel(_name:string,value:string){if(value==="not_required")return "当前未要求启用";if(value==="issuer_matches")return "签发地址一致";if(value==="http_200")return "公网访问成功";if(value==="enabled/active")return "已启用并运行";if(value==="success")return "最近执行成功";if(value.startsWith("age_seconds=")){const seconds=Number(value.slice(12));return Number.isFinite(seconds)?`距最近成功 ${formatDuration(seconds*1000)}`:value}if(value.startsWith("remaining_seconds=")){const seconds=Number(value.slice(18));return Number.isFinite(seconds)?`剩余 ${Math.floor(seconds/86400)} 天` : value}if(value.startsWith("used_percent="))return `已使用 ${value.slice(13)}%`;if(value==="running/healthy"||value==="running/none")return "容器正在运行";return value}
|
||||
function downloadJSON(value:unknown,filename:string){const blob=new Blob([JSON.stringify(value,null,2)],{type:"application/json"});const url=URL.createObjectURL(blob);const anchor=document.createElement("a");anchor.href=url;anchor.download=filename;anchor.click();URL.revokeObjectURL(url)}
|
||||
function acceptanceGateLabel(value:OpsAcceptanceSnapshot["gates"][number]["code"]){return ({candidate_build_identified:"候选构建",database_migration_clean:"数据库迁移",required_dependencies_available:"必需外部依赖",integration_issues_clear:"高严重度问题",outbox_clear:"可靠任务",critical_alerts_clear:"高严重度告警"} as Record<OpsAcceptanceSnapshot["gates"][number]["code"],string>)[value]}
|
||||
function acceptanceGateLabel(value:OpsAcceptanceSnapshot["gates"][number]["code"]){return ({candidate_build_identified:"候选构建",database_migration_clean:"数据库迁移",required_dependencies_available:"必需外部依赖",integration_issues_clear:"高严重度问题",outbox_clear:"可靠任务",critical_alerts_clear:"高严重度告警",platform_health_current:"云端主机健康"} as Record<OpsAcceptanceSnapshot["gates"][number]["code"],string>)[value]}
|
||||
function acceptanceResourceStateLabel(value:OpsAcceptanceSnapshot["alerts"]["blocking_items"][number]["resource_state"]){return ({PRESENT:"资源存在",MISSING:"资源已不存在,仍需人工复核关闭",AGGREGATE:"聚合队列",UNKNOWN:"未提供自动核对规则"} as Record<OpsAcceptanceSnapshot["alerts"]["blocking_items"][number]["resource_state"],string>)[value]}
|
||||
|
||||
Reference in New Issue
Block a user