test: 加胶囊内省 seam + 集成测试证明「按下面板键→弹可见胶囊」

emit_capsule 在校验 app 句柄前记录 last_capsule_state(廉价单锁,用户行为不变),
让无 GUI 测试能断言热键事件确实触发了可见胶囊。新增集成测试 coding_agent_panel_
press_emits_visible_capsule:开启功能→驱动 handler→断言下发 Error 胶囊(开发中提示),
headless 复现并验证目标 2(a)「按下有反馈」。

验证:cargo test coding_agent 27 passed / cargo check 仅余既有 warning
This commit is contained in:
吕柏青 committed 2026-06-05 10:09:01 +08:00
1 parent 51d158754c
commit 1b2a15dd75
1 file changed
+36
@@ -259,6 +259,10 @@ struct Inner {
/// 监听器(global-hotkey crate)。`None` 表示功能关闭或还没成功安装。
qa_hotkey: Mutex<Option<QaHotkeyMonitor>>,
coding_agent_hotkey: Mutex<Option<CodingAgentHotkeyMonitor>>,
/// 最近一次 emit_capsule 下发的 state,纯内省/测试用途(在 app 句柄校验之前写入,
/// 因此无 GUI 的测试环境也能断言「按下热键 → 弹了哪种胶囊」)。写入是单次廉价
/// 加锁,对 ~30Hz 录音回调可忽略。
last_capsule_state: Mutex<Option<CapsuleState>>,
/// QA 单独的 session 状态,与 dictation 的 SessionPhase 不冲突。
qa_state: Mutex<QaSessionState>,
/// 最近一次应用到 capsule 窗口的几何状态。避免录音 level tick 反复触发
@@ -340,6 +344,7 @@ impl Coordinator {
translation_modifier_seen: AtomicBool::new(false),
qa_hotkey: Mutex::new(None),
coding_agent_hotkey: Mutex::new(None),
last_capsule_state: Mutex::new(None),
qa_state: Mutex::new(QaSessionState::default()),
capsule_layout: Mutex::new(None),
qa_asr: Mutex::new(None),
@@ -403,6 +408,7 @@ impl Coordinator {
translation_modifier_seen: AtomicBool::new(false),
qa_hotkey: Mutex::new(None),
coding_agent_hotkey: Mutex::new(None),
last_capsule_state: Mutex::new(None),
qa_state: Mutex::new(QaSessionState::default()),
capsule_layout: Mutex::new(None),
qa_asr: Mutex::new(None),
@@ -4214,6 +4220,34 @@ mod tests {
std::env::remove_var("OPENLESS_HOTKEY_INJECTION_DRY_RUN");
}
/// 复现并验证目标 2(a):按下 Cloud Agent 面板键必须弹出可见胶囊。
/// 修复前因热键注册失败,按下根本到不了 handler,屏幕毫无反应;这里直接驱动
/// bridge 会调用的 handler,断言 emit_capsule 确实下发了可见的 Error 胶囊。
#[tokio::test]
async fn coding_agent_panel_press_emits_visible_capsule() {
let _guard = ENV_LOCK.lock().await;
std::env::set_var("OPENLESS_HOTKEY_INJECTION_DRY_RUN", "1");
let coordinator = Coordinator::new();
{
let mut prefs = coordinator.inner.prefs.get();
prefs.coding_agent_enabled = true;
coordinator.inner.prefs.set(prefs).unwrap();
}
// 前置:还没弹过任何胶囊。
assert!(coordinator.inner.last_capsule_state.lock().is_none());
// 等价于「按下面板键」:bridge_loop 收到 PanelPressed 后就是调这个 handler。
super::handle_coding_agent_panel(&coordinator.inner).await;
assert_eq!(
*coordinator.inner.last_capsule_state.lock(),
Some(CapsuleState::Error),
"按下 Cloud Agent 面板键必须弹出可见胶囊(开发中提示)"
);
std::env::remove_var("OPENLESS_HOTKEY_INJECTION_DRY_RUN");
}
#[tokio::test]
async fn begin_session_dry_run_enters_listening_and_clears_stale_edges() {
let _guard = ENV_LOCK.lock().await;
@@ -5404,6 +5438,8 @@ fn emit_capsule(
message: Option<String>,
inserted_chars: Option<u32>,
) {
// 在 app 句柄校验之前记录,便于无 GUI 的测试断言「按下热键 → 弹了哪种胶囊」。
*inner.last_capsule_state.lock() = Some(state);
let app_opt = inner.app.lock().clone();
let Some(app) = app_opt else { return };
let translation = inner.translation_modifier_seen.load(Ordering::SeqCst);