mod common;
use common::{LedgerDir, MINIMAL, qctl, stderr, stdout};
use indoc::indoc;
#[test]
fn init_writes_ledger_without_schema_copy() {
let dir = LedgerDir::empty();
let output = qctl(&["init", "-p", "omx", "-f", dir.path.to_str().unwrap()]);
assert!(output.status.success(), "{}", stderr(&output));
let body = dir.read();
assert!(body.contains("prefix: OMX"));
assert!(body.contains("horizon: []"));
assert!(body.contains("$schema="));
assert!(!dir.parent().join("tasks.schema.json").exists());
}
#[test]
fn init_refuses_overwrite_without_force() {
let dir = LedgerDir::empty();
dir.write(MINIMAL);
let output = qctl(&["init", "-p", "QCTL", "-f", dir.path.to_str().unwrap()]);
assert!(!output.status.success());
assert!(stderr(&output).contains("already exists"));
}
#[test]
fn check_accepts_own_repo_shape() {
let dir = LedgerDir::empty();
dir.write(MINIMAL);
let output = qctl(&["check", "-f", dir.path.to_str().unwrap(), "--no-git"]);
assert!(output.status.success(), "{}", stderr(&output));
assert!(stdout(&output).contains("ok"));
}
#[test]
fn a_schema_error_from_check_names_its_line() {
let (ok, complaint) = common::check(&common::queued(indoc! {"
- id: Q-1
title: t
scope: s
outcome: o
blocked_by: []
acceptance: [a]
"}));
assert!(!ok, "{complaint}");
assert!(
complaint.contains("tasks.yaml:5:9: /queue/0/id:"),
"{complaint}"
);
}
#[test]
fn a_value_error_from_a_verb_names_its_line() {
let dir = LedgerDir::empty();
dir.write(&MINIMAL.replace("prefix: QCTL", "prefix: q"));
let output = qctl(&["status", "-f", dir.path.to_str().unwrap()]);
assert!(!output.status.success());
assert!(
stderr(&output).contains("tasks.yaml:2:9: prefix:"),
"{}",
stderr(&output)
);
}
#[test]
fn a_duplicate_key_is_refused_on_its_line() {
let dir = LedgerDir::empty();
dir.write(&common::queued(indoc! {"
- id: QCTL-001
title: t
title: again
scope: s
outcome: o
blocked_by: []
acceptance: [a]
"}));
let output = qctl(&["status", "-f", dir.path.to_str().unwrap()]);
assert!(!output.status.success());
assert!(
stderr(&output).contains("tasks.yaml:7:"),
"{}",
stderr(&output)
);
}
#[test]
fn check_accepts_archive_notes() {
let dir = LedgerDir::empty();
dir.write(indoc! {"
schema_version: 4
prefix: QCTL
active: null
queue: []
archive:
- id: QCTL-001
title: done
scope: s
completed: 2026-08-17T09:12:00
outcome: o
evidence: [landed]
notes:
- Keep the context that would not fit in evidence.
"});
let output = qctl(&["check", "-f", dir.path.to_str().unwrap(), "--no-git"]);
assert!(output.status.success(), "{}", stderr(&output));
}
#[test]
fn show_reads_a_stamp_without_moving_it() {
let dir = LedgerDir::empty();
dir.write(indoc! {"
schema_version: 4
prefix: QCTL
style:
timezone: \"-03:00\"
active: null
queue: []
archive:
- id: QCTL-001
title: Shipped late
scope: qctl
completed: 2026-08-16T23:08:28
outcome: It shipped.
evidence: [The tag exists.]
"});
let output = qctl(&["show", "QCTL-001", "-f", dir.path.to_str().unwrap()]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(
stdout(&output).trim_end(),
"QCTL-001 Shipped late (archived 2026-08-16 23:08:28)"
);
}
#[test]
fn piped_show_without_columns_stays_within_80_columns() {
let dir = LedgerDir::empty();
dir.write(indoc! {"
schema_version: 4
prefix: QCTL
active: null
queue: []
archive:
- id: QCTL-001
title: Keep every piped line inside the fallback width even when a title runs far past what one line holds
scope: qctl
completed: 2026-08-16T23:08:28
outcome: It shipped.
evidence: [The tag exists.]
"});
let output = std::process::Command::new(env!("CARGO_BIN_EXE_qctl"))
.args(["show", "QCTL-001", "-f", dir.path.to_str().unwrap()])
.env_remove("TASKS_LEDGER")
.env_remove("COLUMNS")
.output()
.expect("spawn qctl");
assert!(output.status.success(), "{}", stderr(&output));
let out = stdout(&output);
assert!(out.lines().count() > 1, "{out}");
assert!(out.lines().all(|line| line.chars().count() <= 80), "{out}");
}
#[test]
fn archive_stamps_in_the_zone_the_ledger_declares() {
let dir = LedgerDir::empty();
dir.write(indoc! {"
schema_version: 4
prefix: QCTL
style:
timezone: \"-03:00\"
active: QCTL-001
queue:
- id: QCTL-001
title: About to close
scope: qctl
outcome: Something is true.
blocked_by: []
acceptance: [It holds.]
archive: []
"});
let archived = qctl(&[
"archive",
"QCTL-001",
"-f",
dir.path.to_str().unwrap(),
"-e",
"It shipped.",
]);
assert!(archived.status.success(), "{}", stderr(&archived));
let body = dir.read();
let stamp = body
.lines()
.find_map(|line| line.trim().strip_prefix("completed: "))
.expect("a stamp");
let written = time::PrimitiveDateTime::parse(
stamp,
time::macros::format_description!("[year]-[month]-[day]T[hour]:[minute]:[second]"),
)
.unwrap_or_else(|error| panic!("{stamp}: {error}"))
.assume_offset(time::macros::offset!(-3));
let drift = time::OffsetDateTime::now_utc() - written;
assert!(
drift.abs() < time::Duration::seconds(30),
"{stamp} read as -03:00 is {drift} from now, so it was not written in that zone"
);
}
#[test]
fn fmt_check_names_the_line_and_writes_nothing() {
let dir = LedgerDir::empty();
let untidy = indoc! {"
schema_version: 4
prefix: QCTL
active: null
queue: []
archive: []
horizon: []
"};
dir.write(untidy);
let output = qctl(&["fmt", "--check", "-f", dir.path.to_str().unwrap()]);
assert!(!output.status.success(), "accepted an untidy ledger");
assert!(
stderr(&output).contains("is not in its declared style"),
"{}",
stderr(&output)
);
assert_eq!(dir.read(), untidy, "--check wrote to the file");
let fixed = qctl(&["fmt", "-f", dir.path.to_str().unwrap()]);
assert!(fixed.status.success(), "{}", stderr(&fixed));
assert!(dir.read().ends_with("horizon: []\n"));
assert!(
qctl(&["fmt", "--check", "-f", dir.path.to_str().unwrap()])
.status
.success()
);
}
#[test]
fn fmt_moves_lists_around_a_comment_that_belongs_to_none() {
let dir = LedgerDir::empty();
let orphaned = indoc! {"
schema_version: 4
prefix: QCTL
style:
section_order: [queue, horizon, archive]
active: null
queue: []
# A note with a blank line under it, so it sits between the lists.
archive: []
horizon: []
"};
dir.write(orphaned);
let output = qctl(&["fmt", "-f", dir.path.to_str().unwrap()]);
assert!(output.status.success(), "{}", stderr(&output));
let expected = indoc! {"
schema_version: 4
prefix: QCTL
style:
section_order: [queue, horizon, archive]
active: null
queue: []
# A note with a blank line under it, so it sits between the lists.
horizon: []
archive: []
"};
assert_eq!(dir.read(), expected);
}
#[test]
fn check_rejects_unknown_field() {
let dir = LedgerDir::empty();
dir.write(indoc! {"
schema_version: 4
prefix: QCTL
active: null
queue: []
archive: []
nope: true
"});
let output = qctl(&["check", "-f", dir.path.to_str().unwrap(), "--no-git"]);
assert!(!output.status.success());
}
#[test]
fn add_start_archive_round_trip() {
let dir = LedgerDir::empty();
assert!(
qctl(&["init", "-p", "QCTL", "-f", dir.path.to_str().unwrap()])
.status
.success()
);
let add = qctl(&[
"add",
"-f",
dir.path.to_str().unwrap(),
"-t",
"First",
"-s",
"qctl",
"-o",
"done",
"-a",
"shipped",
]);
assert!(add.status.success(), "{}", stderr(&add));
assert_eq!(stdout(&add).trim(), "QCTL-001");
let start = qctl(&["start", "QCTL-001", "-f", dir.path.to_str().unwrap()]);
assert!(start.status.success(), "{}", stderr(&start));
let check = qctl(&["check", "-f", dir.path.to_str().unwrap(), "--no-git"]);
assert!(check.status.success(), "{}", stderr(&check));
let archive = qctl(&[
"archive",
"QCTL-001",
"-f",
dir.path.to_str().unwrap(),
"-e",
"landed",
]);
assert!(archive.status.success(), "{}", stderr(&archive));
let after = qctl(&["check", "-f", dir.path.to_str().unwrap(), "--no-git"]);
assert!(after.status.success(), "{}", stderr(&after));
let body = dir.read();
assert!(body.contains("disposition: completed"));
assert!(body.contains("QCTL-001"));
}
#[test]
fn add_writes_notes_blockers_plan_and_links() {
let dir = LedgerDir::empty();
assert!(
qctl(&["init", "-p", "QCTL", "-f", dir.path.to_str().unwrap()])
.status
.success()
);
let first = qctl(&[
"add",
"-f",
dir.path.to_str().unwrap(),
"-t",
"First",
"-s",
"qctl",
"-o",
"done",
"-a",
"shipped",
]);
assert!(first.status.success(), "{}", stderr(&first));
dir.plant("docs/plan.md", "# Plan\n");
let second = qctl(&[
"add",
"-f",
dir.path.to_str().unwrap(),
"-t",
"Second",
"-s",
"qctl",
"-o",
"done",
"-a",
"shipped",
"--note",
"Why this exists.",
"--blocked-by",
"QCTL-001",
"--plan",
"docs/plan.md",
"--link",
"https://example.com/x",
"--after",
"QCTL-001",
]);
assert!(second.status.success(), "{}", stderr(&second));
let body = dir.read();
assert!(
body.contains("notes:") && body.contains("Why this exists."),
"{body}"
);
assert!(
body.contains("blocked_by:\n - QCTL-001") || body.contains("blocked_by: [QCTL-001]"),
"{body}"
);
assert!(body.contains("plan: docs/plan.md"), "{body}");
assert!(body.contains("https://example.com/x"), "{body}");
let check = qctl(&["check", "-f", dir.path.to_str().unwrap(), "--no-git"]);
assert!(check.status.success(), "{}", stderr(&check));
}
#[test]
fn add_refuses_a_blocker_that_would_not_sit_earlier() {
let dir = LedgerDir::empty();
dir.write(indoc! {"
schema_version: 4
prefix: QCTL
active: QCTL-001
queue:
- id: QCTL-001
title: First
scope: s
outcome: o
blocked_by: []
acceptance: [It holds.]
- id: QCTL-002
title: Second
scope: s
outcome: o
blocked_by: []
acceptance: [It holds.]
archive: []
horizon: []
"});
let output = qctl(&[
"add",
"-f",
dir.path.to_str().unwrap(),
"-t",
"Third",
"-s",
"s",
"-o",
"o",
"-a",
"a",
"--before",
"QCTL-002",
"--blocked-by",
"QCTL-002",
]);
assert!(!output.status.success());
assert!(
stderr(&output).contains("is not earlier"),
"{}",
stderr(&output)
);
}
#[test]
fn add_refuses_to_become_queue_head_while_active_is_set() {
let dir = LedgerDir::empty();
dir.write(indoc! {"
schema_version: 4
prefix: QCTL
active: QCTL-001
queue:
- id: QCTL-001
title: First
scope: s
outcome: o
blocked_by: []
acceptance: [It holds.]
archive: []
horizon: []
"});
let output = qctl(&[
"add",
"-f",
dir.path.to_str().unwrap(),
"-t",
"Ahead",
"-s",
"s",
"-o",
"o",
"-a",
"a",
"--before",
"QCTL-001",
]);
assert!(!output.status.success());
assert!(stderr(&output).contains("queue[0]"), "{}", stderr(&output));
}
#[test]
fn promote_refuses_a_queued_id() {
let dir = LedgerDir::empty();
dir.write(indoc! {"
schema_version: 4
prefix: QCTL
active: QCTL-001
queue:
- id: QCTL-001
title: First
scope: s
outcome: o
blocked_by: []
acceptance: [It holds.]
archive: []
horizon: []
"});
let output = qctl(&[
"promote",
"QCTL-001",
"-f",
dir.path.to_str().unwrap(),
"-a",
"a",
]);
assert!(!output.status.success());
assert!(
stderr(&output).contains("not on the horizon"),
"{}",
stderr(&output)
);
}
#[test]
fn promote_keeps_active_on_the_queue_head() {
let dir = LedgerDir::empty();
dir.write(indoc! {"
schema_version: 4
prefix: QCTL
active: QCTL-001
queue:
- id: QCTL-001
title: First
scope: s
outcome: o
blocked_by: []
acceptance: [It holds.]
archive: []
horizon:
- id: QCTL-002
title: Parked
scope: s
outcome: o
kind: research
open: missing
"});
let output = qctl(&[
"promote",
"QCTL-002",
"-f",
dir.path.to_str().unwrap(),
"-a",
"It holds.",
]);
assert!(output.status.success(), "{}", stderr(&output));
let body = dir.read();
assert!(body.contains("active: QCTL-001"), "{body}");
assert!(!body.contains("kind: research"), "{body}");
let check = qctl(&["check", "-f", dir.path.to_str().unwrap(), "--no-git"]);
assert!(check.status.success(), "{}", stderr(&check));
}
#[test]
fn start_refuses_blocked_or_horizon() {
let dir = LedgerDir::empty();
dir.write(indoc! {"
schema_version: 4
prefix: QCTL
active: null
queue:
- id: QCTL-002
title: blocked
scope: s
outcome: o
blocked_by: [QCTL-001]
acceptance: [a]
- id: QCTL-001
title: first
scope: s
outcome: o
blocked_by: []
acceptance: [a]
archive: []
horizon:
- id: QCTL-009
title: later
scope: s
outcome: o
kind: research
open: unknown
"});
let blocked = qctl(&["start", "QCTL-002", "-f", dir.path.to_str().unwrap()]);
assert!(!blocked.status.success());
assert!(stderr(&blocked).contains("blocked"));
let horizon = qctl(&["start", "QCTL-009", "-f", dir.path.to_str().unwrap()]);
assert!(!horizon.status.success());
assert!(stderr(&horizon).contains("not queued"));
}
#[test]
fn instructions_prints_the_installed_contract_exactly() {
let output = qctl(&["instructions"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(output.stderr, &[] as &[u8]);
let expected =
std::fs::read(std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src/instructions.md"))
.unwrap();
assert_eq!(output.stdout, expected);
}
#[test]
fn bundled_skill_names_the_package_version() {
let skill = std::fs::read_to_string(
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("skills/qctl/SKILL.md"),
)
.unwrap();
let version = skill
.lines()
.find_map(|line| line.strip_prefix("version: "))
.unwrap_or_else(|| panic!("skill has no version:\n{skill}"));
assert_eq!(version, env!("CARGO_PKG_VERSION"));
}
#[test]
fn status_lists_horizon() {
let dir = LedgerDir::empty();
dir.write(indoc! {"
schema_version: 4
prefix: QCTL
active: QCTL-001
queue:
- id: QCTL-001
title: now
scope: s
outcome: o
blocked_by: []
acceptance: [a]
archive: []
horizon:
- id: QCTL-008
title: later
scope: s
outcome: o
kind: evaluation
open: wait
"});
let output = qctl(&["status", "-f", dir.path.to_str().unwrap()]);
assert!(output.status.success(), "{}", stderr(&output));
let text = stdout(&output);
assert!(text.contains("QCTL-001"));
assert!(text.contains("QCTL-008"));
assert!(text.contains("evaluation"));
}
#[test]
fn park_demotes_a_queued_row_and_writes_a_missing_horizon_key() {
let dir = LedgerDir::empty();
dir.write(indoc! {"
schema_version: 4
prefix: QCTL
active: QCTL-001
queue:
- id: QCTL-001
title: later
scope: s
outcome: o
blocked_by: []
acceptance: [It holds.]
archive: []
"});
let output = qctl(&[
"park",
"QCTL-001",
"-f",
dir.path.to_str().unwrap(),
"-k",
"research",
"-O",
"wait",
]);
assert!(output.status.success(), "{}", stderr(&output));
let body = dir.read();
assert!(body.contains("horizon:"), "{body}");
assert!(body.contains("kind: research"), "{body}");
assert!(body.contains("active: null"), "{body}");
assert!(!body.contains("acceptance:"), "{body}");
}
#[test]
fn add_refuses_a_plan_that_is_not_a_file() {
let dir = LedgerDir::empty();
dir.write(MINIMAL);
let output = qctl(&[
"add",
"-f",
dir.path.to_str().unwrap(),
"-t",
"t",
"-s",
"s",
"-o",
"o",
"-a",
"a",
"--plan",
"docs/nope.md",
]);
assert!(!output.status.success());
assert!(
stderr(&output).contains("missing plan docs/nope.md"),
"{}",
stderr(&output)
);
assert_eq!(dir.read(), MINIMAL);
}
#[test]
fn add_refuses_an_empty_plan() {
let dir = LedgerDir::empty();
dir.write(MINIMAL);
let output = qctl(&[
"add",
"-f",
dir.path.to_str().unwrap(),
"-t",
"t",
"-s",
"s",
"-o",
"o",
"-a",
"a",
"--plan",
"",
]);
assert!(!output.status.success());
assert!(
stderr(&output).contains("--plan needs a path"),
"{}",
stderr(&output)
);
assert_eq!(dir.read(), MINIMAL);
}
#[test]
fn add_refuses_a_plan_outside_the_repository() {
let dir = LedgerDir::empty();
dir.write(MINIMAL);
let output = qctl(&[
"add",
"-f",
dir.path.to_str().unwrap(),
"-t",
"t",
"-s",
"s",
"-o",
"o",
"-a",
"a",
"--plan",
"../elsewhere.md",
]);
assert!(!output.status.success());
assert!(
stderr(&output).contains("must stay inside the repository"),
"{}",
stderr(&output)
);
assert_eq!(dir.read(), MINIMAL);
}
#[test]
fn add_refuses_a_plan_that_is_not_markdown() {
let dir = LedgerDir::empty();
dir.write(MINIMAL);
dir.plant("docs/plan.txt", "not markdown\n");
let output = qctl(&[
"add",
"-f",
dir.path.to_str().unwrap(),
"-t",
"t",
"-s",
"s",
"-o",
"o",
"-a",
"a",
"--plan",
"docs/plan.txt",
]);
assert!(!output.status.success());
assert!(
stderr(&output).contains("must be a markdown document"),
"{}",
stderr(&output)
);
assert_eq!(dir.read(), MINIMAL);
}
#[test]
fn add_accepts_a_plan_whose_name_contains_dotdot() {
let dir = LedgerDir::empty();
dir.write(MINIMAL);
dir.plant("docs/foo..bar.md", "# Plan\n");
let output = qctl(&[
"add",
"-f",
dir.path.to_str().unwrap(),
"-t",
"t",
"-s",
"s",
"-o",
"o",
"-a",
"a",
"--plan",
"docs/foo..bar.md",
]);
assert!(output.status.success(), "{}", stderr(&output));
let check = qctl(&["check", "-f", dir.path.to_str().unwrap(), "--no-git"]);
assert!(check.status.success(), "{}", stderr(&check));
assert!(dir.read().contains("plan: docs/foo..bar.md"));
}
#[test]
fn add_to_an_empty_queue_does_not_invent_before() {
let dir = LedgerDir::empty();
dir.write(indoc! {"
schema_version: 4
prefix: QCTL
active: QCTL-099
queue: []
archive: []
horizon: []
"});
let output = qctl(&[
"add",
"-f",
dir.path.to_str().unwrap(),
"-t",
"t",
"-s",
"s",
"-o",
"o",
"-a",
"a",
]);
assert!(output.status.success(), "{}", stderr(&output));
assert!(!stderr(&output).contains("--before"), "{}", stderr(&output));
assert!(dir.read().contains("QCTL-001"));
}
#[test]
fn status_and_check_share_typed_json_output() {
let dir = LedgerDir::empty();
dir.write(indoc! {"
schema_version: 4
prefix: QCTL
active: QCTL-001
queue:
- id: QCTL-001
title: now
scope: qctl
outcome: done
blocked_by: []
acceptance: [shipped]
archive: []
horizon:
- id: QCTL-002
title: later
scope: qctl
outcome: mapped
kind: research
open: missing fact
"});
let path = dir.path.to_str().unwrap();
let status = qctl(&["status", "-f", path, "--format", "json"]);
assert!(status.status.success(), "{}", stderr(&status));
assert_eq!(status.stderr, &[] as &[u8]);
assert!(!status.stdout.contains(&b'\x1b'));
let status_json: serde_json::Value = serde_json::from_slice(&status.stdout).unwrap();
assert_eq!(status_json["type"], "status");
assert_eq!(status_json["ledger"]["active"], "QCTL-001");
assert_eq!(status_json["ledger"]["queue"][0]["title"], "now");
assert_eq!(status_json["ledger"]["horizon"][0]["kind"], "research");
let check = qctl(&["check", "-f", path, "--no-git", "--format", "json"]);
assert!(check.status.success(), "{}", stderr(&check));
assert_eq!(check.stderr, &[] as &[u8]);
let check_json: serde_json::Value = serde_json::from_slice(&check.stdout).unwrap();
assert_eq!(check_json["type"], "check");
assert_eq!(check_json["problems"], serde_json::json!([]));
}
#[test]
fn failed_check_is_a_json_problem_array_on_stdout() {
let dir = LedgerDir::empty();
dir.write(indoc! {"
schema_version: 4
prefix: QCTL
active: null
queue: []
archive: []
nope: true
"});
let output = qctl(&[
"check",
"-f",
dir.path.to_str().unwrap(),
"--no-git",
"--format",
"json",
]);
assert!(!output.status.success());
assert_eq!(output.stderr, &[] as &[u8]);
assert!(!output.stdout.contains(&b'\x1b'));
let report: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(report["type"], "check");
let problems = report["problems"].as_array().unwrap();
assert!(!problems.is_empty());
assert!(
problems
.iter()
.any(|problem| problem.as_str().unwrap().contains("nope"))
);
}
#[test]
fn quiet_suppresses_only_successful_human_output() {
let dir = LedgerDir::empty();
dir.write(MINIMAL);
let path = dir.path.to_str().unwrap();
let quiet = qctl(&["status", "-f", path, "--quiet"]);
assert!(quiet.status.success(), "{}", stderr(&quiet));
assert_eq!(quiet.stdout, &[] as &[u8]);
assert_eq!(quiet.stderr, &[] as &[u8]);
let json = qctl(&["status", "-f", path, "--quiet", "--format", "json"]);
assert!(json.status.success(), "{}", stderr(&json));
assert_eq!(json.stderr, &[] as &[u8]);
assert_eq!(
serde_json::from_slice::<serde_json::Value>(&json.stdout).unwrap()["type"],
"status"
);
let failed = qctl(&["show", "QCTL-999", "-f", path, "--quiet"]);
assert!(!failed.status.success());
assert_eq!(failed.stdout, &[] as &[u8]);
assert!(stderr(&failed).contains("no task QCTL-999"));
}
#[test]
fn colorless_flags_render_the_same_document() {
let dir = LedgerDir::empty();
dir.write(MINIMAL);
let path = dir.path.to_str().unwrap();
let color = qctl(&["status", "-f", path, "--color", "never"]);
let no_color = qctl(&["status", "-f", path, "--no-color"]);
assert!(color.status.success(), "{}", stderr(&color));
assert!(no_color.status.success(), "{}", stderr(&no_color));
assert_eq!(color.stderr, &[] as &[u8]);
assert_eq!(no_color.stderr, &[] as &[u8]);
assert_eq!(color.stdout, no_color.stdout);
assert!(!color.stdout.contains(&b'\x1b'));
}
#[test]
fn every_long_option_has_a_short() {
ctl_core::Surface::new::<qctl::cli::Cli>("qctl")
.require_shorts(["--format", "--color", "--no-color"])
.expect("every other long option has an operator-visible short");
}
#[test]
fn notes_flag_is_note_not_notes() {
let dir = LedgerDir::empty();
dir.write(MINIMAL);
let rejected = qctl(&[
"add",
"-f",
dir.path.to_str().unwrap(),
"-t",
"t",
"-s",
"s",
"-o",
"o",
"-a",
"a",
"--notes",
"old",
]);
assert!(!rejected.status.success());
}
#[test]
fn park_refuses_when_a_queued_row_still_names_it() {
let dir = LedgerDir::empty();
dir.write(indoc! {"
schema_version: 4
prefix: QCTL
active: QCTL-001
queue:
- id: QCTL-001
title: First
scope: s
outcome: o
blocked_by: []
acceptance: [It holds.]
- id: QCTL-002
title: Second
scope: s
outcome: o
blocked_by: [QCTL-001]
acceptance: [It holds.]
archive: []
horizon: []
"});
let output = qctl(&[
"park",
"QCTL-001",
"-f",
dir.path.to_str().unwrap(),
"-k",
"research",
"-O",
"wait",
]);
assert!(!output.status.success());
assert!(
stderr(&output).contains("still blocks"),
"{}",
stderr(&output)
);
}