fix(growth,memory,hands): 穷尽审计后 4 项修复 — 伪造时间戳+测试覆盖+注释纠正
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled

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
This commit is contained in:
iven
2026-04-21 18:45:10 +08:00
parent 13507682f7
commit b726d0cd5e
4 changed files with 121 additions and 12 deletions

View File

@@ -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());
}
}

View File

@@ -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),

View File

@@ -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());
}
}

View File

@@ -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<Utc>,
) -> Result<Vec<PainPoint>> {
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();
}
}