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.
|
// Content should reflect the latest version.
|
||||||
assert_eq!(found[0].context, "context v3");
|
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
|
//! 1. Yesterday's conversation summary
|
||||||
//! 2. Unresolved pain points follow-up
|
//! 2. Unresolved pain points follow-up
|
||||||
//! 3. Recent experience highlights
|
//! 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 async_trait::async_trait;
|
||||||
use serde_json::Value;
|
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![
|
let mut sections = vec![
|
||||||
format!("# {} 管家日报 — {}", industry_label, date),
|
format!("# {} 管家日报 — {}", industry_label, date),
|
||||||
|
|||||||
@@ -603,4 +603,27 @@ mod tests {
|
|||||||
assert_eq!(remaining.len(), 1);
|
assert_eq!(remaining.len(), 1);
|
||||||
assert_eq!(remaining[0].id, "recent-evt");
|
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
|
self.upsert(&profile).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Find active pain points created since the given datetime.
|
/// Return all active pain points for a user as structured PainPoint objects.
|
||||||
/// Converts the flat `active_pain_points` strings into structured PainPoint
|
///
|
||||||
/// objects. Since the existing schema stores only strings, the structured
|
/// Note: the existing schema stores pain points as flat strings without
|
||||||
/// metadata uses sensible defaults.
|
/// timestamps. The returned `PainPoint.created_at` is set to the profile's
|
||||||
pub async fn find_active_pains_since(
|
/// `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,
|
&self,
|
||||||
user_id: &str,
|
user_id: &str,
|
||||||
since: DateTime<Utc>,
|
|
||||||
) -> Result<Vec<PainPoint>> {
|
) -> Result<Vec<PainPoint>> {
|
||||||
let profile = self.get(user_id).await?;
|
let profile = self.get(user_id).await?;
|
||||||
Ok(match profile {
|
Ok(match profile {
|
||||||
Some(p) => p
|
Some(p) => p
|
||||||
.active_pain_points
|
.active_pain_points
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|_| true)
|
|
||||||
.map(|content| PainPoint {
|
.map(|content| PainPoint {
|
||||||
content,
|
content,
|
||||||
created_at: since,
|
created_at: p.updated_at,
|
||||||
last_mentioned_at: Utc::now(),
|
last_mentioned_at: p.updated_at,
|
||||||
status: PainStatus::Active,
|
status: PainStatus::Active,
|
||||||
occurrence_count: 1,
|
occurrence_count: 1,
|
||||||
})
|
})
|
||||||
@@ -678,4 +679,64 @@ mod tests {
|
|||||||
assert_eq!(decoded.communication_style, Some(CommStyle::Detailed));
|
assert_eq!(decoded.communication_style, Some(CommStyle::Detailed));
|
||||||
assert_eq!(decoded.recent_topics, vec!["exports", "customs"]);
|
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