fix(gitea-integration): 删除项目登记时一并解除 Gitea 绑定,轮询不再写入已删除或已解绑的项目(G8) #35

Merged
lanrtop merged 3 commits from fix/gitea-unbind-on-unregister into master 2026-10-03 09:59:08 +08:00
Owner

做了什么

蓝图卡:.blueprint/modules/gitea-integration.md G8(complexity M)。

起因:删除项目登记时不清理 Gitea 仓库绑定,后台轮询每 10 分钟仍把 commit 和 PR 事件写到已删除的项目 ID 下(重复登记排查会话发现)。

  • 解绑本体拆成两段:gitea::remove_repo_link_records 只改数据库,可以放进调用方的事务;gitea::cleanup_removed_repo_link 在提交后尽力删除 Gitea 侧 webhook(加了 10 秒超时)和本机的签名凭证。UI「解绑」和删除登记都调用这两段。
  • 删除登记本体 projects::delete_project_registration(conn, store, id),连接和凭证存储都由调用方注入。它先调 delete_project_records,在同一个事务里删关联表、Gitea 绑定、workspace 和孤儿 profile;提交后再做 Gitea 外部清理。UI 的 delete_project(薄包装)和 MCP 的 unregister_project 都走这个本体。
  • 轮询兜底:
    • load_poll_targets 跳过所属项目已不存在的遗留绑定。每条遗留绑定每次运行只记一次 warn,日志里写明清理办法:对该 ID 调 unregister_project。
    • commit 和 PR 的写入抽成 ingest_polled_commits / ingest_polled_prs,写之前用 poll_target_still_bound 复核两件事:绑定还在(owner/repo 一致),项目仍在登记中。
  • gitea_unlink_repo:读和删放进同一个事务。
  • 契约同步:unregister_project 的描述补上「Gitea 仓库绑定(含 Gitea 侧 webhook 与本机签名凭证)」,docs/ai-context/project-context.yaml 同步更新。Tauri command 签名没变,前端和 schema 都没改。

自验

  • 全量 cargo test(src-tauri):310 passed,0 failed,5 ignored,编译无警告。
  • 回归测试:
    • projects::tests::deleting_project_registration_unbinds_its_gitea_repo:修复前失败,残留 1 条绑定。
    • gitea_unbinding_rolls_back_with_failed_project_deletion:用触发器让删除 workspace 失败,确认绑定被回滚。
    • deleting_project_registration_removes_its_webhook_secret:被删项目的签名凭证已清理,另一个项目的凭证保留。
    • unregistering_orphaned_binding_id_cleans_the_binding:对孤儿绑定的 ID 删除登记,绑定和凭证都被清掉。
    • gitea::tests::poll_targets_skip_bindings_whose_project_no_longer_exists
    • polled_results_are_dropped_when_binding_is_gone_mid_round:快照里有三个项目,之后删除一个登记、UI 解绑另一个,只有仍绑定的那个写入了 commit 和 PR 事件。
    • unlink_removes_binding_and_webhook_secret_reference
  • 变异验证(先暂存再改,结束后从暂存区还原)。下面每个变异都让对应测试失败,还原后全部通过:
    • 删除登记里去掉解绑。
    • 轮询去掉项目存在过滤。
    • delete_project_registration 去掉外部清理。
    • 两个 ingest 函数分别去掉复核。
    • 复核只查项目、不查绑定。
  • 只读查询运行中的炼境 DB:7 条 Gitea 绑定都属于已登记的项目,兜底过滤不会误跳过正在用的绑定;目前没有遗留的孤儿绑定。

审查处置

独立只读审查对象是 c1632a0,结论为可合并,共 5 条发现:

  1. 低|采纳:删除登记提交后的 Gitea 外部清理没有测试。处理:抽出注入式本体 delete_project_registration,补了凭证清理测试。变异「去掉清理」会让测试失败。
  2. 低|采纳:轮询的快照窗口。处理:commit 和每页 PR 写入前都做复核。比审查建议多核对了绑定行和 owner/repo,所以 UI 解绑、换绑也一样生效。窗口从「本轮剩余的若干分钟」缩到「复核到写入之间的毫秒级」。
  3. 提示|采纳:孤儿绑定的日志没写怎么清理。处理:日志写明清理路径,并用测试校验了这条路径的数据库和凭证部分。
  4. 提示|采纳:gitea_unlink_repo 的读和删不在同一个事务里(���有问题)。处理:改用 unchecked_transaction 包住,并发设置 webhook 时由 WAL 读快照过期报 busy,整体回滚。
  5. 提示|不成立:「生产连接池只有迁移那条连接开了外键」。rusqlite 的 bundled 特性编译 SQLite 时带了 -DSQLITE_DEFAULT_FOREIGN_KEYS=1(libsqlite3-sys 0.30.1 build.rs:123),所以每条新连接默认 foreign_keys=1。我写了一个临时探针测试:从 r2d2 内存池取 3 条连接,另开 1 条裸连接,PRAGMA foreign_keys 都是 1。探针跑完已还原,没有提交。测试环境和生产一致,ON DELETE CASCADE 照常生效,不需要修改。

断言变更报备:没有修改任何已有断言,只新增了 3 条测试。

没验什么

  • 没在运行中的炼境实测:删除一个已绑定的测试项目,确认 Gitea 绑定和 webhook 一并清理、下一轮轮询不再写这个 ID。这一步要写运行中的 DB 并删除登记,留给统筹会话合并后实测。
  • Gitea 侧 webhook 删除没测:webhook_id 非空时的删除分支没对真实 Gitea 验证。测试特意把 webhook_id 设成 NULL,避免发网络请求。
  • 轮询复核不是原子的:复核和写入之间还剩毫秒级窗口,没有用 BEGIN IMMEDIATE 消除,因为那样轮询会和 UI 写入抢写锁。
  • gitea_unlink_repo 事务化没实测:它的效果是按 SQLite WAL 语义推理出来的,没做并发实测(不对真实 DB 做并发实验)。
  • 薄包装 delete_project 没测:它本身(取连接、purge_project、remove_project_mcp 和本体的先后顺序)没有测试,只测了本体。
  • CI 不覆盖 Rust 测试:CI(pr-check)只跑前端的 typecheck 和 vitest 以及 commit message 校验,上面的 Rust 测试只在本地跑过。
  • 分支落后 master:分支基于 23943a1,落后 origin/master 5 个提交,那些提交只改文档、manifest 和 .codex/config.toml,和本 PR 的文件没有重叠,所以没有合并 master。

🤖 Generated with Claude Code

## 做了什么 蓝图卡:`.blueprint/modules/gitea-integration.md` G8(complexity M)。 起因:删除项目登记时不清理 Gitea 仓库绑定,后台轮询每 10 分钟仍把 commit 和 PR 事件写到已删除的项目 ID 下(重复登记排查会话发现)。 - **解绑本体拆成两段**:`gitea::remove_repo_link_records` 只改数据库,可以放进调用方的事务;`gitea::cleanup_removed_repo_link` 在提交后尽力删除 Gitea 侧 webhook(加了 10 秒超时)和本机的签名凭证。UI「解绑」和删除登记都调用这两段。 - **删除登记本体** `projects::delete_project_registration(conn, store, id)`,连接和凭证存储都由调用方注入。它先调 `delete_project_records`,在同一个事务里删关联表、Gitea 绑定、workspace 和孤儿 profile;提交后再做 Gitea 外部清理。UI 的 `delete_project`(薄包装)和 MCP 的 `unregister_project` 都走这个本体。 - **轮询兜底**: - `load_poll_targets` 跳过所属项目已不存在的遗留绑定。每条遗留绑定每次运行只记一次 warn,日志里写明清理办法:对该 ID 调 unregister_project。 - commit 和 PR 的写入抽成 `ingest_polled_commits` / `ingest_polled_prs`,写之前用 `poll_target_still_bound` 复核两件事:绑定还在(owner/repo 一致),项目仍在登记中。 - **`gitea_unlink_repo`**:读和删放进同一个事务。 - **契约同步**:`unregister_project` 的描述补上「Gitea 仓库绑定(含 Gitea 侧 webhook 与本机签名凭证)」,`docs/ai-context/project-context.yaml` 同步更新。Tauri command 签名没变,前端和 schema 都没改。 ## 自验 - 全量 `cargo test`(src-tauri):**310 passed,0 failed,5 ignored**,编译无警告。 - 回归测试: - `projects::tests::deleting_project_registration_unbinds_its_gitea_repo`:修复前失败,残留 1 条绑定。 - `gitea_unbinding_rolls_back_with_failed_project_deletion`:用触发器让删除 workspace 失败,确认绑定被回滚。 - `deleting_project_registration_removes_its_webhook_secret`:被删项目的签名凭证已清理,另一个项目的凭证保留。 - `unregistering_orphaned_binding_id_cleans_the_binding`:对孤儿绑定的 ID 删除登记,绑定和凭证都被清掉。 - `gitea::tests::poll_targets_skip_bindings_whose_project_no_longer_exists` - `polled_results_are_dropped_when_binding_is_gone_mid_round`:快照里有三个项目,之后删除一个登记、UI 解绑另一个,只有仍绑定的那个写入了 commit 和 PR 事件。 - `unlink_removes_binding_and_webhook_secret_reference` - 变异验证(先暂存再改,结束后从暂存区还原)。下面每个变异都让对应测试失败,还原后全部通过: - 删除登记里去掉解绑。 - 轮询去掉项目存在过滤。 - `delete_project_registration` 去掉外部清理。 - 两个 ingest 函数分别去掉复核。 - 复核只查项目、不查绑定。 - 只读查询运行中的炼境 DB:7 条 Gitea 绑定都属于已登记的项目,兜底过滤不会误跳过正在用的绑定;目前没有遗留的孤儿绑定。 ## 审查处置 独立只读审查对象是 c1632a0,结论为可合并,共 5 条发现: 1. **低|采纳**:删除登记提交后的 Gitea 外部清理没有测试。处理:抽出注入式本体 `delete_project_registration`,补了凭证清理测试。变异「去掉清理」会让测试失败。 2. **低|采纳**:轮询的快照窗口。处理:commit 和每页 PR 写入前都做复核。比审查建议多核对了绑定行和 owner/repo,所以 UI 解绑、换绑也一样生效。窗口从「本轮剩余的若干分钟」缩到「复核到写入之间的毫秒级」。 3. **提示|采纳**:孤儿绑定的日志没写怎么清理。处理:日志写明清理路径,并用测试校验了这条路径的数据库和凭证部分。 4. **提示|采纳**:`gitea_unlink_repo` 的读和删不在同一个事务里(���有问题)。处理:改用 unchecked_transaction 包住,并发设置 webhook 时由 WAL 读快照过期报 busy,整体回滚。 5. **提示|不成立**:「生产连接池只有迁移那条连接开了外键」。rusqlite 的 bundled 特性编译 SQLite 时带了 `-DSQLITE_DEFAULT_FOREIGN_KEYS=1`(libsqlite3-sys 0.30.1 build.rs:123),所以每条新连接默认 foreign_keys=1。我写了一个临时探针测试:从 r2d2 内存池取 3 条连接,另开 1 条裸连接,`PRAGMA foreign_keys` 都是 1。探针跑完已还原,没有提交。测试环境和生产一致,ON DELETE CASCADE 照常生效,不需要修改。 **断言变更报备**:没有修改任何已有断言,只新增了 3 条测试。 ## 没验什么 - **没在运行中的炼境实测**:删除一个已绑定的测试项目,确认 Gitea 绑定和 webhook 一并清理、下一轮轮询不再写这个 ID。这一步要写运行中的 DB 并删除登记,留给统筹会话合并后实测。 - **Gitea 侧 webhook 删除没测**:`webhook_id` 非空时的删除分支没对真实 Gitea 验证。测试特意把 webhook_id 设成 NULL,避免发网络请求。 - **轮询复核不是原子的**:复核和写入之间还剩毫秒级窗口,没有用 BEGIN IMMEDIATE 消除,因为那样轮询会和 UI 写入抢写锁。 - **`gitea_unlink_repo` 事务化没实测**:它的效果是按 SQLite WAL 语义推理出来的,没做并发实测(不对真实 DB 做并发实验)。 - **薄包装 `delete_project` 没测**:它本身(取连接、purge_project、remove_project_mcp 和本体的先后顺序)没有测试,只测了本体。 - **CI 不覆盖 Rust 测试**:CI(pr-check)只跑前端的 typecheck 和 vitest 以及 commit message 校验,上面的 Rust 测试只在本地跑过。 - **分支落后 master**:分支基于 23943a1,落后 origin/master 5 个提交,那些提交只改文档、manifest 和 .codex/config.toml,和本 PR 的文件没有重叠,所以没有合并 master。 🤖 Generated with [Claude Code](https://claude.com/claude-code)
lanrtop added 3 commits 2026-10-03 09:51:09 +08:00
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
删除登记(UI 删除项目、MCP unregister_project)原来不清 gitea_repos,
后台轮询每 10 分钟仍把 commit 与 PR 事件写到已删除的项目 ID 下。

- gitea:解绑本体拆为 remove_repo_link_records(只动数据库,可进调用方事务)
  与 cleanup_removed_repo_link(提交后尽力删 Gitea 侧 webhook 与本机签名凭证),
  UI 解绑 gitea_unlink_repo 与删除登记共用
- projects:抽出 delete_project_records(连接注入),关联表、Gitea 绑定、
  workspace、孤儿 profile 同一事务删除;delete_project 提交后再做 Gitea 外部清理
- 轮询兜底:load_poll_targets 把所属项目已不存在的遗留绑定排除在 commit / PR
  轮询之外,每个绑定每次运行只记一次 warn
- 契约:unregister_project 描述与 project-context.yaml 同步

回归测试 deleting_project_registration_unbinds_its_gitea_repo 修复前失败、修复后通过;
变异验证通过;全量 cargo test 307 passed / 0 failed / 5 ignored。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- 抽出连接与凭证存储注入的删除登记本体 projects::delete_project_registration
  (delete_project_records + 提交后 Gitea 外部清理),delete_project 只做薄包装;
  补测试校验被删项目的 webhook 签名凭证被清理、他项目凭证保留,以及孤儿绑定可经删除登记清理
- 轮询 commit 与 PR 写入抽成 ingest_polled_commits / ingest_polled_prs,写入前用
  poll_target_still_bound 复核绑定仍在且项目仍登记,避免按本轮开头的旧快照写入已删除或已解绑的项目
- gitea_unlink_repo 的读与删放进同一事务,与删除登记路径同写法
- 孤儿绑定 warn 写明清理路径(对该 ID 调 unregister_project)
- 审查第 5 条(连接池外键只在迁移连接开启)经探针证伪:bundled SQLite 默认 foreign_keys=1,不改

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
lanrtop merged commit 8b9ae81f78 into master 2026-10-03 09:59:08 +08:00
Sign in to join this conversation.
No reviewers
No Label
No Milestone
No project
No Assignees
1 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Dependencies

No dependencies set.

Reference: lanrtop/dev-manager-tauri#35
No description provided.