From b726d0cd5efc5ac944796cec4256239b2246e448 Mon Sep 17 00:00:00 2001 From: iven Date: Tue, 21 Apr 2026 18:45:10 +0800 Subject: [PATCH] =?UTF-8?q?fix(growth,memory,hands):=20=E7=A9=B7=E5=B0=BD?= =?UTF-8?q?=E5=AE=A1=E8=AE=A1=E5=90=8E=204=20=E9=A1=B9=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=20=E2=80=94=20=E4=BC=AA=E9=80=A0=E6=97=B6=E9=97=B4=E6=88=B3+?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=E8=A6=86=E7=9B=96+=E6=B3=A8=E9=87=8A?= =?UTF-8?q?=E7=BA=A0=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CRITICAL: - user_profile_store: find_active_pains_since 改为 find_active_pains, 移除无意义 .filter(|_| true),不再伪造 created_at=since HIGH: - daily_report: 移除虚假的 "Emits Tauri event" 注释(事件发射是调用方职责) - daily_report: chrono::Local → chrono::Utc 一致性修复 - 新增 8 个单元测试: PainPoint 系列测试 + find_since + get_events_since 验证: zclaw-memory 54 PASS, zclaw-growth 151 PASS, zclaw-hands 5 PASS --- crates/zclaw-growth/src/experience_store.rs | 22 ++++++ crates/zclaw-hands/src/hands/daily_report.rs | 9 ++- crates/zclaw-memory/src/trajectory_store.rs | 23 ++++++ crates/zclaw-memory/src/user_profile_store.rs | 79 ++++++++++++++++--- 4 files changed, 121 insertions(+), 12 deletions(-) diff --git a/crates/zclaw-growth/src/experience_store.rs b/crates/zclaw-growth/src/experience_store.rs index bb0bfbc..d2c094d 100644 --- a/crates/zclaw-growth/src/experience_store.rs +++ b/crates/zclaw-growth/src/experience_store.rs @@ -457,4 +457,26 @@ mod tests { // Content should reflect the latest version. assert_eq!(found[0].context, "context v3"); } + + #[tokio::test] + async fn test_find_since_filters_by_date() { + let viking = Arc::new(VikingAdapter::in_memory()); + let store = ExperienceStore::new(viking); + + let exp = Experience::new( + "agent-1", "recent pattern", "ctx", + vec!["step".into()], "ok", + ); + store.store_experience(&exp).await.unwrap(); + + // Query with since=far past → should find it + let old_since = Utc::now() - chrono::Duration::days(365); + let found = store.find_since("agent-1", old_since).await.unwrap(); + assert_eq!(found.len(), 1); + + // Query with since=far future → should not find it + let future_since = Utc::now() + chrono::Duration::days(365); + let found = store.find_since("agent-1", future_since).await.unwrap(); + assert!(found.is_empty()); + } } diff --git a/crates/zclaw-hands/src/hands/daily_report.rs b/crates/zclaw-hands/src/hands/daily_report.rs index 9700e0a..7211ab5 100644 --- a/crates/zclaw-hands/src/hands/daily_report.rs +++ b/crates/zclaw-hands/src/hands/daily_report.rs @@ -5,9 +5,12 @@ //! 1. Yesterday's conversation summary //! 2. Unresolved pain points follow-up //! 3. Recent experience highlights -//! 4. Industry news placeholder +//! 4. Industry-specific daily reminder //! -//! Emits `daily-report:ready` Tauri event and persists to VikingStorage. +//! The caller (SchedulerService or Tauri command) is responsible for: +//! - Assembling input data (trajectory summary, pain points, experiences) +//! - Emitting `daily-report:ready` Tauri event after execution +//! - Persisting the report to VikingStorage use async_trait::async_trait; use serde_json::Value; @@ -119,7 +122,7 @@ impl DailyReportHand { _ => "综合", }; - let date = chrono::Local::now().format("%Y年%m月%d日").to_string(); + let date = chrono::Utc::now().format("%Y年%m月%d日").to_string(); let mut sections = vec![ format!("# {} 管家日报 — {}", industry_label, date), diff --git a/crates/zclaw-memory/src/trajectory_store.rs b/crates/zclaw-memory/src/trajectory_store.rs index 4f283ea..41683e7 100644 --- a/crates/zclaw-memory/src/trajectory_store.rs +++ b/crates/zclaw-memory/src/trajectory_store.rs @@ -603,4 +603,27 @@ mod tests { assert_eq!(remaining.len(), 1); assert_eq!(remaining[0].id, "recent-evt"); } + + #[tokio::test] + async fn test_get_events_since() { + let store = test_store().await; + + // Insert event for agent-1 + let event = sample_event(0); + store.insert_event(&event).await.unwrap(); + + // Query with since=far past → should find it + let old_since = Utc::now() - chrono::Duration::days(365); + let found = store.get_events_since("agent-1", old_since).await.unwrap(); + assert_eq!(found.len(), 1); + + // Query with since=far future → should not find it + let future_since = Utc::now() + chrono::Duration::days(365); + let found = store.get_events_since("agent-1", future_since).await.unwrap(); + assert!(found.is_empty()); + + // Query for different agent → should not find it + let found = store.get_events_since("other-agent", old_since).await.unwrap(); + assert!(found.is_empty()); + } } diff --git a/crates/zclaw-memory/src/user_profile_store.rs b/crates/zclaw-memory/src/user_profile_store.rs index bf54ff0..ecd6f96 100644 --- a/crates/zclaw-memory/src/user_profile_store.rs +++ b/crates/zclaw-memory/src/user_profile_store.rs @@ -417,25 +417,26 @@ impl UserProfileStore { self.upsert(&profile).await } - /// Find active pain points created since the given datetime. - /// Converts the flat `active_pain_points` strings into structured PainPoint - /// objects. Since the existing schema stores only strings, the structured - /// metadata uses sensible defaults. - pub async fn find_active_pains_since( + /// Return all active pain points for a user as structured PainPoint objects. + /// + /// Note: the existing schema stores pain points as flat strings without + /// timestamps. The returned `PainPoint.created_at` is set to the profile's + /// `updated_at` as the best available approximation. The `since` parameter + /// is accepted for API consistency but cannot truly filter by creation time + /// with the current schema. + pub async fn find_active_pains( &self, user_id: &str, - since: DateTime, ) -> Result> { let profile = self.get(user_id).await?; Ok(match profile { Some(p) => p .active_pain_points .into_iter() - .filter(|_| true) .map(|content| PainPoint { content, - created_at: since, - last_mentioned_at: Utc::now(), + created_at: p.updated_at, + last_mentioned_at: p.updated_at, status: PainStatus::Active, occurrence_count: 1, }) @@ -678,4 +679,64 @@ mod tests { assert_eq!(decoded.communication_style, Some(CommStyle::Detailed)); assert_eq!(decoded.recent_topics, vec!["exports", "customs"]); } + + #[test] + fn test_pain_status_roundtrip() { + assert_eq!(PainStatus::from_str_lossy(PainStatus::Active.as_str()), PainStatus::Active); + assert_eq!(PainStatus::from_str_lossy(PainStatus::Resolved.as_str()), PainStatus::Resolved); + assert_eq!(PainStatus::from_str_lossy(PainStatus::Deferred.as_str()), PainStatus::Deferred); + assert_eq!(PainStatus::from_str_lossy("unknown"), PainStatus::Active); + } + + #[test] + fn test_pain_point_new() { + let pp = PainPoint::new("scheduling conflict"); + assert_eq!(pp.content, "scheduling conflict"); + assert_eq!(pp.status, PainStatus::Active); + assert_eq!(pp.occurrence_count, 1); + } + + #[tokio::test] + async fn test_find_active_pains() { + let store = test_store().await; + + store.add_pain_point("user", "pain_a", 5).await.unwrap(); + store.add_pain_point("user", "pain_b", 5).await.unwrap(); + + let pains = store.find_active_pains("user").await.unwrap(); + assert_eq!(pains.len(), 2); + assert!(pains.iter().any(|p| p.content == "pain_a")); + assert!(pains.iter().any(|p| p.content == "pain_b")); + assert_eq!(pains[0].status, PainStatus::Active); + } + + #[tokio::test] + async fn test_find_active_pains_empty() { + let store = test_store().await; + let pains = store.find_active_pains("nonexistent").await.unwrap(); + assert!(pains.is_empty()); + } + + #[tokio::test] + async fn test_resolve_pain() { + let store = test_store().await; + + store.add_pain_point("user", "pain_a", 5).await.unwrap(); + store.add_pain_point("user", "pain_b", 5).await.unwrap(); + + store.resolve_pain("user", "pain_a").await.unwrap(); + + let loaded = store.get("user").await.unwrap().unwrap(); + assert_eq!(loaded.active_pain_points, vec!["pain_b"]); + } + + #[tokio::test] + async fn test_resolve_pain_nonexistent_is_noop() { + let store = test_store().await; + let profile = UserProfile::blank("user"); + store.upsert(&profile).await.unwrap(); + + // Should not error when pain doesn't exist + store.resolve_pain("user", "nonexistent_pain").await.unwrap(); + } }