feat(cli): close audit gaps — TOTP edit, history, detach, status, generator defaults
One coherent CLI completeness pass driven by the 2026-04-27 state-of-the-
project audit. All TDD; 6 new integration tests (workspace 158→164).
Stubs and dead state fixed:
- TOTP edit was an explicit stub at main.rs:925 ("delete and re-add for
now"). Now supports editing issuer, label, and rotating the secret;
rotated secrets are pushed to field_history under core:totp_secret.
- VaultSettings.generator_defaults was stored but never read by the CLI.
cmd_generate now consults it when invoked inside an initialized vault;
explicit flags override. Behavior outside a vault unchanged.
New commands:
- relicario settings generator-defaults [--random|--bip39] [--length |
--words | --symbols | --separator] — view/edit the stored generator
defaults.
- relicario history <query> [--show] [--field <name>] — view captured
field history. Values masked by default.
- relicario detach <query> <aid> — remove an individual attachment +
blob. Refuses to drop a Document item's primary attachment.
- relicario status — vault summary: root path, item counts (active /
trashed), attachment count + total bytes, registered device count,
last commit (%h %s).
Internal refactor (pure mechanical, no behavior change):
- cmd_add: 217-line match split into one build_<type>_item helper per
ItemCore variant + a 7-arm dispatcher.
- cmd_edit: same treatment — edit_login, edit_card, edit_totp, etc. The
history-tracking ones take a &mut FieldHistory alias for clarity.
Existing tests cover the refactor; the new helpers are tested through
the same integration paths.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -29,6 +29,67 @@ fn attach_list_extract_round_trip() {
|
||||
assert_eq!(std::fs::read(out_path).unwrap(), b"attached-bytes");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detach_removes_attachment_and_blob() {
|
||||
let v = TestVault::init();
|
||||
v.run(&["add", "login", "--title", "thing",
|
||||
"--username", "u", "--password", "p"]);
|
||||
|
||||
let payload_path = v.path().join("payload.txt");
|
||||
std::fs::write(&payload_path, b"attached-bytes").unwrap();
|
||||
let attach = v.run(&["attach", "thing", payload_path.to_str().unwrap()]);
|
||||
assert!(attach.status.success());
|
||||
|
||||
let list = v.run(&["attachments", "thing"]);
|
||||
let stdout = String::from_utf8(list.stdout).unwrap();
|
||||
let aid = stdout.lines()
|
||||
.find(|l| l.contains("payload.txt"))
|
||||
.and_then(|l| l.split_whitespace().next())
|
||||
.expect("aid token")
|
||||
.to_string();
|
||||
|
||||
// Detach removes the attachment from the item AND deletes the blob.
|
||||
let out = v.run(&["detach", "thing", &aid]);
|
||||
assert!(
|
||||
out.status.success(),
|
||||
"detach failed:\nstdout: {}\nstderr: {}",
|
||||
String::from_utf8_lossy(&out.stdout),
|
||||
String::from_utf8_lossy(&out.stderr),
|
||||
);
|
||||
|
||||
// Item no longer lists the attachment.
|
||||
let list2 = v.run(&["attachments", "thing"]);
|
||||
let stdout2 = String::from_utf8(list2.stdout).unwrap();
|
||||
assert!(
|
||||
!stdout2.contains("payload.txt"),
|
||||
"attachment still listed after detach: {stdout2}"
|
||||
);
|
||||
|
||||
// Encrypted blob file is gone.
|
||||
let blob_path = v.path()
|
||||
.join("attachments")
|
||||
.join(stdout.lines().nth(1).is_some().then_some("").unwrap_or(""));
|
||||
let item_attach_dir = std::fs::read_dir(v.path().join("attachments"))
|
||||
.unwrap().next().unwrap().unwrap().path();
|
||||
let blob = item_attach_dir.join(format!("{aid}.enc"));
|
||||
assert!(!blob.exists(), "blob still on disk: {}", blob.display());
|
||||
let _ = blob_path; // keep the variable to avoid an unused warning
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detach_refuses_unknown_aid() {
|
||||
let v = TestVault::init();
|
||||
v.run(&["add", "login", "--title", "thing",
|
||||
"--username", "u", "--password", "p"]);
|
||||
|
||||
let out = v.run(&["detach", "thing", "deadbeef"]);
|
||||
assert!(!out.status.success(), "expected failure: {:?}", out);
|
||||
assert!(
|
||||
String::from_utf8_lossy(&out.stderr).to_lowercase().contains("no attachment"),
|
||||
"expected 'no attachment' error in stderr"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attach_rejects_over_cap() {
|
||||
let v = TestVault::init();
|
||||
|
||||
Reference in New Issue
Block a user