Repository navigation
ci(android): 修完 Android CI 剩余四层 —— 该检查首次全绿(22/22 步骤通过) - #122
Merged
Merged
Conversation
`Android arm64 Release + JNI Guards` 此前**从未通过过**(直推绕过,没人跑过)。
前序 PR 已修掉前 3 层(setup-android action 跑已移除的 `sdkmanager tools`、
`yes |` 的 SIGPIPE、仓库里写死的 Windows `-Djava.io.tmpdir`)。
本提交修掉**剩余四层**,构建从「5 秒即挂」跑到 **BUILD SUCCESSFUL**:
### ① 胶水生成改用 `cargo metadata`(替换 main 上的 glob 版)
main 上那一版用 `ls -d ~/.cargo/registry/src/*/` 探测注册表,但该步骤之前**没有任何
cargo 步骤**、`registry/src` 还没被解包 → glob 无匹配 → `ls` 返回 2 → 配合
`set -euo pipefail` 让脚本**静默 exit 2**(日志零输出,极难定位)。
改用 `cargo metadata --format-version 1 --locked`:它自己完成解析与源码解包,
`manifest_path` 直接给出每个 crate 的真实根目录。插件集合取自真实依赖图,
路径统一用 `/` 拼接(`os.path.join` 在 Windows 上插回反斜杠,Groovy 里 `\a` `\u` 是转义)。
### ② 装 `cargo-ndk`
本仓走 `gradle → BuildTask.kt → cargo ndk`(**不是** `tauri android build`),
runner 上必须有该子命令,否则:
error: no such command: `ndk`
> Task :app:rustBuildArm64Release FAILED
### ③ 生成一次性签名密钥
cargo-ndk 装上后构建跑到**最后一步**,只剩打包失败:
> Task :app:packageArm64Release FAILED
> SigningConfig "release" is missing required property "storeFile".
**修正一处误判**:`app/build.gradle` 对「缺 keystore.properties」只 warn,但 AGP 的
打包任务在 `storeFile` 未设置时仍会**硬失败** —— 不能指望它优雅降级。
本 job 只做构建验证、不产出可分发产物,故用 `keytool` 生成一次性自签密钥。
### ④ 跨 target clippy 放行 dead_code / unused_imports
APK 构建成功后,**之后**的 `Cargo clippy (aarch64-linux-android)` 步骤失败:
CI 报 27 个 dead_code、本地复现 6 个 unused_imports,**全部**是桌面专用的符号与导入
在移动端天然无人使用(`get_active_window_info` / `win32` / `window_config` /
`Emitter` / `WebviewWindow` / `build_configured_window` …)。它们不是真的死代码,
只是**另一个 target 的入口**。
该步骤加 `-A dead_code -A unused_imports`,**放行范围克制**:host-target 的 clippy
(`ci.yml` 的 `Rust Clippy (Lint)`,必需检查)仍全量强制,所以桌面侧的 unused_imports
依然会被拦下 —— 这里只抵消「同一份代码在另一个 target 上不用」这一类假阳性。
更彻底的做法是加 `#[cfg(not(mobile))]`,要动 5 个产品代码文件,留作后续。
## 验证
**CI(本轮实测)**:22/22 步骤全部 success ——
`Locate Android SDK` / `Install Android SDK components` / `Install cargo-ndk` /
`Generate throwaway signing key` / `Generate Tauri Android glue` /
**`Build arm64 release APK`** / **`Cargo clippy (aarch64-linux-android)`** /
**`JNI keeps guard (DEX + ELF)`** / `Upload APK artifact`
→ 产出制品 `spiritpal-arm64-release-apk`(13.8 MB)。
**本地对照**:`assembleArm64Release` BUILD SUCCESSFUL(8m54s,231 任务);
跨 target clippy rc=0;生成的 include 项与本地手工版逐项一致。
## 说明
本分支从 main 重开(原 #120 因 main 已含 glob 版胶水步骤而冲突;
本仓钩子禁止 merge 提交,故不 rebase、不 force push,改为重开 PR)。
GOTCHAS #185 / #186
Signed-off-by: ReSerendipity <259081123+ReSerendipity@users.noreply.github.com>
This was referenced Oct 16, 2026
ReSerendipity
added a commit
that referenced
this pull request
Oct 16, 2026
* refactor(rust): 跨 target lint 放行从 CI flag 改为 crate 根声明 ## 动机 #122 里为让 `Cargo clippy (aarch64-linux-android)` 通过,在 workflow 里加了 `-A dead_code -A unused_imports`。两个问题: 1. **本地与 CI 行为不一致** —— 本地不带这些 flag 跑 clippy 会红(27 个 error), 容易误判成「代码坏了」; 2. **意图藏在 CI 配置里**,读代码的人看不到「这些符号在移动端天然未使用是设计如此」。 ## 改动 `src-tauri/src/lib.rs` 加一行 crate 根声明,并删掉 workflow 里的 `-A` 放行: ```rust #![cfg_attr(mobile, allow(dead_code, unused_imports))] ``` 这 27 处(21 dead_code + 6 unused_imports)横跨 6 个文件,全部是**桌面专用符号在移动端 天然未使用**:`win32`(get_active_window_info / get_idle_ms / is_foreground_fullscreen)、 `commands::window`(WindowConfigDto / WindowConfig / into_dto / window_config / get_window_config / parse_hex_color / build_configured_window / open_path)、 `mcp_bridge`(Pending / PENDING / pending / NEXT_ID / to_hex / gen_token / spawn / mcp_respond / mcp_respond_core)、`keychain::SystemKeychainStore`、 `antidebug` 的 `log::warn`、`setup::build_configured_window` 及 lib.rs 顶层 `WebviewWindow` / `Emitter` / `get_window_config`。 **为什么用一行声明而不是 27 个 `#[cfg(not(mobile))]`**:后者每个注解都要重新验证桌面构建 (改错一处就编译失败),且把「这是另一个 target 的入口」这一事实稀释在 27 处; 一行声明语义集中、与 CI 放行**同范围**,但自解释、对本地/CI/IDE 一致生效。 `#[cfg(mobile)]` 是本仓既有惯例(`src/ondevice/engine.rs` / `image.rs` 均在用)。 ## 放行范围(已实测验证) | target | 行为 | |---|---| | `mobile` | 放行 `dead_code` + `unused_imports` | | **host** | **不受影响,仍全量强制** | host 侧用探针函数实测确认:往 lib.rs 塞一个无属性死函数 `fn zz_probe_dead_fn() {}` → `cargo clippy -- -D warnings` 报 `error: function zz_probe_dead_fn is never used`(rc=1); 并用 `#[cfg(mobile)] compile_error!()` 确认 `mobile` 在 host **不**成立。 ## 验证 - 移动端 clippy(**不带任何 `-A`**)`-- -D warnings` → rc=0 - host clippy `-- -D warnings` → rc=0 - `cargo fmt --check` → rc=0 - `cargo test` → 229/230 通过;唯一失败的 `integrity::tests::embedded_assets_match_sri_manifest` 经核实是**本地 dist/sri 陈旧产物不匹配**(与本次改动无关):重跑 `node scripts/obfuscate-and-sri.mjs` 后该测试即通过,且 main 上 CI 的 Rust Tests 一向通过。 GOTCHAS #186/#187 Signed-off-by: ReSerendipity <259081123+ReSerendipity@users.noreply.github.com> * test(pet): 修掉 usePetBehavior 定时行为用例的间歇性失败 ## 现象 #124 的 CI 上 `Lint and Test (Frontend)` 偶发失败: FAIL src/hooks/pet/usePetBehavior.test.tsx > usePetBehavior > scheduleNextBehavior 定时触发行为选择(mood=70 → 情绪动画) AssertionError: expected 'sit' to be 'happy' 本地单独跑该文件却稳定通过 —— 典型的**顺序/时间相关**偶发。 ## 根因 断言「mood=70 → renderState 为 happy」依赖两个**未固定的隐藏输入**: 1. **模块级单例的冷却表跨用例持久化** —— `getAnimationStateMachine()` 是懒加载单例, `lastPlayedAt` 一直累积。`pickEmotion` 把 `['happy','laugh','giggle']` 按 `isOnCooldown` 过滤,**若这三个被同批次的其它用例打上 30s 冷却** → 过滤空 → 返回 null → 落进 `pickBasic` → 取到 `'sit'`(`basics` 里随机)。CI 里测试并行/分批顺序与本地不同, 所以只有 CI 复现。 2. **`Math.random()` 贯穿决策** —— `scheduleNextBehavior` 的延迟区间(5s+rand×25s)、 贴边 5% 躲藏、候选内随机取一个,都随随机数漂移。 另有一个次要因素:`ctx.time = new Date().getHours()`,`pickBasic` 在 0-8 点优先 `sleep`、23 点或 <2 点优先 `drowsy` —— 夜间跑 CI 时兜底行为也不同。 ## 修法(只改测试,不动生产逻辑) `beforeEach` 里固定这两个隐藏输入: ```ts getAnimationStateMachine().resetCooldowns() // 已有公开 API,恢复「mood=70 必命中情绪候选」的前提 vi.spyOn(Math, 'random').mockReturnValue(0.5) // 让行为选择完全确定 ``` `afterEach` 里 `vi.restoreAllMocks()` 归还 random 间谍。生产逻辑本身是有意设计 (情绪优先 + 冷却 + 时段兜底),不应为测试而改。 ## 验证 - 该文件连跑 3 次 → 0 失败 - **全量 `vitest run` → 233 文件 / 2919 用例全过**(此前 2918 过 + 1 失败) - `prettier --check` 通过 GOTCHAS #187 Signed-off-by: ReSerendipity <259081123+ReSerendipity@users.noreply.github.com> --------- Signed-off-by: ReSerendipity <259081123+ReSerendipity@users.noreply.github.com> Co-authored-by: ReSerendipity <259081123+ReSerendipity@users.noreply.github.com>
ReSerendipity
added a commit
that referenced
this pull request
Oct 16, 2026
* refactor(rust): 跨 target lint 放行从 CI flag 改为 crate 根声明 ## 动机 #122 里为让 `Cargo clippy (aarch64-linux-android)` 通过,在 workflow 里加了 `-A dead_code -A unused_imports`。两个问题: 1. **本地与 CI 行为不一致** —— 本地不带这些 flag 跑 clippy 会红(27 个 error), 容易误判成「代码坏了」; 2. **意图藏在 CI 配置里**,读代码的人看不到「这些符号在移动端天然未使用是设计如此」。 ## 改动 `src-tauri/src/lib.rs` 加一行 crate 根声明,并删掉 workflow 里的 `-A` 放行: ```rust #![cfg_attr(mobile, allow(dead_code, unused_imports))] ``` 这 27 处(21 dead_code + 6 unused_imports)横跨 6 个文件,全部是**桌面专用符号在移动端 天然未使用**:`win32`(get_active_window_info / get_idle_ms / is_foreground_fullscreen)、 `commands::window`(WindowConfigDto / WindowConfig / into_dto / window_config / get_window_config / parse_hex_color / build_configured_window / open_path)、 `mcp_bridge`(Pending / PENDING / pending / NEXT_ID / to_hex / gen_token / spawn / mcp_respond / mcp_respond_core)、`keychain::SystemKeychainStore`、 `antidebug` 的 `log::warn`、`setup::build_configured_window` 及 lib.rs 顶层 `WebviewWindow` / `Emitter` / `get_window_config`。 **为什么用一行声明而不是 27 个 `#[cfg(not(mobile))]`**:后者每个注解都要重新验证桌面构建 (改错一处就编译失败),且把「这是另一个 target 的入口」这一事实稀释在 27 处; 一行声明语义集中、与 CI 放行**同范围**,但自解释、对本地/CI/IDE 一致生效。 `#[cfg(mobile)]` 是本仓既有惯例(`src/ondevice/engine.rs` / `image.rs` 均在用)。 ## 放行范围(已实测验证) | target | 行为 | |---|---| | `mobile` | 放行 `dead_code` + `unused_imports` | | **host** | **不受影响,仍全量强制** | host 侧用探针函数实测确认:往 lib.rs 塞一个无属性死函数 `fn zz_probe_dead_fn() {}` → `cargo clippy -- -D warnings` 报 `error: function zz_probe_dead_fn is never used`(rc=1); 并用 `#[cfg(mobile)] compile_error!()` 确认 `mobile` 在 host **不**成立。 ## 验证 - 移动端 clippy(**不带任何 `-A`**)`-- -D warnings` → rc=0 - host clippy `-- -D warnings` → rc=0 - `cargo fmt --check` → rc=0 - `cargo test` → 229/230 通过;唯一失败的 `integrity::tests::embedded_assets_match_sri_manifest` 经核实是**本地 dist/sri 陈旧产物不匹配**(与本次改动无关):重跑 `node scripts/obfuscate-and-sri.mjs` 后该测试即通过,且 main 上 CI 的 Rust Tests 一向通过。 GOTCHAS #186/#187 Signed-off-by: ReSerendipity <259081123+ReSerendipity@users.noreply.github.com> * test(rust): 补测 sqlite 数据访问辅助函数(10 个用例) `Rust Tests` 的覆盖率阈值在 #119 里被重置到 9%(承认 21.49% → 9.87% 的回退)。 本提交是「把覆盖率捡回来」的第一步:给**此前零覆盖的数据访问辅助函数**补测试。 ## 新增覆盖(`src-tauri/src/sqlite.rs`,10 个用例) | 函数 | 用例 | 覆盖点 | |---|---|---| | `json_rows` + `vref_to_json` | `..._maps_columns_and_all_types` | 列名映射 + Null/Integer/Real/Text 四个分支 | | 同上 | `..._blob_becomes_base64` | **BLOB → base64** 分支(历史 embedding 列的兼容路径) | | 同上 | `..._multiple_rows_and_params` | 多行 + 参数化 + ORDER BY | | 同上 | `..._sql_error_is_reported` | 预编译失败的错误文案 | | `exec` | `..._returns_affected_rows` | INSERT/UPDATE 的 affected rows | | `exec` | `..._sql_error_is_reported` | 执行失败的错误文案 | | `scalar` | `..._some_none_and_error` | Some / None / 参数化 / 错误四条路径 | | `scalar` | `..._type_mismatch_is_error` | FromSql 类型不符走错误路径(而非 panic) | | `column_exists` | `..._false_for_unknown` | 未知列 + 未知表均为 false | | `ensure_column` | `..._adds_then_is_idempotent` | 加列 + 二次调用幂等跳过 | 全部沿用既有 `mem_conn()`(内存库 + `ensure_schema`)基建,风格与现有用例一致。 ## 为什么挑这些 `sqlite.rs` 是全仓最大的 Rust 文件(2890 行)却只有 10 个用例,且其余函数都是 需要 `AppHandle` 的 Tauri 命令(单测难以直接驱动)—— 这几个私有辅助函数是 **性价比最高的可测面**,且错误路径(SQL 失败、类型不符)此前完全没被验证过, 属于「真会被走到但没人测」的代码。 ## 验证 - `cargo test sqlite::tests` → **20/20 通过**(原 10 + 新 10) - 全量 `cargo test` → 239 通过 / 1 失败;唯一失败是 `integrity::tests::embedded_assets_match_sri_manifest`,属**本地 dist/sri 陈旧产物不匹配** (与本次改动无关,重跑 `node scripts/obfuscate-and-sri.mjs` 即通过,main 上 CI 一向通过) - `cargo clippy -- -D warnings` → rc=0;`cargo fmt --check` → rc=0 ## 说明 覆盖率**只能在 CI 上量**(tarpaulin 仅支持 Linux)。本提交先加测试, 实际提升多少由本轮 CI 的 `Rust Tests` 读数决定,随后再按棘轮约定上调 `--fail-under`。 GOTCHAS #186 Signed-off-by: ReSerendipity <259081123+ReSerendipity@users.noreply.github.com> * ci: 覆盖率棘轮收紧到 9.9 —— #125 补测后实测 9.95% #125 给 sqlite 数据访问辅助函数补了 10 个用例,CI 实测覆盖率 **9.95%(856/8604 行)**,高于先前基线 9.87%。 按棘轮约定(实测值高于基线即收紧),`--fail-under` 由 9 上调到 **9.9**, 留 0.05 点余量防抖。⚠️ 9.9 仍是**承认回退后的低点**、不是目标值:历史基线 21.49%, 回到那里需要给本仓 ~15900 行 Rust 大量补测试(sqlite.rs 2890 行、 ondevice 模块等),属长期工程。棘轮现恢复了「不许回退」的语义。 GOTCHAS #186 Signed-off-by: ReSerendipity <259081123+ReSerendipity@users.noreply.github.com> --------- Signed-off-by: ReSerendipity <259081123+ReSerendipity@users.noreply.github.com> Co-authored-by: ReSerendipity <259081123+ReSerendipity@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
动机
Android arm64 Release + JNI Guards此前从未通过过(直推绕过,没人跑过)。前序 PR 已修掉前 3 层(setup-android action 跑已移除的
sdkmanager tools、yes |的 SIGPIPE、仓库里写死的 Windows-Djava.io.tmpdir)。本 PR 修掉剩余四层,构建从「5 秒即挂」跑到 BUILD SUCCESSFUL。
① 胶水生成改用
cargo metadata(替换 main 上的 glob 版)main 上那版用
ls -d ~/.cargo/registry/src/*/探测注册表,但该步骤之前没有任何 cargo 步骤、registry/src还没被解包 → glob 无匹配 →ls返回 2 → 配合set -euo pipefail让脚本静默 exit 2(日志零输出,极难定位)。
改用
cargo metadata --format-version 1 --locked:它自己完成解析与源码解包,manifest_path直接给出每个 crate 的真实根目录。插件集合取自真实依赖图;路径统一用
/拼接(os.path.join在 Windows 上插回反斜杠,Groovy 里\a\u是转义)。② 装
cargo-ndk本仓走
gradle → BuildTask.kt → cargo ndk(不是tauri android build),runner 上必须有该子命令,否则
error: no such command: 'ndk'→rustBuildArm64Release FAILED。③ 生成一次性签名密钥
cargo-ndk 装上后构建跑到最后一步,只剩打包失败:
SigningConfig "release" is missing required property "storeFile"。修正一处误判:
app/build.gradle对「缺 keystore.properties」只 warn,但 AGP 的打包任务在
storeFile未设置时仍会硬失败 —— 不能指望它优雅降级。本 job 只做构建验证、不产出可分发产物,故用
keytool生成一次性自签密钥。④ 跨 target clippy 放行 dead_code / unused_imports
APK 构建成功后,之后的
Cargo clippy (aarch64-linux-android)失败:CI 27 个 dead_code + 本地 6 个 unused_imports,全部是桌面专用符号与导入在移动端天然无人使用
(
get_active_window_info/win32/window_config/Emitter/WebviewWindow/build_configured_window…)。它们不是真的死代码,只是另一个 target 的入口。该步骤加
-A dead_code -A unused_imports,放行范围克制:host-target 的 clippy(
ci.yml的Rust Clippy (Lint),必需检查)仍全量强制,桌面侧的 unused_imports 依然会被拦下 ——这里只抵消「同一份代码在另一个 target 上不用」这一类假阳性。
更彻底的做法是加
#[cfg(not(mobile))],要动 5 个产品代码文件,留作后续。验证
CI(本轮实测):22/22 步骤全部 success ——
Locate Android SDK/Install Android SDK components/Install cargo-ndk/Generate throwaway signing key/Generate Tauri Android glue/Build arm64 release APK/Cargo clippy (aarch64-linux-android)/JNI keeps guard (DEX + ELF)/Upload APK artifact→ 产出制品
spiritpal-arm64-release-apk(13.8 MB)。本地对照:
assembleArm64ReleaseBUILD SUCCESSFUL(8m54s / 231 任务,产出 27.0 MB APK);跨 target clippy
rc=0;生成的 include 项与本地手工版逐项一致。说明
本分支从 main 重开:原 #120 因 main 已含 glob 版胶水步骤而冲突;本仓钩子禁止 merge 提交,
故不 rebase、不 force push,改为重开 PR(原 #120 已关闭)。
风险
低。只改
.github/workflows/android.yml,不碰业务代码。唯一「放宽」的是跨 target clippy 的两个 lint 放行,已在注释里写明范围与理由,并保留 host-target 的全量强制。
GOTCHAS #185 / #186