#[cfg(test)]
mod tests {
use serial_test::serial;
use std::path::Path;
use std::process::{Command, Stdio};
use tempfile::TempDir;
fn kasl_cmd(dir: &Path) -> Command {
let mut cmd = Command::new(env!("CARGO_BIN_EXE_kasl"));
cmd.env("HOME", dir).env("LOCALAPPDATA", dir).stdin(Stdio::null());
cmd
}
fn seed_one_issue(dir: &Path) {
unsafe {
std::env::set_var("HOME", dir);
std::env::set_var("LOCALAPPDATA", dir);
}
kasl::db::jira_inbox::JiraInbox::new()
.unwrap()
.upsert_batch(&[kasl::db::jira_inbox::JiraInboxUpsert {
issue_key: "PROJ-1".to_string(),
issue_id: "1".to_string(),
summary: "Something to decide about".to_string(),
status_id: None,
status_name: String::new(),
priority: Some("Medium".to_string()),
priority_rank: 3,
sort_value: Some(5.0),
url: "https://jira.example.com/browse/PROJ-1".to_string(),
raw_updated: None,
}])
.unwrap();
}
#[serial]
#[test]
fn task_add_with_a_name_needs_no_terminal() {
let dir = TempDir::new().unwrap();
let out = kasl_cmd(dir.path())
.args(["task", "add", "--name", "Scripted task", "--completeness", "30"])
.output()
.unwrap();
assert!(out.status.success(), "task add failed: {}", String::from_utf8_lossy(&out.stderr));
let listed = kasl_cmd(dir.path()).args(["task", "list"]).output().unwrap();
let listed = String::from_utf8_lossy(&listed.stdout);
assert!(listed.contains("Scripted task"), "task missing from list:\n{listed}");
assert!(listed.contains("30%"), "completeness not applied:\n{listed}");
}
#[serial]
#[test]
fn task_add_from_a_template_needs_no_terminal() {
let dir = TempDir::new().unwrap();
kasl_cmd(dir.path())
.args([
"template",
"add",
"--name",
"review",
"--task-name",
"Code review",
"--comment",
"daily",
"--completeness",
"80",
])
.output()
.unwrap();
let out = kasl_cmd(dir.path()).args(["task", "add", "--template", "review"]).output().unwrap();
assert!(
out.status.success(),
"task add --template failed: {}{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
let listed = kasl_cmd(dir.path()).args(["task", "list"]).output().unwrap();
let listed = String::from_utf8_lossy(&listed.stdout);
assert!(
listed.contains("Code review"),
"template task missing from list:
{listed}"
);
assert!(
listed.contains("80%"),
"template completeness not applied:
{listed}"
);
}
#[serial]
#[test]
fn flags_win_over_the_template_they_are_passed_with() {
let dir = TempDir::new().unwrap();
kasl_cmd(dir.path())
.args([
"template",
"add",
"--name",
"review",
"--task-name",
"Code review",
"--comment",
"daily",
"--completeness",
"80",
])
.output()
.unwrap();
let out = kasl_cmd(dir.path())
.args(["task", "add", "--template", "review", "--name", "Review PR 318", "--completeness", "40"])
.output()
.unwrap();
assert!(out.status.success(), "task add failed: {}", String::from_utf8_lossy(&out.stderr));
let listed = kasl_cmd(dir.path()).args(["task", "list"]).output().unwrap();
let listed = String::from_utf8_lossy(&listed.stdout);
assert!(
listed.contains("Review PR 318"),
"--name did not win over the template:
{listed}"
);
assert!(
!listed.contains("Code review"),
"the template name was used despite --name:
{listed}"
);
assert!(
listed.contains("40%"),
"--completeness did not win over the template:
{listed}"
);
assert!(
listed.contains("daily"),
"template comment was lost:
{listed}"
);
}
#[serial]
#[test]
fn template_add_without_a_name_fails_instead_of_hanging() {
let dir = TempDir::new().unwrap();
let out = kasl_cmd(dir.path()).args(["template", "add"]).output().unwrap();
assert!(!out.status.success(), "expected a refusal without a name");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("--name"),
"error should name the missing flag:
{stderr}"
);
assert!(
!stderr.contains("not a terminal"),
"the raw dialoguer error leaked through:
{stderr}"
);
}
#[serial]
#[test]
fn task_add_without_a_name_fails_instead_of_hanging() {
let dir = TempDir::new().unwrap();
let out = kasl_cmd(dir.path()).args(["task", "add"]).output().unwrap();
assert!(!out.status.success(), "expected a refusal without a name");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stderr.contains("--name"), "error should name the missing flag:\n{stderr}");
}
#[serial]
#[test]
fn destructive_commands_refuse_without_yes() {
let dir = TempDir::new().unwrap();
kasl_cmd(dir.path()).args(["task", "add", "--name", "Doomed"]).output().unwrap();
let out = kasl_cmd(dir.path()).args(["task", "remove", "1"]).output().unwrap();
assert!(!out.status.success(), "expected a refusal without --yes");
let out = kasl_cmd(dir.path()).args(["task", "remove", "1", "--yes"]).output().unwrap();
assert!(out.status.success(), "task remove --yes failed: {}", String::from_utf8_lossy(&out.stderr));
}
#[serial]
#[test]
fn server_connect_refuses_before_touching_the_network() {
let dir = TempDir::new().unwrap();
let out = kasl_cmd(dir.path())
.args(["server", "connect", "--url", "http://127.0.0.1:1"])
.output()
.unwrap();
assert!(!out.status.success(), "expected a refusal with no terminal");
let combined = format!("{}{}", String::from_utf8_lossy(&out.stdout), String::from_utf8_lossy(&out.stderr));
assert!(combined.contains("terminal"), "the refusal should name the cause:\n{combined}");
assert!(
!combined.contains("cannot reach"),
"the server must not be contacted by a run that cannot finish:\n{combined}"
);
}
#[serial]
#[test]
fn server_status_and_disconnect_work_unattended() {
let dir = TempDir::new().unwrap();
for args in [vec!["server", "status"], vec!["server", "disconnect"]] {
let out = kasl_cmd(dir.path()).args(&args).output().unwrap();
assert!(
out.status.success(),
"`kasl {}` failed unattended: {}",
args.join(" "),
String::from_utf8_lossy(&out.stderr)
);
}
}
#[serial]
#[test]
fn the_queue_commands_are_quiet_when_nothing_is_owed() {
let dir = TempDir::new().unwrap();
for args in [vec!["server", "queue"], vec!["server", "flush"]] {
let out = kasl_cmd(dir.path()).args(&args).output().unwrap();
assert!(
out.status.success(),
"`kasl {}` must succeed with an empty queue: {}",
args.join(" "),
String::from_utf8_lossy(&out.stderr)
);
}
}
#[serial]
#[test]
fn read_only_commands_work_unattended() {
let dir = TempDir::new().unwrap();
for args in [
vec!["pauses", "list"],
vec!["task", "list"],
vec!["tag", "list"],
vec!["report"],
vec!["sum"],
vec!["completions", "bash"],
] {
let out = kasl_cmd(dir.path()).args(&args).output().unwrap();
assert!(
out.status.success(),
"`kasl {}` failed unattended: {}",
args.join(" "),
String::from_utf8_lossy(&out.stderr)
);
}
}
#[serial]
#[test]
fn triage_refuses_without_a_terminal_and_names_what_to_use_instead() {
let dir = TempDir::new().unwrap();
seed_one_issue(dir.path());
let out = kasl_cmd(dir.path()).args(["inbox", "triage"]).output().unwrap();
assert!(!out.status.success(), "triage with no terminal must fail rather than proceed");
let combined = format!("{}{}", String::from_utf8_lossy(&out.stdout), String::from_utf8_lossy(&out.stderr));
assert!(
combined.contains("terminal"),
"the refusal must say a terminal is needed:
{combined}"
);
assert!(
combined.contains("snooze") || combined.contains("take"),
"the refusal must name what a script should use instead:
{combined}"
);
}
#[serial]
#[test]
fn triage_rejects_an_unreadable_snooze_window_before_asking_anything() {
let dir = TempDir::new().unwrap();
let out = kasl_cmd(dir.path()).args(["inbox", "triage", "--snooze-for", "soon"]).output().unwrap();
assert!(!out.status.success(), "an unreadable window must fail");
let combined = format!("{}{}", String::from_utf8_lossy(&out.stdout), String::from_utf8_lossy(&out.stderr));
assert!(
combined.contains("window"),
"the error must name the problem:
{combined}"
);
}
#[serial]
#[test]
fn snooze_rejects_an_unreadable_window() {
let dir = TempDir::new().unwrap();
let out = kasl_cmd(dir.path()).args(["inbox", "snooze", "PROJ-1", "soon"]).output().unwrap();
assert!(!out.status.success());
let combined = format!("{}{}", String::from_utf8_lossy(&out.stdout), String::from_utf8_lossy(&out.stderr));
assert!(combined.contains("window"), "{combined}");
}
}