diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..569ce8d
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,32 @@
+name: CI
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+
+jobs:
+ ci:
+ name: Check, test, lint
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: dtolnay/rust-toolchain@stable
+ with:
+ components: rustfmt, clippy
+
+ - uses: Swatinem/rust-cache@v2
+
+ - name: Format
+ run: cargo fmt --check
+
+ - name: Build
+ run: cargo build
+
+ - name: Clippy
+ run: cargo clippy -- -D warnings
+
+ - name: Test
+ run: cargo test
diff --git a/p/src/commands/ls.rs b/p/src/commands/ls.rs
index 3bf1ebb..a851287 100644
--- a/p/src/commands/ls.rs
+++ b/p/src/commands/ls.rs
@@ -44,18 +44,15 @@ pub fn execute() -> Result<()> {
let now = chrono::Utc::now().timestamp();
for &i in indices {
let id = jobs[i].id.clone();
- if let Some(maybe_ec) = results.get(&id) {
- if let Some(ec) = maybe_ec {
- jobs[i].status = if *ec == 0 {
- JobStatus::Done
- } else {
- JobStatus::Failed
- };
- jobs[i].exit_code = Some(*ec);
- jobs[i].finished_at = Some(now);
- db::save(&jobs[i])?;
- }
- // None means still running — no update needed.
+ if let Some(Some(ec)) = results.get(&id) {
+ jobs[i].status = if *ec == 0 {
+ JobStatus::Done
+ } else {
+ JobStatus::Failed
+ };
+ jobs[i].exit_code = Some(*ec);
+ jobs[i].finished_at = Some(now);
+ db::save(&jobs[i])?;
}
}
}
diff --git a/p/src/config.rs b/p/src/config.rs
index ba9e4c4..4e7f560 100644
--- a/p/src/config.rs
+++ b/p/src/config.rs
@@ -72,3 +72,68 @@ impl Config {
}
}
}
+
+// ── Tests ─────────────────────────────────────────────────────────────────────
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ fn two_worker_config() -> Config {
+ Config {
+ default_worker: Some("beefy".to_string()),
+ workers: vec![
+ WorkerConfig {
+ name: "beefy".to_string(),
+ connection: "user@192.168.1.50".to_string(),
+ },
+ WorkerConfig {
+ name: "cloud".to_string(),
+ connection: "user@cloud.example.com".to_string(),
+ },
+ ],
+ }
+ }
+
+ #[test]
+ fn get_worker_found() {
+ assert!(two_worker_config().get_worker("beefy").is_some());
+ }
+
+ #[test]
+ fn get_worker_not_found() {
+ assert!(two_worker_config().get_worker("unknown").is_none());
+ }
+
+ #[test]
+ fn default_worker_returns_correct_name() {
+ let cfg = two_worker_config();
+ assert_eq!(cfg.default_worker().unwrap().name, "beefy");
+ }
+
+ #[test]
+ fn resolve_worker_by_name() {
+ let cfg = two_worker_config();
+ assert_eq!(cfg.resolve_worker(Some("cloud")).unwrap().name, "cloud");
+ }
+
+ #[test]
+ fn resolve_worker_falls_back_to_default() {
+ let cfg = two_worker_config();
+ assert_eq!(cfg.resolve_worker(None).unwrap().name, "beefy");
+ }
+
+ #[test]
+ fn resolve_worker_unknown_name_errors() {
+ assert!(two_worker_config().resolve_worker(Some("unknown")).is_err());
+ }
+
+ #[test]
+ fn resolve_worker_no_default_errors() {
+ let cfg = Config {
+ default_worker: None,
+ workers: vec![],
+ };
+ assert!(cfg.resolve_worker(None).is_err());
+ }
+}
diff --git a/p/src/db.rs b/p/src/db.rs
index 5f59a85..c243685 100644
--- a/p/src/db.rs
+++ b/p/src/db.rs
@@ -31,19 +31,6 @@ pub fn save(job: &Job) -> Result<()> {
.with_context(|| format!("failed to write job {}", job.id))
}
-/// Load a job by exact UUID.
-pub fn load(id: &str) -> Result