mirror of
https://github.com/instructkr/claw-code.git
synced 2026-04-05 23:54:50 +08:00
Ultraclaw mode results from 10 parallel opencode sessions: - PARITY.md: Updated both copies with all 9 landed lanes, commit hashes, line counts, and test counts. All checklist items marked complete. - MCP bridge: McpToolRegistry.call_tool now wired to real McpServerManager via async JSON-RPC (discover_tools -> tools/call -> shutdown) - Registry tests: Added coverage for TaskRegistry, TeamRegistry, CronRegistry, PermissionEnforcer, LspRegistry (branch-focused tests) - Permissions refactor: Simplified authorize_with_context, extracted helpers, added characterization tests (185 runtime tests pass) - AI slop cleanup: Removed redundant comments, unused_self suppressions, tightened unreachable branches - CLI fixes: Minor adjustments in main.rs and hooks.rs All 363+ tests pass. Workspace compiles clean.
509 lines
15 KiB
Rust
509 lines
15 KiB
Rust
//! In-memory registries for Team and Cron lifecycle management.
|
|
//!
|
|
//! Provides TeamCreate/Delete and CronCreate/Delete/List runtime backing
|
|
//! to replace the stub implementations in the tools crate.
|
|
|
|
use std::collections::HashMap;
|
|
use std::sync::{Arc, Mutex};
|
|
use std::time::{SystemTime, UNIX_EPOCH};
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
fn now_secs() -> u64 {
|
|
SystemTime::now()
|
|
.duration_since(UNIX_EPOCH)
|
|
.unwrap_or_default()
|
|
.as_secs()
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct Team {
|
|
pub team_id: String,
|
|
pub name: String,
|
|
pub task_ids: Vec<String>,
|
|
pub status: TeamStatus,
|
|
pub created_at: u64,
|
|
pub updated_at: u64,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum TeamStatus {
|
|
Created,
|
|
Running,
|
|
Completed,
|
|
Deleted,
|
|
}
|
|
|
|
impl std::fmt::Display for TeamStatus {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
match self {
|
|
Self::Created => write!(f, "created"),
|
|
Self::Running => write!(f, "running"),
|
|
Self::Completed => write!(f, "completed"),
|
|
Self::Deleted => write!(f, "deleted"),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Default)]
|
|
pub struct TeamRegistry {
|
|
inner: Arc<Mutex<TeamInner>>,
|
|
}
|
|
|
|
#[derive(Debug, Default)]
|
|
struct TeamInner {
|
|
teams: HashMap<String, Team>,
|
|
counter: u64,
|
|
}
|
|
|
|
impl TeamRegistry {
|
|
#[must_use]
|
|
pub fn new() -> Self {
|
|
Self::default()
|
|
}
|
|
|
|
pub fn create(&self, name: &str, task_ids: Vec<String>) -> Team {
|
|
let mut inner = self.inner.lock().expect("team registry lock poisoned");
|
|
inner.counter += 1;
|
|
let ts = now_secs();
|
|
let team_id = format!("team_{:08x}_{}", ts, inner.counter);
|
|
let team = Team {
|
|
team_id: team_id.clone(),
|
|
name: name.to_owned(),
|
|
task_ids,
|
|
status: TeamStatus::Created,
|
|
created_at: ts,
|
|
updated_at: ts,
|
|
};
|
|
inner.teams.insert(team_id, team.clone());
|
|
team
|
|
}
|
|
|
|
pub fn get(&self, team_id: &str) -> Option<Team> {
|
|
let inner = self.inner.lock().expect("team registry lock poisoned");
|
|
inner.teams.get(team_id).cloned()
|
|
}
|
|
|
|
pub fn list(&self) -> Vec<Team> {
|
|
let inner = self.inner.lock().expect("team registry lock poisoned");
|
|
inner.teams.values().cloned().collect()
|
|
}
|
|
|
|
pub fn delete(&self, team_id: &str) -> Result<Team, String> {
|
|
let mut inner = self.inner.lock().expect("team registry lock poisoned");
|
|
let team = inner
|
|
.teams
|
|
.get_mut(team_id)
|
|
.ok_or_else(|| format!("team not found: {team_id}"))?;
|
|
team.status = TeamStatus::Deleted;
|
|
team.updated_at = now_secs();
|
|
Ok(team.clone())
|
|
}
|
|
|
|
pub fn remove(&self, team_id: &str) -> Option<Team> {
|
|
let mut inner = self.inner.lock().expect("team registry lock poisoned");
|
|
inner.teams.remove(team_id)
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn len(&self) -> usize {
|
|
let inner = self.inner.lock().expect("team registry lock poisoned");
|
|
inner.teams.len()
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn is_empty(&self) -> bool {
|
|
self.len() == 0
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct CronEntry {
|
|
pub cron_id: String,
|
|
pub schedule: String,
|
|
pub prompt: String,
|
|
pub description: Option<String>,
|
|
pub enabled: bool,
|
|
pub created_at: u64,
|
|
pub updated_at: u64,
|
|
pub last_run_at: Option<u64>,
|
|
pub run_count: u64,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Default)]
|
|
pub struct CronRegistry {
|
|
inner: Arc<Mutex<CronInner>>,
|
|
}
|
|
|
|
#[derive(Debug, Default)]
|
|
struct CronInner {
|
|
entries: HashMap<String, CronEntry>,
|
|
counter: u64,
|
|
}
|
|
|
|
impl CronRegistry {
|
|
#[must_use]
|
|
pub fn new() -> Self {
|
|
Self::default()
|
|
}
|
|
|
|
pub fn create(&self, schedule: &str, prompt: &str, description: Option<&str>) -> CronEntry {
|
|
let mut inner = self.inner.lock().expect("cron registry lock poisoned");
|
|
inner.counter += 1;
|
|
let ts = now_secs();
|
|
let cron_id = format!("cron_{:08x}_{}", ts, inner.counter);
|
|
let entry = CronEntry {
|
|
cron_id: cron_id.clone(),
|
|
schedule: schedule.to_owned(),
|
|
prompt: prompt.to_owned(),
|
|
description: description.map(str::to_owned),
|
|
enabled: true,
|
|
created_at: ts,
|
|
updated_at: ts,
|
|
last_run_at: None,
|
|
run_count: 0,
|
|
};
|
|
inner.entries.insert(cron_id, entry.clone());
|
|
entry
|
|
}
|
|
|
|
pub fn get(&self, cron_id: &str) -> Option<CronEntry> {
|
|
let inner = self.inner.lock().expect("cron registry lock poisoned");
|
|
inner.entries.get(cron_id).cloned()
|
|
}
|
|
|
|
pub fn list(&self, enabled_only: bool) -> Vec<CronEntry> {
|
|
let inner = self.inner.lock().expect("cron registry lock poisoned");
|
|
inner
|
|
.entries
|
|
.values()
|
|
.filter(|e| !enabled_only || e.enabled)
|
|
.cloned()
|
|
.collect()
|
|
}
|
|
|
|
pub fn delete(&self, cron_id: &str) -> Result<CronEntry, String> {
|
|
let mut inner = self.inner.lock().expect("cron registry lock poisoned");
|
|
inner
|
|
.entries
|
|
.remove(cron_id)
|
|
.ok_or_else(|| format!("cron not found: {cron_id}"))
|
|
}
|
|
|
|
/// Disable a cron entry without removing it.
|
|
pub fn disable(&self, cron_id: &str) -> Result<(), String> {
|
|
let mut inner = self.inner.lock().expect("cron registry lock poisoned");
|
|
let entry = inner
|
|
.entries
|
|
.get_mut(cron_id)
|
|
.ok_or_else(|| format!("cron not found: {cron_id}"))?;
|
|
entry.enabled = false;
|
|
entry.updated_at = now_secs();
|
|
Ok(())
|
|
}
|
|
|
|
/// Record a cron run.
|
|
pub fn record_run(&self, cron_id: &str) -> Result<(), String> {
|
|
let mut inner = self.inner.lock().expect("cron registry lock poisoned");
|
|
let entry = inner
|
|
.entries
|
|
.get_mut(cron_id)
|
|
.ok_or_else(|| format!("cron not found: {cron_id}"))?;
|
|
entry.last_run_at = Some(now_secs());
|
|
entry.run_count += 1;
|
|
entry.updated_at = now_secs();
|
|
Ok(())
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn len(&self) -> usize {
|
|
let inner = self.inner.lock().expect("cron registry lock poisoned");
|
|
inner.entries.len()
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn is_empty(&self) -> bool {
|
|
self.len() == 0
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
// ── Team tests ──────────────────────────────────────
|
|
|
|
#[test]
|
|
fn creates_and_retrieves_team() {
|
|
let registry = TeamRegistry::new();
|
|
let team = registry.create("Alpha Squad", vec!["task_001".into(), "task_002".into()]);
|
|
assert_eq!(team.name, "Alpha Squad");
|
|
assert_eq!(team.task_ids.len(), 2);
|
|
assert_eq!(team.status, TeamStatus::Created);
|
|
|
|
let fetched = registry.get(&team.team_id).expect("team should exist");
|
|
assert_eq!(fetched.team_id, team.team_id);
|
|
}
|
|
|
|
#[test]
|
|
fn lists_and_deletes_teams() {
|
|
let registry = TeamRegistry::new();
|
|
let t1 = registry.create("Team A", vec![]);
|
|
let t2 = registry.create("Team B", vec![]);
|
|
|
|
let all = registry.list();
|
|
assert_eq!(all.len(), 2);
|
|
|
|
let deleted = registry.delete(&t1.team_id).expect("delete should succeed");
|
|
assert_eq!(deleted.status, TeamStatus::Deleted);
|
|
|
|
// Team is still listable (soft delete)
|
|
let still_there = registry.get(&t1.team_id).unwrap();
|
|
assert_eq!(still_there.status, TeamStatus::Deleted);
|
|
|
|
// Hard remove
|
|
registry.remove(&t2.team_id);
|
|
assert_eq!(registry.len(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_missing_team_operations() {
|
|
let registry = TeamRegistry::new();
|
|
assert!(registry.delete("nonexistent").is_err());
|
|
assert!(registry.get("nonexistent").is_none());
|
|
}
|
|
|
|
// ── Cron tests ──────────────────────────────────────
|
|
|
|
#[test]
|
|
fn creates_and_retrieves_cron() {
|
|
let registry = CronRegistry::new();
|
|
let entry = registry.create("0 * * * *", "Check status", Some("hourly check"));
|
|
assert_eq!(entry.schedule, "0 * * * *");
|
|
assert_eq!(entry.prompt, "Check status");
|
|
assert!(entry.enabled);
|
|
assert_eq!(entry.run_count, 0);
|
|
assert!(entry.last_run_at.is_none());
|
|
|
|
let fetched = registry.get(&entry.cron_id).expect("cron should exist");
|
|
assert_eq!(fetched.cron_id, entry.cron_id);
|
|
}
|
|
|
|
#[test]
|
|
fn lists_with_enabled_filter() {
|
|
let registry = CronRegistry::new();
|
|
let c1 = registry.create("* * * * *", "Task 1", None);
|
|
let c2 = registry.create("0 * * * *", "Task 2", None);
|
|
registry
|
|
.disable(&c1.cron_id)
|
|
.expect("disable should succeed");
|
|
|
|
let all = registry.list(false);
|
|
assert_eq!(all.len(), 2);
|
|
|
|
let enabled_only = registry.list(true);
|
|
assert_eq!(enabled_only.len(), 1);
|
|
assert_eq!(enabled_only[0].cron_id, c2.cron_id);
|
|
}
|
|
|
|
#[test]
|
|
fn deletes_cron_entry() {
|
|
let registry = CronRegistry::new();
|
|
let entry = registry.create("* * * * *", "To delete", None);
|
|
let deleted = registry
|
|
.delete(&entry.cron_id)
|
|
.expect("delete should succeed");
|
|
assert_eq!(deleted.cron_id, entry.cron_id);
|
|
assert!(registry.get(&entry.cron_id).is_none());
|
|
assert!(registry.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn records_cron_runs() {
|
|
let registry = CronRegistry::new();
|
|
let entry = registry.create("*/5 * * * *", "Recurring", None);
|
|
registry.record_run(&entry.cron_id).unwrap();
|
|
registry.record_run(&entry.cron_id).unwrap();
|
|
|
|
let fetched = registry.get(&entry.cron_id).unwrap();
|
|
assert_eq!(fetched.run_count, 2);
|
|
assert!(fetched.last_run_at.is_some());
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_missing_cron_operations() {
|
|
let registry = CronRegistry::new();
|
|
assert!(registry.delete("nonexistent").is_err());
|
|
assert!(registry.disable("nonexistent").is_err());
|
|
assert!(registry.record_run("nonexistent").is_err());
|
|
assert!(registry.get("nonexistent").is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn team_status_display_all_variants() {
|
|
// given
|
|
let cases = [
|
|
(TeamStatus::Created, "created"),
|
|
(TeamStatus::Running, "running"),
|
|
(TeamStatus::Completed, "completed"),
|
|
(TeamStatus::Deleted, "deleted"),
|
|
];
|
|
|
|
// when
|
|
let rendered: Vec<_> = cases
|
|
.into_iter()
|
|
.map(|(status, expected)| (status.to_string(), expected))
|
|
.collect();
|
|
|
|
// then
|
|
assert_eq!(
|
|
rendered,
|
|
vec![
|
|
("created".to_string(), "created"),
|
|
("running".to_string(), "running"),
|
|
("completed".to_string(), "completed"),
|
|
("deleted".to_string(), "deleted"),
|
|
]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn new_team_registry_is_empty() {
|
|
// given
|
|
let registry = TeamRegistry::new();
|
|
|
|
// when
|
|
let teams = registry.list();
|
|
|
|
// then
|
|
assert!(registry.is_empty());
|
|
assert_eq!(registry.len(), 0);
|
|
assert!(teams.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn team_remove_nonexistent_returns_none() {
|
|
// given
|
|
let registry = TeamRegistry::new();
|
|
|
|
// when
|
|
let removed = registry.remove("missing");
|
|
|
|
// then
|
|
assert!(removed.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn team_len_transitions() {
|
|
// given
|
|
let registry = TeamRegistry::new();
|
|
|
|
// when
|
|
let alpha = registry.create("Alpha", vec![]);
|
|
let beta = registry.create("Beta", vec![]);
|
|
let after_create = registry.len();
|
|
registry.remove(&alpha.team_id);
|
|
let after_first_remove = registry.len();
|
|
registry.remove(&beta.team_id);
|
|
|
|
// then
|
|
assert_eq!(after_create, 2);
|
|
assert_eq!(after_first_remove, 1);
|
|
assert_eq!(registry.len(), 0);
|
|
assert!(registry.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn cron_list_all_disabled_returns_empty_for_enabled_only() {
|
|
// given
|
|
let registry = CronRegistry::new();
|
|
let first = registry.create("* * * * *", "Task 1", None);
|
|
let second = registry.create("0 * * * *", "Task 2", None);
|
|
registry
|
|
.disable(&first.cron_id)
|
|
.expect("disable should succeed");
|
|
registry
|
|
.disable(&second.cron_id)
|
|
.expect("disable should succeed");
|
|
|
|
// when
|
|
let enabled_only = registry.list(true);
|
|
let all_entries = registry.list(false);
|
|
|
|
// then
|
|
assert!(enabled_only.is_empty());
|
|
assert_eq!(all_entries.len(), 2);
|
|
}
|
|
|
|
#[test]
|
|
fn cron_create_without_description() {
|
|
// given
|
|
let registry = CronRegistry::new();
|
|
|
|
// when
|
|
let entry = registry.create("*/15 * * * *", "Check health", None);
|
|
|
|
// then
|
|
assert!(entry.cron_id.starts_with("cron_"));
|
|
assert_eq!(entry.description, None);
|
|
assert!(entry.enabled);
|
|
assert_eq!(entry.run_count, 0);
|
|
assert_eq!(entry.last_run_at, None);
|
|
}
|
|
|
|
#[test]
|
|
fn new_cron_registry_is_empty() {
|
|
// given
|
|
let registry = CronRegistry::new();
|
|
|
|
// when
|
|
let enabled_only = registry.list(true);
|
|
let all_entries = registry.list(false);
|
|
|
|
// then
|
|
assert!(registry.is_empty());
|
|
assert_eq!(registry.len(), 0);
|
|
assert!(enabled_only.is_empty());
|
|
assert!(all_entries.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn cron_record_run_updates_timestamp_and_counter() {
|
|
// given
|
|
let registry = CronRegistry::new();
|
|
let entry = registry.create("*/5 * * * *", "Recurring", None);
|
|
|
|
// when
|
|
registry
|
|
.record_run(&entry.cron_id)
|
|
.expect("first run should succeed");
|
|
registry
|
|
.record_run(&entry.cron_id)
|
|
.expect("second run should succeed");
|
|
let fetched = registry.get(&entry.cron_id).expect("entry should exist");
|
|
|
|
// then
|
|
assert_eq!(fetched.run_count, 2);
|
|
assert!(fetched.last_run_at.is_some());
|
|
assert!(fetched.updated_at >= entry.updated_at);
|
|
}
|
|
|
|
#[test]
|
|
fn cron_disable_updates_timestamp() {
|
|
// given
|
|
let registry = CronRegistry::new();
|
|
let entry = registry.create("0 0 * * *", "Nightly", None);
|
|
|
|
// when
|
|
registry
|
|
.disable(&entry.cron_id)
|
|
.expect("disable should succeed");
|
|
let fetched = registry.get(&entry.cron_id).expect("entry should exist");
|
|
|
|
// then
|
|
assert!(!fetched.enabled);
|
|
assert!(fetched.updated_at >= entry.updated_at);
|
|
}
|
|
}
|