Update: 将子项目从 submodule 转为完整内容
- 移除 GovAI, nomifun-tauri, 算力盒子 的 submodule 引用 - 添加所有子项目的完整源代码 - 保留原始 .git 为 .git.bak 备份
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
//! Verifies `RequirementService::set_status` fires the `CompletionNotifier`
|
||||
//! exactly once on a terminal transition, and not on no-op / non-terminal ones.
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
use std::time::Duration;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use nomifun_api_types::{CreateRequirementRequest, RequirementStatus};
|
||||
use nomifun_db::models::RequirementRow;
|
||||
use nomifun_db::{IRequirementRepository, SqliteRequirementRepository, init_database_memory};
|
||||
use nomifun_realtime::EventBroadcaster;
|
||||
use nomifun_requirement::{CompletionNotifier, RequirementEventEmitter, RequirementService};
|
||||
|
||||
#[derive(Default)]
|
||||
struct NoopBroadcaster;
|
||||
impl EventBroadcaster for NoopBroadcaster {
|
||||
fn broadcast(&self, _event: nomifun_api_types::WebSocketMessage<serde_json::Value>) {}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct RecordingNotifier {
|
||||
ids: Mutex<Vec<i64>>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl CompletionNotifier for RecordingNotifier {
|
||||
async fn notify_completion(&self, requirement: &RequirementRow) {
|
||||
self.ids.lock().unwrap().push(requirement.id);
|
||||
}
|
||||
}
|
||||
|
||||
async fn svc(notifier: Arc<RecordingNotifier>) -> RequirementService {
|
||||
let db = init_database_memory().await.unwrap();
|
||||
let repo: Arc<dyn IRequirementRepository> = Arc::new(SqliteRequirementRepository::new(db.pool().clone()));
|
||||
let emitter = RequirementEventEmitter::new(Arc::new(NoopBroadcaster));
|
||||
Box::leak(Box::new(db));
|
||||
RequirementService::new(repo, emitter).with_completion_notifier(notifier)
|
||||
}
|
||||
|
||||
fn new_req(tag: &str) -> CreateRequirementRequest {
|
||||
CreateRequirementRequest {
|
||||
title: "T".into(),
|
||||
content: "body".into(),
|
||||
tag: tag.into(),
|
||||
order_key: Some("1".into()),
|
||||
status: None,
|
||||
created_by: None,
|
||||
attachments: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
/// Let the detached `tokio::spawn`(notify) task run.
|
||||
async fn settle() {
|
||||
tokio::time::sleep(Duration::from_millis(50)).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn fires_once_on_done() {
|
||||
let notifier = Arc::new(RecordingNotifier::default());
|
||||
let s = svc(notifier.clone()).await;
|
||||
let r = s.create(new_req("alpha")).await.unwrap();
|
||||
|
||||
s.set_status(r.id, RequirementStatus::Done, Some("note".into()))
|
||||
.await
|
||||
.unwrap();
|
||||
settle().await;
|
||||
assert_eq!(notifier.ids.lock().unwrap().as_slice(), std::slice::from_ref(&r.id));
|
||||
|
||||
// Re-setting the same terminal status is an idempotent no-op → no extra fire.
|
||||
let _ = s.set_status(r.id, RequirementStatus::Done, None).await.unwrap();
|
||||
settle().await;
|
||||
assert_eq!(notifier.ids.lock().unwrap().len(), 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn fires_on_failed() {
|
||||
let notifier = Arc::new(RecordingNotifier::default());
|
||||
let s = svc(notifier.clone()).await;
|
||||
let r = s.create(new_req("alpha")).await.unwrap();
|
||||
s.set_status(r.id, RequirementStatus::Failed, Some("oops".into()))
|
||||
.await
|
||||
.unwrap();
|
||||
settle().await;
|
||||
assert_eq!(notifier.ids.lock().unwrap().len(), 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn does_not_fire_on_non_terminal() {
|
||||
let notifier = Arc::new(RecordingNotifier::default());
|
||||
let s = svc(notifier.clone()).await;
|
||||
let r = s.create(new_req("alpha")).await.unwrap();
|
||||
s.set_status(r.id, RequirementStatus::InProgress, None).await.unwrap();
|
||||
settle().await;
|
||||
assert!(notifier.ids.lock().unwrap().is_empty());
|
||||
}
|
||||
Reference in New Issue
Block a user