Initial release: Citrine v1.0.0
Minimalist plaintext project-based todo CLI in Rust. Commands: init, add, list, set, done, rm. Supports .citrine + .citrine.local merging, partial hash matching, status filtering, and colorized output. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
9
tests/fixtures/sample.citrine
vendored
Normal file
9
tests/fixtures/sample.citrine
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
## Backend
|
||||
[a4f9] [ ] Implement caching layer @priority:high @backend
|
||||
[b2e1] [>] Refactor auth module @assigned:cursor
|
||||
[c3d8] [-] Deploy to staging @blocked:waiting-for-credentials
|
||||
[f1a2] [x] Set up Docker config @completed:2025-02-14
|
||||
|
||||
## Frontend
|
||||
[d4b3] [ ] Build settings page
|
||||
[e5c2] [>] Fix mobile layout issues @priority:medium
|
||||
204
tests/integration.rs
Normal file
204
tests/integration.rs
Normal file
@@ -0,0 +1,204 @@
|
||||
use std::process::Command;
|
||||
use std::path::PathBuf;
|
||||
use tempfile::TempDir;
|
||||
|
||||
fn citrine_bin() -> PathBuf {
|
||||
PathBuf::from(env!("CARGO_BIN_EXE_citrine"))
|
||||
}
|
||||
|
||||
fn run_citrine(dir: &std::path::Path, args: &[&str]) -> std::process::Output {
|
||||
Command::new(citrine_bin())
|
||||
.current_dir(dir)
|
||||
.args(args)
|
||||
.output()
|
||||
.expect("Failed to execute citrine")
|
||||
}
|
||||
|
||||
fn stdout(output: &std::process::Output) -> String {
|
||||
String::from_utf8_lossy(&output.stdout).to_string()
|
||||
}
|
||||
|
||||
fn stderr(output: &std::process::Output) -> String {
|
||||
String::from_utf8_lossy(&output.stderr).to_string()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_init() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let out = run_citrine(dir.path(), &["init"]);
|
||||
assert!(out.status.success());
|
||||
assert!(stdout(&out).contains("Initialized citrine"));
|
||||
assert!(dir.path().join(".citrine").exists());
|
||||
assert!(dir.path().join(".gitignore").exists());
|
||||
let gitignore = std::fs::read_to_string(dir.path().join(".gitignore")).unwrap();
|
||||
assert!(gitignore.contains(".citrine.local"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_init_idempotent() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
run_citrine(dir.path(), &["init"]);
|
||||
let out = run_citrine(dir.path(), &["init"]);
|
||||
assert!(out.status.success());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_and_list() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
run_citrine(dir.path(), &["init"]);
|
||||
|
||||
let out = run_citrine(dir.path(), &["add", "My first task"]);
|
||||
assert!(out.status.success());
|
||||
assert!(stdout(&out).contains("Added task"));
|
||||
assert!(stdout(&out).contains("My first task"));
|
||||
|
||||
let out = run_citrine(dir.path(), &["list"]);
|
||||
assert!(out.status.success());
|
||||
assert!(stdout(&out).contains("My first task"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_local() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
run_citrine(dir.path(), &["init"]);
|
||||
|
||||
let out = run_citrine(dir.path(), &["add", "Local task", "--local"]);
|
||||
assert!(out.status.success());
|
||||
assert!(stdout(&out).contains("(local)"));
|
||||
assert!(dir.path().join(".citrine.local").exists());
|
||||
|
||||
// Should appear in list
|
||||
let out = run_citrine(dir.path(), &["list"]);
|
||||
assert!(stdout(&out).contains("Local task"));
|
||||
|
||||
// Should appear in list --local
|
||||
let out = run_citrine(dir.path(), &["list", "--local"]);
|
||||
assert!(stdout(&out).contains("Local task"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_not_initialized() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let out = run_citrine(dir.path(), &["add", "Some task"]);
|
||||
assert!(!out.status.success());
|
||||
assert_eq!(out.status.code(), Some(3));
|
||||
assert!(stderr(&out).contains("Not initialized"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_set_status() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
run_citrine(dir.path(), &["init"]);
|
||||
let out = run_citrine(dir.path(), &["add", "Test task"]);
|
||||
let hash = extract_hash(&stdout(&out));
|
||||
|
||||
let out = run_citrine(dir.path(), &["set", &hash, "progress"]);
|
||||
assert!(out.status.success());
|
||||
assert!(stdout(&out).contains("→"));
|
||||
|
||||
let out = run_citrine(dir.path(), &["list"]);
|
||||
assert!(stdout(&out).contains("[>]"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_done_adds_completed() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
run_citrine(dir.path(), &["init"]);
|
||||
let out = run_citrine(dir.path(), &["add", "Finish this"]);
|
||||
let hash = extract_hash(&stdout(&out));
|
||||
|
||||
let out = run_citrine(dir.path(), &["done", &hash]);
|
||||
assert!(out.status.success());
|
||||
assert!(stdout(&out).contains("@completed:"));
|
||||
|
||||
let content = std::fs::read_to_string(dir.path().join(".citrine")).unwrap();
|
||||
assert!(content.contains("[x]"));
|
||||
assert!(content.contains("@completed:"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rm() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
run_citrine(dir.path(), &["init"]);
|
||||
let out = run_citrine(dir.path(), &["add", "Remove me"]);
|
||||
let hash = extract_hash(&stdout(&out));
|
||||
|
||||
let out = run_citrine(dir.path(), &["rm", &hash]);
|
||||
assert!(out.status.success());
|
||||
assert!(stdout(&out).contains("Removed"));
|
||||
|
||||
let out = run_citrine(dir.path(), &["list"]);
|
||||
assert!(!stdout(&out).contains("Remove me"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_partial_hash() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
run_citrine(dir.path(), &["init"]);
|
||||
let out = run_citrine(dir.path(), &["add", "Partial hash test"]);
|
||||
let hash = extract_hash(&stdout(&out));
|
||||
let partial = &hash[..2];
|
||||
|
||||
let out = run_citrine(dir.path(), &["set", partial, "progress"]);
|
||||
assert!(out.status.success());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_status() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
run_citrine(dir.path(), &["init"]);
|
||||
let out = run_citrine(dir.path(), &["add", "Task"]);
|
||||
let hash = extract_hash(&stdout(&out));
|
||||
|
||||
let out = run_citrine(dir.path(), &["set", &hash, "invalid"]);
|
||||
assert!(!out.status.success());
|
||||
assert_eq!(out.status.code(), Some(2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hash_not_found() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
run_citrine(dir.path(), &["init"]);
|
||||
let out = run_citrine(dir.path(), &["set", "ffff", "done"]);
|
||||
assert!(!out.status.success());
|
||||
assert!(stderr(&out).contains("Task not found"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_status_filter() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
run_citrine(dir.path(), &["init"]);
|
||||
|
||||
run_citrine(dir.path(), &["add", "Todo task"]);
|
||||
let out = run_citrine(dir.path(), &["add", "Done task"]);
|
||||
let hash = extract_hash(&stdout(&out));
|
||||
run_citrine(dir.path(), &["done", &hash]);
|
||||
|
||||
let out = run_citrine(dir.path(), &["list", "--status", "todo"]);
|
||||
let s = stdout(&out);
|
||||
assert!(s.contains("Todo task"));
|
||||
assert!(!s.contains("Done task"));
|
||||
|
||||
let out = run_citrine(dir.path(), &["list", "--status", "done"]);
|
||||
let s = stdout(&out);
|
||||
assert!(!s.contains("Todo task"));
|
||||
assert!(s.contains("Done task"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_merge_main_and_local() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
run_citrine(dir.path(), &["init"]);
|
||||
run_citrine(dir.path(), &["add", "Main task"]);
|
||||
run_citrine(dir.path(), &["add", "Local task", "--local"]);
|
||||
|
||||
let out = run_citrine(dir.path(), &["list"]);
|
||||
let s = stdout(&out);
|
||||
assert!(s.contains("Main task"));
|
||||
assert!(s.contains("Local task"));
|
||||
}
|
||||
|
||||
fn extract_hash(output: &str) -> String {
|
||||
let re = regex::Regex::new(r"\[([0-9a-f]{4})\]").unwrap();
|
||||
re.captures(output).unwrap()[1].to_string()
|
||||
}
|
||||
Reference in New Issue
Block a user