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
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:
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user