feat: add main features
Agents for developpement added + frontend add + backend added. Git viewer created + agent and template creator + layout and project creator
This commit is contained in:
240
crates/application/tests/git_usecases.rs
Normal file
240
crates/application/tests/git_usecases.rs
Normal file
@ -0,0 +1,240 @@
|
||||
//! L8 tests for the Git use cases with a faked [`GitPort`] (no real repo):
|
||||
//! pass-through to the port, event emission on state changes, and input
|
||||
//! validation (empty message, non-absolute root).
|
||||
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use domain::events::DomainEvent;
|
||||
use domain::ports::{
|
||||
EventBus, EventStream, GitCommitInfo, GitError, GitFileStatus, GitPort, GraphCommit,
|
||||
};
|
||||
use domain::{ProjectId, ProjectPath};
|
||||
use uuid::Uuid;
|
||||
|
||||
use application::{
|
||||
GitBranches, GitBranchesInput, GitCheckout, GitCheckoutInput, GitCommit, GitCommitInput,
|
||||
GitStage, GitStagePathInput, GitStatus, GitStatusInput,
|
||||
};
|
||||
|
||||
/// A recording [`GitPort`] with canned return values.
|
||||
#[derive(Default)]
|
||||
struct FakeGitInner {
|
||||
calls: Vec<String>,
|
||||
status: Vec<GitFileStatus>,
|
||||
branches: Vec<String>,
|
||||
current: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
struct FakeGit(Arc<Mutex<FakeGitInner>>);
|
||||
impl FakeGit {
|
||||
fn calls(&self) -> Vec<String> {
|
||||
self.0.lock().unwrap().calls.clone()
|
||||
}
|
||||
fn set_status(&self, s: Vec<GitFileStatus>) {
|
||||
self.0.lock().unwrap().status = s;
|
||||
}
|
||||
fn set_branches(&self, b: Vec<String>, current: Option<String>) {
|
||||
let mut i = self.0.lock().unwrap();
|
||||
i.branches = b;
|
||||
i.current = current;
|
||||
}
|
||||
fn record(&self, c: &str) {
|
||||
self.0.lock().unwrap().calls.push(c.to_owned());
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl GitPort for FakeGit {
|
||||
async fn init(&self, _r: &ProjectPath) -> Result<(), GitError> {
|
||||
self.record("init");
|
||||
Ok(())
|
||||
}
|
||||
async fn status(&self, _r: &ProjectPath) -> Result<Vec<GitFileStatus>, GitError> {
|
||||
self.record("status");
|
||||
Ok(self.0.lock().unwrap().status.clone())
|
||||
}
|
||||
async fn stage(&self, _r: &ProjectPath, path: &str) -> Result<(), GitError> {
|
||||
self.record(&format!("stage:{path}"));
|
||||
Ok(())
|
||||
}
|
||||
async fn unstage(&self, _r: &ProjectPath, path: &str) -> Result<(), GitError> {
|
||||
self.record(&format!("unstage:{path}"));
|
||||
Ok(())
|
||||
}
|
||||
async fn commit(&self, _r: &ProjectPath, message: &str) -> Result<GitCommitInfo, GitError> {
|
||||
self.record(&format!("commit:{message}"));
|
||||
Ok(GitCommitInfo {
|
||||
hash: "abc123".to_owned(),
|
||||
summary: message.to_owned(),
|
||||
})
|
||||
}
|
||||
async fn branches(&self, _r: &ProjectPath) -> Result<Vec<String>, GitError> {
|
||||
self.record("branches");
|
||||
Ok(self.0.lock().unwrap().branches.clone())
|
||||
}
|
||||
async fn current_branch(&self, _r: &ProjectPath) -> Result<Option<String>, GitError> {
|
||||
self.record("current_branch");
|
||||
Ok(self.0.lock().unwrap().current.clone())
|
||||
}
|
||||
async fn checkout(&self, _r: &ProjectPath, branch: &str) -> Result<(), GitError> {
|
||||
self.record(&format!("checkout:{branch}"));
|
||||
Ok(())
|
||||
}
|
||||
async fn log(
|
||||
&self,
|
||||
_r: &ProjectPath,
|
||||
_limit: usize,
|
||||
) -> Result<Vec<GitCommitInfo>, GitError> {
|
||||
self.record("log");
|
||||
Ok(Vec::new())
|
||||
}
|
||||
async fn log_graph(
|
||||
&self,
|
||||
_r: &ProjectPath,
|
||||
_limit: usize,
|
||||
) -> Result<Vec<GraphCommit>, GitError> {
|
||||
self.record("log_graph");
|
||||
Ok(Vec::new())
|
||||
}
|
||||
async fn pull(&self, _r: &ProjectPath) -> Result<(), GitError> {
|
||||
Ok(())
|
||||
}
|
||||
async fn push(&self, _r: &ProjectPath) -> Result<(), GitError> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
struct SpyBus(Arc<Mutex<Vec<DomainEvent>>>);
|
||||
impl SpyBus {
|
||||
fn events(&self) -> Vec<DomainEvent> {
|
||||
self.0.lock().unwrap().clone()
|
||||
}
|
||||
}
|
||||
impl EventBus for SpyBus {
|
||||
fn publish(&self, event: DomainEvent) {
|
||||
self.0.lock().unwrap().push(event);
|
||||
}
|
||||
fn subscribe(&self) -> EventStream {
|
||||
Box::new(std::iter::empty())
|
||||
}
|
||||
}
|
||||
|
||||
fn pid() -> ProjectId {
|
||||
ProjectId::from_uuid(Uuid::from_u128(1))
|
||||
}
|
||||
const ROOT: &str = "/home/me/repo";
|
||||
|
||||
#[tokio::test]
|
||||
async fn status_passes_through_to_port() {
|
||||
let git = FakeGit::default();
|
||||
git.set_status(vec![GitFileStatus {
|
||||
path: "a.txt".to_owned(),
|
||||
staged: true,
|
||||
}]);
|
||||
let out = GitStatus::new(Arc::new(git.clone()))
|
||||
.execute(GitStatusInput { root: ROOT.to_owned() })
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(out.entries.len(), 1);
|
||||
assert_eq!(out.entries[0].path, "a.txt");
|
||||
assert_eq!(git.calls(), vec!["status"]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn status_rejects_non_absolute_root() {
|
||||
let git = FakeGit::default();
|
||||
let err = GitStatus::new(Arc::new(git.clone()))
|
||||
.execute(GitStatusInput {
|
||||
root: "relative/path".to_owned(),
|
||||
})
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert_eq!(err.code(), "INVALID", "got {err:?}");
|
||||
assert!(git.calls().is_empty(), "no port call on invalid root");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn stage_calls_port_with_path() {
|
||||
let git = FakeGit::default();
|
||||
GitStage::new(Arc::new(git.clone()))
|
||||
.execute(GitStagePathInput {
|
||||
root: ROOT.to_owned(),
|
||||
path: "src/x.rs".to_owned(),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(git.calls(), vec!["stage:src/x.rs"]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn commit_returns_commit_and_publishes_event() {
|
||||
let git = FakeGit::default();
|
||||
let bus = SpyBus::default();
|
||||
let out = GitCommit::new(Arc::new(git.clone()), Arc::new(bus.clone()))
|
||||
.execute(GitCommitInput {
|
||||
project_id: pid(),
|
||||
root: ROOT.to_owned(),
|
||||
message: "feat: x".to_owned(),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(out.commit.hash, "abc123");
|
||||
assert_eq!(out.commit.summary, "feat: x");
|
||||
assert_eq!(git.calls(), vec!["commit:feat: x"]);
|
||||
assert_eq!(
|
||||
bus.events(),
|
||||
vec![DomainEvent::GitStateChanged { project_id: pid() }]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn commit_rejects_empty_message_without_touching_port() {
|
||||
let git = FakeGit::default();
|
||||
let bus = SpyBus::default();
|
||||
let err = GitCommit::new(Arc::new(git.clone()), Arc::new(bus.clone()))
|
||||
.execute(GitCommitInput {
|
||||
project_id: pid(),
|
||||
root: ROOT.to_owned(),
|
||||
message: " ".to_owned(),
|
||||
})
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert_eq!(err.code(), "INVALID", "got {err:?}");
|
||||
assert!(git.calls().is_empty(), "no commit attempted");
|
||||
assert!(bus.events().is_empty(), "no event on rejected commit");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn checkout_publishes_event() {
|
||||
let git = FakeGit::default();
|
||||
let bus = SpyBus::default();
|
||||
GitCheckout::new(Arc::new(git.clone()), Arc::new(bus.clone()))
|
||||
.execute(GitCheckoutInput {
|
||||
project_id: pid(),
|
||||
root: ROOT.to_owned(),
|
||||
branch: "dev".to_owned(),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(git.calls(), vec!["checkout:dev"]);
|
||||
assert_eq!(
|
||||
bus.events(),
|
||||
vec![DomainEvent::GitStateChanged { project_id: pid() }]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn branches_returns_list_and_current() {
|
||||
let git = FakeGit::default();
|
||||
git.set_branches(vec!["main".to_owned(), "dev".to_owned()], Some("main".to_owned()));
|
||||
let out = GitBranches::new(Arc::new(git.clone()))
|
||||
.execute(GitBranchesInput { root: ROOT.to_owned() })
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(out.branches, vec!["main", "dev"]);
|
||||
assert_eq!(out.current.as_deref(), Some("main"));
|
||||
assert_eq!(git.calls(), vec!["branches", "current_branch"]);
|
||||
}
|
||||
Reference in New Issue
Block a user