From 33bef6e470a77b927be4c2bab61e0c1e203d46d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Thu, 1 Oct 2026 16:42:02 +0800 Subject: [PATCH] =?UTF-8?q?AGC=20=E5=A3=B3=E9=80=9A=E7=9F=A5=E8=AE=A1?= =?UTF-8?q?=E6=95=B0=E6=8C=89=E7=BA=BF=E7=A8=8B=E9=9A=94=E7=A6=BB=EF=BC=8C?= =?UTF-8?q?=E4=BF=AE=E6=8E=89=E8=B7=A8=E7=94=A8=E4=BE=8B=E5=90=8E=E5=8F=B0?= =?UTF-8?q?=E5=B9=BF=E6=92=AD=E4=B8=B2=E5=8F=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - runtime_driver/entrypoints.rs:测试通知计数从进程级 AtomicU64 改成线程作用域 Cell,只统计本测试线程发出的通知 - runtime_driver/entrypoints.rs:`--test-threads=1` 只串行测试线程,前一个用例放行留下的后台回合仍在宿主 async runtime 工作线程上广播「运行中的项目」通知,不再是断言的取样来源 - thread_manager/mod.rs:同步用例注释,说明计数为何按线程隔离,断言语义(放行 1 / 内容变化 1 / 重复进度 0 / 收口 1)不变 - pitfalls:记录现场、根因、现行口径与验证(修复前 5 次红 3 次,修复后 CI 那一片连跑 18 次全绿) --- .../src/agent/runtime_driver/entrypoints.rs | 13 ++++++++++--- .../src-tauri/src/agent/thread_manager/mod.rs | 3 ++- docs/project-memory/shared-memory/pitfalls.md | 9 +++++++++ 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/entrypoints.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/entrypoints.rs index be807ca6c..dafa34ab1 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/entrypoints.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/entrypoints.rs @@ -4,7 +4,14 @@ pub(crate) const DIRECT_ACTIVE_TURNS_CHANGED_EVENT: &str = "game-creator-direct-active-turns-changed"; static DIRECT_ACTIVE_TURNS_EVENT_REVISION: AtomicU64 = AtomicU64::new(0); #[cfg(test)] -static DIRECT_ACTIVE_TURNS_EVENT_TEST_COUNT: AtomicU64 = AtomicU64::new(0); +thread_local! { + /// 只统计**当前线程**发出的通知。`--test-threads=1` 只串行测试线程,宿主 + /// `tauri::async_runtime` 的后台回合仍在自己的工作线程上跑(放行任务随 + /// `TurnReservation::drop` 起整轮,见 `thread_manager::dispatch`),并会在任意时刻广播 + /// 「运行中的项目」变了。断言要观测的是本测试自己触发的通知,不该被别的后台广播串台。 + static DIRECT_ACTIVE_TURNS_EVENT_TEST_COUNT: std::cell::Cell = + const { std::cell::Cell::new(0) }; +} const GAME_CREATOR_MANIFEST_INVALIDATION_RELAY_MAX_BYTES: u64 = 64 * 1024; const GAME_CREATOR_MANIFEST_INVALIDATION_EVENT_SINK_MAX: usize = 16; @@ -30,7 +37,7 @@ pub(crate) fn set_game_creator_agent_runtime_update_app_handle(app: tauri::AppHa pub(crate) fn emit_direct_active_turns_changed() { let revision = DIRECT_ACTIVE_TURNS_EVENT_REVISION.fetch_add(1, Ordering::AcqRel) + 1; #[cfg(test)] - DIRECT_ACTIVE_TURNS_EVENT_TEST_COUNT.fetch_add(1, Ordering::AcqRel); + let _ = DIRECT_ACTIVE_TURNS_EVENT_TEST_COUNT.try_with(|count| count.set(count.get() + 1)); let Some(app) = GAME_CREATOR_AGENT_RUNTIME_UPDATE_APP_HANDLE.get() else { return; }; @@ -42,7 +49,7 @@ pub(crate) fn emit_direct_active_turns_changed() { #[cfg(test)] pub(crate) fn direct_active_turns_event_test_count() -> u64 { - DIRECT_ACTIVE_TURNS_EVENT_TEST_COUNT.load(Ordering::Acquire) + DIRECT_ACTIVE_TURNS_EVENT_TEST_COUNT.with(std::cell::Cell::get) } pub(crate) fn emit_direct_game_creator_progress(root: &Path, stage: &str, message: &str) { diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/thread_manager/mod.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/thread_manager/mod.rs index b9ee45c96..87b19c252 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/thread_manager/mod.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/thread_manager/mod.rs @@ -1791,7 +1791,8 @@ mod tests { /// 首页快照的变更通知必须跟着快照自己的事实源走:放行(登记占用)、进度内容变化、收口各通知一次, /// 状态与活动都没变的重复进度不重复通知;入队与取消只改队列,不进快照,不通知。 /// - /// 计数器是进程级的,因此本用例与仓库既有的 Rust 定向命令一样按 `--test-threads=1` 跑。 + /// 计数器按线程隔离:`--test-threads=1` 只串行测试线程,前一个用例放行留下的后台回合仍会在 + /// 宿主 async runtime 的工作线程上广播通知(见 `direct_active_turns_event_test_count` 的说明)。 #[test] fn active_turn_changes_publish_one_notification_per_real_change() { let thread_id = format!("/tmp/事件项目-{}", Uuid::new_v4()); diff --git a/docs/project-memory/shared-memory/pitfalls.md b/docs/project-memory/shared-memory/pitfalls.md index 0602a9974..7a0f37a75 100644 --- a/docs/project-memory/shared-memory/pitfalls.md +++ b/docs/project-memory/shared-memory/pitfalls.md @@ -6183,3 +6183,12 @@ Cocos Creator 根目录由 `package.json.creator.version` 与普通 `assets/` - **同时**:主站顶栏(`platform-desktop-topbar`)在窄屏必须保持单行——加 `flex-wrap` 后新增一个动作入口就会让品牌折到第二行;窄屏降级靠 `< 640px` 图标化下载入口、`< 480px` 只留品牌 IP 标识。 - **验证**:真机尺寸下用 `document.elementFromPoint(按钮中心)` 断言命中的是按钮自身而不是底部菜单(`overlay.parentElement === document.body`);顶栏断点矩阵(320–768)断言单行且无横向溢出。 - **关联**:`src/components/common/SquareImageCropModal.tsx`、`src/components/platform-entry/PlatformProfileModalShell.tsx`、`src/components/platform-entry/PlatformEntryActiveFlowShell.tsx`、`src/index.css`。 + +## 2026-10-01 AGC 壳 Rust 单测片内 `--test-threads=1` 不等于没有并发:宿主后台回合会跨用例发通知 + +- **现象**:`agent::thread_manager::tests::active_turn_changes_publish_one_notification_per_real_change` 在 CI 偶发红,`进度内容变化必须通知一次` 拿到 6 而非 5(也见过入队断言就多 1);本地要跑 3–5 次才复现。这条用例靠一个进程级计数器数「运行中的项目」通知。 +- **原因**:`--test-threads=1` 只串行**测试线程**。上一个用例里 `TurnReservation::drop` → `kick_queue_dispatch` 会用 `tauri::async_runtime::spawn` 起整轮,这轮在 tokio 工作线程上继续跑(项目路径不存在 → 落盘失败 → `complete_turn_if_reserved` → `emit_direct_active_turns_changed`),恰好落在下一个用例的取样窗口里。用工作线程 ID + backtrace 才抓到现场。 +- **处理(现行口径)**:通知计数器改成线程作用域,只统计本测试线程发出的通知,断言语义(放行 1 / 内容变化 1 / 重复进度 0 / 收口 1)不变。「进程级全局量 + 片内串行 ⇒ 安全」这个假设在 AGC 壳单测里不成立,要么按线程隔离,要么别用全局量当断言依据。 +- **同时**:分片器的失败复核原先只认 `tests::` / `process_session::tests::` 前缀,而 bin 目标用例名是 `agent::…::tests::…`,于是 3143 条里 2218 条永远进不了复核,偶发假红直接判红;现在按本片名单精确匹配,并且复核也失败时保留首次现场。 +- **验证**:本机用 CI 那一片(`--shards=4 --shard-index=1` 的第 3 块)连跑:修复前 5 次红 3 次,修复后 18 次全绿;`node --test apps/ai-game-creator-shell/scripts/run-rust-shell-test-shards.test.mjs` 3 项通过。 +- **关联**:`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/entrypoints.rs`、`apps/ai-game-creator-shell/src-tauri/src/agent/thread_manager/dispatch.rs`、`apps/ai-game-creator-shell/scripts/run-rust-shell-test-shards.mjs`。