231 lines
6.3 KiB
Rust
231 lines
6.3 KiB
Rust
use std::collections::HashMap;
|
|
use std::sync::{Arc, Mutex};
|
|
|
|
use async_trait::async_trait;
|
|
use domain::ports::{ChatDraftStore, Clock, ProjectStore, StoreError};
|
|
use domain::{
|
|
ChatDraft, ChatDraftKey, LayoutId, NodeId, Project, ProjectId, ProjectPath, RemoteRef,
|
|
};
|
|
use uuid::Uuid;
|
|
|
|
use application::{
|
|
ChatDraftScopeInput, ClearChatDraft, ReadChatDraft, SaveChatDraft, SaveChatDraftInput,
|
|
};
|
|
|
|
#[derive(Default)]
|
|
struct FakeProjects {
|
|
projects: Mutex<HashMap<ProjectId, Project>>,
|
|
}
|
|
|
|
#[async_trait]
|
|
impl ProjectStore for FakeProjects {
|
|
async fn list_projects(&self) -> Result<Vec<Project>, StoreError> {
|
|
Ok(self.projects.lock().unwrap().values().cloned().collect())
|
|
}
|
|
|
|
async fn load_project(&self, id: ProjectId) -> Result<Project, StoreError> {
|
|
self.projects
|
|
.lock()
|
|
.unwrap()
|
|
.get(&id)
|
|
.cloned()
|
|
.ok_or(StoreError::NotFound)
|
|
}
|
|
|
|
async fn save_project(&self, project: &Project) -> Result<(), StoreError> {
|
|
self.projects
|
|
.lock()
|
|
.unwrap()
|
|
.insert(project.id, project.clone());
|
|
Ok(())
|
|
}
|
|
|
|
async fn save_workspace(
|
|
&self,
|
|
_workspace: &domain::layout::Workspace,
|
|
) -> Result<(), StoreError> {
|
|
Ok(())
|
|
}
|
|
|
|
async fn load_workspace(&self) -> Result<domain::layout::Workspace, StoreError> {
|
|
Err(StoreError::NotFound)
|
|
}
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct FakeDrafts {
|
|
drafts: Mutex<HashMap<ChatDraftKey, ChatDraft>>,
|
|
saves: Mutex<Vec<ChatDraft>>,
|
|
clears: Mutex<Vec<ChatDraftKey>>,
|
|
}
|
|
|
|
#[async_trait]
|
|
impl ChatDraftStore for FakeDrafts {
|
|
async fn read(
|
|
&self,
|
|
_root: &ProjectPath,
|
|
key: ChatDraftKey,
|
|
) -> Result<Option<ChatDraft>, StoreError> {
|
|
Ok(self.drafts.lock().unwrap().get(&key).cloned())
|
|
}
|
|
|
|
async fn save(&self, _root: &ProjectPath, draft: ChatDraft) -> Result<(), StoreError> {
|
|
self.saves.lock().unwrap().push(draft.clone());
|
|
self.drafts.lock().unwrap().insert(draft.key, draft);
|
|
Ok(())
|
|
}
|
|
|
|
async fn clear(&self, _root: &ProjectPath, key: ChatDraftKey) -> Result<(), StoreError> {
|
|
self.clears.lock().unwrap().push(key);
|
|
self.drafts.lock().unwrap().remove(&key);
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
struct FixedClock;
|
|
|
|
impl Clock for FixedClock {
|
|
fn now_millis(&self) -> i64 {
|
|
1234
|
|
}
|
|
}
|
|
|
|
fn pid() -> ProjectId {
|
|
ProjectId::from_uuid(Uuid::from_u128(1))
|
|
}
|
|
|
|
fn lid(n: u128) -> LayoutId {
|
|
LayoutId::from_uuid(Uuid::from_u128(n))
|
|
}
|
|
|
|
fn nid(n: u128) -> NodeId {
|
|
NodeId::from_uuid(Uuid::from_u128(n))
|
|
}
|
|
|
|
fn project() -> Project {
|
|
Project::new(
|
|
pid(),
|
|
"P",
|
|
ProjectPath::new("/tmp/idea-chat-drafts-test").unwrap(),
|
|
RemoteRef::Local,
|
|
0,
|
|
)
|
|
.unwrap()
|
|
}
|
|
|
|
fn scope(layout_id: LayoutId, node_id: NodeId) -> ChatDraftScopeInput {
|
|
ChatDraftScopeInput {
|
|
project_id: pid(),
|
|
layout_id,
|
|
node_id,
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn save_and_read_chat_draft_round_trip_text_only() {
|
|
let projects = Arc::new(FakeProjects::default());
|
|
projects.save_project(&project()).await.unwrap();
|
|
let drafts = Arc::new(FakeDrafts::default());
|
|
let save = SaveChatDraft::new(
|
|
Arc::clone(&projects) as Arc<dyn ProjectStore>,
|
|
Arc::clone(&drafts) as Arc<dyn ChatDraftStore>,
|
|
Arc::new(FixedClock),
|
|
);
|
|
let read = ReadChatDraft::new(
|
|
Arc::clone(&projects) as Arc<dyn ProjectStore>,
|
|
Arc::clone(&drafts) as Arc<dyn ChatDraftStore>,
|
|
);
|
|
|
|
save.execute(SaveChatDraftInput {
|
|
scope: scope(lid(2), nid(3)),
|
|
text: "hello draft".to_owned(),
|
|
})
|
|
.await
|
|
.unwrap();
|
|
|
|
let output = read.execute(scope(lid(2), nid(3))).await.unwrap();
|
|
assert_eq!(output.text.as_deref(), Some("hello draft"));
|
|
assert_eq!(output.updated_at_ms, Some(1234));
|
|
assert_eq!(drafts.saves.lock().unwrap()[0].text, "hello draft");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn save_empty_text_clears_draft() {
|
|
let projects = Arc::new(FakeProjects::default());
|
|
projects.save_project(&project()).await.unwrap();
|
|
let drafts = Arc::new(FakeDrafts::default());
|
|
let key = ChatDraftKey::new(pid(), lid(2), nid(3));
|
|
drafts
|
|
.drafts
|
|
.lock()
|
|
.unwrap()
|
|
.insert(key, ChatDraft::new(key, "old", 1).unwrap());
|
|
let save = SaveChatDraft::new(
|
|
Arc::clone(&projects) as Arc<dyn ProjectStore>,
|
|
Arc::clone(&drafts) as Arc<dyn ChatDraftStore>,
|
|
Arc::new(FixedClock),
|
|
);
|
|
|
|
save.execute(SaveChatDraftInput {
|
|
scope: scope(lid(2), nid(3)),
|
|
text: String::new(),
|
|
})
|
|
.await
|
|
.unwrap();
|
|
|
|
assert!(drafts.drafts.lock().unwrap().is_empty());
|
|
assert_eq!(drafts.clears.lock().unwrap().as_slice(), &[key]);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn clear_chat_draft_is_scoped_by_layout_and_node() {
|
|
let projects = Arc::new(FakeProjects::default());
|
|
projects.save_project(&project()).await.unwrap();
|
|
let drafts = Arc::new(FakeDrafts::default());
|
|
let keep = ChatDraftKey::new(pid(), lid(2), nid(4));
|
|
let clear = ChatDraftKey::new(pid(), lid(2), nid(3));
|
|
drafts
|
|
.drafts
|
|
.lock()
|
|
.unwrap()
|
|
.insert(keep, ChatDraft::new(keep, "keep", 1).unwrap());
|
|
drafts
|
|
.drafts
|
|
.lock()
|
|
.unwrap()
|
|
.insert(clear, ChatDraft::new(clear, "clear", 1).unwrap());
|
|
let usecase = ClearChatDraft::new(
|
|
Arc::clone(&projects) as Arc<dyn ProjectStore>,
|
|
Arc::clone(&drafts) as Arc<dyn ChatDraftStore>,
|
|
);
|
|
|
|
usecase.execute(scope(lid(2), nid(3))).await.unwrap();
|
|
|
|
let drafts = drafts.drafts.lock().unwrap();
|
|
assert!(drafts.contains_key(&keep));
|
|
assert!(!drafts.contains_key(&clear));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn oversized_chat_draft_is_rejected_before_store_call() {
|
|
let projects = Arc::new(FakeProjects::default());
|
|
projects.save_project(&project()).await.unwrap();
|
|
let drafts = Arc::new(FakeDrafts::default());
|
|
let save = SaveChatDraft::new(
|
|
Arc::clone(&projects) as Arc<dyn ProjectStore>,
|
|
Arc::clone(&drafts) as Arc<dyn ChatDraftStore>,
|
|
Arc::new(FixedClock),
|
|
);
|
|
|
|
let err = save
|
|
.execute(SaveChatDraftInput {
|
|
scope: scope(lid(2), nid(3)),
|
|
text: "x".repeat(domain::CHAT_DRAFT_MAX_BYTES + 1),
|
|
})
|
|
.await
|
|
.unwrap_err();
|
|
|
|
assert_eq!(err.code(), "INVALID");
|
|
assert!(drafts.saves.lock().unwrap().is_empty());
|
|
}
|