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:
295
crates/domain/tests/serde_roundtrip.rs
Normal file
295
crates/domain/tests/serde_roundtrip.rs
Normal file
@ -0,0 +1,295 @@
|
||||
//! JSON round-trip (serde) of persisted domain types, plus camelCase / tagging
|
||||
//! checks (ARCHITECTURE §7.3, §9).
|
||||
|
||||
mod helpers;
|
||||
|
||||
use domain::{
|
||||
Agent, AgentManifest, AgentOrigin, AgentProfile, AgentTemplate, ContextInjection, Direction,
|
||||
LayoutNode, LayoutTree, LeafCell, ManifestEntry, MarkdownDoc, Project, ProjectPath, RemoteRef,
|
||||
SplitContainer, SshAuth, TemplateVersion, WeightedChild,
|
||||
};
|
||||
use helpers::{node, session};
|
||||
use uuid::Uuid;
|
||||
|
||||
fn pid(n: u128) -> domain::ProjectId {
|
||||
domain::ProjectId::from_uuid(Uuid::from_u128(n))
|
||||
}
|
||||
fn profid(n: u128) -> domain::ProfileId {
|
||||
domain::ProfileId::from_uuid(Uuid::from_u128(n))
|
||||
}
|
||||
fn tid(n: u128) -> domain::TemplateId {
|
||||
domain::TemplateId::from_uuid(Uuid::from_u128(n))
|
||||
}
|
||||
fn aid(n: u128) -> domain::AgentId {
|
||||
domain::AgentId::from_uuid(Uuid::from_u128(n))
|
||||
}
|
||||
|
||||
fn roundtrip<T>(value: &T) -> T
|
||||
where
|
||||
T: serde::Serialize + serde::de::DeserializeOwned + PartialEq + std::fmt::Debug,
|
||||
{
|
||||
let json = serde_json::to_string(value).expect("serialize");
|
||||
serde_json::from_str(&json).expect("deserialize")
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Project
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn project_roundtrip() {
|
||||
let p = Project::new(
|
||||
pid(1),
|
||||
"demo",
|
||||
ProjectPath::new("/srv/demo").unwrap(),
|
||||
RemoteRef::ssh("h", 22, "u", SshAuth::Agent, "/srv").unwrap(),
|
||||
123,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(roundtrip(&p), p);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn project_uses_camel_case_and_tagged_remote() {
|
||||
let p = Project::new(
|
||||
pid(1),
|
||||
"demo",
|
||||
ProjectPath::new("/srv/demo").unwrap(),
|
||||
RemoteRef::local(),
|
||||
123,
|
||||
)
|
||||
.unwrap();
|
||||
let json = serde_json::to_string(&p).unwrap();
|
||||
assert!(json.contains("\"createdAt\":123"), "json was {json}");
|
||||
// RemoteRef tagged with `kind`, camelCased "local".
|
||||
assert!(json.contains("\"kind\":\"local\""), "json was {json}");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// RemoteRef variants
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn remote_ssh_roundtrip_and_tags() {
|
||||
let r = RemoteRef::ssh("host", 2222, "me", SshAuth::Key { path: "/k".into() }, "/srv").unwrap();
|
||||
assert_eq!(roundtrip(&r), r);
|
||||
let json = serde_json::to_string(&r).unwrap();
|
||||
assert!(json.contains("\"kind\":\"ssh\""), "json was {json}");
|
||||
// SshAuth tagged with `type`.
|
||||
assert!(json.contains("\"type\":\"key\""), "json was {json}");
|
||||
// Enum-variant fields must be camelCased on the wire (ARCHITECTURE §9).
|
||||
assert!(json.contains("\"remoteRoot\""), "json was {json}");
|
||||
assert!(!json.contains("\"remote_root\""), "json was {json}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remote_wsl_roundtrip() {
|
||||
let r = RemoteRef::wsl("Ubuntu").unwrap();
|
||||
assert_eq!(roundtrip(&r), r);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// AgentProfile + ContextInjection (tagged with `strategy`)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn profile_roundtrip_all_injection_variants() {
|
||||
for ci in [
|
||||
ContextInjection::convention_file("CLAUDE.md").unwrap(),
|
||||
ContextInjection::flag("-f {path}").unwrap(),
|
||||
ContextInjection::stdin(),
|
||||
ContextInjection::env("CTX").unwrap(),
|
||||
] {
|
||||
let p = AgentProfile::new(
|
||||
profid(1),
|
||||
"Name",
|
||||
"claude",
|
||||
vec!["a".into(), "b".into()],
|
||||
ci,
|
||||
Some("claude --version".into()),
|
||||
"{projectRoot}",
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(roundtrip(&p), p);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn context_injection_strategy_tag_is_camel_case() {
|
||||
let json = serde_json::to_string(&ContextInjection::convention_file("CLAUDE.md").unwrap())
|
||||
.unwrap();
|
||||
assert!(json.contains("\"strategy\":\"conventionFile\""), "json was {json}");
|
||||
let json = serde_json::to_string(&ContextInjection::stdin()).unwrap();
|
||||
assert!(json.contains("\"strategy\":\"stdin\""), "json was {json}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn profile_cwd_template_is_camel_case() {
|
||||
let p = AgentProfile::new(
|
||||
profid(1),
|
||||
"n",
|
||||
"c",
|
||||
vec![],
|
||||
ContextInjection::stdin(),
|
||||
None,
|
||||
"{projectRoot}",
|
||||
)
|
||||
.unwrap();
|
||||
let json = serde_json::to_string(&p).unwrap();
|
||||
assert!(json.contains("\"cwdTemplate\""), "json was {json}");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// AgentTemplate
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn template_roundtrip() {
|
||||
let t = AgentTemplate::new(tid(1), "T", MarkdownDoc::new("# hi"), profid(2))
|
||||
.unwrap()
|
||||
.with_updated_content(MarkdownDoc::new("# bye"));
|
||||
assert_eq!(roundtrip(&t), t);
|
||||
let json = serde_json::to_string(&t).unwrap();
|
||||
assert!(json.contains("\"contentMd\""), "json was {json}");
|
||||
assert!(json.contains("\"defaultProfileId\""), "json was {json}");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Agent + manifest
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn agent_roundtrip_from_template() {
|
||||
let a = Agent::new(
|
||||
aid(1),
|
||||
"Backend",
|
||||
"agents/backend.md",
|
||||
profid(2),
|
||||
AgentOrigin::FromTemplate {
|
||||
template_id: tid(3),
|
||||
synced_template_version: TemplateVersion(4),
|
||||
},
|
||||
true,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(roundtrip(&a), a);
|
||||
let json = serde_json::to_string(&a).unwrap();
|
||||
assert!(json.contains("\"contextPath\""), "json was {json}");
|
||||
// AgentOrigin tagged with `type`, camelCased.
|
||||
assert!(json.contains("\"type\":\"fromTemplate\""), "json was {json}");
|
||||
// Inner fields must be camelCased per ARCHITECTURE §9.1:
|
||||
// { "type":"fromTemplate", "templateId":"...", "syncedTemplateVersion":N }.
|
||||
assert!(json.contains("\"templateId\""), "json was {json}");
|
||||
assert!(json.contains("\"syncedTemplateVersion\":4"), "json was {json}");
|
||||
assert!(!json.contains("\"template_id\""), "json was {json}");
|
||||
assert!(!json.contains("\"synced_template_version\""), "json was {json}");
|
||||
assert!(!json.contains("\"synced_version\""), "json was {json}");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SessionKind (tagged enum: `type`, camelCased variant fields)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn session_kind_agent_roundtrip_and_camel_case() {
|
||||
use domain::SessionKind;
|
||||
let k = SessionKind::Agent { agent_id: aid(7) };
|
||||
assert_eq!(roundtrip(&k), k);
|
||||
let json = serde_json::to_string(&k).unwrap();
|
||||
assert!(json.contains("\"type\":\"agent\""), "json was {json}");
|
||||
assert!(json.contains("\"agentId\""), "json was {json}");
|
||||
assert!(!json.contains("\"agent_id\""), "json was {json}");
|
||||
|
||||
// Plain variant carries no fields.
|
||||
let plain = serde_json::to_string(&SessionKind::Plain).unwrap();
|
||||
assert!(plain.contains("\"type\":\"plain\""), "json was {plain}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn manifest_roundtrip_and_camel_case() {
|
||||
let e1 = ManifestEntry::new(
|
||||
aid(1),
|
||||
"Alpha",
|
||||
"agents/a.md",
|
||||
profid(9),
|
||||
Some(tid(2)),
|
||||
true,
|
||||
Some(TemplateVersion(5)),
|
||||
)
|
||||
.unwrap();
|
||||
let e2 = ManifestEntry::new(aid(3), "Beta", "agents/b.md", profid(9), None, false, None).unwrap();
|
||||
let m = AgentManifest::new(1, vec![e1, e2]).unwrap();
|
||||
assert_eq!(roundtrip(&m), m);
|
||||
let json = serde_json::to_string(&m).unwrap();
|
||||
// entries are serialized under "agents".
|
||||
assert!(json.contains("\"agents\":["), "json was {json}");
|
||||
assert!(json.contains("\"mdPath\""), "json was {json}");
|
||||
assert!(json.contains("\"syncedTemplateVersion\":5"), "json was {json}");
|
||||
// Non-synchronized entry omits optional template fields (skip_serializing_if).
|
||||
assert!(!json.contains("\"templateId\":null"), "json was {json}");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// LayoutTree (tagged enum: type/node)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn layout_roundtrip() {
|
||||
let tree = LayoutTree::new(LayoutNode::Split(SplitContainer {
|
||||
id: node(9),
|
||||
direction: Direction::Column,
|
||||
children: vec![
|
||||
WeightedChild {
|
||||
node: LayoutNode::Leaf(LeafCell {
|
||||
id: node(1),
|
||||
session: Some(session(100)),
|
||||
agent: None,
|
||||
}),
|
||||
weight: 1.5,
|
||||
},
|
||||
WeightedChild {
|
||||
node: LayoutNode::Leaf(LeafCell {
|
||||
id: node(2),
|
||||
session: None,
|
||||
agent: None,
|
||||
}),
|
||||
weight: 2.5,
|
||||
},
|
||||
],
|
||||
}));
|
||||
assert_eq!(roundtrip(&tree), tree);
|
||||
let json = serde_json::to_string(&tree).unwrap();
|
||||
// enum adjacently tagged: type + node ; direction camelCase.
|
||||
assert!(json.contains("\"type\":\"split\""), "json was {json}");
|
||||
assert!(json.contains("\"type\":\"leaf\""), "json was {json}");
|
||||
assert!(json.contains("\"direction\":\"column\""), "json was {json}");
|
||||
// empty session leaf omits the field.
|
||||
assert!(!json.contains("\"session\":null"), "json was {json}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn leaf_with_agent_roundtrip_and_omits_null() {
|
||||
use domain::ids::AgentId;
|
||||
let agent_uuid = Uuid::from_u128(0xABC);
|
||||
let tree = LayoutTree::new(LayoutNode::Leaf(LeafCell {
|
||||
id: node(1),
|
||||
session: None,
|
||||
agent: Some(AgentId::from_uuid(agent_uuid)),
|
||||
}));
|
||||
let rt = roundtrip(&tree);
|
||||
match rt.root {
|
||||
LayoutNode::Leaf(l) => assert_eq!(l.agent, Some(AgentId::from_uuid(agent_uuid))),
|
||||
_ => panic!("expected leaf"),
|
||||
}
|
||||
let json = serde_json::to_string(&tree).unwrap();
|
||||
// agent present when set
|
||||
assert!(json.contains("\"agent\""), "agent field should be present when set; json was {json}");
|
||||
// null variant omitted
|
||||
let tree_no_agent = LayoutTree::new(LayoutNode::Leaf(LeafCell {
|
||||
id: node(2),
|
||||
session: None,
|
||||
agent: None,
|
||||
}));
|
||||
let json2 = serde_json::to_string(&tree_no_agent).unwrap();
|
||||
assert!(!json2.contains("\"agent\""), "agent field should be omitted when None; json was {json2}");
|
||||
}
|
||||
Reference in New Issue
Block a user