test(api): 固化 Rust-Go RPC 契约

This commit is contained in:
2026-07-31 18:20:37 +08:00
parent 1e466d3374
commit df361cff28
17 changed files with 284 additions and 9 deletions
+112
View File
@@ -0,0 +1,112 @@
package api
import (
"bytes"
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
"bat-api/internal/backendrpc"
)
func readContractFixture(t *testing.T, name string) []byte {
t.Helper()
data, err := os.ReadFile(filepath.Join("testdata", "contract", name))
if err != nil {
t.Fatalf("read contract fixture %s: %v", name, err)
}
return data
}
func TestRustContractFixturesPreserveGoMirror(t *testing.T) {
availableRaw := readContractFixture(t, "catalog-status.available.json")
unavailableRaw := readContractFixture(t, "catalog-status.unavailable.json")
manifestRaw := readContractFixture(t, "resource-manifest.page0.json")
snapshotRaw := readContractFixture(t, "official-sync-snapshot.json")
for name, raw := range map[string][]byte{
"catalog available": availableRaw,
"catalog unavailable": unavailableRaw,
"resource manifest": manifestRaw,
"snapshot": snapshotRaw,
} {
if bytes.Contains(raw, []byte("/tmp/")) {
t.Fatalf("%s contains an absolute temporary path", name)
}
if bytes.Contains(raw, []byte("r93_fixture")) {
t.Fatalf("%s contains a concrete release token", name)
}
}
summary, resourceRoot, available := parseCatalogStatus(availableRaw)
if !available || summary == nil {
t.Fatal("available catalog fixture was not accepted")
}
if summary.Status != "published" || summary.StatusCode != "official.published" {
t.Fatalf("catalog status=%+v", summary)
}
if summary.DistributionStatusCode != "distribution.ready" {
t.Fatalf("distribution status=%q", summary.DistributionStatusCode)
}
if resourceRoot != "${RESOURCE_ROOT}" || summary.VersionID != "${VERSION_ID}" {
t.Fatalf("catalog root/version=%q/%q", resourceRoot, summary.VersionID)
}
if summary.LauncherMetadata == nil || summary.GameMainConfig == nil {
t.Fatalf("catalog optional objects missing: %+v", summary)
}
if summary.LauncherMetadata.GameLowestVersion != "" {
t.Fatalf("null optional field became non-empty: %q", summary.LauncherMetadata.GameLowestVersion)
}
if summary.GameMainConfig.DefaultConnectionGroup != "${CONNECTION_GROUP}" {
t.Fatalf("game config=%+v", summary.GameMainConfig)
}
if got, _, ok := parseCatalogStatus(unavailableRaw); got != nil || ok {
t.Fatalf("unavailable catalog parsed as available: got=%+v ok=%v", got, ok)
}
var manifest backendrpc.ResourceManifestPage
if err := json.Unmarshal(manifestRaw, &manifest); err != nil {
t.Fatalf("decode resource manifest: %v", err)
}
if !manifest.Available || manifest.ManifestVersion != 1 || manifest.TotalEntries != 2 {
t.Fatalf("manifest header=%+v", manifest)
}
if len(manifest.Entries) != 2 {
t.Fatalf("manifest entries=%d", len(manifest.Entries))
}
if manifest.Entries[0].Bytes == nil || *manifest.Entries[0].Bytes != 21 {
t.Fatalf("manifest first bytes=%v", manifest.Entries[0].Bytes)
}
if manifest.Entries[0].BLAKE3 == "" || !strings.Contains(manifest.Entries[0].Destination, "{addressables-root}") {
t.Fatalf("manifest first entry=%+v", manifest.Entries[0])
}
var snapshot struct {
AppVersion string `json:"app_version"`
BundleVersion string `json:"bundle_version"`
GameMainConfigBootstrap *GameMainConfigSummary `json:"game_main_config_bootstrap"`
LauncherMetadata *LauncherMetadataSummary `json:"launcher_metadata"`
LegacyGameMainConfig json.RawMessage `json:"game_main_config"`
}
if err := json.Unmarshal(snapshotRaw, &snapshot); err != nil {
t.Fatalf("decode snapshot: %v", err)
}
if snapshot.AppVersion != "${APP_VERSION}" || snapshot.BundleVersion != "${BUNDLE_VERSION}" {
t.Fatalf("snapshot versions=%q/%q", snapshot.AppVersion, snapshot.BundleVersion)
}
if snapshot.GameMainConfigBootstrap == nil || snapshot.GameMainConfigBootstrap.ServerInfoDataURL == "" {
t.Fatalf("snapshot game config=%+v", snapshot.GameMainConfigBootstrap)
}
if snapshot.GameMainConfigBootstrap.ServerInfoDataURL != "https://yostar-serverinfo.bluearchiveyostar.com/{server-info-path}" {
t.Fatalf("snapshot server info=%q", snapshot.GameMainConfigBootstrap.ServerInfoDataURL)
}
if snapshot.LauncherMetadata == nil || snapshot.LauncherMetadata.ManifestSource == "" {
t.Fatalf("snapshot launcher metadata=%+v", snapshot.LauncherMetadata)
}
if len(snapshot.LegacyGameMainConfig) != 0 {
t.Fatalf("legacy game_main_config field unexpectedly present: %s", snapshot.LegacyGameMainConfig)
}
}
+15
View File
@@ -0,0 +1,15 @@
# Rust contract fixtures
本目录保存 Go `bat-api` 对 Rust `bat.sock` / snapshot JSON 的跨语言
contract fixture。JSON 由 Rust 代码路径产出后归一化,只替换本机路径、版本号、
时间戳和 release URL token,不改字段名、层级、null 语义或数字类型。
覆盖范围:
- `catalog.status` 可用与不可用响应。
- `resource.manifest` 第一页分页响应。
- 对应 release 的 `official-sync-snapshot.json`
- `launcher_metadata``game_main_config_bootstrap` 的 Go mirror 解码。
这些 fixture 只用于 schema / mirror 回归,不代表真实资源版本,也不替代 live
daemon socket 或完整发布切换验证。
@@ -0,0 +1,41 @@
{
"addressables_catalog_marker_count": 0,
"addressables_root": "https://prod-clientpatch.bluearchiveyostar.com/{addressables-root}",
"app_version": "${APP_VERSION}",
"available": true,
"bundle_version": "${BUNDLE_VERSION}",
"connection_group_name": "${CONNECTION_GROUP}",
"distribution_status": "ready",
"distribution_status_code": "distribution.ready",
"endpoint_count": 0,
"endpoint_marker_count": 0,
"game_main_config_bootstrap": {
"default_connection_group": "${CONNECTION_GROUP}",
"server_info_data_url": "https://yostar-serverinfo.bluearchiveyostar.com/{server-info-path}"
},
"launcher_metadata": {
"game_latest_file_path": "${GAME_PACKAGE_PATH}",
"game_latest_version": "${APP_VERSION}",
"game_lowest_version": null,
"game_start_exe_name": "${GAME_START_EXE_NAME}",
"game_start_params": [
"${GAME_START_PARAM}"
],
"launcher_version": "${LAUNCHER_VERSION}",
"manifest_file_count": 2,
"manifest_source": "${LAUNCHER_MANIFEST_SOURCE}",
"manifest_url": "https://launcher-pkg-ba-jp.yo-star.com/{launcher-manifest-path}"
},
"official_seed_hash_marker_count": 0,
"snapshot_version": 2,
"status": "published",
"status_code": "official.published",
"status_phase": "official_sync",
"status_retryable": false,
"status_terminal": true,
"version": {
"completed_unix_seconds": 1000,
"id": "${VERSION_ID}",
"resource_root": "${RESOURCE_ROOT}"
}
}
@@ -0,0 +1,10 @@
{
"available": false,
"distribution_status": "blocked",
"distribution_status_code": "distribution.blocked",
"status": "unavailable",
"status_code": "official.unavailable",
"status_phase": "official_sync",
"status_retryable": false,
"status_terminal": true
}
@@ -0,0 +1,26 @@
{
"addressables_root": "https://prod-clientpatch.bluearchiveyostar.com/{addressables-root}",
"app_version": "${APP_VERSION}",
"bundle_version": "${BUNDLE_VERSION}",
"connection_group_name": "${CONNECTION_GROUP}",
"endpoint_markers": [],
"endpoints": [],
"game_main_config_bootstrap": {
"default_connection_group": "${CONNECTION_GROUP}",
"server_info_data_url": "https://yostar-serverinfo.bluearchiveyostar.com/{server-info-path}"
},
"launcher_metadata": {
"game_latest_file_path": "${GAME_PACKAGE_PATH}",
"game_latest_version": "${APP_VERSION}",
"game_lowest_version": null,
"game_start_exe_name": "${GAME_START_EXE_NAME}",
"game_start_params": [
"${GAME_START_PARAM}"
],
"launcher_version": "${LAUNCHER_VERSION}",
"manifest_file_count": 2,
"manifest_source": "${LAUNCHER_MANIFEST_SOURCE}",
"manifest_url": "https://launcher-pkg-ba-jp.yo-star.com/{launcher-manifest-path}"
},
"snapshot_version": 2
}
@@ -0,0 +1,22 @@
{
"available": true,
"entries": [
{
"blake3": "0000000000000000000000000000000000000000000000000000000000000000",
"bytes": 21,
"destination": "prod-clientpatch.bluearchiveyostar.com/{addressables-root}/TableBundles/TableCatalog.bytes",
"url": "https://prod-clientpatch.bluearchiveyostar.com/{addressables-root}/TableBundles/TableCatalog.bytes"
},
{
"blake3": "0000000000000000000000000000000000000000000000000000000000000000",
"bytes": 10,
"destination": "prod-clientpatch.bluearchiveyostar.com/{addressables-root}/TableBundles/TableCatalog.hash",
"url": "https://prod-clientpatch.bluearchiveyostar.com/{addressables-root}/TableBundles/TableCatalog.hash"
}
],
"limit": 2,
"manifest_version": 1,
"offset": 0,
"resource_root": "${RESOURCE_ROOT}",
"total_entries": 2
}