修复 AGC 壳 Rust shard-4 的两条偶发断言,并补齐 pitfalls 记录

- 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 复核
This commit is contained in:
2026-10-04 03:25:54 +08:00
parent 977b2f0c75
commit 9e2cea5b72
3 changed files with 54 additions and 5 deletions
@@ -28,7 +28,22 @@ 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` 起整轮),并会在任意时刻广播「运行中的项目」变了。断言要观测的
/// 是本测试自己触发的通知,不该被别的后台广播串台。
///
/// 这里必须留在测试线程作用域内:退役 `runtime_driver` 时这条口径曾被降级回进程级
/// `AtomicU64`,于是 `agent::thread_manager::tests::active_turn_changes_publish_one_notification_per_real_change`
/// 又回到偶发(同一片内前一个用例留下的后台回合广播进采样窗口)。
///
/// 代价:计数改成线程作用域后,**别的线程**上的重复 / 丢失通知不再被这条用例覆盖(那是
/// 宿主后台回合的行为,本身就不该由单线程断言口径表达);要覆盖跨线程序,应另加用例
/// 观察回合身份/序列号,而不是把计数器退回进程级。
static DIRECT_ACTIVE_TURNS_EVENT_TEST_COUNT: std::cell::Cell<u64> =
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;
@@ -54,7 +69,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;
};
@@ -66,7 +81,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) {
@@ -630,12 +630,20 @@ fn process_session_graceful_terminate_keeps_wrapper_alive_for_target_cleanup() {
let root = directory.path();
init_local_game_project_at(root, "graceful-process-project", "Graceful Process Project")
.expect("initialize project");
// leader 输出 READY 后 **等** 同组后台子进程,不自己先退出。terminate 必须先送到 trampoline
// 的 target group、再让组内后代把延迟清理跑完,这条断言才有确定性:leader 一旦先自然退出,
// trampoline 的 800ms 宽限就开始计时,客户端只要在那之后才发出 terminate(CI 高负载下调度
// 完全可能 >800ms),会话就会先以 `exited` 收口,客户端再 terminate 只能读到既成事实。
// 后台子进程仍留在同一进程组里、仍靠 TERM 触发延迟 400ms 的 marker 清理,语义不变。
// 注意:这条用例的确定性靠的是「trap 里 400ms 清理 < 800ms 宽限」的时间余量(实测 0.41s),
// 不是真正的同步原语;若以后清理耗时逼近或超过宽限,应把 marker 拆成「收到 TERM 即时落盘 +
// 延迟内容」两步,或让宽限可注入,而不是放宽断言。
let spec = resolve_project_command_spec_at(
root,
"bash",
&[
"-lc".to_string(),
"(trap 'sleep 0.4; printf done > graceful-marker.txt; exit 0' TERM; while :; do sleep 1; done) & printf 'READY\\n'; exit 0".to_string(),
"(trap 'sleep 0.4; printf done > graceful-marker.txt; exit 0' TERM; while :; do sleep 1; done) & printf 'READY\\n'; wait".to_string(),
],
".",
30,