feat: integration test suite — 416 tests, 61% coverage

Add OpenBao and Kratos to docker-compose dev stack with bootstrap
seeding. Full integration tests hitting real services:

- Vault SDK: KV read/write/delete, re-auth on bad token, new_with_token
  constructor for dev mode
- Kratos SDK: list/get/create/disable/enable users, session listing
- Token store: PAT lifecycle with OpenBao backing, expiry handling
- Identity tools: full tool dispatch through Kratos admin API
- Gitea SDK: resolve_username, ensure_token (PAT auto-provisioning),
  list/get repos, issues, comments, branches, file content
- Devtools: tool dispatch for all gitea_* tools against live Gitea
- Archive indexer: batch flush, periodic flush task, edit/redact/reaction
  updates against OpenSearch
- Memory store: set/query/get_recent with user scoping in OpenSearch
- Room history: context retrieval by timestamp and event_id, access
  control enforcement
- Search archive: keyword search with room/sender filters, room scoping
- Code search: language filter, repo filter, branch scoping
- Breadcrumbs: symbol retrieval, empty index handling, token budget
- Bridge: full event lifecycle mapping, request ID filtering
- Evaluator: DM/mention/silence short-circuits, LLM evaluation path,
  reply-to-human suppression
- Agent registry: list/get_id, prompt reuse, prompt-change recreation
- Conversations: token tracking, multi-turn context recall, room
  isolation

Bug fixes caught by tests:
- AgentRegistry in-memory cache skipped hash comparison on prompt change
- KratosClient::set_state sent bare PUT without traits (400 error)
- find_code_session returns None on NULL conversation_id
This commit is contained in:
2026-03-24 14:34:03 +00:00
parent b3a38767e0
commit 5dc739b800
8 changed files with 3105 additions and 3 deletions

View File

@@ -59,9 +59,29 @@ impl AgentRegistry {
let current_instructions = definitions::orchestrator_instructions(system_prompt, active_agents);
let current_hash = instructions_hash(&current_instructions);
// Check in-memory cache
// Check in-memory cache — but verify instructions haven't changed
if let Some(agent) = agents.get(&agent_name) {
return Ok((agent.id.clone(), false));
// Compare stored hash in SQLite against current hash
if let Some((_id, stored_hash)) = self.store.get_agent(&agent_name) {
if stored_hash == current_hash {
return Ok((agent.id.clone(), false));
}
// Hash mismatch — prompt changed at runtime. Delete and recreate.
info!(
old_hash = stored_hash.as_str(),
new_hash = current_hash.as_str(),
"System prompt changed at runtime — recreating orchestrator agent"
);
let old_id = agent.id.clone();
agents.remove(&agent_name);
if let Err(e) = mistral.delete_agent_async(&old_id).await {
warn!("Failed to delete stale orchestrator agent: {}", e.message);
}
self.store.delete_agent(&agent_name);
} else {
// In-memory but not in SQLite (shouldn't happen) — trust cache
return Ok((agent.id.clone(), false));
}
}
// Check SQLite for persisted agent ID

File diff suppressed because it is too large Load Diff

View File

@@ -237,9 +237,16 @@ impl KratosClient {
async fn set_state(&self, email_or_id: &str, state: &str) -> Result<Identity, String> {
let id = self.resolve_id(email_or_id).await?;
// Fetch current identity first — PUT replaces the whole resource
let current = self.get_user(&id).await?;
let url = format!("{}/admin/identities/{}", self.admin_url, id);
let body = serde_json::json!({ "state": state });
let body = serde_json::json!({
"schema_id": "default",
"state": state,
"traits": current.traits,
});
let resp = self
.http
.put(&url)

View File

@@ -47,6 +47,18 @@ impl VaultClient {
}
}
/// Create a VaultClient with a pre-set token (for dev mode / testing).
/// Skips Kubernetes auth entirely.
pub fn new_with_token(url: &str, kv_mount: &str, token: &str) -> Self {
Self {
url: url.trim_end_matches('/').to_string(),
role: String::new(),
kv_mount: kv_mount.to_string(),
http: HttpClient::new(),
token: Mutex::new(Some(token.to_string())),
}
}
/// Authenticate with OpenBao via Kubernetes auth method.
/// Reads the service account JWT from the mounted token file.
async fn authenticate(&self) -> Result<String, String> {