use std::time::{Duration, Instant};
use qq_cli::core::domain::Scope;
use qq_cli::requests::ledger::{self, Begin, Outcome};
use qq_cli::storage::state::Store;
use serde_json::json;
fn scope(instance: &str, account: &str) -> Scope {
Scope {
instance_id: instance.into(),
account: account.into(),
}
}
fn deadline() -> Instant {
Instant::now() + Duration::from_secs(5)
}
#[test]
fn accepted_keys_are_global_per_state_and_do_not_store_message_bodies() {
let temp = tempfile::tempdir().unwrap();
let store = Store::new(temp.path().join("state"));
assert_eq!(
ledger::get(&store, "synthetic-key").unwrap_err().code,
"REQUEST_NOT_FOUND"
);
assert!(!store.root().exists());
let payload = json!({"conversation":"group:20001","text":"synthetic body"});
let Begin::Fresh(intent) = ledger::begin(
&store,
"synthetic-key",
"send",
&scope("one", "10001"),
&payload,
deadline(),
)
.unwrap() else {
panic!("fresh key expected")
};
assert_eq!(
ledger::get(&store, "synthetic-key").unwrap().outcome,
Outcome::Pending
);
intent
.finish(Outcome::Accepted, Some(json!({"messageId":"50001"})))
.unwrap();
let Begin::Existing(record) = ledger::begin(
&store,
"synthetic-key",
"send",
&scope("two", "10001"),
&payload,
deadline(),
)
.unwrap() else {
panic!("must not replay")
};
assert_eq!(record.outcome, Outcome::Accepted);
assert_eq!(record.scope.instance_id, "one");
for file in std::fs::read_dir(store.root().join("requests")).unwrap() {
assert!(
!std::fs::read_to_string(file.unwrap().path())
.unwrap()
.contains("synthetic body")
);
}
for (operation, account, payload) in [
("reply", "10001", payload.clone()),
("send", "10002", payload.clone()),
("send", "10001", json!({"text":"changed"})),
] {
assert_eq!(
ledger::begin(
&store,
"synthetic-key",
operation,
&scope("one", account),
&payload,
deadline()
)
.err()
.unwrap()
.code,
"REQUEST_CONFLICT"
);
}
}
#[test]
fn crash_residue_becomes_unknown_and_cannot_be_claimed_again() {
let temp = tempfile::tempdir().unwrap();
let store = Store::new(temp.path().join("state"));
let scope = scope("one", "10001");
let payload = json!({"text":"synthetic crash body"});
let Begin::Fresh(intent) =
ledger::begin(&store, "crash-key", "send", &scope, &payload, deadline()).unwrap()
else {
panic!()
};
drop(intent); for _ in 0..2 {
let Begin::Existing(record) =
ledger::begin(&store, "crash-key", "send", &scope, &payload, deadline()).unwrap()
else {
panic!("must not replay")
};
assert_eq!(record.outcome, Outcome::Unknown);
}
assert_eq!(
ledger::get(&store, "crash-key").unwrap().outcome,
Outcome::Unknown
);
}
#[test]
fn lock_deadline_protects_concurrent_writers_and_failed_receipt_stays_unknown() {
let temp = tempfile::tempdir().unwrap();
let store = Store::new(temp.path().join("state"));
let scope = scope("one", "10001");
let payload = json!({"text":"sample"});
let Begin::Fresh(intent) =
ledger::begin(&store, "locked-key", "send", &scope, &payload, deadline()).unwrap()
else {
panic!()
};
assert_eq!(
ledger::begin(
&store,
"locked-key",
"send",
&scope,
&payload,
Instant::now() + Duration::from_millis(30)
)
.err()
.unwrap()
.code,
"STATE_BUSY"
);
std::fs::rename(
store.root().join("requests"),
store.root().join("saved-requests"),
)
.unwrap();
std::fs::write(store.root().join("requests"), b"synthetic obstruction").unwrap();
let error = intent
.finish(Outcome::Accepted, Some(json!({"messageId":"50002"})))
.unwrap_err();
assert_eq!(error.code, "WRITE_OUTCOME_UNKNOWN");
assert_eq!(
error.outcome,
Some(qq_cli::core::contracts::write::Outcome::Unknown)
);
assert!(!error.retry_safe);
std::fs::remove_file(store.root().join("requests")).unwrap();
std::fs::rename(
store.root().join("saved-requests"),
store.root().join("requests"),
)
.unwrap();
let Begin::Existing(record) =
ledger::begin(&store, "locked-key", "send", &scope, &payload, deadline()).unwrap()
else {
panic!()
};
assert_eq!(record.outcome, Outcome::Unknown);
}