fix(repo):统一发布健康与只读质量门禁
bat-rust / Build and test Rust (push) Canceled after 0s
bat-rust / Build and test Go API (push) Canceled after 0s

This commit is contained in:
2026-09-13 22:09:44 +08:00
parent 32fc64fa83
commit c17904ee1c
25 changed files with 869 additions and 184 deletions
+297 -17
View File
@@ -32,6 +32,89 @@ func fixtureRoot(t *testing.T) string {
return abs
}
func copyFixtureRoot(t *testing.T) string {
t.Helper()
source := fixtureRoot(t)
target := filepath.Join(t.TempDir(), "release")
if err := filepath.Walk(source, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
rel, err := filepath.Rel(source, path)
if err != nil {
return err
}
destination := filepath.Join(target, rel)
if info.IsDir() {
return os.MkdirAll(destination, info.Mode().Perm())
}
data, err := os.ReadFile(path)
if err != nil {
return err
}
return os.WriteFile(destination, data, info.Mode().Perm())
}); err != nil {
t.Fatal(err)
}
return target
}
func fixtureRPCBackend(t *testing.T, root string) *fakeBackend {
t.Helper()
idx, err := LoadIndexFromResourceRoot(root)
if err != nil {
t.Fatal(err)
}
manifestEntries := make([]backendrpc.ResourceManifestEntry, 0, len(idx.Entries))
for _, entry := range idx.Entries {
size := entry.Bytes
manifestEntries = append(manifestEntries, backendrpc.ResourceManifestEntry{
URL: entry.URL,
Destination: entry.RelativePath,
Bytes: &size,
BLAKE3: entry.BLAKE3,
})
}
catalog, err := json.Marshal(map[string]any{
"available": true,
"status": "published",
"version": map[string]any{
"id": "official-fixture",
"resource_root": root,
},
})
if err != nil {
t.Fatal(err)
}
return &fakeBackend{
status: &backendrpc.DaemonStatusReport{Status: "ok", Running: true, RPCAvailable: true},
doctor: &backendrpc.DoctorReport{Healthy: true, Status: "ok"},
catalog: catalog,
manifest: &backendrpc.ResourceManifestPage{
Available: true,
ResourceRoot: root,
ManifestVersion: 1,
TotalEntries: len(manifestEntries),
Entries: manifestEntries,
},
releaseStatus: &backendrpc.ReleaseStatusReport{
Status: "ready",
StatusCode: "distribution.ready",
OfficialCurrentReleaseID: "official-fixture",
DefaultDistributionChannel: "official",
OfficialDistributionReady: true,
Releases: []backendrpc.ReleaseSummary{{
Channel: "official",
ID: "official-fixture",
Current: true,
DistributionIntegrityStatus: "valid",
}},
},
}
}
const fixtureCurrentCDNPath = "/prod-clientpatch.bluearchiveyostar.com/r93_fixture/TableBundles/TableCatalog.bytes"
func TestLoadIndexFromResourceRoot(t *testing.T) {
idx, err := LoadIndexFromResourceRoot(fixtureRoot(t))
if err != nil {
@@ -89,6 +172,188 @@ func TestReleaseSummaryRequiresCompleteManifest(t *testing.T) {
}
}
func TestCurrentCDNRequiresRustWholeReleaseHealth(t *testing.T) {
tests := []struct {
name string
mutate func(t *testing.T, root string)
}{
{
name: "missing entry",
mutate: func(t *testing.T, root string) {
t.Helper()
if err := os.Remove(filepath.Join(root, "prod-clientpatch.bluearchiveyostar.com/r93_fixture/TableBundles/TableCatalog.hash")); err != nil {
t.Fatal(err)
}
},
},
{
name: "size mismatch",
mutate: func(t *testing.T, root string) {
t.Helper()
if err := os.WriteFile(filepath.Join(root, "prod-clientpatch.bluearchiveyostar.com/r93_fixture/TableBundles/TableCatalog.hash"), []byte("too-large"), 0o644); err != nil {
t.Fatal(err)
}
},
},
{
name: "same-size corruption",
mutate: func(t *testing.T, root string) {
t.Helper()
if err := os.WriteFile(filepath.Join(root, "prod-clientpatch.bluearchiveyostar.com/r93_fixture/TableBundles/TableCatalog.hash"), []byte("CORRUPTED!"), 0o644); err != nil {
t.Fatal(err)
}
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
root := copyFixtureRoot(t)
tt.mutate(t, root)
backend := fixtureRPCBackend(t, root)
backend.releaseStatus = &backendrpc.ReleaseStatusReport{
Status: "blocked",
StatusCode: "distribution.blocked",
OfficialCurrentReleaseID: "official-fixture",
DefaultDistributionChannel: "official",
OfficialDistributionReady: false,
Releases: []backendrpc.ReleaseSummary{{
Channel: "official",
ID: "official-fixture",
Current: true,
DistributionIntegrityStatus: "invalid",
Diagnostics: []string{"fixture integrity failure"},
}},
}
cfg := DefaultConfig()
cfg.RequireIndexed = false
cfg.RefreshInterval = 0
if err := cfg.Normalize(); err != nil {
t.Fatal(err)
}
server := NewServer(cfg, backend, log.New(io.Discard, "", 0))
if err := server.Refresh(context.Background()); err != nil {
t.Fatal(err)
}
summary := server.index().Summary()
if summary.Ready || summary.Distribution.Ready ||
summary.Distribution.StatusCode != "distribution.blocked" {
t.Fatalf("summary=%+v", summary)
}
for _, path := range []string{"/readyz", "/v1/bootstrap"} {
recorder := httptest.NewRecorder()
server.Handler().ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, path, nil))
if recorder.Code != http.StatusServiceUnavailable {
t.Fatalf("%s status=%d body=%s", path, recorder.Code, recorder.Body.String())
}
}
recorder := httptest.NewRecorder()
server.Handler().ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, fixtureCurrentCDNPath, nil))
if recorder.Code != http.StatusServiceUnavailable {
t.Fatalf("current CDN status=%d body=%s", recorder.Code, recorder.Body.String())
}
if recorder.Header().Get("ETag") != "" || recorder.Header().Get("Cache-Control") != "" {
t.Fatalf("unhealthy CDN headers=%v", recorder.Header())
}
})
}
}
func TestRefreshCurrentReleaseHealthTransitionsAndClearsFailure(t *testing.T) {
root := copyFixtureRoot(t)
backend := fixtureRPCBackend(t, root)
cfg := DefaultConfig()
cfg.RefreshInterval = 0
if err := cfg.Normalize(); err != nil {
t.Fatal(err)
}
server := NewServer(cfg, backend, log.New(io.Discard, "", 0))
if err := server.Refresh(context.Background()); err != nil {
t.Fatal(err)
}
if !server.index().Summary().Ready {
t.Fatal("initial release is not ready")
}
get := func() *httptest.ResponseRecorder {
recorder := httptest.NewRecorder()
server.Handler().ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, fixtureCurrentCDNPath, nil))
return recorder
}
if recorder := get(); recorder.Code != http.StatusOK || recorder.Body.String() != "TABLE_CATALOG_FIXTURE" {
t.Fatalf("healthy CDN status=%d body=%q", recorder.Code, recorder.Body.String())
}
// The bytes stay the same size, but Rust's next release.status result
// revokes whole-release distribution authorization.
if err := os.WriteFile(filepath.Join(root, "prod-clientpatch.bluearchiveyostar.com/r93_fixture/TableBundles/TableCatalog.hash"), []byte("CORRUPTED!"), 0o644); err != nil {
t.Fatal(err)
}
backend.releaseStatus = &backendrpc.ReleaseStatusReport{
Status: "blocked",
StatusCode: "distribution.blocked",
OfficialCurrentReleaseID: "official-fixture",
DefaultDistributionChannel: "official",
Releases: []backendrpc.ReleaseSummary{{
Channel: "official",
ID: "official-fixture",
Current: true,
DistributionIntegrityStatus: "invalid",
}},
}
if err := server.Refresh(context.Background()); err != nil {
t.Fatal(err)
}
if server.index().Summary().Ready || get().Code != http.StatusServiceUnavailable {
t.Fatalf("unhealthy refresh summary=%+v", server.index().Summary())
}
backend.releaseStatusErr = errors.New("release.status transport failure")
if err := server.Refresh(context.Background()); err == nil {
t.Fatal("expected refresh failure")
}
if summary := server.index().Summary(); summary.Ready || summary.ResourceRoot != "" {
t.Fatalf("failed refresh retained snapshot=%+v", summary)
}
healthRecorder := httptest.NewRecorder()
server.Handler().ServeHTTP(healthRecorder, httptest.NewRequest(http.MethodGet, "/healthz", nil))
var health map[string]any
if err := json.Unmarshal(healthRecorder.Body.Bytes(), &health); err != nil {
t.Fatal(err)
}
refresh := health["refresh"].(map[string]any)
if refresh["last_error"] == "" {
t.Fatalf("refresh diagnostics=%v", refresh)
}
if err := os.WriteFile(filepath.Join(root, "prod-clientpatch.bluearchiveyostar.com/r93_fixture/TableBundles/TableCatalog.hash"), []byte("1234567890"), 0o644); err != nil {
t.Fatal(err)
}
backend.releaseStatusErr = nil
backend.releaseStatus = &backendrpc.ReleaseStatusReport{
Status: "ready",
StatusCode: "distribution.ready",
OfficialCurrentReleaseID: "official-fixture",
DefaultDistributionChannel: "official",
OfficialDistributionReady: true,
Releases: []backendrpc.ReleaseSummary{{
Channel: "official",
ID: "official-fixture",
Current: true,
DistributionIntegrityStatus: "valid",
}},
}
if err := server.Refresh(context.Background()); err != nil {
t.Fatal(err)
}
if summary := server.index().Summary(); !summary.Ready || !summary.Distribution.Ready {
t.Fatalf("recovered summary=%+v", summary)
}
if recorder := get(); recorder.Code != http.StatusOK || recorder.Body.String() != "TABLE_CATALOG_FIXTURE" {
t.Fatalf("recovered CDN status=%d body=%q", recorder.Code, recorder.Body.String())
}
}
func TestSplitCDNPathRejectsEscape(t *testing.T) {
if _, _, err := SplitCDNPath("/prod-clientpatch.bluearchiveyostar.com/../etc/passwd"); err == nil {
t.Fatal("expected error")
@@ -553,18 +818,21 @@ func TestServerInfoRewritesAddressablesOnly(t *testing.T) {
}
type fakeBackend struct {
statusCalls int
doctorCalls int
status *backendrpc.DaemonStatusReport
doctor *backendrpc.DoctorReport
catalog json.RawMessage
resource *backendrpc.ResourceState
manifest *backendrpc.ResourceManifestPage
daemonLogs *backendrpc.LogsReport
taskList *backendrpc.TaskList
taskStatus *backendrpc.TaskRecord
taskLogs *backendrpc.TaskLogs
taskCancel *backendrpc.TaskCancelResult
statusCalls int
doctorCalls int
releaseStatusCalls int
status *backendrpc.DaemonStatusReport
doctor *backendrpc.DoctorReport
releaseStatus *backendrpc.ReleaseStatusReport
releaseStatusErr error
catalog json.RawMessage
resource *backendrpc.ResourceState
manifest *backendrpc.ResourceManifestPage
daemonLogs *backendrpc.LogsReport
taskList *backendrpc.TaskList
taskStatus *backendrpc.TaskRecord
taskLogs *backendrpc.TaskLogs
taskCancel *backendrpc.TaskCancelResult
}
func (f *fakeBackend) DaemonStatus(ctx context.Context) (*backendrpc.DaemonStatusReport, error) {
@@ -573,11 +841,23 @@ func (f *fakeBackend) DaemonStatus(ctx context.Context) (*backendrpc.DaemonStatu
}
func (f *fakeBackend) DaemonDoctor(ctx context.Context) (*backendrpc.DoctorReport, error) {
f.doctorCalls++
if f.statusCalls == 0 {
// status must be called first in real DiscoverAndIndex; this is asserted by call order.
}
return f.doctor, nil
}
func (f *fakeBackend) ReleaseStatus(ctx context.Context) (*backendrpc.ReleaseStatusReport, error) {
f.releaseStatusCalls++
if f.releaseStatusErr != nil {
return nil, f.releaseStatusErr
}
if f.releaseStatus != nil {
return f.releaseStatus, nil
}
return &backendrpc.ReleaseStatusReport{
Status: "ready",
StatusCode: "distribution.ready",
DefaultDistributionChannel: "official",
OfficialDistributionReady: true,
}, nil
}
func (f *fakeBackend) ResourceState(ctx context.Context) (*backendrpc.ResourceState, error) {
if f.resource != nil {
return f.resource, nil
@@ -1243,8 +1523,8 @@ func TestDiscoverCallsStatusBeforeDoctor(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if fb.statusCalls != 1 || fb.doctorCalls != 1 {
t.Fatalf("status=%d doctor=%d", fb.statusCalls, fb.doctorCalls)
if fb.statusCalls != 1 || fb.doctorCalls != 1 || fb.releaseStatusCalls != 1 {
t.Fatalf("status=%d doctor=%d release_status=%d", fb.statusCalls, fb.doctorCalls, fb.releaseStatusCalls)
}
if !result.RPCAvailable || result.Index == nil || !result.Index.Summary().Ready {
t.Fatalf("result=%+v summary=%+v", result, result.Index.Summary())