//! L2 统一状态机 — `ConvState` enum(单一真相源)+ 合法转换守卫。
//!
//! 来源:`generating状态机加固-2026-06-15.md` §3(轻量状态机决策)+ `aichat体验与
//! agent能力系统化重构-2026-06-21.md` §3(L2 统一状态机)。
//!
//! # 背景(为什么单独建这个 enum)
//!
//! 旧实现会话状态由三个标志组合隐式表达(`generating: bool` + `stop_flag: AtomicBool`
//! + `pending_approvals` HashMap),判别逻辑散落 `try_continue_agent_loop` /
//! `ai_chat_stop` / `ai_conversation_create` 三处(写侧散布 + 读侧散布双根因,见
//! generating状态机加固-2026-06-15.md §2.3)。本模块提供**显式的、单一真相的**状态枚举:
//!
//! - **写侧收敛**:`ConvState` 仅经 [`ConvState::transition_to`] 守卫方法迁移,非法转换
//!   直接拒绝(`Err(InvalidTransition)`),调用方无法绕过守卫写入非法组合。
//! - **读侧收敛**:停止按钮三态(可停 / 停中 / 停失败可重试)、MaxRoundsCard 是否弹等
//!   判别逻辑由 `ConvState` 变体直接表达,不再靠多变量组合反推。
//!
//! # 当前架构
//!
//! 本模块是**纯逻辑、无 IO**的 enum + 转换守卫。`generating` bool
//! 与 `CONV_STATE_ENABLED` 开关已退役,`ConvState` 成为唯一真相源:guard 接入点
//! (`GeneratingGuard` new/reset/drop 时同步迁移 `ConvState`)无条件执行迁移 + emit。
//!
//! 散落判别点(`try_continue` / `ai_chat_stop` / MaxRoundsCard)已逐个迁到读
//! `ConvState`(`is_active()` / `can_accept_request()`),不再回退旧 bool。
//!
//! # 与目标钉扎(G1)的衔接(2026-06-26)
//!
//! `ConvState` 管**生成生命周期**(Idle/Generating/Stopping/Error/Compressed 5 态 7 边);
//! 目标 / 进度等**内容态**挂 [`PerConvState`](../mod.rs) 兄弟字段(如 `pinned_goals`),
//! 两者**正交**。**不要把目标塞进 `ConvState` 变体** —— 否则 5 态会膨胀成
//! `GeneratingWithGoal` / `IdleWithGoal` 爆炸组合,违反「轻量状态机不引入框架」原则。
//! 目标钉扎字段(G1)与本 enum 互不感知:G1 改 `PerConvState.pinned_goals`,
//! 本文件 enum/impl/transition_to 守卫/guard.rs 零改动。

use serde::{Deserialize, Serialize};

// ============================================================
// ConvState enum
// ============================================================

/// 对话生命周期状态(单一真相源,5 态)。
///
/// 设计取舍(对齐 generating状态机加固-2026-06-15.md §3.2 「轻量状态机,不引入框架」):
///
/// - **不引入状态机框架**(codegen / FSM 库):5 态 7 边转换简单,枚举 + 显式 `transition_to`
///   守卫即可表达,过度封装得不偿失。
/// - **派生关系明确**:`Compressed` 是 `Generating`/`Idle` 期间的瞬时派生态(压缩跑完
///   回到原态),非终端态;`Error` 可经用户「重试」回到 `Idle` 再 `Generating`。
/// - **与读视图 [`SessionState`](../enum.SessionState.html) 区分**:`SessionState`
///   (Streaming/AwaitingApproval/Idle)是面向「是否有审批挂起」的只读视图(读侧收敛①),
///   `ConvState` 是面向「生成生命周期」的写侧真相(写收敛)。两者正交:如 `Generating` 态
///   同时有审批挂起时,`ConvState=Generating` 而 `SessionState=AwaitingApproval`。
///
/// 序列化(`Serialize`/`Deserialize`):前端经事件总线读 enum 视图时使用(本批未接,
/// 预留,避免后续改动序列化兼容性)。
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ConvState {
    /// 空闲:无 agentic loop 活跃,可接新请求。
    ///
    /// 终态目标:所有正常退出路径(完成 / 停止完成 / 错误恢复)的归处。
    Idle,
    /// 生成中:agentic loop 流式生成 + 工具执行活跃中(`generating=true`)。
    ///
    /// 审批挂起期间 `ConvState` 仍为 `Generating`(审批是 loop 内的暂停点,非独立状态)。
    Generating,
    /// 停止中:用户点了停止(`stop_flag` 已置位),loop 检测中或正在收尾退出。
    ///
    /// 瞬态:loop 检测到 `stop_flag` 后迁移到 `Idle`(完成收尾),或停止过程出错到 `Error`。
    Stopping,
    /// 错误态:生成过程出错(provider 失败 / Fatal / 断路器熔断等),loop 已退出。
    ///
    /// 非终态:用户可「重试」从 `Error` → `Idle` → `Generating` 重新发起,或继续空闲。
    Error,
    /// 压缩派生态:loop 期间触发上下文压缩(LLM 摘要),生成流程的瞬时派生。
    ///
    /// 派生而非独立态:压缩完成(成功 / 失败兜底)后回到原 `Generating`(loop 内压缩)或
    /// `Idle`(手动压缩 IPC 触发)。前端可据此展示「压缩中」spinner(本批未接)。
    Compressed,
}

impl Default for ConvState {
    /// 新会话默认 `Idle`(无 loop 活跃)。
    fn default() -> Self {
        ConvState::Idle
    }
}

impl ConvState {
    /// 尝试迁移到目标态。合法返回 `Ok(新态)`,非法拒绝返回 `Err(InvalidTransition)`。
    ///
    /// 调用方持锁后经本方法迁移状态(写收敛):不允许直接写字段绕过守卫。
    /// 非法转换(如 `Idle → Stopping`,无活跃 loop 不可能停止)直接拒绝——调用方应视为
    /// 逻辑 bug 并记录 warn 日志,不静默忽略(状态机完整性优先)。
    ///
    /// # 合法转换表(7 边)
    ///
    /// | from | to | 触发场景 |
    /// |------|----|---------|
    /// | Idle | Generating | `run_agentic_loop` 入口,guard set |
    /// | Generating | Stopping | 用户点停止,置 `stop_flag` |
    /// | Generating | Idle | loop 正常收敛退出,guard reset |
    /// | Generating | Error | provider Fatal / 断路器熔断等 |
    /// | Generating | Compressed | loop 内自动压缩派生 |
    /// | Stopping | Idle | stop 收尾完成,guard reset |
    /// | Stopping | Error | 停止过程出错 |
    /// | Error | Idle | 用户重试前置位(准备再 `Generating`) |
    /// | Error | Generating | 直接从错误态重新生成(重试) |
    /// | Compressed | Generating | 压缩完成,loop 内继续 |
    /// | Compressed | Idle | 手动压缩 IPC 完成(无 loop 活跃) |
    ///
    /// 自环(同态 → 同态)允许(幂等写,如 guard 多次 set 同态),返回 `Ok(self)`。
    pub fn transition_to(self, target: ConvState) -> Result<ConvState, InvalidTransition> {
        // 自环幂等:同态迁移直接通过(防调用方重复 set 报错)。
        if self == target {
            return Ok(target);
        }
        // 合法转换白名单(显式列举,非 `match _ => Ok` 兜底——新加态必须显式补边,
        // 编译器不会漏)。
        let valid = matches!(
            (self, target),
            // Idle 起步
            (ConvState::Idle, ConvState::Generating)
            // Generating 分流:停 / 完成 / 错误 / 压缩派生
            | (ConvState::Generating, ConvState::Stopping)
            | (ConvState::Generating, ConvState::Idle)
            | (ConvState::Generating, ConvState::Error)
            | (ConvState::Generating, ConvState::Compressed)
            // Stopping 收尾:完成 / 出错
            | (ConvState::Stopping, ConvState::Idle)
            | (ConvState::Stopping, ConvState::Error)
            // Error 恢复:重试前置位 / 直接重新生成
            | (ConvState::Error, ConvState::Idle)
            | (ConvState::Error, ConvState::Generating)
            // Compressed 派生回退:loop 继续 / 手动压缩完成
            | (ConvState::Compressed, ConvState::Generating)
            | (ConvState::Compressed, ConvState::Idle)
        );
        if valid {
            Ok(target)
        } else {
            Err(InvalidTransition { from: self, to: target })
        }
    }

    /// 是否处于活跃生成态(`Generating` / 压缩派生 `Compressed`)。
    ///
    /// 读侧便利方法:压缩派生期间 loop 仍活跃(只是临时跑压缩 LLM),归「活跃」。
    /// 审批挂起(读视图 `SessionState::AwaitingApproval`)期间 `ConvState` 仍是 `Generating`,
    /// 故本方法返回 true——审批挂起不算「停止生成」。
    ///
    /// 已接入 ai_is_generating / ai_chat_stop / try_continue_agent_loop,不再标 allow(dead_code)。
    pub fn is_active(self) -> bool {
        matches!(self, ConvState::Generating | ConvState::Compressed)
    }

    /// 是否可接受新请求(非活跃、非停止中、非压缩派生)。
    ///
    /// 读侧便利方法:用于 `ai_conversation_create` / `ai_chat_send` 等入口判断
    /// 「能否接新请求」。`Error` 态视为可接(用户重试即从 Error 起步)。
    ///
    /// chat 域入口拦截(ai_regenerate / ai_chat_send /
    /// ai_chat_edit)已接入,作真实读侧方法消费。
    pub fn can_accept_request(self) -> bool {
        matches!(self, ConvState::Idle | ConvState::Error)
    }
}

// ============================================================
// InvalidTransition(非法转换错误)
// ============================================================

/// 状态机非法转换错误。
///
/// 携带 `from` / `to` 供调用方记录诊断日志(不 panic:状态机完整性违反应记 warn +
/// 兜底走旧 bool 行为,不阻断核心生成流程——对齐「机制优先,失败兜底」原则)。
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct InvalidTransition {
    pub from: ConvState,
    pub to: ConvState,
}

impl std::fmt::Display for InvalidTransition {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "ConvState 非法转换:{:?} → {:?}", self.from, self.to)
    }
}

impl std::error::Error for InvalidTransition {}

// ============================================================
// ConvStateStore — 无锁并发 ConvState 存储(session 锁重构方案 B-Phase0)
//
// 背景:AiSession 全局 Mutex 把 ConvState(高频读 + 敏感脏读)与 messages/pending_approvals
// (长持锁源)同锁串行化,致 guard.reset 等 lock 竞争 800ms fallback(AiCompleted 延迟 / 工具后
// 中断 / 第二条进队列同源根因)。本 Store 把 ConvState 提到独立 DashMap,guard.reset/new/drop
// 直接 transition(同步无 await,不竞争 session lock),ai_is_generating 直接读(零锁竞争)。
//
// 设计:
// - 基于 dashmap::DashMap<String, ConvState>(行级锁,不同 conv 不互斥,无 tokio runtime 阻塞)
// - 全方法同步无 await(transition 内仅 copy + write enum,纳秒级)
// - transition 经 ConvState::transition_to 守卫(复用状态机语义,非法转换拒绝)
// - get 对不存在的 conv_id 返 Idle(惰性默认,对齐 PerConvState 新建语义)
//
// 方案 B 分阶段迁移完成。
// - Phase0~1: ConvStateStore 骨架 + AppState 接入
// - Phase2~3: 写/读侧迁移至无锁 ConvStateStore,PerConvState.conv_state 字段已删
// - Phase4: conversation_delete 同步清理 conv_states 条目
// ============================================================

use dashmap::DashMap;

/// ConvState 的无锁并发存储(方案 B 核心)。
///
/// 经 `Arc<ConvStateStore>` 共享(app_state.conv_states)。所有方法同步无 await,可在任意
/// async 上下文直接调(不竞争 session lock,不阻塞 tokio runtime)。
///
/// 注:`transition` 用 DashMap entry 原子(get + transition_to + write 一致,无 TOCTOU 窗口)。
pub struct ConvStateStore {
    inner: DashMap<String, ConvState>,
}

impl ConvStateStore {
    /// 创建空 Store。
    pub fn new() -> Self {
        Self { inner: DashMap::new() }
    }

    /// 读 conv_id 的 ConvState(不存在返 Idle 默认,对齐 PerConvState 新建语义)。
    pub fn get(&self, conv_id: &str) -> ConvState {
        self.inner.get(conv_id).map(|r| *r.value()).unwrap_or(ConvState::Idle)
    }

    /// 读 conv_id 是否活跃生成态(Generating/Compressed)—— ai_is_generating 零锁读。
    pub fn is_active(&self, conv_id: &str) -> bool {
        self.get(conv_id).is_active()
    }

    /// 读 conv_id 是否可接受新请求(Idle/Error)—— can_accept_request 零锁读。
    pub fn can_accept_request(&self, conv_id: &str) -> bool {
        self.get(conv_id).can_accept_request()
    }

    /// 原子迁移 conv_id 的 ConvState 到 target(经 transition_to 守卫)。
    ///
    /// DashMap entry 原子(get + transition + write 一致,无 TOCTOU)。不存在的 conv_id 视为
    /// Idle(对齐新建语义),Idle→target 经守卫。返回 Ok(新态) 或 Err(InvalidTransition)。
    pub fn transition(
        &self,
        conv_id: &str,
        target: ConvState,
    ) -> Result<ConvState, InvalidTransition> {
        // get_mut 持写锁原子迁移(Occupied);Vacant 时 insert(Idle 起步)。
        // 注:transition 调用点(guard.new/reset/drop)同 conv 单 loop 不并发,TOCTOU 风险低;
        // 跨 conv 各自条目行级锁不互斥(对齐 DashMap 设计)。
        if let Some(mut r) = self.inner.get_mut(conv_id) {
            let cur = *r.value();
            match cur.transition_to(target) {
                Ok(ns) => {
                    *r.value_mut() = ns;
                    Ok(ns)
                }
                Err(e) => Err(e),
            }
        } else {
            match ConvState::Idle.transition_to(target) {
                Ok(ns) => {
                    self.inner.insert(conv_id.to_string(), ns);
                    Ok(ns)
                }
                Err(e) => Err(e),
            }
        }
    }

    /// 删除 conv_id 的条目(会话删除时同步清,防已删 conv 残留 Generating 致 id 复用脏状态)。
    pub fn remove(&self, conv_id: &str) {
        self.inner.remove(conv_id);
    }

    /// 所有活跃生成态的 conv_id 快照(L0 握手批量 stop / 恢复生成态用)。
    pub fn active_convs(&self) -> Vec<String> {
        self.inner
            .iter()
            .filter(|r| r.value().is_active())
            .map(|r| r.key().clone())
            .collect()
    }
}

impl Default for ConvStateStore {
    fn default() -> Self {
        Self::new()
    }
}

// ============================================================
// 单元测试(纯逻辑无 IO)
// ============================================================

#[cfg(test)]
mod tests {
    use super::*;

    // ---- 合法转换通过 ----

    #[test]
    fn test_idle_to_generating() {
        assert_eq!(
            ConvState::Idle.transition_to(ConvState::Generating),
            Ok(ConvState::Generating)
        );
    }

    #[test]
    fn test_generating_to_stopping() {
        assert_eq!(
            ConvState::Generating.transition_to(ConvState::Stopping),
            Ok(ConvState::Stopping)
        );
    }

    #[test]
    fn test_generating_to_idle_normal_complete() {
        // loop 正常收敛退出
        assert_eq!(
            ConvState::Generating.transition_to(ConvState::Idle),
            Ok(ConvState::Idle)
        );
    }

    #[test]
    fn test_generating_to_error_provider_fatal() {
        // provider Fatal / 断路器熔断
        assert_eq!(
            ConvState::Generating.transition_to(ConvState::Error),
            Ok(ConvState::Error)
        );
    }

    #[test]
    fn test_generating_to_compressed_loop_auto_compress() {
        // loop 内自动压缩派生
        assert_eq!(
            ConvState::Generating.transition_to(ConvState::Compressed),
            Ok(ConvState::Compressed)
        );
    }

    #[test]
    fn test_stopping_to_idle_complete() {
        // stop 收尾完成
        assert_eq!(
            ConvState::Stopping.transition_to(ConvState::Idle),
            Ok(ConvState::Idle)
        );
    }

    #[test]
    fn test_stopping_to_error() {
        assert_eq!(
            ConvState::Stopping.transition_to(ConvState::Error),
            Ok(ConvState::Error)
        );
    }

    #[test]
    fn test_error_to_idle_retry_prep() {
        // 用户重试前置位
        assert_eq!(
            ConvState::Error.transition_to(ConvState::Idle),
            Ok(ConvState::Idle)
        );
    }

    #[test]
    fn test_error_to_generating_retry() {
        // 直接从错误态重新生成
        assert_eq!(
            ConvState::Error.transition_to(ConvState::Generating),
            Ok(ConvState::Generating)
        );
    }

    #[test]
    fn test_compressed_to_generating_loop_continue() {
        // 压缩完成 loop 内继续
        assert_eq!(
            ConvState::Compressed.transition_to(ConvState::Generating),
            Ok(ConvState::Generating)
        );
    }

    #[test]
    fn test_compressed_to_idle_manual_compress_done() {
        // 手动压缩 IPC 完成(无 loop 活跃)
        assert_eq!(
            ConvState::Compressed.transition_to(ConvState::Idle),
            Ok(ConvState::Idle)
        );
    }

    // ---- 自环幂等 ----

    #[test]
    fn test_self_transition_idempotent() {
        // 同态迁移直接通过(防调用方重复 set 报错)
        for s in [
            ConvState::Idle,
            ConvState::Generating,
            ConvState::Stopping,
            ConvState::Error,
            ConvState::Compressed,
        ] {
            assert_eq!(s.transition_to(s), Ok(s), "{:?} 自环应通过", s);
        }
    }

    // ---- 非法转换拒绝 ----

    #[test]
    fn test_idle_to_stopping_rejected() {
        // 无活跃 loop 不可能停止
        let err = ConvState::Idle
            .transition_to(ConvState::Stopping)
            .unwrap_err();
        assert_eq!(err.from, ConvState::Idle);
        assert_eq!(err.to, ConvState::Stopping);
    }

    #[test]
    fn test_idle_to_error_rejected() {
        // Idle 直接到 Error 无意义(错误必经活跃态产生)
        assert!(ConvState::Idle.transition_to(ConvState::Error).is_err());
    }

    #[test]
    fn test_idle_to_compressed_rejected() {
        // 无 loop 活跃不会触发压缩派生(手动压缩 IPC 不经状态机派生)
        assert!(ConvState::Idle.transition_to(ConvState::Compressed).is_err());
    }

    #[test]
    fn test_stopping_to_generating_rejected() {
        // 停止中不能直接回生成(须先回 Idle 再起)
        assert!(ConvState::Stopping
            .transition_to(ConvState::Generating)
            .is_err());
    }

    #[test]
    fn test_stopping_to_compressed_rejected() {
        assert!(ConvState::Stopping
            .transition_to(ConvState::Compressed)
            .is_err());
    }

    #[test]
    fn test_error_to_stopping_rejected() {
        // 错误态已停止,不能再停
        assert!(ConvState::Error.transition_to(ConvState::Stopping).is_err());
    }

    #[test]
    fn test_error_to_compressed_rejected() {
        assert!(ConvState::Error.transition_to(ConvState::Compressed).is_err());
    }

    #[test]
    fn test_compressed_to_stopping_rejected() {
        // 压缩派生期间不直接到停止(压缩完成后由原态分流)
        assert!(ConvState::Compressed
            .transition_to(ConvState::Stopping)
            .is_err());
    }

    #[test]
    fn test_compressed_to_error_rejected() {
        assert!(ConvState::Compressed
            .transition_to(ConvState::Error)
            .is_err());
    }

    // ---- 便利方法 ----

    #[test]
    fn test_is_active() {
        assert!(!ConvState::Idle.is_active());
        assert!(ConvState::Generating.is_active());
        assert!(!ConvState::Stopping.is_active());
        assert!(!ConvState::Error.is_active());
        // 压缩派生期间 loop 仍活跃(临时跑压缩 LLM)
        assert!(ConvState::Compressed.is_active());
    }

    #[test]
    fn test_can_accept_request() {
        assert!(ConvState::Idle.can_accept_request());
        assert!(!ConvState::Generating.can_accept_request());
        assert!(!ConvState::Stopping.can_accept_request());
        // Error 态可接(用户重试即从 Error 起步)
        assert!(ConvState::Error.can_accept_request());
        assert!(!ConvState::Compressed.can_accept_request());
    }

    #[test]
    fn test_default_is_idle() {
        assert_eq!(ConvState::default(), ConvState::Idle);
    }

    // ---- 完整生命周期链 ----

    #[test]
    fn test_full_lifecycle_normal_complete() {
        // 正常生命周期:Idle → Generating → Idle
        let s = ConvState::Idle;
        let s = s.transition_to(ConvState::Generating).unwrap();
        let s = s.transition_to(ConvState::Idle).unwrap();
        assert_eq!(s, ConvState::Idle);
    }

    #[test]
    fn test_full_lifecycle_with_stop() {
        // 停止生命周期:Idle → Generating → Stopping → Idle
        let s = ConvState::Idle;
        let s = s.transition_to(ConvState::Generating).unwrap();
        let s = s.transition_to(ConvState::Stopping).unwrap();
        let s = s.transition_to(ConvState::Idle).unwrap();
        assert_eq!(s, ConvState::Idle);
    }

    #[test]
    fn test_full_lifecycle_with_compress() {
        // 压缩派生:Idle → Generating → Compressed → Generating → Idle
        let s = ConvState::Idle;
        let s = s.transition_to(ConvState::Generating).unwrap();
        let s = s.transition_to(ConvState::Compressed).unwrap();
        let s = s.transition_to(ConvState::Generating).unwrap();
        let s = s.transition_to(ConvState::Idle).unwrap();
        assert_eq!(s, ConvState::Idle);
    }

    #[test]
    fn test_full_lifecycle_error_retry() {
        // 错误恢复:Idle → Generating → Error → Generating → Idle
        let s = ConvState::Idle;
        let s = s.transition_to(ConvState::Generating).unwrap();
        let s = s.transition_to(ConvState::Error).unwrap();
        let s = s.transition_to(ConvState::Generating).unwrap();
        let s = s.transition_to(ConvState::Idle).unwrap();
        assert_eq!(s, ConvState::Idle);
    }

    #[test]
    fn test_invalid_transition_error_display() {
        // Display 包含 from / to,供日志诊断
        let err = ConvState::Idle
            .transition_to(ConvState::Stopping)
            .unwrap_err();
        let msg = format!("{}", err);
        assert!(msg.contains("Idle"), "Display 应含 from: {}", msg);
        assert!(msg.contains("Stopping"), "Display 应含 to: {}", msg);
    }

    // ---- ConvStateStore(方案 B-Phase0,无锁并发存储)----

    #[test]
    fn test_store_get_default_idle() {
        let s = ConvStateStore::new();
        assert_eq!(s.get("conv-1"), ConvState::Idle, "不存在 conv 应返 Idle 默认");
    }

    #[test]
    fn test_store_transition_occupied() {
        let s = ConvStateStore::new();
        s.transition("conv-1", ConvState::Generating).unwrap();
        assert_eq!(s.get("conv-1"), ConvState::Generating);
        s.transition("conv-1", ConvState::Idle).unwrap();
        assert_eq!(s.get("conv-1"), ConvState::Idle);
    }

    #[test]
    fn test_store_transition_guard_rejects() {
        let s = ConvStateStore::new();
        s.transition("conv-1", ConvState::Generating).unwrap();
        // Generating → Stopping 合法
        s.transition("conv-1", ConvState::Stopping).unwrap();
        // Stopping → Generating 非法(须先回 Idle 再起)
        assert!(s.transition("conv-1", ConvState::Generating).is_err());
    }

    #[test]
    fn test_store_remove() {
        let s = ConvStateStore::new();
        s.transition("conv-1", ConvState::Generating).unwrap();
        s.remove("conv-1");
        assert_eq!(s.get("conv-1"), ConvState::Idle, "remove 后应返 Idle 默认");
    }

    #[test]
    fn test_store_is_active_and_can_accept() {
        let s = ConvStateStore::new();
        assert!(!s.is_active("conv-1"), "Idle 不活跃");
        assert!(s.can_accept_request("conv-1"), "Idle 可接");
        s.transition("conv-1", ConvState::Generating).unwrap();
        assert!(s.is_active("conv-1"), "Generating 活跃");
        assert!(!s.can_accept_request("conv-1"), "Generating 不可接");
    }

    #[test]
    fn test_store_active_convs() {
        let s = ConvStateStore::new();
        s.transition("a", ConvState::Generating).unwrap();
        s.transition("b", ConvState::Idle).unwrap();
        s.transition("c", ConvState::Generating).unwrap();
        let mut active = s.active_convs();
        active.sort();
        assert_eq!(active, vec!["a".to_string(), "c".to_string()], "仅活跃 conv");
    }
}
