feat(maestro): project discovery scanner and GET /api/projects (#54)

This commit is contained in:
Miguel Palhas
2026-08-23 20:48:59 +01:00
parent 1809f5d581
commit 4a814d5b17
6 changed files with 637 additions and 1 deletions
+79
View File
@@ -26,6 +26,17 @@ struct Harness {
}
async fn spawn(hostname: &str) -> Harness {
spawn_with(
hostname,
std::sync::Arc::new(maestro_daemon::projects::ProjectScanner::disabled()),
)
.await
}
async fn spawn_with(
hostname: &str,
projects: std::sync::Arc<maestro_daemon::projects::ProjectScanner>,
) -> Harness {
let fx = DaemonFixture::new();
let manager = fx.manager.clone();
// Built-in presets (claude/opencode/zsh + custom) so `/api/info` advertises
@@ -36,6 +47,7 @@ async fn spawn(hostname: &str) -> Harness {
hostname,
"/home/test",
presets,
projects,
));
let listener = TcpListener::bind(SocketAddr::from((Ipv4Addr::LOCALHOST, 0)))
.await
@@ -153,6 +165,73 @@ async fn recent_dirs_returns_empty_without_history() {
assert_eq!(resp["truncated"], false);
}
/// #54: `GET /api/projects` lists every git repo under the scan roots, with
/// real git checkouts — not mocks.
#[tokio::test]
async fn projects_lists_repos_under_the_scan_roots() {
let tmp = tempfile::tempdir().unwrap();
for name in ["alpha", "beta"] {
let repo = tmp.path().join(name);
std::fs::create_dir_all(&repo).unwrap();
for args in [
vec!["init", "-q"],
vec![
"-c",
"user.email=t@t",
"-c",
"user.name=t",
"commit",
"-qm",
"init",
"--allow-empty",
],
] {
let out = std::process::Command::new("git")
.arg("-C")
.arg(&repo)
.args(&args)
.output()
.unwrap();
assert!(out.status.success());
}
}
let scanner = std::sync::Arc::new(maestro_daemon::projects::ProjectScanner::new(
vec![tmp.path().to_path_buf()],
3,
tmp.path().to_path_buf(),
));
let h = spawn_with("naps-laptop", scanner).await;
let projects: Vec<maestro_protocol::Project> = reqwest::get(url(h.addr, "/api/projects"))
.await
.unwrap()
.json()
.await
.unwrap();
let mut names: Vec<&str> = projects.iter().map(|p| p.name.as_str()).collect();
names.sort();
assert_eq!(names, ["alpha", "beta"]);
for p in &projects {
assert!(!p.is_worktree);
assert_eq!(p.base_repo, None);
assert!(p.last_activity.is_some(), "fresh commit ⇒ HEAD mtime known");
}
}
#[tokio::test]
async fn projects_returns_empty_for_a_disabled_scanner() {
let h = spawn("naps-laptop").await;
let projects: Vec<maestro_protocol::Project> = reqwest::get(url(h.addr, "/api/projects"))
.await
.unwrap()
.json()
.await
.unwrap();
assert!(projects.is_empty());
}
#[tokio::test]
async fn gitroot_probes_path() {
// #26: resolves the query against the daemon home dir and walks up for a