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
+1 -1
View File
@@ -264,7 +264,7 @@ cargo run -p bat-infrastructure --bin bat -- \
--watch
```
资源 HTTP bootstrap / 只读分发入口是 Go `cmd/bat-api`。生产拓扑下它与 Rust `bat` 同环境运行,经 `bat.sock` RPC 获取当前 `resource_root`,不在配置里写死资源目录;本地开发不能全量跑 `bat` 时用 fixture 和 Go 门禁验证。`bat-api` 已补 launcher 资源引导兼容端点和玩家-facing HTTP 控制面(token 鉴权、限流、访问日志、反代 IP 适配、动态 JSON no-store、OpenAPI、管理控制白名单;`reload` / `refresh` / `restart` / `sync` / `verify` / `repair` / `catalog-refresh` 可经 Web 转发),响应只来自已发布 snapshot/RPC,不提供登录、网关、鉴权或完整 package update manifest。
资源 HTTP bootstrap / 只读分发入口是 Go `cmd/bat-api`。生产拓扑下它与 Rust `bat` 同环境运行,经 `bat.sock` RPC 获取当前 `resource_root`,不在配置里写死资源目录;本地开发不能全量跑 `bat` 时用 fixture 和 Go 门禁验证。`internal/api/testdata/contract/` 已固化来自 Rust 输出并经归一化的 `catalog.status``resource.manifest``official-sync-snapshot.json` contract fixtureGo mirror 测试会防止字段名、null 语义和 `game_main_config_bootstrap` 再次漂移。`bat-api` 已补 launcher 资源引导兼容端点和玩家-facing HTTP 控制面(token 鉴权、限流、访问日志、反代 IP 适配、动态 JSON no-store、OpenAPI、管理控制白名单;`reload` / `refresh` / `restart` / `sync` / `verify` / `repair` / `catalog-refresh` 可经 Web 转发),响应只来自已发布 snapshot/RPC,不提供登录、网关、鉴权或完整 package update manifest。
生产要求:
+1 -1
View File
@@ -32,7 +32,7 @@ BlueArchiveToolkit 不是一次性脚本,也不是演示项目。最终交付
4. `bat-cas-engine` 已完成 CAS V1:原子写入、BLAKE3 Hash、SQLite 引用计数、GC、并发测试、损坏检测。
5. `bat-infrastructure` 已改为 CAS 仓储适配层,不再重复实现对象存储。
6. `bat-infrastructure` 已提供官方资源 pull/update 服务,正式入口是 Rust binary `bat`
7. `bat` 支持 `--auto-discover``--watch``--daemon`、默认 1 小时间隔、本地 manifest audit/repair、官方 seed `.hash` 校验、snapshot/cache,以及基于 Unix socket JSON-RPC 的 live control/backend 方法(`daemon.status/logs/stop/reload/refresh/doctor``resource.sync/verify/repair/state/manifest/list/index``parse.status/text_units/errors``localized.status``catalog.*``task.*`);`restart` `clean-stable` 仍由 CLI 侧按进程生命周期显式执行。
7. `bat` 支持 `--auto-discover``--watch``--daemon`、默认 1 小时间隔、本地 manifest audit/repair、官方 seed `.hash` 校验、snapshot/cache,以及基于 Unix socket JSON-RPC 的 live control/backend 方法(`daemon.status/logs/stop/restart/reload/refresh/doctor``resource.sync/verify/repair/state/manifest/list/index``parse.status/text_units/errors``localized.status``catalog.*``task.*`);`daemon.restart` 通过 Rust lifecycle controller 复用 CLI restart 路径,`clean-stable` 仍由 CLI 侧按进程生命周期显式执行。
8. `bat-ffi` 已提供 Manifest inspect 和官方 sync plan 的可选无状态粗粒度 JSON C ABI helper。
9. 官方原版资源默认发布到 `./bat-resources`,汉化产物默认发布到独立的 `./bat-localized`;当前官方同步报告会标记 `localized_release_status=not_localized`,表示原版资源已发布、汉化资源未发布。
10. 官方同步校验完成并发布新 release 后会生成 `official-resource-changes.json``crowdin-translation-handoff.json``official-parse-cache.json``official-textunit-index.json``official-textunit-tasks.json``crowdin-textunit-queue.json`,用 Added/Modified 资源驱动后续解析/翻译增量;up-to-date 轮询在已有有效缓存、TextUnit 明细索引和队列时只读取摘要,不重复解析。
+1 -1
View File
@@ -101,7 +101,7 @@ cargo run -p bat-infrastructure --bin bat -- reload
cargo run -p bat-infrastructure --bin bat -- stop
```
`status``stop``logs``reload`、默认形态的 `refresh` 和默认形态的 `repair` 会优先连接 live RPC socketsocket 不可用时,状态和停止命令会回退到 PID/状态文件兼容路径。`reload` 不再强制重启进程,而是让后台 watch 循环重新自动发现并执行强制刷新:空闲睡眠时立即唤醒,正在同步时排队到当前轮结束后执行。确实需要替换启动参数时使用 `restart` 或给 `reload` 显式传入同步参数。后台 daemon 正在管理某个资源目录时,前台 `run/watch/refresh/repair` 不能直接写同一目录;默认形态的 `refresh`/`repair` 会改走 RPC,显式参数导致无法走 RPC 时需要先 `stop`
`status``stop``restart``logs``reload`、默认形态的 `refresh` 和默认形态的 `repair` 会优先连接 live RPC socketsocket 不可用时,状态和停止命令会回退到 PID/状态文件兼容路径。`restart` 会通过 Rust lifecycle controller 复用 CLI restart 路径替换后台进程;`reload` 不再强制重启进程,而是让后台 watch 循环重新自动发现并执行强制刷新:空闲睡眠时立即唤醒,正在同步时排队到当前轮结束后执行。确实需要替换启动参数时使用 `restart` 或给 `reload` 显式传入同步参数。后台 daemon 正在管理某个资源目录时,前台 `run/watch/refresh/repair` 不能直接写同一目录;默认形态的 `refresh`/`repair` 会改走 RPC,显式参数导致无法走 RPC 时需要先 `stop`
`bat` 会拒绝危险输出目录、路径逃逸和现有 symlink 路径组件;下载目标、`.part`、manifest、snapshot、PID、status、log 和控制锁文件不会跟随 symlink,daemon 状态类文件默认以 `0600` 权限创建。
+1 -1
View File
@@ -120,7 +120,7 @@ current symlink → official-sync-snapshot.json + official-download-manifest.jso
- `refresh --force` 可手动强制刷新;`verify` 只读校验当前官方计划、本地 manifest 和官方 seed hash`repair` 尝试修复异常资源。
- 非 dry-run 同步先写 `.staging/<id>`,校验完成后发布 `versions/<id>` 并原子切换 `current` symlink。
- `--daemon` 使用状态目录下的 `bat.sock` 作为 Unix socket JSON-RPC live control planePID、状态和日志文件是快照与 fallback,`bat-events.jsonl` 是结构化轮转日志。
- `status``logs``reload``stop` 和默认形态的 `refresh` 优先通过 RPC 管理后台进程;控制命令通过 `bat-control.lock` 串行化;`restart` 负责重启或替换启动参数;live daemon 会阻止前台写命令直接修改同一资源目录;`doctor` 做运行时诊断;`clean-stable` 清理临时文件和失效/损坏状态。
- `status``logs``restart``reload``stop` 和默认形态的 `refresh` 优先通过 RPC 管理后台进程;控制命令通过 `bat-control.lock` 串行化;`restart` 通过 Rust lifecycle controller 复用 CLI restart 路径重启或替换启动参数;live daemon 会阻止前台写命令直接修改同一资源目录;`doctor` 做运行时诊断;`clean-stable` 清理临时文件和失效/损坏状态。
- 远端 marker 无变化且本地 manifest clean 时不下载。
- 本地文件损坏时 repair。
- 官方 seed `.hash` 强校验;Addressables `catalog_*.hash` 作为变更 marker。
+1 -1
View File
@@ -253,7 +253,7 @@ sudo -u bat /opt/bluearchive-toolkit/bin/bat reload --state-dir /var/lib/bluearc
sudo -u bat /opt/bluearchive-toolkit/bin/bat stop --state-dir /var/lib/bluearchive-toolkit/daemon-state
```
`--daemon` 会在 `--state-dir` 下创建 `bat.sock``bat.pid``bat-status.json``bat-daemon.log``bat-events.jsonl` 和短生命周期的 `bat-control.lock``bat.sock` 是 Unix socket JSON-RPC 控制通道;`status``stop``logs``reload`、默认形态的 `refresh` 和默认形态的 `repair` 会优先连接 live daemon。PID、状态和日志文件保留为快照、诊断和 socket 不可用时的兼容路径;`bat-events.jsonl` 是带轮转的结构化 JSONL 事件日志;`bat-status.json` 会暴露最后成功时间、下次检查时间、最后错误摘要和当前下载进度;`bat-control.lock` 串行化控制命令,并能在 stale/corrupt 时由下一次控制命令或 `clean-stable` 恢复。`reload` 默认不会重启进程,而是让 watch 循环重新自动发现并强制刷新:空闲睡眠时立即唤醒,正在同步时排队到当前轮结束后执行;需要替换启动参数或 binary 时用 `restart`
`--daemon` 会在 `--state-dir` 下创建 `bat.sock``bat.pid``bat-status.json``bat-daemon.log``bat-events.jsonl` 和短生命周期的 `bat-control.lock``bat.sock` 是 Unix socket JSON-RPC 控制通道;`status``stop``restart``logs``reload`、默认形态的 `refresh` 和默认形态的 `repair` 会优先连接 live daemon。PID、状态和日志文件保留为快照、诊断和 socket 不可用时的兼容路径;`bat-events.jsonl` 是带轮转的结构化 JSONL 事件日志;`bat-status.json` 会暴露最后成功时间、下次检查时间、最后错误摘要和当前下载进度;`bat-control.lock` 串行化控制命令,并能在 stale/corrupt 时由下一次控制命令或 `clean-stable` 恢复。`restart` 会通过 Rust lifecycle controller 复用 CLI restart 路径替换后台进程;`reload` 默认不会重启进程,而是让 watch 循环重新自动发现并强制刷新:空闲睡眠时立即唤醒,正在同步时排队到当前轮结束后执行;需要替换启动参数或 binary 时用 `restart`
不要同时运行 systemd `--watch` 和 standalone `--daemon` 指向同一个 `--output`。二者都会被资源锁和 live daemon 互斥保护,但生产运维上应保持单一 owner。
+1 -1
View File
@@ -277,7 +277,7 @@ cargo run -p bat-infrastructure --bin bat -- reload
cargo run -p bat-infrastructure --bin bat -- stop
```
`status``stop``logs``reload`、默认形态的 `refresh` 和默认形态的 `repair` 会优先连接 `bat.sock`,通过 Unix socket JSON-RPC 和 live daemon 通信;socket 不可用时,`status``stop` 会回退到 PID/状态文件兼容路径。`status` 会显示最后成功时间、下次检查时间、最后错误摘要、当前阶段、当前下载 URL 进度、版本状态摘要、最近历史失败版本和原因、文本日志路径、结构化日志路径和轮转日志路径;正在重新拉取同一版本时,对应旧失败不会作为当前历史失败摘要展示;人类输出不会把完整 `official-version-state.json` 内联打印成 JSON。控制命令会通过 `bat-control.lock` 做跨进程互斥,失效或损坏的控制锁会在下次控制命令或 `clean-stable` 时恢复。`restart` 会停止旧后台进程并按保存参数或显式参数重新启动;`reload` 在未显式传入同步参数时不会重启进程,而是唤醒或排队 watch 循环重新执行自动发现和强制刷新:空闲睡眠时立即执行,正在同步时等当前轮结束;如果显式传入 `--proxy``--no-proxy`,会按新代理配置重启后台进程。所有命令默认输出人类可读摘要,脚本集成时加 `--json`
`status``stop``restart``logs``reload`、默认形态的 `refresh` 和默认形态的 `repair` 会优先连接 `bat.sock`,通过 Unix socket JSON-RPC 和 live daemon 通信;socket 不可用时,`status``stop` 会回退到 PID/状态文件兼容路径。`status` 会显示最后成功时间、下次检查时间、最后错误摘要、当前阶段、当前下载 URL 进度、版本状态摘要、最近历史失败版本和原因、文本日志路径、结构化日志路径和轮转日志路径;正在重新拉取同一版本时,对应旧失败不会作为当前历史失败摘要展示;人类输出不会把完整 `official-version-state.json` 内联打印成 JSON。控制命令会通过 `bat-control.lock` 做跨进程互斥,失效或损坏的控制锁会在下次控制命令或 `clean-stable` 时恢复。`restart`通过 Rust lifecycle controller 复用 CLI restart 路径停止旧后台进程并按保存参数或显式参数重新启动;`reload` 在未显式传入同步参数时不会重启进程,而是唤醒或排队 watch 循环重新执行自动发现和强制刷新:空闲睡眠时立即执行,正在同步时等当前轮结束;如果显式传入 `--proxy``--no-proxy`,会按新代理配置重启后台进程。所有命令默认输出人类可读摘要,脚本集成时加 `--json`
如果要把后台状态目录改到其他位置,使用 `--state-dir <目录>`
+6
View File
@@ -327,6 +327,12 @@ CLI 对应关系:
`--class-id``--field-path``--format`;这些过滤参数不适用于
`parse-status``localized-status`
Go mirror contract fixture 固化在 `internal/api/testdata/contract/`,覆盖
`catalog.status` available/unavailable、`resource.manifest` page0 和对应
`official-sync-snapshot.json`。这些 fixture 由 Rust 输出归一化而来,只用于
schema / mirror 回归;live daemon socket 和完整 release 切换仍需在允许 smoke 的
隔离环境中验证。
禁止事项:
- Go 服务层不直接读写 `bat-status.json``bat-tasks.json` 等 daemon 内部状态文件。
@@ -1,9 +1,14 @@
# bat-api / Rust bat Contract Fixture Handoff
更新时间:2026-07-28
更新时间:2026-07-31
本文用于两个 Codex 窗口之间间接联调 `bat-api` 与 Rust `bat` 的跨语言 contract fixture。它只定义协作协议和验收标准,不包含已审核 fixture。
2026-07-31 更新:已审核归一化 fixture 已落入
`internal/api/testdata/contract/`Go 侧通过
`internal/api/contract_fixture_test.go` 固化 mirror struct 验证。本文件保留为
后续重新生成或扩展 contract fixture 时的协作协议。
## 最小上下文包
另一个窗口不需要知道本窗口的完整对话,只需要遵守以下上下文:
@@ -193,4 +198,11 @@ contract fixture 工作只有在以下条件同时满足时才算完成:
- Go `bat-api` 已具备消费 `launcher_metadata` / `game_main_config_bootstrap` 的 mirror struct。
- Go `bat-api` 已具备 player-facing HTTP 控制面、OpenAPI 和管理控制白名单。
- contract fixture 尚未落仓库,等待 Rust 侧真实输出与用户审核。
- 已归一化的 Rust contract fixture 落仓库
- `internal/api/testdata/contract/catalog-status.available.json`
- `internal/api/testdata/contract/catalog-status.unavailable.json`
- `internal/api/testdata/contract/resource-manifest.page0.json`
- `internal/api/testdata/contract/official-sync-snapshot.json`
- 原始交接产物仍位于 `/tmp/bat-contract-fixture/`;当前受本地 sandbox 限制,live socket daemon 无法启动,原始 JSON 通过临时 Rust 测试调用同一 dispatch/report 代码路径生成。
- Go contract 测试读取仓库内归一化 fixture,不依赖 `/tmp/bat-contract-fixture/`、开发机资源目录或远端长期运行的 `bat`
- 仍未覆盖真实长期 daemon socket 的端到端调用和完整发布切换;该项需要在允许 live daemon / smoke 的隔离环境中单独验证。
+1
View File
@@ -231,6 +231,7 @@
- **拉取归属 Rust `bat`**`bat-api` 不做下载器。
- 发现经 `bat.sock`:先 `daemon.status`,再 `daemon.doctor`,再 `catalog.status` / `resource.manifest`
- `bat-api` 通过 RPC 读取的 `resource.state``catalog.status``parse.status``localized.status` 会获得短状态 `status` 与稳定生命周期状态码 `status_code`;错误原因仍以 `BAT-ERR-*` 为准。
- `bat-api` 可通过受限 Web 控制面转发 `reload` / `refresh` / `restart` / `sync` / `verify` / `repair` / `catalog-refresh`,其中 `restart``parse.*``localized.status` 和文件级 `unityfs.patch_*` 均走 Rust live RPC`daemon.clean-stable` 等危险或离线生命周期命令不经 Web 转发。
- 生产与 Rust `bat` 同环境运行,资源根来自 RPC 返回的 `resource_root``--resource-root` 仅用于 fixture 或应急只读诊断。
- `.env` 配置端口 / public base / RPC socket / RPC 刷新周期;预留 database/redis。
- `/v1/bootstrap` 返回 RPC 健康、release 摘要、server-info URL、client-patch base 和改写后的 Addressables root。
+1 -1
View File
@@ -95,7 +95,7 @@
| 组件 | 路径 | 状态 | 说明 |
|---|---|---|---|
| Module | `go.mod``bat-api` | 已用 | 服务层模块名 |
| RPC client | `internal/backendrpc` | **完成** | typed JSON-RPC;覆盖 daemon restart、resource/catalog/task、parse/localized 和文件级 UnityFS patch 调用;fake transport 单测 |
| RPC client | `internal/backendrpc` | **完成** | typed JSON-RPC;覆盖 daemon restart、resource/catalog/task、parse/localized 和文件级 UnityFS patch 调用;fake transport 单测,配合 `internal/api/testdata/contract/` 固化 Rust 输出 mirror |
| 资源 bootstrap/分发 | `cmd/bat-api` + `internal/api` | **MVP+生产控制面** | RPC 发现 + 周期刷新/诊断 + `/v1/bootstrap` + `/v1/launcher/bootstrap` + launcher 资源 metadata 兼容 + `/readyz` + CDN Range/缓存头 + 鉴权/限流/访问日志/反代适配 + OpenAPI + 管理控制白名单 + `.env` |
| 试验 CLI | `cmd/bat` | **试验** | doctor 固定 okmanifest/sync 走 FFI |
| FFI | `internal/ffi` | **可选** | 需 `build-ffi` |
@@ -8951,6 +8951,36 @@ mod tests {
assert_eq!(value["error"]["code"], "BAT-ERR-700002");
}
#[test]
fn dispatch_unityfs_patch_methods_are_live_handlers_and_require_params() {
let temp = tempfile::TempDir::new().unwrap();
let control = new_daemon_control();
for method in [
"unityfs.patch_text_asset",
"unityfs.patch_string_field",
"unityfs.patch_field",
] {
let envelope = dispatch_rpc_method(
&rpc_request(method, None),
temp.path(),
&control,
&test_task_context(),
format!("req-test-{method}"),
);
let value = serde_json::to_value(&envelope).unwrap();
assert_eq!(value["ok"], false, "method={method}");
assert_eq!(value["error"]["code"], "BAT-ERR-700002", "method={method}");
assert_eq!(value["error"]["location"], method, "method={method}");
assert!(
value["error"]["message"]
.as_str()
.unwrap()
.contains("params 不能为空"),
"method={method}"
);
}
}
#[test]
fn dispatch_patch_apply_writes_text_patch_target() {
let temp = tempfile::TempDir::new().unwrap();
+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
}