feat(memory): config embedders (LOT C2) + suggestion contextuelle (LOT C3) + contexte projet partagé
- LOT C2 (§14.5.3) : use cases de configuration des embedders déclaratifs (List/Save/Delete + DescribeEmbedderEngines : modèles ONNX recommandés, environnement local détecté, stratégies compilées). UI EmbedderSettings. - LOT C3 (§14.5.5) : suggestion contextuelle best-effort à l'activation quand la mémoire dépasse le budget de recall sans embedder configuré (event EmbedderSuggested, anti-spam 1×/session, « ne plus demander »). - Contexte projet partagé .ideai/CONTEXT.md (model-agnostic) injecté à tous les agents/profils au lancement, avant la persona. UI ProjectContextPanel. Tests : backend workspace vert (0 échec) ; frontend 306/306. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -5,8 +5,8 @@ mod helpers;
|
||||
|
||||
use domain::{
|
||||
Agent, AgentManifest, AgentOrigin, AgentProfile, AgentTemplate, ContextInjection, DomainError,
|
||||
ManifestEntry, MarkdownDoc, ProfileId, Project, ProjectPath, PtySize, RemoteRef, SessionStrategy,
|
||||
Skill, SkillId, SkillRef, SkillScope, SshAuth, TemplateId, TemplateVersion,
|
||||
ManifestEntry, MarkdownDoc, ProfileId, Project, ProjectPath, PtySize, RemoteRef,
|
||||
SessionStrategy, Skill, SkillId, SkillRef, SkillScope, SshAuth, TemplateId, TemplateVersion,
|
||||
};
|
||||
use helpers::{AtomicSeqIdGenerator, FixedClock};
|
||||
use uuid::Uuid;
|
||||
@ -203,8 +203,17 @@ fn profile_rejects_empty_command() {
|
||||
|
||||
#[test]
|
||||
fn profile_rejects_empty_name() {
|
||||
let err = AgentProfile::new(profile_id(), "", "claude", vec![], ci_stdin(), None, "{r}", None)
|
||||
.unwrap_err();
|
||||
let err = AgentProfile::new(
|
||||
profile_id(),
|
||||
"",
|
||||
"claude",
|
||||
vec![],
|
||||
ci_stdin(),
|
||||
None,
|
||||
"{r}",
|
||||
None,
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, DomainError::EmptyField { field } if field == "profile.name"));
|
||||
}
|
||||
|
||||
@ -331,7 +340,8 @@ fn template_version_next_increments() {
|
||||
|
||||
#[test]
|
||||
fn template_rejects_empty_name() {
|
||||
let err = AgentTemplate::new(template_id(), "", MarkdownDoc::new(""), profile_id()).unwrap_err();
|
||||
let err =
|
||||
AgentTemplate::new(template_id(), "", MarkdownDoc::new(""), profile_id()).unwrap_err();
|
||||
assert!(matches!(err, DomainError::EmptyField { .. }));
|
||||
}
|
||||
|
||||
@ -345,9 +355,16 @@ fn agent_id(n: u128) -> domain::AgentId {
|
||||
|
||||
#[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();
|
||||
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.
|
||||
@ -366,9 +383,16 @@ fn manifest_entry_synchronized_requires_template_metadata() {
|
||||
|
||||
#[test]
|
||||
fn manifest_entry_rejects_empty_name() {
|
||||
let err =
|
||||
ManifestEntry::new(agent_id(1), " ", "agents/a.md", profile_id(), None, false, None)
|
||||
.unwrap_err();
|
||||
let err = ManifestEntry::new(
|
||||
agent_id(1),
|
||||
" ",
|
||||
"agents/a.md",
|
||||
profile_id(),
|
||||
None,
|
||||
false,
|
||||
None,
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, DomainError::EmptyField { .. }));
|
||||
}
|
||||
|
||||
@ -416,24 +440,52 @@ fn manifest_entry_agent_roundtrip() {
|
||||
|
||||
#[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 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();
|
||||
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());
|
||||
}
|
||||
|
||||
@ -465,7 +517,12 @@ fn skill_rejects_empty_name() {
|
||||
SkillScope::Project,
|
||||
)
|
||||
.unwrap_err();
|
||||
assert_eq!(err, DomainError::EmptyField { field: "skill.name" });
|
||||
assert_eq!(
|
||||
err,
|
||||
DomainError::EmptyField {
|
||||
field: "skill.name"
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user