修复 master CI 红:收银台深链前缀路由、外部 MCP 工具数断言、并发 span 采集竞态、AGC 壳 shard-4 断言 #609

Merged
suzmii merged 6 commits from fix/ci-master-red into master 2026-10-04 09:21:00 +08:00
Member

这个 PR 修什么

修 master(ccd271572)上已红的检查项,外加两处会真实影响线上/网关行为的漂移与 AGC 壳 shard-4 的偶发断言。不放宽断言、不 sleep 赌时序、不起 dev 栈。


1. Nginx SPA allowlist 缺 /pay、/profile/payment,收银台深链 /pay/<checkoutToken> 仍然 404

为什么红:src/routing/activeAppPageRoutes.ts 的 STAGE_ROUTE_ENTRIES 早已把 payment-checkout → /pay、profile-payment → /profile/payment 定义为主站 SPA 路由,三份模板的 allowlist 没跟上 → check:nginx-spa-routes 失败。

更严重的同一处漂移:payment.rs 生成的 checkoutUrl 是 format!("/pay/{}", checkout_token),前端用 startsWith('/pay/') 判定并取最后一个路径段当 token;只把裸前缀加进 allowlist 时深链会落默认 location / 的 try_files $uri $uri/ =404 → 真实收银台链接 404。同批漂移里 check:pingora-route-parity 也红(Pingora MAIN_SPA_PATHS 缺 /pay、/profile/payment),只是被 lint 链里先失败的门禁掩盖。

改法:前缀路由进真相源 ——

  • APP_PREFIX_ROUTE_ENTRIES('/pay' → payment-checkout)+ resolveSelectionStageFromPath 改用它;
  • 三份模板各加锚定前缀 location location ~* "^/pay/[^/]+/?$"(裸前缀仍由精确 location 负责;前缀 location 镜像精确 location 的维护闸与 try_files $uri /index.html =404;);
  • check-nginx-spa-routes 按真相源强制前缀形状,新增 scripts/check-nginx-spa-routes.test.mjs 正/反用例(写成精确匹配 ^/pay/?$ 或过宽 ^/pay 都会红),由 npm run check:nginx-spa-routes 一起跑;
  • Pingora:MAIN_SPA_PATHS 补 /pay、/profile/payment,新增 MAIN_SPA_PREFIX_PATHS + is_main_spa_prefix_path(大小写不敏感、只认「前缀 + 恰好一段」),矩阵新增 pay_checkout_spa_fallback 用例(/pay/checkout-token → static/web/spa_fallback)+ 网关前缀正/反单测;check-pingora-route-parity 增加 MAIN_SPA_PREFIX_PATHS 与前端逐条比对;
  • 文档:Pingora 试点文档路由表/门禁说明、deploy/nginx/README、本地开发与生产运维文档(含线上 curl /pay/<token> 复验方式)。

本地实跑:

node --test scripts/check-nginx-spa-routes.test.mjs      → 4 passed
node scripts/check-nginx-spa-routes.mjs                  → OK (14 SPA routes, 1 prefix routes, 3 Nginx templates)
npm run check:pingora-route-parity                       → OK (25 routes)
cargo test -p pingora-gateway -- pay_checkout_deep_link matches_nginx_route_parity_matrix → 2 passed
npx vitest run src/routing/activeAppPageRoutes.test.ts   → 4 passed

2. external_mcp::semantic 写死的 legacy 工具数 30 → 32

4b529a895(支付服务接入)在内置 OpenAPI 新增 createExternalPaymentOrder、getExternalPaymentOrder 两条未被 x-mcp-excluded 排除的操作,legacy 可调用工具由 30 合法增至 32(共 37 条操作 − 5 条元数据/入口操作)。断言改为 32 并注明来源,保留「语义工具不得覆盖 legacy 工具」的原意。总数断言两次修正:先是改成派生式,评审指出 BTreeSet 构造下恒真后又改回字面量 47(32 + 15)。

cargo test --locked -p api-server --manifest-path server-rs/Cargo.toml --bin api-server semantic → 18 passed
修复前基线(worktree at origin/master,全量 bin − env 模块)连跑 6 次 → 6/6 红在同一条断言

3. 并发测试下 HTTP/Provider span 偶发采集为空(tracing callsite interest 竞态)

根因(代码级链条,tracing 0.1.44 / tracing-core 0.1.36):

  1. span!/info_span! 在 callsite 缓存 interest 为 never 时静默返回空 span,连 new_span 都不调(tracing-0.1.44/src/macros.rs 的 span! 分支);
  2. DefaultCallsite 的 interest 只在调用点首次命中时算一次,计算时用 DISPATCHERS.rebuilder(),进程里只注册过一个 dispatcher 时退化成 dispatcher::get_default(),即命中线程自己的 dispatcher(tracing-core-0.1.36/callsite.rs 的 Rebuilder::JustOne);
  3. libtest 默认并发跑同一二进制上千用例,没有 subscriber 的测试线程(api-server 大量 build_router 用例、platform-llm 大量直连客户端用例)抢到 http.request / llm.request 首次注册就会把它永久缓存成 never → 测试看到 0 个 span。这正是「同批代码时而绿时而红」。

修法:set_default 与 with_subscriber 在 interest 缓存上等价(都只是新建 Dispatch 触发一次 rebuild_interest,只能纠正「已经注册过」的调用点),所以关键是在自家 subscriber 下命中同一调用点热身:

  • api-server app::tests::http_tracing:请求前用同一 http.request 调用点热身到连续两轮采集成功,并在整个请求期间持有 scoped default;
  • platform-llm observability_tests:run_under_capture 固定整段流程的 scoped default + warm_up_provider_span_callsite(指向刚释放 loopback 端口的最小失败请求命中同一 llm.request 调用点,连续两轮采集成功才继续);
  • register_callsite 只用于避免本 subscriber 触发的重建把其它调用点永久标成 never;
  • 断言一字未改(每个被拒绝请求恰好 1 个 HTTP span、每次 Provider 调用恰好 1 个 llm.request span),无 sleep。

本地实跑:

api-server app::tests::http_tracing ×20(默认并发)→ 20/20 绿
api-server app::tests::http_tracing ×20(--test-threads=1)→ 20/20 绿
api-server app::tests:: ×10(91 用例同进程并发)→ 10/10 绿
api-server 全量 bin(--skip bgfilter_worker --skip wallet_refund_outbox,1133 用例)×2 → 2/2 绿
platform-llm --lib(162 用例,含 stream_run 观测用例)××20 → 20/20 绿

未复现说明:该竞态窗口极窄,本机在 origin/master 上跑 app::tests::×12 与全量 bin×6 都没复现(CI 只偶发一次);结论来自上面的代码级链条 + CI 现场日志(master-backend-19291.log 的 left: 0 / right: 1)。

4. AGC 壳 Rust shard-4 的两条偶发断言(#607 lane 2/2)

  • 通知计数串台:DIRECT_ACTIVE_TURNS_EVENT_TEST_COUNT 曾在 2026-10-01 按线程作用域隔离,2026-10-02 退役 runtime_driver 搬进 agent/direct_events.rs 时降级回进程级 static AtomicU64,断言会取到宿主 tauri::async_runtime 后台回合在别的线程上的广播(--test-threads=1 只串行测试线程)。改回 thread_local! Cell。
    证据:修复前把计数器临时改回 AtomicU64 → 同一并行口径 17/20 红(thread_manager/mod.rs:1796/1802/1817/1827 多条「通知」断言);修复后并行 20 次 0 红、agent::thread_manager:: 5 连跑 72 passed。
  • terminate 与 800ms 宽限竞速:测试命令里 leader 打印 READY 后立刻 exit 0,同组后代仍存活,trampoline 从 leader 被回收起开始 PROCESS_SESSION_TARGET_TERMINATE_GRACE_MS=800ms 宽限;客户端只要晚于宽限才发出 terminate 就只能读到 exited。改为 leader 用 wait 等后台子进程(trap / sleep 0.4 / marker / 断言均未改)。
    验证:该用例是 #[cfg(target_os = "linux")],Windows 本机跑不到;已在真实 Linux 内核(WSL Alpine)验证命令形状:leader 活到 TERM、同组后代完成 400ms 延迟清理(marker=done,real 0.41s)、清理后组内零残留。CI 侧仍需跑 node apps/ai-game-creator-shell/scripts/run-rust-shell-test-shards.mjs --shards=4 --shard-index=4 复核。
  • pitfalls.md:更新 graceful terminate 已过时的验证口径,补三条 2026-10-04 条目(tracing interest 竞态、AGC 偶发串台、SPA 深链前缀路由)。

收尾自检(本地实跑)

npm run check:nginx-spa-routes      OK(含 4 条新正/反用例)
npm run check:pingora-route-parity  OK (25 routes)
npm run check:production-ops        OK
npm run check:encoding              passed for 5221 file(s)
npm run check:doc-index             通过
npm run typecheck                   exit 0
npx eslint <改动文件>               无告警
git diff --check                    干净
cargo fmt --all(server-rs / agc shell)--check  干净

未验证 / 存疑项

  1. 本机 Windows 无法完整跑 cargo test --workspace:bgfilter_worker::tests::cold_start_flat_retries_past_regular_quota_until_worker_listens 挂起 >60s、wallet_refund_outbox::tests::worker_recovers_temp_file_immediately_on_startup 失败(均依赖外部环境,master 的 Linux CI 上是绿的),与本 PR 改动无关;workspace 级结论以 CI 为准。
  2. 第 4 项里 linux-only 的 process_session 用例本机无法执行(见上)。
  3. 第 3 项的竞态未在本机复现(窗口极窄);修复的正确性依赖「热身命中同一调用点」的确定性论证 + platform-llm 的 20 次高并发连跑。
  4. AGC lane 里另一条 project::export::npm_export_tests::npm_package_contains_only_dist_and_publish_readme(2026-10-03 的多次 lane run 里左右值差一个 assets/hero.png)不在本 PR 范围,疑似用例间共享临时目录/环境变量,仅记录待另行排查。

合并顺序

本 PR 必须先合,再合 fix/agc-canvas-reference-insert、fix/home-project-naming-async、fix/agc-composer-layout(它们会与本次 pitfalls.md 尾部追加与 Nginx/Pingora 门禁同文件冲突)。

## 这个 PR 修什么 修 master(`ccd271572`)上已红的检查项,外加两处会真实影响线上/网关行为的漂移与 AGC 壳 shard-4 的偶发断言。不放宽断言、不 sleep 赌时序、不起 dev 栈。 --- ### 1. Nginx SPA allowlist 缺 `/pay`、`/profile/payment`,收银台深链 `/pay/<checkoutToken>` 仍然 404 **为什么红**:`src/routing/activeAppPageRoutes.ts` 的 `STAGE_ROUTE_ENTRIES` 早已把 `payment-checkout → /pay`、`profile-payment → /profile/payment` 定义为主站 SPA 路由,三份模板的 allowlist 没跟上 → `check:nginx-spa-routes` 失败。 **更严重的同一处漂移**:`payment.rs` 生成的 `checkoutUrl` 是 `format!("/pay/{}", checkout_token)`,前端用 `startsWith('/pay/')` 判定并取最后一个路径段当 token;只把裸前缀加进 allowlist 时深链会落默认 `location /` 的 `try_files $uri $uri/ =404` → 真实收银台链接 404。同批漂移里 `check:pingora-route-parity` 也红(Pingora `MAIN_SPA_PATHS` 缺 `/pay`、`/profile/payment`),只是被 lint 链里先失败的门禁掩盖。 **改法**:前缀路由进真相源 —— - `APP_PREFIX_ROUTE_ENTRIES`('/pay' → payment-checkout)+ `resolveSelectionStageFromPath` 改用它; - 三份模板各加锚定前缀 location `location ~* "^/pay/[^/]+/?$"`(裸前缀仍由精确 location 负责;前缀 location 镜像精确 location 的维护闸与 `try_files $uri /index.html =404;`); - `check-nginx-spa-routes` 按真相源强制前缀形状,新增 `scripts/check-nginx-spa-routes.test.mjs` 正/反用例(写成精确匹配 `^/pay/?$` 或过宽 `^/pay` 都会红),由 `npm run check:nginx-spa-routes` 一起跑; - Pingora:`MAIN_SPA_PATHS` 补 `/pay`、`/profile/payment`,新增 `MAIN_SPA_PREFIX_PATHS` + `is_main_spa_prefix_path`(大小写不敏感、只认「前缀 + 恰好一段」),矩阵新增 `pay_checkout_spa_fallback` 用例(`/pay/checkout-token` → static/web/spa_fallback)+ 网关前缀正/反单测;`check-pingora-route-parity` 增加 `MAIN_SPA_PREFIX_PATHS` 与前端逐条比对; - 文档:Pingora 试点文档路由表/门禁说明、`deploy/nginx/README`、本地开发与生产运维文档(含线上 `curl /pay/<token>` 复验方式)。 **本地实跑**: ``` node --test scripts/check-nginx-spa-routes.test.mjs → 4 passed node scripts/check-nginx-spa-routes.mjs → OK (14 SPA routes, 1 prefix routes, 3 Nginx templates) npm run check:pingora-route-parity → OK (25 routes) cargo test -p pingora-gateway -- pay_checkout_deep_link matches_nginx_route_parity_matrix → 2 passed npx vitest run src/routing/activeAppPageRoutes.test.ts → 4 passed ``` ### 2. `external_mcp::semantic` 写死的 legacy 工具数 30 → 32 `4b529a895`(支付服务接入)在内置 OpenAPI 新增 `createExternalPaymentOrder`、`getExternalPaymentOrder` 两条未被 `x-mcp-excluded` 排除的操作,legacy 可调用工具由 30 **合法增至 32**(共 37 条操作 − 5 条元数据/入口操作)。断言改为 32 并注明来源,保留「语义工具不得覆盖 legacy 工具」的原意。总数断言两次修正:先是改成派生式,评审指出 `BTreeSet` 构造下恒真后又改回字面量 `47`(32 + 15)。 ``` cargo test --locked -p api-server --manifest-path server-rs/Cargo.toml --bin api-server semantic → 18 passed 修复前基线(worktree at origin/master,全量 bin − env 模块)连跑 6 次 → 6/6 红在同一条断言 ``` ### 3. 并发测试下 HTTP/Provider span 偶发采集为空(tracing callsite interest 竞态) **根因(代码级链条,tracing 0.1.44 / tracing-core 0.1.36)**: 1. `span!`/`info_span!` 在 callsite 缓存 interest 为 `never` 时**静默返回空 span**,连 `new_span` 都不调(`tracing-0.1.44/src/macros.rs` 的 `span!` 分支); 2. `DefaultCallsite` 的 interest **只在调用点首次命中时算一次**,计算时用 `DISPATCHERS.rebuilder()`,**进程里只注册过一个 dispatcher 时退化成 `dispatcher::get_default()`,即命中线程自己的 dispatcher**(`tracing-core-0.1.36/callsite.rs` 的 `Rebuilder::JustOne`); 3. libtest 默认并发跑同一二进制上千用例,没有 subscriber 的测试线程(api-server 大量 `build_router` 用例、platform-llm 大量直连客户端用例)抢到 `http.request` / `llm.request` 首次注册就会把它永久缓存成 `never` → 测试看到 0 个 span。这正是「同批代码时而绿时而红」。 **修法**:`set_default` 与 `with_subscriber` 在 interest 缓存上**等价**(都只是新建 `Dispatch` 触发一次 `rebuild_interest`,只能纠正「已经注册过」的调用点),所以关键是**在自家 subscriber 下命中同一调用点热身**: - api-server `app::tests::http_tracing`:请求前用同一 `http.request` 调用点热身到连续两轮采集成功,并在整个请求期间持有 scoped default; - platform-llm `observability_tests`:`run_under_capture` 固定整段流程的 scoped default + `warm_up_provider_span_callsite`(指向刚释放 loopback 端口的最小失败请求命中同一 `llm.request` 调用点,连续两轮采集成功才继续); - `register_callsite` 只用于避免本 subscriber 触发的重建把其它调用点永久标成 `never`; - 断言一字未改(每个被拒绝请求恰好 1 个 HTTP span、每次 Provider 调用恰好 1 个 `llm.request` span),无 sleep。 **本地实跑**: ``` api-server app::tests::http_tracing ×20(默认并发)→ 20/20 绿 api-server app::tests::http_tracing ×20(--test-threads=1)→ 20/20 绿 api-server app::tests:: ×10(91 用例同进程并发)→ 10/10 绿 api-server 全量 bin(--skip bgfilter_worker --skip wallet_refund_outbox,1133 用例)×2 → 2/2 绿 platform-llm --lib(162 用例,含 stream_run 观测用例)××20 → 20/20 绿 ``` **未复现说明**:该竞态窗口极窄,本机在 origin/master 上跑 `app::tests::`×12 与全量 bin×6 都没复现(CI 只偶发一次);结论来自上面的代码级链条 + CI 现场日志(`master-backend-19291.log` 的 `left: 0 / right: 1`)。 ### 4. AGC 壳 Rust shard-4 的两条偶发断言(#607 lane 2/2) - **通知计数串台**:`DIRECT_ACTIVE_TURNS_EVENT_TEST_COUNT` 曾在 2026-10-01 按线程作用域隔离,2026-10-02 退役 `runtime_driver` 搬进 `agent/direct_events.rs` 时降级回进程级 `static AtomicU64`,断言会取到宿主 `tauri::async_runtime` 后台回合在别的线程上的广播(`--test-threads=1` 只串行测试线程)。改回 `thread_local! Cell`。 证据:**修复前把计数器临时改回 AtomicU64 → 同一并行口径 17/20 红**(`thread_manager/mod.rs:1796/1802/1817/1827` 多条「通知」断言);修复后并行 20 次 0 红、`agent::thread_manager::` 5 连跑 72 passed。 - **terminate 与 800ms 宽限竞速**:测试命令里 leader 打印 READY 后立刻 `exit 0`,同组后代仍存活,trampoline 从 leader 被回收起开始 `PROCESS_SESSION_TARGET_TERMINATE_GRACE_MS=800ms` 宽限;客户端只要晚于宽限才发出 terminate 就只能读到 `exited`。改为 leader 用 `wait` 等后台子进程(trap / `sleep 0.4` / marker / 断言均未改)。 验证:该用例是 `#[cfg(target_os = "linux")]`,Windows 本机跑不到;已在真实 Linux 内核(WSL Alpine)验证命令形状:leader 活到 TERM、同组后代完成 400ms 延迟清理(marker=done,real 0.41s)、清理后组内零残留。**CI 侧仍需跑 `node apps/ai-game-creator-shell/scripts/run-rust-shell-test-shards.mjs --shards=4 --shard-index=4` 复核。** - `pitfalls.md`:更新 graceful terminate 已过时的验证口径,补三条 2026-10-04 条目(tracing interest 竞态、AGC 偶发串台、SPA 深链前缀路由)。 --- ## 收尾自检(本地实跑) ``` npm run check:nginx-spa-routes OK(含 4 条新正/反用例) npm run check:pingora-route-parity OK (25 routes) npm run check:production-ops OK npm run check:encoding passed for 5221 file(s) npm run check:doc-index 通过 npm run typecheck exit 0 npx eslint <改动文件> 无告警 git diff --check 干净 cargo fmt --all(server-rs / agc shell)--check 干净 ``` ## 未验证 / 存疑项 1. 本机 Windows 无法完整跑 `cargo test --workspace`:`bgfilter_worker::tests::cold_start_flat_retries_past_regular_quota_until_worker_listens` 挂起 >60s、`wallet_refund_outbox::tests::worker_recovers_temp_file_immediately_on_startup` 失败(均依赖外部环境,master 的 Linux CI 上是绿的),与本 PR 改动无关;workspace 级结论以 CI 为准。 2. 第 4 项里 linux-only 的 `process_session` 用例本机无法执行(见上)。 3. 第 3 项的竞态未在本机复现(窗口极窄);修复的正确性依赖「热身命中同一调用点」的确定性论证 + platform-llm 的 20 次高并发连跑。 4. AGC lane 里另一条 `project::export::npm_export_tests::npm_package_contains_only_dist_and_publish_readme`(2026-10-03 的多次 lane run 里左右值差一个 `assets/hero.png`)**不在本 PR 范围**,疑似用例间共享临时目录/环境变量,仅记录待另行排查。 ## 合并顺序 **本 PR 必须先合**,再合 `fix/agc-canvas-reference-insert`、`fix/home-project-naming-async`、`fix/agc-composer-layout`(它们会与本次 `pitfalls.md` 尾部追加与 Nginx/Pingora 门禁同文件冲突)。
suzmii self-assigned this 2026-10-04 03:27:38 +08:00
suzmii added 6 commits 2026-10-04 03:27:38 +08:00
- src/routing/activeAppPageRoutes.ts 的 STAGE_ROUTE_ENTRIES 已把 payment-checkout→/pay、
  profile-payment→/profile/payment 定义为主站对外可直达的 SPA 路由(分别渲染
  PaymentCheckoutView 与 PlatformPaymentServiceView),但三份 Nginx 模板的 SPA allowlist
  仍停留在加入支付入口之前的集合,导致 check:nginx-spa-routes 在 master 上失败
- deploy/nginx/genarrative.conf、deploy/nginx/genarrative-dev-http.conf、
  deploy/container/nginx.conf 的 SPA regex 补上 pay、profile/payment,
  保持既有形状(锚定完整路径 + 大小写不敏感 + 允许一个尾部斜杠)
- 本地实跑:node scripts/check-nginx-spa-routes.mjs → OK (14 SPA routes, 3 Nginx templates)
- 断言 MCP_OPERATIONS.len() == 30 自 #592 起写死。4b529a895(新增支付服务接入与订单收银台)
  在内置 OpenAPI 中新增 createExternalPaymentOrder、getExternalPaymentOrder 两条未被
  x-mcp-excluded 排除的操作,legacy 可调用工具由 30 合法增至 32(非重复注册、非覆盖)
- 断言更新为 32 并注明来源(内置 OpenAPI 去掉 5 条元数据/入口操作后的可调用集合),
  保留“语义工具是增量、不得覆盖 legacy 工具”的原意;总数断言改为
  MCP_OPERATIONS.len() + TOOLS.len() 派生,避免再次写死
- 本地实跑:cargo test --locked -p api-server --no-fail-fast --manifest-path server-rs/Cargo.toml semantic
  → 18 passed(含 semantic_catalog_adds_fifteen_tools_without_replacing_legacy_tools)
- 根因:span!/info_span! 宏在 callsite 的缓存 interest 为 never 时会静默返回空 span
  (tracing-0.1.44/src/macros.rs 的 span! 分支),而 DefaultCallsite::register 只在调用点
  首次被命中时计算一次 interest,且当进程里只注册过一个 dispatcher 时会退化成
  dispatcher::get_default()——也就是命中线程自己的 dispatcher(tracing-core-0.1.36
  callsite.rs 的 Rebuilder::JustOne)。libtest 默认并发下,没有 subscriber 的普通测试线程
  一旦抢到 http.request / llm.request 调用点的首次注册,就会把它永久缓存成 never,
  于是 CI 偶发看到 0 个 span(app.rs:603、observability_tests.rs:98)
- 关键:set_default 与 with_subscriber 在 interest 缓存这件事上**等价**(都只是新建 Dispatch
  并触发一次 rebuild_interest,只能纠正“已经注册过”的调用点,纠正不了 JustOne→get_default
  这条首次注册分支),所以真正起作用的是**在自家 subscriber 下命中同一个调用点做热身**,
  把首次注册的顺序握在自己手里;register_callsite 覆写只用于避免本 subscriber 触发的重建
  把其它调用点永久标记成 never
- api-server app::tests::http_tracing:不再用 with_subscriber,改为在请求前用同一
  http.request 调用点热身到连续两轮采集成功,并在整个请求期间持有 scoped default
  (已注明 set_default 是线程绑定,只适用于默认的 current_thread #[tokio::test])
- platform-llm observability_tests:新增 run_under_capture 固定整段流程的 scoped default,
  并新增 warm_up_provider_span_callsite(用指向刚释放 loopback 端口的最小失败请求命中同一
  llm.request 调用点,连续两轮采集成功才继续)
- 断言未放宽:仍要求每个被拒绝请求恰好 1 个 HTTP span、每次 Provider 调用恰好 1 个
  llm.request span;未使用 sleep
- 本地实跑:cargo test -p api-server --bin api-server app::tests::http_tracing(默认并发与
  --test-threads=1 各 20 次全绿)、app::tests:: 91 用例并发 10 次全绿、--skip bgfilter_worker
  --skip wallet_refund_outbox 的 1133 用例全量 bin 2 次全绿;cargo test -p platform-llm --lib
  (162 passed,含 3 条观测用例)连跑 20 次全绿
- agent::thread_manager::tests::active_turn_changes_publish_one_notification_per_real_change
  偶发 left 8/right 7:测试计数器 DIRECT_ACTIVE_TURNS_EVENT_TEST_COUNT 曾在 2026-10-01 按线程
  作用域隔离,2026-10-02 退役 runtime_driver 把它搬进 agent/direct_events.rs 时降级回进程级
  static AtomicU64,于是断言会取到宿主 tauri::async_runtime 后台回合在别的线程上的广播
  (--test-threads=1 只串行测试线程)。改回 thread_local! Cell,并注明跨线程口径的覆盖取舍
- process_session::tests::process_session_graceful_terminate_keeps_wrapper_alive_for_target_cleanup
  偶发 left "exited"/right "terminated":测试命令里 leader 打印 READY 后立刻 exit 0,同组后代
  仍存活,trampoline 从 leader 被回收起开始 800ms 宽限;客户端只要晚于宽限才发出 terminate
  就只能读到既成事实。改为 leader 用 wait 等后台子进程,让 terminate 必然落在会话仍 running 时
  (trap / sleep 0.4 / marker / 断言均未改),并注明该用例的确定性来自 400ms < 800ms 的时间余量
- docs/project-memory/shared-memory/pitfalls.md:更新 graceful terminate 那条已过时的验证口径,
  补三条 2026-10-04 条目(tracing interest 竞态、AGC 两条偶发的串台根因、SPA 深链前缀路由)
- 本地实跑:修复前把计数器临时改回 AtomicU64 时同一并行口径 17/20 红;修复后并行 20 次全绿、
  agent::thread_manager:: 连跑 5 次 72 passed;第 2 条用例是 #[cfg(target_os = "linux")],
  Windows 本机跑不到,已用真实 Linux 内核(WSL Alpine)验证命令形状(leader 活到 TERM、
  同组后代完成 400ms 延迟清理 marker=done 0.41s、清理后组内零残留),CI 侧仍需跑
  node apps/ai-game-creator-shell/scripts/run-rust-shell-test-shards.mjs --shards=4 --shard-index=4 复核
- 现状:只把 `/pay`、`/profile/payment` 加进 allowlist 只能让 check:nginx-spa-routes 变绿;
  payment.rs 生成的 checkoutUrl 是 `/pay/<checkoutToken>`,深链仍落默认 location 的
  try_files → 404。同一批漂移里 check:pingora-route-parity 也是红的(Pingora MAIN_SPA_PATHS
  缺 /pay、/profile/payment),只是被 lint 链里先失败的门禁掩盖,修一条要跑到链尾
- 真相源:src/routing/activeAppPageRoutes.ts 新增 APP_PREFIX_ROUTE_ENTRIES
  ('/pay' → payment-checkout),resolveSelectionStageFromPath 改用它
- 门禁:scripts/check-nginx-spa-routes.mjs 要求三份模板都有锚定前缀 location
  `location ~* "^/pay/[^/]+/?$"`(裸前缀仍由精确 location 负责;前缀 location 必须镜像精确
  location 的维护闸与 try_files 回退);新增 scripts/check-nginx-spa-routes.test.mjs 正/反用例
  (把前缀写成精确匹配或过宽裸前缀都会红),由 npm run check:nginx-spa-routes 一起执行;
  check-pingora-route-parity 新增 MAIN_SPA_PREFIX_PATHS 与前端前缀路由的逐条比对
- 模板:deploy/nginx/genarrative.conf、deploy/nginx/genarrative-dev-http.conf、
  deploy/container/nginx.conf 各加一条锚定前缀 location
- Pingora:MAIN_SPA_PATHS 补 /pay、/profile/payment;新增 MAIN_SPA_PREFIX_PATHS 与
  is_main_spa_prefix_path(大小写不敏感,只认「前缀 + 恰好一段」),矩阵新增
  pay_checkout_spa_fallback 用例,并给网关补一条前缀正/反单测
- 文档:Pingora 试点文档的路由表与门禁说明、deploy/nginx/README 与本地开发/生产运维文档
  同步前缀路由口径与线上 curl 复验方式
- 本地实跑:node --test scripts/check-nginx-spa-routes.test.mjs(4 passed)、
  node scripts/check-nginx-spa-routes.mjs(OK,14 SPA routes / 1 prefix routes / 3 templates)、
  npm run check:pingora-route-parity(OK,25 routes)、
  cargo test -p pingora-gateway -- pay_checkout_deep_link matches_nginx_route_parity_matrix(2 passed)
语义目录测试的总数断言改回字面量,避免恒真
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m50s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 4m10s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 5m5s
Project CI / Backend tests (pull_request) Successful in 6m30s
Project CI / Frontend tests (pull_request) Successful in 3m12s
Project CI / AI game creator shell web tests (pull_request) Successful in 2m44s
Project CI / Native shell tests (pull_request) Successful in 6m33s
Project CI / Repository checks (pull_request) Successful in 5m5s
8b11dc4e7a
- names.len() == MCP_OPERATIONS.len() + TOOLS.len() 在 BTreeSet 去重构造下恒真、没有判别力;
  改回固定 47(32 条 legacy 工具 + 15 条语义工具),并注明上面的插入断言已保证两组名字互不覆盖
- 本地实跑:cargo test --locked -p api-server --manifest-path server-rs/Cargo.toml --bin api-server semantic
  → 18 passed
suzmii merged commit afc2068fb3 into master 2026-10-04 09:21:00 +08:00
suzmii deleted branch fix/ci-master-red 2026-10-04 09:21:00 +08:00
Sign in to join this conversation.