From 25c2c4d40fb540fada4f3aa38c11ed4e0683d83e Mon Sep 17 00:00:00 2001 From: Yuyi-Oak <1722157266@qq.com> Date: Tue, 14 Jul 2026 00:48:59 +0800 Subject: [PATCH] feat: add official full pull smoke Emit runtime download progress and validation summaries for official sync. Add repeatable real-network full pull smoke runbook and script. Fixes #4 Fixes #6 --- CHANGELOG.md | 4 +- CURRENT_STATUS.md | 17 +- DOCS_INDEX.md | 18 +- Makefile | 6 +- README.md | 17 +- .../architecture/official-resource-backend.md | 4 +- docs/guides/baseline.md | 9 +- docs/guides/official-full-pull-smoke.md | 99 ++++++++++ docs/guides/official-resource-test-pull.md | 19 +- docs/reports/CURRENT_GAPS.md | 26 +-- docs/reports/current-stage-prepush.md | 11 +- docs/reports/current-status-handoff.md | 11 +- infrastructure/src/official_update.rs | 50 ++++- .../official_game_main_config_bootstrap.rs | 17 ++ scripts/official-full-pull-smoke.sh | 184 ++++++++++++++++++ 15 files changed, 435 insertions(+), 57 deletions(-) create mode 100644 docs/guides/official-full-pull-smoke.md create mode 100755 scripts/official-full-pull-smoke.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index ab5e1b5..c6a25d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,7 +21,9 @@ - 新增官方同步路径安全边界:拒绝危险输出目录和 snapshot 路径逃逸,下载目标、manifest、daemon PID/status/log/control 文件不跟随 symlink,daemon 状态文件默认使用 `0600` 权限 - 新增官方资源原子发布布局:非 dry-run 下载先进入 `.staging/`,校验和 manifest/snapshot 写入完成后发布到 `versions/`,再原子切换 `current` symlink - 新增 daemon 可观测性:`bat-events.jsonl` 结构化 JSONL 日志、日志轮转、status 中的当前下载进度、最后成功时间、下次检查时间和最后错误摘要 +- 新增运行时下载与校验 progress log:总体下载进度、单文件开始/完成状态、官方 `.hash`、本地 BLAKE3、需修复项和 ZIP 结构校验摘要 - 新增官方资源同步生产部署模板:release binary symlink 路径、systemd unit、运行用户、日志位置、升级和回滚流程 +- 新增真实官方网络全量拉取 smoke:`scripts/official-full-pull-smoke.sh`、`make official-smoke` 和 `docs/guides/official-full-pull-smoke.md` - 新增官方资源下载校验:官方 URL 拒绝、`.part` 续传、重试、本地 size+BLAKE3、官方 seed `.hash` 校验 - 新增 Addressables 当前真实形态 fixture/golden 测试 - 新增 SQLite Resource Repository 和粗粒度 FFI JSON 接口 @@ -31,7 +33,7 @@ - [x] 实现 CAS 存储引擎 - [x] 实现 Rust 官方资源同步核心链路 - [ ] 实现 Go CLI 最小可用入口 -- [ ] 记录真实官方网络全量下载 smoke test +- [x] 固化真实官方网络全量下载 smoke test 文档和可重复命令 - [ ] 实现 AssetBundle 解析器 - [ ] 实现翻译系统 - [ ] 实现 Patch 引擎 diff --git a/CURRENT_STATUS.md b/CURRENT_STATUS.md index eb48cd5..94da020 100644 --- a/CURRENT_STATUS.md +++ b/CURRENT_STATUS.md @@ -1,9 +1,9 @@ # BlueArchiveToolkit 当前工作区状态 -- **更新时间**:2026-07-11 +- **更新时间**:2026-07-14 - **状态来源**:本地工作区盘点、代码验证和最新提交 - **状态分支**:`experiment` -- **最新已推送功能提交**:`2bdc55a feat: support cancellable official sync` +- **最新已推送功能提交**:以当前 `git log --oneline -1` 为准 - **权威计划**:`PROJECT_PLAN.md` --- @@ -26,7 +26,7 @@ Rust 侧官方日服资源链路已经从实验验证推进到正式入口: 10. 官方同步会拒绝危险输出目录、路径逃逸和现有 symlink 路径组件;下载目标、`.part`、manifest、snapshot、PID、status、log 和控制锁文件不会跟随 symlink,daemon 状态类文件默认以 `0600` 权限创建。 11. `bat status` 会显示最后成功时间、下次检查时间、最后错误摘要、当前阶段、当前下载 URL 进度、结构化日志路径和轮转日志路径。 -仍需明确:这不是完整产品完成。Go CLI 最小入口、完整 AssetBundle 解析、Patch、翻译系统、API Server 和 Web 仍是后续工作;真实官方网络全量下载 smoke test 尚未记录在仓库文档中。 +仍需明确:这不是完整产品完成。Go CLI 最小入口、完整 AssetBundle 解析、Patch、翻译系统、API Server 和 Web 仍是后续工作;真实官方网络全量拉取 smoke 已固化为可重复脚本和 runbook,真实大文件产物与运行报告默认保存在 `/tmp` 隔离目录,不纳入 Git。 --- @@ -36,6 +36,7 @@ Rust 侧官方日服资源链路已经从实验验证推进到正式入口: - `PROJECT_PLAN.md`:最终目标、里程碑和近期任务。 - `DOCS_INDEX.md`:文档阅读顺序和索引。 - `docs/guides/official-resource-test-pull.md`:官方资源拉取与自动更新用户指南。 +- `docs/guides/official-full-pull-smoke.md`:真实官方全量拉取 smoke runbook 和可重复命令。 - `docs/architecture/official-resource-backend.md`:官方资源后端设计和审核说明。 - `docs/reports/CURRENT_GAPS.md`:当前缺口和关闭顺序。 @@ -134,7 +135,7 @@ Rust 侧官方日服资源链路已经从实验验证推进到正式入口: 待完成: - 将官方同步下载结果作为用户级流程自动导入 CAS + ResourceRepository。 -- 为真实线上全量下载建立受控 smoke test 记录。 +- 真实线上全量下载 smoke 已固化为 `scripts/official-full-pull-smoke.sh` 和 `make official-smoke`;实际运行报告由脚本写入隔离输出目录。 - 增加更多失败恢复和权限场景测试。 ### `bat-assetbundle` @@ -220,7 +221,7 @@ git status --short 未执行: -- 真实官方网络全量下载 smoke test。 +- 本次状态更新未执行真实官方网络全量下载 smoke;该流程已由 `docs/guides/official-full-pull-smoke.md` 和 `scripts/official-full-pull-smoke.sh` 固化,运行后会在隔离目录生成 `SMOKE_REPORT.md`。 - Go CLI 端到端测试,因为 Go CLI 尚未实现。 - Web/API 测试,因为 Web/API 尚未实现。 @@ -257,7 +258,7 @@ cargo run -p bat-infrastructure --bin bat -- \ 2. 官方同步结果接入 CAS + ResourceRepository 的用户级工作流。 3. AssetBundle UnityFS 基础解析。 4. Addressables parser 对更多官方 catalog 结构的覆盖。 -5. 真实官方网络全量下载 smoke test 记录。 +5. 按 smoke runbook 在具备网络和磁盘窗口的环境中执行真实官方全量拉取,并保留本地报告。 6. Patch 和翻译系统仍应后置。 --- @@ -268,11 +269,11 @@ cargo run -p bat-infrastructure --bin bat -- \ 1. 实现 Go CLI 最小框架和 `doctor`。 2. 为 `bat` 增加发布型构建/安装说明和服务化验证。 -3. 补端到端 smoke:dry-run、真实下载到隔离目录、二次运行静默 up-to-date、本地损坏后 repair。 +3. 按端到端 smoke runbook 执行 dry-run、真实下载到隔离目录、二次运行静默 up-to-date、本地损坏后 repair。 4. 开始 AssetBundle parser 的 UnityFS header/block/directory。 --- - **当前总体完成度**:约 22% - **当前基线状态**:Rust 官方资源同步链路已具备可运行闭环;产品级 CLI/API/Web 仍未完成。 -- **下一工程里程碑**:Go CLI 最小可用 + 官方同步端到端 smoke + AssetBundle 解析起步。 +- **下一工程里程碑**:Go CLI 最小可用 + 官方同步结果接入 CAS/ResourceRepository + AssetBundle 解析起步。 diff --git a/DOCS_INDEX.md b/DOCS_INDEX.md index 9ffef72..35159d3 100644 --- a/DOCS_INDEX.md +++ b/DOCS_INDEX.md @@ -1,6 +1,6 @@ # BlueArchiveToolkit 文档索引 -- **更新时间**:2026-07-06 +- **更新时间**:2026-07-14 - **说明**:本索引用于快速定位当前权威文档和历史资料。 --- @@ -12,6 +12,7 @@ - `CURRENT_STATUS.md`:当前工作区真实状态。 - `docs/reports/CURRENT_GAPS.md`:当前实现缺口和关闭顺序。 - `docs/guides/official-resource-test-pull.md`:官方资源拉取与自动更新用户指南。 +- `docs/guides/official-full-pull-smoke.md`:真实官方全量拉取 smoke runbook 和可重复命令。 - `docs/architecture/official-resource-backend.md`:官方资源后端职责、工作原理和审核说明。 - `CHANGELOG.md`:版本变更记录。 - `CLAUDE.md`:长期开发约束和项目要求。 @@ -26,6 +27,7 @@ - `docs/guides/deployment.md`:部署指南。 - `deployments/systemd/`:官方资源同步生产 systemd unit 和环境文件示例。 - `docs/guides/official-resource-test-pull.md`:官方资源拉取与自动更新用户指南。 +- `docs/guides/official-full-pull-smoke.md`:真实官方全量拉取 smoke runbook 和可重复命令。 - `docs/reports/current-stage-prepush.md`:当前阶段说明与推送前核查记录。 - `docs/reports/current-status-handoff.md`:给下一次对话使用的当前进度交接说明。 - `docs/guides/baseline.md`:稳定工程基线指南。 @@ -73,11 +75,12 @@ 1. `CURRENT_STATUS.md` 2. `PROJECT_PLAN.md` 3. `docs/guides/official-resource-test-pull.md` -4. `docs/architecture/official-resource-backend.md` -5. `docs/reports/CURRENT_GAPS.md` -6. `docs/guides/baseline.md` -7. `docs/architecture/README.md` -8. `docs/guides/development.md` +4. `docs/guides/official-full-pull-smoke.md` +5. `docs/architecture/official-resource-backend.md` +6. `docs/reports/CURRENT_GAPS.md` +7. `docs/guides/baseline.md` +8. `docs/architecture/README.md` +9. `docs/guides/development.md` --- @@ -92,12 +95,13 @@ - CAS V1:原子写入、BLAKE3 校验、引用计数、GC、并发测试和损坏检测。 - 文档整理和路线图重制。 - Rust 官方资源同步闭环:`bat`、`--auto-discover`、`--watch`、`--daemon`、Unix socket JSON-RPC 后台控制、`status`、`stop`、`restart`、`reload`、`refresh`、`logs`、`verify`、`repair`、`doctor`、`clean-stable`、北京时间固定强制刷新、snapshot、manifest audit/repair、官方 seed `.hash` 校验。 +- 真实官方网络全量拉取 smoke 已固化为 `scripts/official-full-pull-smoke.sh` 和 `make official-smoke`,默认写入 `/tmp` 隔离目录并输出本地运行报告。 +- `bat` 运行时 progress log 已覆盖总体下载进度、单文件下载进度和校验结果摘要。 - Addressables 当前真实形态 fixture/golden 覆盖。 - SQLite Resource Repository 和粗粒度 FFI JSON 接口。 优先待办: - 落地 Go CLI 最小可用入口。 -- 记录真实官方网络全量下载 smoke test。 - 将官方同步结果接入 CAS + ResourceRepository 的用户级流程。 - 开始 AssetBundle UnityFS 解析。 diff --git a/Makefile b/Makefile index 20ec5de..d1dcf74 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: help build test clean check fmt lint install dev docker-build docker-up docker-down +.PHONY: help build test clean check fmt lint install dev docker-build docker-up docker-down official-smoke # 默认目标 .DEFAULT_GOAL := help @@ -67,6 +67,10 @@ bench: ## 运行性能基准测试 echo "$(YELLOW)No Go packages yet, skipping Go benchmarks...$(NC)"; \ fi +official-smoke: ## 运行真实官方全量拉取 smoke(默认写入 /tmp 隔离目录) + @echo "$(BLUE)Running official full pull smoke...$(NC)" + ./scripts/official-full-pull-smoke.sh + # ============================================================================ # 代码质量 # ============================================================================ diff --git a/README.md b/README.md index cb21978..e46188f 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,6 @@ - 真实 Patch apply/diff。 - Translation Memory、Glossary、AI Provider。 - API Server、SDK、Web 管理后台。 -- 真实官方网络全量下载 smoke test 尚未在本仓库记录。 详细状态见: @@ -33,6 +32,7 @@ - [文档索引](DOCS_INDEX.md) - [当前缺口清单](docs/reports/CURRENT_GAPS.md) - [官方资源拉取与自动更新指南](docs/guides/official-resource-test-pull.md) +- [官方全量拉取 Smoke Runbook](docs/guides/official-full-pull-smoke.md) - [官方资源后端说明](docs/architecture/official-resource-backend.md) --- @@ -111,7 +111,18 @@ cargo run -p bat-infrastructure --bin bat -- clean-stable `verify` 会以只读方式检查当前官方计划、`current` 指向的 active release 中 download manifest 的 size+BLAKE3、ZIP 结构,以及本地已有官方 seed `.bytes/.hash` 对的 xxHash32;发现缺失、远端变化或本地损坏会返回非 0。`repair` 会在异常资源存在时复用当前同步链路重新下载必要文件。`clean-stable` 只清理 `.part`、临时状态文件、失效或损坏的 PID/锁/socket,不删除正式资源。 -CLI 默认启动时会向 stderr 打印 `BlueArchiveToolkit` ASCII banner,并继续把阶段进度日志写到 stderr,例如自动发现、拉取 catalog、audit、下载第 N/总数个 URL 等;命令结果默认以人类可读摘要写到 stdout。需要给上层程序保留稳定结构化输出时加 `--json --no-progress`,只想关闭横幅但保留日志时可加 `--no-banner`。 +CLI 默认启动时会向 stderr 打印 `BlueArchiveToolkit` ASCII banner,并继续把阶段进度日志写到 stderr,例如自动发现、拉取 catalog、audit、总体下载进度、单文件下载进度、校验结果摘要、snapshot 和 publish;命令结果默认以人类可读摘要写到 stdout。需要给上层程序保留稳定结构化输出时加 `--json --no-progress`,只想关闭横幅但保留日志时可加 `--no-banner`。 + +真实官方网络全量拉取 smoke 已固化为可重复命令,默认使用 `/tmp/bat-official-smoke-/` 隔离目录,不会写入已有客户端、生产目录或开发机人工维护资源目录: + +```bash +scripts/official-full-pull-smoke.sh + +# 或 +make official-smoke +``` + +该 smoke 会执行 dry-run plan、首次全量拉取、二次 `up_to_date` 检查、本地文件破坏后的 `repair`、repair 后 `verify`,并在 `report/SMOKE_REPORT.md` 记录命令、输出目录、active release、文件数量、release 大小和被破坏文件。大型官方资源文件不纳入 Git。 生产资源输出目录必须使用独立目录,不要指向已有客户端目录,也不要指向 `/home/wanye/D/BlueArchive` 这类人工维护或开发资源目录。需要覆盖默认位置时,用 `--output <资源目录>`;需要覆盖后台状态目录时,用 `--state-dir <状态目录>`。 @@ -162,7 +173,7 @@ BlueArchiveToolkit/ 2. 补齐 AssetBundle UnityFS header/block/directory 解析。 3. 扩展 Addressables catalog 解析覆盖,继续用真实形态 fixture/golden 锁定行为。 4. 将官方同步结果接入 CAS + ResourceRepository 的用户级工作流。 -5. 执行并记录一次真实官方网络全量下载 smoke test。 +5. 按 smoke runbook 在具备网络和磁盘窗口的环境中执行真实官方全量拉取,并保留本地报告。 不建议在 Go CLI、资源解析和文本提取基础能力完成前优先开发 Web UI。 diff --git a/docs/architecture/official-resource-backend.md b/docs/architecture/official-resource-backend.md index ec1daa0..87f368c 100644 --- a/docs/architecture/official-resource-backend.md +++ b/docs/architecture/official-resource-backend.md @@ -233,12 +233,12 @@ Linux 生产路径: - pull plan 会同时包含 discovery URLs 和 content URLs - 全量样本下是 `2` 个 discovery URL + `5` 个内容 URL = `7` 个 URL - `OfficialUpdateService` 能持久化 v2 snapshot,并在远端 marker 内容变化时触发下载决策 -- `bat` 默认向 stderr 输出 `BlueArchiveToolkit` ASCII banner 和 progress log,stdout 默认输出人类可读摘要;支持 `--json` 输出稳定 JSON,支持 `--no-progress` 关闭进度日志,支持 `--no-banner` 只关闭横幅,支持 `--watch --interval 1h --error-retry 60s` 常驻运行,支持 `--daemon` Unix socket JSON-RPC 控制、`status`、`stop`、`restart`、`reload`、`logs`、`refresh --force`、`verify`、`repair`、`doctor`、`clean-stable`,非 dry-run 使用 `.official-sync.lock` 防止并发写资源目录,控制命令使用 `bat-control.lock` 防止并发状态修改,资源发布使用 `.staging`、`versions` 和 `current` 原子切换,daemon 写 `bat-events.jsonl` 结构化日志并在 `status` 中暴露下载进度和调度状态 +- `bat` 默认向 stderr 输出 `BlueArchiveToolkit` ASCII banner 和 progress log,stdout 默认输出人类可读摘要;progress log 覆盖总体下载进度、单文件开始/完成状态和校验结果摘要;支持 `--json` 输出稳定 JSON,支持 `--no-progress` 关闭进度日志,支持 `--no-banner` 只关闭横幅,支持 `--watch --interval 1h --error-retry 60s` 常驻运行,支持 `--daemon` Unix socket JSON-RPC 控制、`status`、`stop`、`restart`、`reload`、`logs`、`refresh --force`、`verify`、`repair`、`doctor`、`clean-stable`,非 dry-run 使用 `.official-sync.lock` 防止并发写资源目录,控制命令使用 `bat-control.lock` 防止并发状态修改,资源发布使用 `.staging`、`versions` 和 `current` 原子切换,daemon 写 `bat-events.jsonl` 结构化日志并在 `status` 中暴露下载进度和调度状态 - `OfficialUpdateService` 能读写 `official-bootstrap-cache.json`,并支持默认开启的 `audit_local` / `repair` CLI 行为 - 下载层能在本地文件 size/BLAKE3/path、ZIP 结构或 manifest 不匹配时重新下载 - 官方 seed `.hash` mismatch 会导致下载失败,而不是降级为本地 BLAKE3 猜测 - 非官方 URL 会在 fetch/download 入口被拒绝 -- 尚未在仓库中记录真实官方网络全量下载 smoke test +- 真实官方网络全量下载 smoke 已固化为 `scripts/official-full-pull-smoke.sh`、`make official-smoke` 和 `docs/guides/official-full-pull-smoke.md`;大型资源文件和运行报告默认保留在 `/tmp` 隔离目录,不纳入 Git 相关验证主要来自: diff --git a/docs/guides/baseline.md b/docs/guides/baseline.md index f44eb2e..36e1d3e 100644 --- a/docs/guides/baseline.md +++ b/docs/guides/baseline.md @@ -89,7 +89,7 @@ git check-ignore -v Cargo.lock CLAUDE.md CAS V1 和 Rust 官方同步闭环完成后,下一阶段优先推进: 1. Go CLI 的 `doctor` 和基础命令框架。 -2. 真实官方网络全量下载 smoke 记录。 +2. 按 `docs/guides/official-full-pull-smoke.md` 执行真实官方网络全量下载 smoke,并保留隔离目录报告。 3. 官方同步结果接入 CAS + ResourceRepository。 4. AssetBundle UnityFS 解析。 @@ -99,6 +99,7 @@ CAS V1 和 Rust 官方同步闭环完成后,下一阶段优先推进: 2. `CURRENT_STATUS.md` 3. `docs/reports/CURRENT_GAPS.md` 4. `docs/guides/official-resource-test-pull.md` -5. `docs/architecture/official-resource-backend.md` -6. `docs/architecture/adr/0001-engine-and-application-boundaries.md` -7. `docs/architecture/adr/0002-cas-v1-design-boundary.md` +5. `docs/guides/official-full-pull-smoke.md` +6. `docs/architecture/official-resource-backend.md` +7. `docs/architecture/adr/0001-engine-and-application-boundaries.md` +8. `docs/architecture/adr/0002-cas-v1-design-boundary.md` diff --git a/docs/guides/official-full-pull-smoke.md b/docs/guides/official-full-pull-smoke.md new file mode 100644 index 0000000..3b2623f --- /dev/null +++ b/docs/guides/official-full-pull-smoke.md @@ -0,0 +1,99 @@ +# 官方全量拉取 Smoke Runbook + +本文档固定真实官方网络全量拉取 smoke 的可重复命令。该流程会访问官方日服 HTTP 资源并下载完整 Windows + Android 资源集合,输出目录必须是隔离目录。 + +## 目标 + +验证当前 `bat` 能在真实官方网络环境下完成: + +1. `--auto-discover --dry-run --plan` +2. 首次全量拉取并发布到 `current` +3. 第二次运行返回 `up_to_date` +4. 人工破坏 active release 中一个资源文件 +5. `repair` 检出并重新下载损坏文件 +6. `verify` 在 repair 后通过 + +## 一条命令 + +默认写入 `/tmp/bat-official-smoke-/`: + +```bash +scripts/official-full-pull-smoke.sh +``` + +也可以通过 Makefile 执行: + +```bash +make official-smoke +``` + +脚本默认会先构建 release binary: + +```bash +cargo build --release -p bat-infrastructure --bin bat +``` + +## 输出 + +默认目录结构: + +```text +/tmp/bat-official-smoke-/ + resources/ + current -> versions/ + versions// + .staging/ + state/ + report/ + 01-dry-run-plan.command.txt + 01-dry-run-plan.stdout.json + 01-dry-run-plan.stderr.log + 02-first-full-pull.command.txt + 02-first-full-pull.stdout.json + 02-first-full-pull.stderr.log + 03-second-up-to-date.command.txt + 03-second-up-to-date.stdout.json + 03-second-up-to-date.stderr.log + 04-repair-after-damage.command.txt + 04-repair-after-damage.stdout.json + 04-repair-after-damage.stderr.log + 05-verify-after-repair.command.txt + 05-verify-after-repair.stdout.json + 05-verify-after-repair.stderr.log + damaged-file.txt + damaged-file.before.txt + SMOKE_REPORT.md +``` + +`damaged-file.before.txt` 只记录被破坏文件的路径、大小和 sha256,不复制原始资源文件。`SMOKE_REPORT.md` 记录本次 smoke 的实际输出目录、active release、文件数量、release 大小和被破坏的文件。大型资源文件不进入 Git。 + +脚本会在关键步骤后自动检查: + +- 首次全量拉取 stderr log 包含总体下载进度、单文件进度和校验结果。 +- 二次运行 stdout JSON 包含 `update_status=up_to_date`。 +- repair stdout JSON 包含 `command=repair` 和 `status=completed`。 +- repair stderr log 包含总体下载进度、单文件进度和校验结果。 +- repair 后 verify stdout JSON 包含 `healthy=true`。 + +## 环境变量 + +- `BAT_SMOKE_ROOT`:覆盖默认根目录。 +- `BAT_SMOKE_OUTPUT`:覆盖资源发布根目录。 +- `BAT_SMOKE_STATE_DIR`:覆盖 daemon/status 目录。 +- `BAT_SMOKE_REPORT_DIR`:覆盖报告目录。 +- `BAT_BIN`:使用已有 `bat` binary。 +- `BAT_SMOKE_SKIP_BUILD=1`:跳过 release 构建。 +- `BAT_SMOKE_ALLOW_NON_TMP=1`:允许 `BAT_SMOKE_OUTPUT` 指向非 `/tmp` 路径。只可用于确认隔离的测试目录。 + +## 安全边界 + +脚本默认拒绝非 `/tmp` 输出目录,且拒绝复用非空输出目录。不要把输出目录指向已有客户端、官方启动器安装目录、生产资源目录或开发机人工维护资源目录。 + +## 成功判定 + +脚本全部步骤退出码为 0 即表示 smoke 通过。重点检查: + +- `03-second-up-to-date.stdout.json` 中 `update_status` 为 `up_to_date`。 +- `04-repair-after-damage.stdout.json` 中 repair 完成,且有重新下载或修复行为。 +- `05-verify-after-repair.stdout.json` 中 `healthy` 为 `true`。 +- `02-first-full-pull.stderr.log` 和 `04-repair-after-damage.stderr.log` 中包含下载总体进度、单文件进度和校验结果日志。 diff --git a/docs/guides/official-resource-test-pull.md b/docs/guides/official-resource-test-pull.md index fc6fb0c..71739ac 100644 --- a/docs/guides/official-resource-test-pull.md +++ b/docs/guides/official-resource-test-pull.md @@ -258,13 +258,26 @@ cargo run -p bat-infrastructure --bin bat -- \ --error-retry 60s ``` -默认平台是 `Windows,Android`,无需显式传 `--platforms`;只有要覆盖默认平台时才传。`--interval` 是正常检查周期,默认 `1h`;watch/daemon 模式还会在每天北京时间(UTC+8)`03:00`、`16:00`、`18:00` 强制执行一次自动刷新,该轮会注入 `force=true`,并且会中断普通 interval 的 sleep。`--error-retry` 是下载、发现或校验失败后的重试周期,默认 `60s`,也可以用 `--error-retry-seconds 60`。CLI 默认启动时向 stderr 打印 `BlueArchiveToolkit` ASCII banner,并把阶段进度日志写到 stderr,包括自动发现、server-info、marker、catalog、audit、download、snapshot 和 publish 阶段;daemon 还会写 `bat-events.jsonl` 结构化日志并按大小轮转。命令结果默认以人类可读摘要写到 stdout。需要纯机器输出时加 `--json --no-progress`,需要显式开启进度日志则用 `--progress`;只想关闭横幅但保留日志时可加 `--no-banner`。错误时 stderr 输出 JSON error,watch 模式下错误 JSON 的 `next_retry_seconds` 使用失败重试周期;如果未关闭 progress,错误 JSON 前可能已有 banner 和进度日志。普通错误 exit `1`,资源目录锁冲突 exit `75`,`verify` 或 `doctor` 发现问题也返回非 0。 +默认平台是 `Windows,Android`,无需显式传 `--platforms`;只有要覆盖默认平台时才传。`--interval` 是正常检查周期,默认 `1h`;watch/daemon 模式还会在每天北京时间(UTC+8)`03:00`、`16:00`、`18:00` 强制执行一次自动刷新,该轮会注入 `force=true`,并且会中断普通 interval 的 sleep。`--error-retry` 是下载、发现或校验失败后的重试周期,默认 `60s`,也可以用 `--error-retry-seconds 60`。CLI 默认启动时向 stderr 打印 `BlueArchiveToolkit` ASCII banner,并把阶段进度日志写到 stderr,包括自动发现、server-info、marker、catalog、audit、download、snapshot 和 publish 阶段;download 阶段会输出总体下载进度和单文件开始/完成状态,audit 阶段会输出官方 `.hash`、本地 BLAKE3、需修复项和 ZIP 结构校验结果摘要。daemon 还会写 `bat-events.jsonl` 结构化日志并按大小轮转。命令结果默认以人类可读摘要写到 stdout。需要纯机器输出时加 `--json --no-progress`,需要显式开启进度日志则用 `--progress`;只想关闭横幅但保留日志时可加 `--no-banner`。错误时 stderr 输出 JSON error,watch 模式下错误 JSON 的 `next_retry_seconds` 使用失败重试周期;如果未关闭 progress,错误 JSON 前可能已有 banner 和进度日志。普通错误 exit `1`,资源目录锁冲突 exit `75`,`verify` 或 `doctor` 发现问题也返回非 0。 生产可以直接运行 `--watch`,也可以用 `--daemon` 后台运行,或者用 systemd service、容器或 Go 进程守护它。cron/systemd timer 仍可调用单次模式,但不再是 Rust 自动更新的唯一方式。项目是否热更新、热重载或重启进程,由上层业务集成决定。生产资源目录应使用独立输出目录,不要指向现有客户端或人工维护的资源目录;上层读取资源时应读取 `--output/current`,不要读取 `.staging` 或 `versions` 中未切换的目录。非 dry-run 每轮会创建 `--output/.official-sync.lock`,防止并发写同一资源目录;live daemon 还会阻止前台写命令直接修改它正在管理的同一目录。 需要只做探测时可以加 `--dry-run`。需要关闭本地 audit 或 repair 时可以显式使用 `--no-audit-local` 或 `--no-repair`,但生产同步默认应保持开启。 -## 6. 例外输入 +## 6. 真实全量 smoke + +真实官方网络全量拉取 smoke 已固化为 runbook 和脚本: + +```bash +scripts/official-full-pull-smoke.sh + +# 或 +make official-smoke +``` + +默认输出在 `/tmp/bat-official-smoke-/`,脚本会执行 dry-run plan、首次全量拉取、二次 `up_to_date`、本地文件破坏后的 `repair`、repair 后 `verify`,并检查 stderr progress log 中存在总体下载进度、单文件进度和校验结果摘要。完整说明见 `docs/guides/official-full-pull-smoke.md`。 + +## 7. 例外输入 可接受的 `server-info` 输入是: @@ -275,7 +288,7 @@ cargo run -p bat-infrastructure --bin bat -- \ 如果没有显式 `server-info` 输入,必须手动加 `--auto-discover` 才会走官方 metadata / `GameMainConfig` 辅助发现链路。不要在 Linux 生产任务里依赖已安装启动器或本地客户端目录。 -## 7. 代码入口 +## 8. 代码入口 当前可用的用户入口: diff --git a/docs/reports/CURRENT_GAPS.md b/docs/reports/CURRENT_GAPS.md index f6823a9..9720f7f 100644 --- a/docs/reports/CURRENT_GAPS.md +++ b/docs/reports/CURRENT_GAPS.md @@ -319,26 +319,30 @@ - GitHub Actions 或等价 CI 执行 format、lint、test、build。 -### G-018:真实官方网络全量下载 smoke test 未记录 +### G-018:真实官方网络全量下载 smoke test 已固化为可重复命令 -状态:**未关闭** +状态:**已关闭(已固化可重复 smoke 命令;真实下载产物不纳入 Git)** -现象: +历史现象: - 本地测试覆盖 mock、fixture、synthetic import 和 CLI 参数。 -- 尚未在隔离输出目录记录一次真实官方网络全量下载。 +- 曾缺少真实官方网络全量下载的固定 runbook 和可重复命令。 -影响: +处理结果: -- 无法用文档证明当前 `bat` 在真实官方网络环境下完成首次下载、二次静默 up-to-date 和本地损坏 repair。 +- 新增 `scripts/official-full-pull-smoke.sh`,默认在 `/tmp/bat-official-smoke-/` 下创建隔离资源目录、状态目录和报告目录。 +- 新增 `make official-smoke` 统一入口。 +- 新增 `docs/guides/official-full-pull-smoke.md`,记录目标、命令、输出结构、环境变量、安全边界和成功判定。 +- smoke 流程覆盖 dry-run plan、首次全量拉取、二次 `up_to_date`、人工破坏 active release 文件后的 `repair`、repair 后 `verify`。 +- 脚本会检查二次 `up_to_date`、repair 完成、verify `healthy=true`,并检查首次拉取和 repair 的 stderr log 中存在总体下载进度、单文件进度和校验结果日志。 +- 运行报告 `SMOKE_REPORT.md` 记录实际输出目录、active release、文件数量、release 大小和被破坏文件;大型官方资源文件保留在隔离输出目录,不纳入 Git。 验收: -- 使用独立目录执行 dry-run。 -- 执行真实首次下载,不指向任何现有客户端或人工维护资源目录。 -- 二次运行返回 `up_to_date`,watch 模式在 up-to-date 时静默。 -- 人工破坏一个本地文件后,audit 检出并 repair。 -- 记录命令、摘要、输出目录和未纳入 Git 的大文件位置。 +- 使用 `scripts/official-full-pull-smoke.sh` 或 `make official-smoke`。 +- 默认输出目录必须是独立 `/tmp` 目录;非 `/tmp` 路径需要显式设置 `BAT_SMOKE_ALLOW_NON_TMP=1`,且输出目录必须为空。 +- 脚本退出码为 0 即表示 runbook 验收通过。 +- 真实网络执行需要外部网络和足够磁盘空间;本仓库只保存 runbook、脚本和测试,不保存官方大文件。 --- diff --git a/docs/reports/current-stage-prepush.md b/docs/reports/current-stage-prepush.md index 6e428fe..afaab88 100644 --- a/docs/reports/current-stage-prepush.md +++ b/docs/reports/current-stage-prepush.md @@ -1,8 +1,8 @@ # 当前阶段说明 -- **更新时间**:2026-07-06 +- **更新时间**:2026-07-14 - **状态分支**:`experiment` -- **最新功能提交**:`789402c feat: add official resource sync pipeline` +- **最新功能提交**:以当前 `git log --oneline -1` 为准 - **用途**:当前阶段交付说明、推送前核查和下一步依据。 ## 1. 当前目标 @@ -28,6 +28,8 @@ - `OfficialResourcePullService` 的官方 URL 拒绝、`.part` 续传、重试、本地 manifest、官方 seed `.hash` 校验。 - `OfficialUpdateService` 的 auto-discover、bootstrap cache、v2 snapshot、remote marker diff、本地 audit/repair。 - `bat` one-shot 和 `--watch`。 +- `bat` 运行时 progress log 覆盖总体下载进度、单文件进度和校验结果摘要。 +- 真实官方网络全量下载 smoke 已固化为 `scripts/official-full-pull-smoke.sh`、`make official-smoke` 和 `docs/guides/official-full-pull-smoke.md`。 - `bat-ffi` 的 Manifest inspect 和 sync plan JSON API。 - `SqliteResourceRepository`。 @@ -35,7 +37,6 @@ - Go CLI 最小可用入口。 - 官方同步下载结果自动导入 CAS + ResourceRepository 的用户级流程。 -- 真实官方网络全量下载 smoke 记录。 - 完整 AssetBundle 解析。 - Patch、翻译系统、API Server、Web。 @@ -62,11 +63,11 @@ git status --short ## 4. 当前结论 -当前 Rust 官方资源同步链路已经具备可运行闭环,但仓库还不是完整产品。下一步不应继续只扩展 example,而应补齐 Go CLI 最小入口、真实网络 smoke、CAS/ResourceRepository 编排和 AssetBundle 解析。 +当前 Rust 官方资源同步链路已经具备可运行闭环,并已固化真实网络 smoke 的可重复命令,但仓库还不是完整产品。下一步不应继续只扩展 example,而应补齐 Go CLI 最小入口、CAS/ResourceRepository 编排和 AssetBundle 解析。 ## 5. 下一步 1. 实现 Go CLI:`bat doctor`、`bat sync --help`、`bat official sync --help`。 -2. 为 `bat` 做真实官方网络 smoke,输出目录必须隔离。 +2. 按 `docs/guides/official-full-pull-smoke.md` 在具备网络和磁盘窗口的环境中执行真实官方网络 smoke,输出目录必须隔离。 3. 将官方同步下载结果接入 CAS + `SqliteResourceRepository`。 4. 开始 AssetBundle UnityFS header/block/directory 解析。 diff --git a/docs/reports/current-status-handoff.md b/docs/reports/current-status-handoff.md index fc0f1a0..f18c77f 100644 --- a/docs/reports/current-status-handoff.md +++ b/docs/reports/current-status-handoff.md @@ -1,13 +1,13 @@ # 当前进度交接 -- **更新时间**:2026-07-06 +- **更新时间**:2026-07-14 - **状态分支**:`experiment` -- **最新功能提交**:`789402c feat: add official resource sync pipeline` +- **最新功能提交**:以当前 `git log --oneline -1` 为准 - **用途**:给下一次对话快速恢复上下文,优先以当前工作区和已验证结果为准。 ## 1. 现在处在什么阶段 -项目已经完成基础整理、CAS V1 和 Rust 官方资源同步闭环。下一阶段重点是 Go CLI 最小可用、真实官方网络 smoke、同步结果进入 CAS/ResourceRepository,以及 AssetBundle 解析起步。 +项目已经完成基础整理、CAS V1、Rust 官方资源同步闭环,以及真实官方网络 smoke 的可重复命令固化。下一阶段重点是 Go CLI 最小可用、同步结果进入 CAS/ResourceRepository,以及 AssetBundle 解析起步。 已确认的事实: @@ -19,6 +19,8 @@ - 默认平台是 `Windows + Android`。 - 真实下载会维护 `official-download-manifest.json`,并使用 size + BLAKE3、本地 audit/repair 和官方 seed `.hash` 校验。 - `--watch` 是 Rust 内部常驻检查模式,正常检查默认 1 小时,失败重试默认 60 秒;外部 systemd/container 只负责守护进程。 +- `bat` progress log 会输出总体下载进度、单文件进度和校验结果摘要。 +- 真实官方网络 smoke 已固化为 `scripts/official-full-pull-smoke.sh`、`make official-smoke` 和 `docs/guides/official-full-pull-smoke.md`,默认写入 `/tmp` 隔离目录。 ## 2. 现在不要误解的点 @@ -34,7 +36,7 @@ 仍待实现的主线工作: 1. Go CLI 最小入口:`bat doctor`、`bat sync --help`、`bat official sync --help`。 -2. 真实官方网络 smoke:dry-run、首次下载、二次 up-to-date、本地损坏 repair。 +2. 按 smoke runbook 在具备网络和磁盘窗口的环境中执行真实官方网络 smoke,并保留隔离目录报告。 3. 官方同步下载结果进入 CAS + `SqliteResourceRepository` 的用户级流程。 4. Addressables parser 覆盖更多真实 catalog 结构。 5. AssetBundle UnityFS header/block/directory 解析。 @@ -57,4 +59,5 @@ - `DOCS_INDEX.md` - `docs/reports/CURRENT_GAPS.md` - `docs/guides/official-resource-test-pull.md` +- `docs/guides/official-full-pull-smoke.md` - `docs/architecture/official-resource-backend.md` diff --git a/infrastructure/src/official_update.rs b/infrastructure/src/official_update.rs index c65ada2..274e612 100644 --- a/infrastructure/src/official_update.rs +++ b/infrastructure/src/official_update.rs @@ -1048,6 +1048,10 @@ impl OfficialUpdateService { }; if !should_download { + progress(OfficialUpdateProgress::new( + "audit", + verification_progress_message(&report.verification_summary), + )); progress(OfficialUpdateProgress::new( "finish", "资源已是最新;无需下载", @@ -1138,6 +1142,16 @@ impl OfficialUpdateService { let final_audit = staging_fetcher .audit_local_manifest(&pull_plan) .map_err(anyhow::Error::msg)?; + let final_verification_summary = OfficialVerificationSummary::new( + final_audit.manifest_blake3_verified_count(), + final_audit.repair_needed_count(), + pull_report.official_hash_verified_count(), + final_audit.zip_structure_verified_count(), + ); + progress(OfficialUpdateProgress::new( + "audit", + verification_progress_message(&final_verification_summary), + )); progress(OfficialUpdateProgress::new( "snapshot", format!("写入快照 {}", staging_snapshot_path.display()), @@ -1175,12 +1189,7 @@ impl OfficialUpdateService { report.official_seed_hash_verified_count = pull_report.official_hash_verified_count(); report.local_manifest_verified_count = final_audit.verified_count(); report.local_manifest_repair_needed_count = final_audit.repair_needed_count(); - report.verification_summary = OfficialVerificationSummary::new( - final_audit.manifest_blake3_verified_count(), - final_audit.repair_needed_count(), - pull_report.official_hash_verified_count(), - final_audit.zip_structure_verified_count(), - ); + report.verification_summary = final_verification_summary; report.snapshot_written = Some(final_snapshot_path); progress(OfficialUpdateProgress::new( @@ -1356,7 +1365,13 @@ fn progress_from_pull_event(event: OfficialResourcePullProgress) -> OfficialUpda match event.kind { OfficialResourcePullProgressKind::Started => OfficialUpdateProgress::new( "download", - format!("({}/{}) 开始 {}", event.index, event.total, event.url), + format!( + "下载进度:总体 {}/{} ({:.1}%);单文件开始 URL={}", + event.index, + event.total, + download_progress_percent(event.index, event.total), + event.url + ), ) .with_download_progress(&event), OfficialResourcePullProgressKind::Finished => { @@ -1364,9 +1379,10 @@ fn progress_from_pull_event(event: OfficialResourcePullProgress) -> OfficialUpda OfficialUpdateProgress::new( "download", format!( - "({}/{}) 完成 状态={} 文件字节={} 本轮传输字节={} {}", + "下载进度:总体 {}/{} ({:.1}%);单文件完成 状态={} 文件字节={} 本轮传输字节={} URL={}", event.index, event.total, + download_progress_percent(event.index, event.total), status, event.bytes.unwrap_or(0), event.transferred_bytes.unwrap_or(0), @@ -1378,6 +1394,24 @@ fn progress_from_pull_event(event: OfficialResourcePullProgress) -> OfficialUpda } } +fn download_progress_percent(index: usize, total: usize) -> f64 { + if total == 0 { + 100.0 + } else { + (index as f64 / total as f64) * 100.0 + } +} + +fn verification_progress_message(summary: &OfficialVerificationSummary) -> String { + format!( + "校验结果:官方 .hash={};本地 BLAKE3 通过={};本地需修复={};ZIP 结构通过={}", + summary.official_hash_verified_count, + summary.local_manifest_blake3_verified_count, + summary.local_manifest_repair_needed_count, + summary.zip_structure_verified_count + ) +} + fn localized_pull_status(status: crate::OfficialResourcePullStatus) -> &'static str { match status { crate::OfficialResourcePullStatus::SkippedExisting => "已复用", diff --git a/infrastructure/tests/official_game_main_config_bootstrap.rs b/infrastructure/tests/official_game_main_config_bootstrap.rs index b466fa0..927daff 100644 --- a/infrastructure/tests/official_game_main_config_bootstrap.rs +++ b/infrastructure/tests/official_game_main_config_bootstrap.rs @@ -229,6 +229,17 @@ fn official_update_first_run_pulls_without_pre_audit() { assert!(events .iter() .any(|event| { event.stage == "decision" && event.message.contains("首次拉取=true") })); + assert!(events.iter().any(|event| { + event.stage == "download" + && event.message.contains("下载进度:总体") + && event.message.contains("单文件") + })); + assert!(events.iter().any(|event| { + event.stage == "audit" + && event.message.contains("校验结果:官方 .hash=5") + && event.message.contains("本地 BLAKE3 通过=19") + && event.message.contains("ZIP 结构通过=6") + })); let published = report.published_version_path.as_ref().unwrap(); assert!(published.exists()); @@ -280,6 +291,12 @@ fn official_update_second_run_audits_existing_resources_before_reuse() { assert!(events.iter().any(|event| { event.stage == "audit" && event.message.contains("审计本地下载 manifest") })); + assert!(events.iter().any(|event| { + event.stage == "audit" + && event.message.contains("校验结果:官方 .hash=0") + && event.message.contains("本地 BLAKE3 通过=19") + && event.message.contains("ZIP 结构通过=6") + })); assert!(events.iter().any(|event| { event.stage == "decision" && event.message.contains("首次拉取=false") })); diff --git a/scripts/official-full-pull-smoke.sh b/scripts/official-full-pull-smoke.sh new file mode 100755 index 0000000..df5f7f0 --- /dev/null +++ b/scripts/official-full-pull-smoke.sh @@ -0,0 +1,184 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" +cd "${REPO_ROOT}" + +timestamp="$(date -u +%Y%m%dT%H%M%SZ)" +SMOKE_ROOT="${BAT_SMOKE_ROOT:-/tmp/bat-official-smoke-${timestamp}}" +OUTPUT_DIR="${BAT_SMOKE_OUTPUT:-${SMOKE_ROOT}/resources}" +STATE_DIR="${BAT_SMOKE_STATE_DIR:-${SMOKE_ROOT}/state}" +REPORT_DIR="${BAT_SMOKE_REPORT_DIR:-${SMOKE_ROOT}/report}" +BAT_BIN="${BAT_BIN:-${REPO_ROOT}/target/release/bat}" + +fail() { + printf 'error: %s\n' "$*" >&2 + exit 1 +} + +real_output="$(realpath -m "${OUTPUT_DIR}")" +case "${real_output}" in + /tmp/*) ;; + *) + if [[ "${BAT_SMOKE_ALLOW_NON_TMP:-}" != "1" ]]; then + fail "BAT_SMOKE_OUTPUT must be under /tmp by default; set BAT_SMOKE_ALLOW_NON_TMP=1 only for an isolated test path" + fi + ;; +esac + +if [[ -e "${OUTPUT_DIR}" ]] && [[ -n "$(find "${OUTPUT_DIR}" -mindepth 1 -maxdepth 1 -print -quit 2>/dev/null)" ]]; then + fail "output directory is not empty: ${OUTPUT_DIR}" +fi + +mkdir -p "${OUTPUT_DIR}" "${STATE_DIR}" "${REPORT_DIR}" + +if [[ "${BAT_SMOKE_SKIP_BUILD:-}" != "1" ]]; then + cargo build --release -p bat-infrastructure --bin bat +fi + +[[ -x "${BAT_BIN}" ]] || fail "bat binary is not executable: ${BAT_BIN}" + +write_command_file() { + local name="$1" + shift + local command_file="${REPORT_DIR}/${name}.command.txt" + : >"${command_file}" + printf '$' >>"${command_file}" + printf ' %q' "$@" >>"${command_file}" + printf '\n' >>"${command_file}" +} + +run_step() { + local name="$1" + shift + printf '==> %s\n' "${name}" + write_command_file "${name}" "$@" + "$@" >"${REPORT_DIR}/${name}.stdout.json" 2>"${REPORT_DIR}/${name}.stderr.log" +} + +require_contains() { + local path="$1" + local needle="$2" + local description="$3" + if ! grep -Fq "${needle}" "${path}"; then + fail "${description}; expected '${needle}' in ${path}" + fi +} + +run_step "01-dry-run-plan" \ + "${BAT_BIN}" \ + --auto-discover \ + --output "${OUTPUT_DIR}" \ + --state-dir "${STATE_DIR}" \ + --dry-run \ + --plan \ + --json \ + --no-progress + +run_step "02-first-full-pull" \ + "${BAT_BIN}" \ + --auto-discover \ + --output "${OUTPUT_DIR}" \ + --state-dir "${STATE_DIR}" \ + --json \ + --progress \ + --no-banner + +require_contains "${REPORT_DIR}/02-first-full-pull.stderr.log" "下载进度:总体" "first pull did not emit total download progress" +require_contains "${REPORT_DIR}/02-first-full-pull.stderr.log" "单文件" "first pull did not emit single-file download progress" +require_contains "${REPORT_DIR}/02-first-full-pull.stderr.log" "校验结果:" "first pull did not emit verification summary" + +run_step "03-second-up-to-date" \ + "${BAT_BIN}" \ + --auto-discover \ + --output "${OUTPUT_DIR}" \ + --state-dir "${STATE_DIR}" \ + --json \ + --no-progress + +require_contains "${REPORT_DIR}/03-second-up-to-date.stdout.json" '"update_status": "up_to_date"' "second run did not report up_to_date" + +current_link="${OUTPUT_DIR}/current" +[[ -e "${current_link}" ]] || fail "current release symlink/path was not created: ${current_link}" +current_dir="$(realpath "${current_link}")" + +damage_file="$(find "${current_dir}" -type f \ + ! -name 'official-sync-snapshot.json' \ + ! -name 'official-download-manifest.json' \ + -print -quit)" +[[ -n "${damage_file}" ]] || fail "no resource file found to damage under ${current_dir}" + +printf '%s\n' "${damage_file}" >"${REPORT_DIR}/damaged-file.txt" +{ + printf 'path: %s\n' "${damage_file}" + printf 'bytes: %s\n' "$(wc -c <"${damage_file}" | tr -d ' ')" + printf 'sha256: %s\n' "$(sha256sum "${damage_file}" | awk '{print $1}')" +} >"${REPORT_DIR}/damaged-file.before.txt" +printf 'BAT official smoke intentional corruption\n' >"${damage_file}" + +run_step "04-repair-after-damage" \ + "${BAT_BIN}" \ + repair \ + --auto-discover \ + --output "${OUTPUT_DIR}" \ + --state-dir "${STATE_DIR}" \ + --json \ + --progress \ + --no-banner + +require_contains "${REPORT_DIR}/04-repair-after-damage.stdout.json" '"command": "repair"' "repair command report missing" +require_contains "${REPORT_DIR}/04-repair-after-damage.stdout.json" '"status": "completed"' "repair command did not complete" +require_contains "${REPORT_DIR}/04-repair-after-damage.stderr.log" "下载进度:总体" "repair did not emit total download progress" +require_contains "${REPORT_DIR}/04-repair-after-damage.stderr.log" "单文件" "repair did not emit single-file download progress" +require_contains "${REPORT_DIR}/04-repair-after-damage.stderr.log" "校验结果:" "repair did not emit verification summary" + +run_step "05-verify-after-repair" \ + "${BAT_BIN}" \ + verify \ + --auto-discover \ + --output "${OUTPUT_DIR}" \ + --state-dir "${STATE_DIR}" \ + --json \ + --no-progress + +require_contains "${REPORT_DIR}/05-verify-after-repair.stdout.json" '"healthy": true' "verify after repair did not report healthy=true" + +file_count="$(find "${current_dir}" -type f | wc -l | tr -d ' ')" +current_size="$(du -sh "${current_dir}" | awk '{print $1}')" +report_file="${REPORT_DIR}/SMOKE_REPORT.md" + +cat >"${report_file}" <.command.txt +- .stdout.json +- .stderr.log + +The large resource output is intentionally outside Git and remains under: + +\`${OUTPUT_DIR}\` +EOF + +printf '\nSmoke report: %s\n' "${report_file}" +printf 'Resource output: %s\n' "${OUTPUT_DIR}"