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:
2026-06-06 01:27:01 +02:00
parent 55b3bee2c8
commit 307ae71857
273 changed files with 48740 additions and 0 deletions

View File

@ -0,0 +1,411 @@
//! Entity & value-object invariant tests: valid construction plus the expected
//! rejections (ARCHITECTURE §3.2).
mod helpers;
use domain::{
Agent, AgentManifest, AgentOrigin, AgentProfile, AgentTemplate, ContextInjection, DomainError,
ManifestEntry, MarkdownDoc, ProfileId, Project, ProjectPath, PtySize, RemoteRef, SshAuth,
TemplateId, TemplateVersion,
};
use helpers::{AtomicSeqIdGenerator, FixedClock};
use uuid::Uuid;
fn profile_id() -> ProfileId {
ProfileId::from_uuid(Uuid::from_u128(42))
}
fn template_id() -> TemplateId {
TemplateId::from_uuid(Uuid::from_u128(7))
}
// ---------------------------------------------------------------------------
// ProjectPath
// ---------------------------------------------------------------------------
#[test]
fn project_path_accepts_posix_absolute() {
assert!(ProjectPath::new("/home/user/proj").is_ok());
}
#[test]
fn project_path_accepts_windows_drive_and_unc() {
assert!(ProjectPath::new("C:\\Users\\x").is_ok());
assert!(ProjectPath::new("C:/Users/x").is_ok());
assert!(ProjectPath::new("\\\\server\\share").is_ok());
}
#[test]
fn project_path_accepts_wsl_mount() {
assert!(ProjectPath::new("/mnt/c/code").is_ok());
}
#[test]
fn project_path_rejects_relative() {
let err = ProjectPath::new("relative/path").unwrap_err();
assert!(matches!(err, DomainError::PathNotAbsolute { .. }));
}
#[test]
fn project_path_rejects_empty() {
let err = ProjectPath::new("").unwrap_err();
assert!(matches!(err, DomainError::EmptyField { .. }));
}
// ---------------------------------------------------------------------------
// Project (also exercises the Clock/IdGenerator port fakes for determinism)
// ---------------------------------------------------------------------------
#[test]
fn project_valid_with_fixed_clock_and_seq_ids() {
use domain::{ports::Clock, ports::IdGenerator, ProjectId};
let clock = FixedClock(1_700_000_000_000);
let ids = AtomicSeqIdGenerator::new();
let id = ProjectId::from_uuid(ids.new_uuid());
let p = Project::new(
id,
"demo",
ProjectPath::new("/srv/demo").unwrap(),
RemoteRef::local(),
clock.now_millis(),
)
.unwrap();
assert_eq!(p.created_at, 1_700_000_000_000);
assert_eq!(p.id.as_uuid(), Uuid::from_u128(1));
}
#[test]
fn project_rejects_empty_name() {
let err = Project::new(
domain::ProjectId::from_uuid(Uuid::nil()),
" ",
ProjectPath::new("/x").unwrap(),
RemoteRef::local(),
0,
)
.unwrap_err();
assert!(matches!(err, DomainError::EmptyField { .. }));
}
// ---------------------------------------------------------------------------
// Agent invariants
// ---------------------------------------------------------------------------
#[test]
fn agent_scratch_not_synchronized_is_ok() {
let a = Agent::new(
domain::AgentId::from_uuid(Uuid::from_u128(1)),
"scratch",
"agents/foo.md",
profile_id(),
AgentOrigin::Scratch,
false,
);
assert!(a.is_ok());
}
#[test]
fn agent_from_template_synchronized_is_ok() {
let a = Agent::new(
domain::AgentId::from_uuid(Uuid::from_u128(1)),
"tpl",
"agents/foo.md",
profile_id(),
AgentOrigin::FromTemplate {
template_id: template_id(),
synced_template_version: TemplateVersion::INITIAL,
},
true,
);
assert!(a.is_ok());
}
#[test]
fn agent_synchronized_without_template_is_rejected() {
let err = Agent::new(
domain::AgentId::from_uuid(Uuid::from_u128(1)),
"bad",
"agents/foo.md",
profile_id(),
AgentOrigin::Scratch,
true,
)
.unwrap_err();
assert_eq!(err, DomainError::SyncRequiresTemplate);
}
#[test]
fn agent_rejects_absolute_context_path() {
let err = Agent::new(
domain::AgentId::from_uuid(Uuid::from_u128(1)),
"x",
"/etc/passwd",
profile_id(),
AgentOrigin::Scratch,
false,
)
.unwrap_err();
assert!(matches!(err, DomainError::PathNotRelativeSafe { .. }));
}
#[test]
fn agent_rejects_dotdot_context_path() {
let err = Agent::new(
domain::AgentId::from_uuid(Uuid::from_u128(1)),
"x",
"agents/../../secret.md",
profile_id(),
AgentOrigin::Scratch,
false,
)
.unwrap_err();
assert!(matches!(err, DomainError::PathNotRelativeSafe { .. }));
}
// ---------------------------------------------------------------------------
// AgentProfile: command non-empty
// ---------------------------------------------------------------------------
fn ci_stdin() -> ContextInjection {
ContextInjection::stdin()
}
#[test]
fn profile_valid() {
let p = AgentProfile::new(
profile_id(),
"Claude",
"claude",
vec!["--yolo".into()],
ci_stdin(),
Some("claude --version".into()),
"{projectRoot}",
);
assert!(p.is_ok());
}
#[test]
fn profile_rejects_empty_command() {
let err = AgentProfile::new(
profile_id(),
"Name",
"",
vec![],
ci_stdin(),
None,
"{projectRoot}",
)
.unwrap_err();
assert!(matches!(err, DomainError::EmptyField { field } if field == "profile.command"));
}
#[test]
fn profile_rejects_empty_name() {
let err = AgentProfile::new(profile_id(), "", "claude", vec![], ci_stdin(), None, "{r}")
.unwrap_err();
assert!(matches!(err, DomainError::EmptyField { field } if field == "profile.name"));
}
// ---------------------------------------------------------------------------
// RemoteRef invariants
// ---------------------------------------------------------------------------
#[test]
fn ssh_valid() {
let r = RemoteRef::ssh("host", 22, "me", SshAuth::Agent, "/srv");
assert!(r.is_ok());
assert_eq!(r.unwrap().kind(), domain::RemoteKind::Ssh);
}
#[test]
fn ssh_port_zero_rejected() {
let err = RemoteRef::ssh("host", 0, "me", SshAuth::Agent, "/srv").unwrap_err();
assert!(matches!(err, DomainError::InvalidPort { port: 0 }));
}
#[test]
fn ssh_max_port_accepted() {
// 65535 is the upper bound of the 1..=65535 range; u16 prevents anything higher.
assert!(RemoteRef::ssh("h", 65535, "u", SshAuth::Password, "/r").is_ok());
}
#[test]
fn ssh_rejects_empty_host_user_root() {
assert!(RemoteRef::ssh("", 22, "u", SshAuth::Agent, "/r").is_err());
assert!(RemoteRef::ssh("h", 22, "", SshAuth::Agent, "/r").is_err());
assert!(RemoteRef::ssh("h", 22, "u", SshAuth::Agent, "").is_err());
}
#[test]
fn wsl_valid() {
assert!(RemoteRef::wsl("Ubuntu-22.04").is_ok());
}
#[test]
fn wsl_empty_distro_rejected() {
let err = RemoteRef::wsl("").unwrap_err();
assert!(matches!(err, DomainError::EmptyField { .. }));
}
// ---------------------------------------------------------------------------
// PtySize invariants
// ---------------------------------------------------------------------------
#[test]
fn pty_size_valid() {
assert!(PtySize::new(24, 80).is_ok());
}
#[test]
fn pty_size_zero_rows_rejected() {
assert!(matches!(
PtySize::new(0, 80).unwrap_err(),
DomainError::InvalidPtySize { rows: 0, cols: 80 }
));
}
#[test]
fn pty_size_zero_cols_rejected() {
assert!(matches!(
PtySize::new(24, 0).unwrap_err(),
DomainError::InvalidPtySize { rows: 24, cols: 0 }
));
}
// ---------------------------------------------------------------------------
// AgentTemplate version monotonicity (via with_updated_content / next)
// ---------------------------------------------------------------------------
#[test]
fn template_starts_at_initial() {
let t = AgentTemplate::new(template_id(), "T", MarkdownDoc::new("a"), profile_id()).unwrap();
assert_eq!(t.version, TemplateVersion::INITIAL);
assert_eq!(t.version.get(), 1);
}
#[test]
fn template_update_bumps_version_monotonically() {
let t0 = AgentTemplate::new(template_id(), "T", MarkdownDoc::new("a"), profile_id()).unwrap();
let t1 = t0.with_updated_content(MarkdownDoc::new("b"));
let t2 = t1.with_updated_content(MarkdownDoc::new("c"));
assert!(t1.version > t0.version);
assert!(t2.version > t1.version);
assert_eq!(t2.version.get(), 3);
assert_eq!(t2.content_md.as_str(), "c");
// id and profile preserved.
assert_eq!(t2.id, t0.id);
assert_eq!(t2.default_profile_id, t0.default_profile_id);
}
#[test]
fn template_version_next_increments() {
assert_eq!(TemplateVersion(5).next(), TemplateVersion(6));
}
#[test]
fn template_rejects_empty_name() {
let err = AgentTemplate::new(template_id(), "", MarkdownDoc::new(""), profile_id()).unwrap_err();
assert!(matches!(err, DomainError::EmptyField { .. }));
}
// ---------------------------------------------------------------------------
// ManifestEntry / AgentManifest invariants
// ---------------------------------------------------------------------------
fn agent_id(n: u128) -> domain::AgentId {
domain::AgentId::from_uuid(Uuid::from_u128(n))
}
#[test]
fn manifest_entry_synchronized_requires_template_metadata() {
let err =
ManifestEntry::new(agent_id(1), "A", "agents/a.md", profile_id(), None, true, None)
.unwrap_err();
assert!(matches!(err, DomainError::InconsistentManifest { .. }));
// template id present but version missing → still rejected.
let err = ManifestEntry::new(
agent_id(1),
"A",
"agents/a.md",
profile_id(),
Some(template_id()),
true,
None,
)
.unwrap_err();
assert!(matches!(err, DomainError::InconsistentManifest { .. }));
}
#[test]
fn manifest_entry_rejects_empty_name() {
let err =
ManifestEntry::new(agent_id(1), " ", "agents/a.md", profile_id(), None, false, None)
.unwrap_err();
assert!(matches!(err, DomainError::EmptyField { .. }));
}
#[test]
fn manifest_entry_synchronized_with_metadata_ok() {
assert!(ManifestEntry::new(
agent_id(1),
"A",
"agents/a.md",
profile_id(),
Some(template_id()),
true,
Some(TemplateVersion::INITIAL)
)
.is_ok());
}
#[test]
fn manifest_entry_rejects_absolute_md_path() {
assert!(matches!(
ManifestEntry::new(agent_id(1), "A", "/abs.md", profile_id(), None, false, None)
.unwrap_err(),
DomainError::PathNotRelativeSafe { .. }
));
}
#[test]
fn manifest_entry_agent_roundtrip() {
// from_agent ∘ to_agent preserves a template-backed, synchronized agent.
let agent = Agent::new(
agent_id(9),
"Backend",
"agents/backend.md",
profile_id(),
AgentOrigin::FromTemplate {
template_id: template_id(),
synced_template_version: TemplateVersion(4),
},
true,
)
.unwrap();
let entry = ManifestEntry::from_agent(&agent);
assert_eq!(entry.to_agent().unwrap(), agent);
}
#[test]
fn manifest_rejects_duplicate_md_path() {
let e1 =
ManifestEntry::new(agent_id(1), "A", "agents/a.md", profile_id(), None, false, None)
.unwrap();
let e2 =
ManifestEntry::new(agent_id(2), "B", "agents/a.md", profile_id(), None, false, None)
.unwrap();
let err = AgentManifest::new(1, vec![e1, e2]).unwrap_err();
assert!(matches!(err, DomainError::InconsistentManifest { .. }));
}
#[test]
fn manifest_unique_md_paths_ok() {
let e1 =
ManifestEntry::new(agent_id(1), "A", "agents/a.md", profile_id(), None, false, None)
.unwrap();
let e2 =
ManifestEntry::new(agent_id(2), "B", "agents/b.md", profile_id(), None, false, None)
.unwrap();
assert!(AgentManifest::new(1, vec![e1, e2]).is_ok());
}