use btctax_cli::cmd;
use btctax_core::event::EventPayload;
use btctax_core::persistence;
use btctax_core::project::FeeTreatment;
use btctax_core::LotMethod;
use btctax_store::Passphrase;
use std::path::Path;
fn pp() -> Passphrase {
Passphrase::new("pw".into())
}
fn run_config(vault: &Path, args: &[&str]) -> (i32, String) {
let bin = env!("CARGO_BIN_EXE_btctax");
let output = std::process::Command::new(bin)
.arg("--vault")
.arg(vault.to_str().expect("vault path is valid UTF-8"))
.arg("config")
.args(args)
.env("BTCTAX_PASSPHRASE", "pw")
.output()
.expect("btctax binary must execute successfully");
let stdout = String::from_utf8_lossy(&output.stdout).into_owned();
let code = output
.status
.code()
.expect("btctax process must exit normally (not via signal)");
(code, stdout)
}
fn run_config_at(vault: &Path, now: &str, args: &[&str]) -> (i32, String) {
let bin = env!("CARGO_BIN_EXE_btctax");
let output = std::process::Command::new(bin)
.arg("--vault")
.arg(vault.to_str().expect("vault path is valid UTF-8"))
.arg("config")
.args(args)
.env("BTCTAX_PASSPHRASE", "pw")
.env("BTCTAX_NOW", now)
.output()
.expect("btctax binary must execute successfully");
let stdout = String::from_utf8_lossy(&output.stdout).into_owned();
(output.status.code().unwrap(), stdout)
}
#[test]
fn config_binary_apply_all_both_flags_take_effect() {
let dir = tempfile::tempdir().unwrap();
let vault = dir.path().join("vault.pgp");
cmd::init::run(&vault, &pp(), &dir.path().join("k.asc")).unwrap();
let (code, stdout) = run_config(
&vault,
&["--set-fee-treatment", "b", "--set-pre2025-method", "lifo"],
);
assert_eq!(
code, 0,
"config --set-fee-treatment b --set-pre2025-method lifo must exit 0; stdout: {stdout}"
);
let cfg = cmd::admin::show_config(&vault, &pp()).unwrap();
assert_eq!(
cfg.fee_treatment,
FeeTreatment::TreatmentB,
"fee_treatment must be B after --set-fee-treatment b (not silently dropped by old dispatch)"
);
assert_eq!(
cfg.pre2025_method,
LotMethod::Lifo,
"pre2025_method must be Lifo after --set-pre2025-method lifo (not silently dropped)"
);
}
#[test]
fn config_binary_set_forward_method_and_fee_treatment_both_apply() {
let dir = tempfile::tempdir().unwrap();
let vault = dir.path().join("vault.pgp");
cmd::init::run(&vault, &pp(), &dir.path().join("k.asc")).unwrap();
let (code, stdout) = run_config(
&vault,
&["--set-forward-method", "hifo", "--set-fee-treatment", "b"],
);
assert_eq!(
code, 0,
"config --set-forward-method hifo --set-fee-treatment b must exit 0; stdout: {stdout}"
);
let s = btctax_cli::Session::open(&vault, &pp()).unwrap();
let events = persistence::load_all(s.conn()).unwrap();
let me = events.iter().find_map(|e| match &e.payload {
EventPayload::MethodElection(m) => Some(m.clone()),
_ => None,
});
assert!(
matches!(me, Some(ref m) if m.method == LotMethod::Hifo),
"a HIFO MethodElection must be persisted via the binary dispatch (--set-forward-method not dropped)"
);
let cfg = s.config().unwrap();
assert_eq!(
cfg.fee_treatment,
FeeTreatment::TreatmentB,
"fee_treatment must be B — co-passed --set-fee-treatment must not be silently dropped by an early return"
);
}
#[test]
fn config_binary_attest_without_set_pre2025_method_is_an_error() {
let dir = tempfile::tempdir().unwrap();
let vault = dir.path().join("vault.pgp");
cmd::init::run(&vault, &pp(), &dir.path().join("k.asc")).unwrap();
let (code, _) = run_config(&vault, &["--attest-pre2025-method"]);
assert_ne!(
code, 0,
"config --attest-pre2025-method without --set-pre2025-method must exit non-zero (Usage error)"
);
}
#[test]
fn config_set_forward_method_exchange_scoped() {
use btctax_core::event::{Acquire, BasisSource};
use btctax_core::identity::{Source, SourceRef, WalletId};
use btctax_core::persistence::append_import_batch;
use btctax_core::{EventId, LedgerEvent};
use time::macros::datetime;
use time::UtcOffset;
let dir = tempfile::tempdir().unwrap();
let vault = dir.path().join("vault.pgp");
cmd::init::run(&vault, &pp(), &dir.path().join("k.asc")).unwrap();
let acquire = LedgerEvent {
id: EventId::import(Source::Coinbase, SourceRef::new("cb-buy-1")),
utc_timestamp: datetime!(2025-02-01 00:00:00 UTC),
original_tz: UtcOffset::UTC,
wallet: Some(WalletId::Exchange {
provider: "coinbase".into(),
account: "main".into(),
}),
payload: EventPayload::Acquire(Acquire {
sat: 100_000,
usd_cost: rust_decimal_macros::dec!(50.00),
fee_usd: rust_decimal_macros::dec!(0),
basis_source: BasisSource::ExchangeProvided,
}),
};
{
let mut s = btctax_cli::Session::open(&vault, &pp()).unwrap();
append_import_batch(s.conn(), &[acquire]).unwrap();
s.save().unwrap();
}
let (code, stdout) = run_config(
&vault,
&[
"--set-forward-method",
"hifo",
"--exchange",
"exchange:coinbase:main",
],
);
assert_eq!(
code, 0,
"scoped election on a known account must exit 0; stdout: {stdout}"
);
{
let s = btctax_cli::Session::open(&vault, &pp()).unwrap();
let events = persistence::load_all(s.conn()).unwrap();
let me = events
.iter()
.find_map(|e| match &e.payload {
EventPayload::MethodElection(m) => Some(m.clone()),
_ => None,
})
.expect("a MethodElection must be persisted");
assert_eq!(me.method, LotMethod::Hifo);
assert_eq!(
me.wallet,
Some(WalletId::Exchange {
provider: "coinbase".into(),
account: "main".into()
}),
"the per-account scope must ride in the MethodElection PAYLOAD"
);
}
let (code_unknown, _) = run_config(
&vault,
&[
"--set-forward-method",
"hifo",
"--exchange",
"exchange:bogus:acct",
],
);
assert_ne!(
code_unknown, 0,
"an unknown exchange account must be rejected (non-zero exit)"
);
let (code_self, _) = run_config(
&vault,
&["--set-forward-method", "hifo", "--exchange", "self:cold"],
);
assert_ne!(
code_self, 0,
"a self:LABEL scope must be rejected (only exchange accounts are electable)"
);
let (code_orphan, _) = run_config(&vault, &["--exchange", "exchange:coinbase:main"]);
assert_ne!(
code_orphan, 0,
"--exchange without --set-forward-method must exit non-zero"
);
let s = btctax_cli::Session::open(&vault, &pp()).unwrap();
let events = persistence::load_all(s.conn()).unwrap();
let n = events
.iter()
.filter(|e| matches!(e.payload, EventPayload::MethodElection(_)))
.count();
assert_eq!(
n, 1,
"rejected scoped-election attempts must append nothing"
);
}
#[test]
fn config_shows_forward_method_standing_order() {
let dir = tempfile::tempdir().unwrap();
let vault = dir.path().join("vault.pgp");
cmd::init::run(&vault, &pp(), &dir.path().join("k.asc")).unwrap();
let (code, fresh) = run_config(&vault, &[]);
assert_eq!(code, 0, "config show exits 0; stdout: {fresh}");
assert!(
fresh.contains("forward_method: HIFO (vault-wide, in force"),
"a fresh vault names the HIFO app default (the fold's applicable_method fall-through):\n{fresh}"
);
let (sc, _) = run_config_at(
&vault,
"2025-06-15T12:00:00Z",
&[
"--set-forward-method",
"lifo",
"--effective-from",
"2025-06-15",
],
);
assert_eq!(sc, 0, "recording a standing order must exit 0");
let (_c, after) = run_config_at(&vault, "2025-06-15T12:00:00Z", &[]);
assert!(
after.contains("forward_method: LIFO (vault-wide, in force as of 2025-06-15)"),
"config resolves the governing vault-wide method:\n{after}"
);
assert!(
after.contains("1 standing order(s) recorded"),
"the recorded-orders pointer is shown:\n{after}"
);
}
#[test]
fn config_scoped_forward_method_is_not_reported_vault_wide() {
use btctax_core::event::{Acquire, BasisSource, EventPayload};
use btctax_core::identity::{Source, SourceRef, WalletId};
use btctax_core::persistence::append_import_batch;
use btctax_core::{EventId, LedgerEvent};
use time::macros::datetime;
use time::UtcOffset;
let dir = tempfile::tempdir().unwrap();
let vault = dir.path().join("vault.pgp");
cmd::init::run(&vault, &pp(), &dir.path().join("k.asc")).unwrap();
let acquire = LedgerEvent {
id: EventId::import(Source::Coinbase, SourceRef::new("cb-buy-scope")),
utc_timestamp: datetime!(2025-02-01 00:00:00 UTC),
original_tz: UtcOffset::UTC,
wallet: Some(WalletId::Exchange {
provider: "coinbase".into(),
account: "main".into(),
}),
payload: EventPayload::Acquire(Acquire {
sat: 100_000,
usd_cost: rust_decimal_macros::dec!(50.00),
fee_usd: rust_decimal_macros::dec!(0),
basis_source: BasisSource::ExchangeProvided,
}),
};
{
let mut s = btctax_cli::Session::open(&vault, &pp()).unwrap();
append_import_batch(s.conn(), &[acquire]).unwrap();
s.save().unwrap();
}
let (sc, set_out) = run_config_at(
&vault,
"2025-06-15T12:00:00Z",
&[
"--set-forward-method",
"lifo",
"--exchange",
"exchange:coinbase:main",
"--effective-from",
"2025-06-15",
],
);
assert_eq!(sc, 0, "scoped election on a known account must exit 0");
assert!(
set_out.contains("attests LIFO") && !set_out.contains("Lifo"),
"the set confirmation reads human, not Debug:\n{set_out}"
);
let (_c, out) = run_config_at(&vault, "2025-06-15T12:00:00Z", &[]);
assert!(
out.contains("forward_method: HIFO (vault-wide, in force"),
"the vault-wide method stays the HIFO default — a scoped order does not change it:\n{out}"
);
assert!(
out.contains("forward_method for exchange:coinbase:main: LIFO (per-account"),
"the scoped order is attributed to its account, not reported vault-wide:\n{out}"
);
}
#[test]
fn config_multi_global_order_resolves_by_effective_date_not_recording_order() {
let dir = tempfile::tempdir().unwrap();
let vault = dir.path().join("vault.pgp");
cmd::init::run(&vault, &pp(), &dir.path().join("k.asc")).unwrap();
let (a, _) = run_config_at(
&vault,
"2025-06-15T12:00:00Z",
&[
"--set-forward-method",
"hifo",
"--effective-from",
"2030-01-01",
],
);
let (b, _) = run_config_at(
&vault,
"2025-06-16T12:00:00Z",
&[
"--set-forward-method",
"lifo",
"--effective-from",
"2027-01-01",
],
);
assert_eq!((a, b), (0, 0), "both orders record");
let (_c, out) = run_config_at(&vault, "2031-01-01T00:00:00Z", &[]);
assert!(
out.contains("forward_method: HIFO (vault-wide, in force as of 2031-01-01)"),
"config names the engine-resolved method (later effective_from wins), not the last recorded:\n{out}"
);
assert!(
out.contains("2 standing order(s) recorded"),
"both recorded orders are disclosed (neither hidden):\n{out}"
);
}
#[test]
fn config_show_uses_human_labels_not_debug_enum_names() {
let dir = tempfile::tempdir().unwrap();
let vault = dir.path().join("vault.pgp");
cmd::init::run(&vault, &pp(), &dir.path().join("k.asc")).unwrap();
let (code, out) = run_config(&vault, &[]);
assert_eq!(code, 0, "config show exits 0; stdout: {out}");
assert!(
out.contains("fee_treatment: non-taxable, basis carries (TP8 c)"),
"fee treatment reads human:\n{out}"
);
assert!(
out.contains("pre2025_method: HIFO"),
"lot method reads human:\n{out}"
);
assert!(
!out.contains("TreatmentC") && !out.contains("Hifo") && !out.contains("Fifo"),
"no raw Debug variant names leak on screen:\n{out}"
);
}