use std::path::Path;
use std::time::Duration;
use crate::drive::files_api::FilesApi;
use crate::drive::lease::ledger::{
default_lock_wait_timeout, offload_short_blocking_io, LeaseBackup, LeaseLedger, LedgerLock,
};
use crate::request_log::AuditOutcome;
pub(crate) mod verdict {
pub const PENDING: &str = "pending";
pub const ALLOWED: &str = "allowed";
pub const FAILED: &str = "failed";
pub const REFUSED_NO_LEASE: &str = "refused-no-lease";
pub const REFUSED_LEASE_EXPIRED: &str = "refused-lease-expired";
pub const REFUSED_LEASE_WRONG_FILE: &str = "refused-lease-wrong-file";
pub const REFUSED_LEASE_STALE: &str = "refused-lease-stale";
}
pub(crate) struct LeaseGrant {
pub(crate) lock: LedgerLock,
pub(crate) backup: LeaseBackup,
pub(crate) token: String,
}
#[derive(Clone, Copy)]
pub(crate) struct LeasedWrite<'a> {
pub log_prefix: &'a str,
pub operation: &'a str,
pub ledger_path: &'a Path,
pub file_id: &'a str,
}
pub(crate) async fn check_and_lock_lease(
write: LeasedWrite<'_>,
lease_token: Option<&str>,
live_version: Option<&str>,
live_modified_time: Option<&str>,
) -> Result<LeaseGrant, LeaseGateRefusal> {
check_and_lock_lease_with_timeout(
write,
lease_token,
live_version,
live_modified_time,
default_lock_wait_timeout(),
)
.await
}
pub(crate) async fn check_and_lock_lease_with_timeout(
write: LeasedWrite<'_>,
lease_token: Option<&str>,
live_version: Option<&str>,
live_modified_time: Option<&str>,
max_wait: Duration,
) -> Result<LeaseGrant, LeaseGateRefusal> {
let LeasedWrite {
log_prefix,
ledger_path,
file_id,
..
} = write;
let before = |lease_id: Option<&str>, verdict: &str| {
let mut outcome = audit_outcome(write, lease_id, verdict);
outcome.version_before = live_version.map(str::to_string);
outcome.modified_time_before = live_modified_time.map(str::to_string);
outcome
};
let refuse = |lease_id: Option<&str>, verdict: &str, error: Option<String>| {
let mut outcome = before(lease_id, verdict);
outcome.error = error;
write_audit_best_effort(log_prefix, outcome);
};
let Some(token) = lease_token else {
refuse(None, verdict::REFUSED_NO_LEASE, None);
return Err(LeaseGateRefusal::NoLease);
};
let lock = match LedgerLock::acquire_waiting_with_timeout(ledger_path, max_wait).await {
Ok(lock) => lock,
Err(err) => {
refuse(Some(token), verdict::FAILED, Some(err.to_string()));
return Err(LeaseGateRefusal::Failed(err.to_string()));
}
};
offload_short_blocking_io(|| {
let ledger = match LeaseLedger::load(ledger_path) {
Ok(ledger) => ledger,
Err(err) => {
let ledger_path = ledger_path.display();
tracing::warn!(
"{log_prefix}: lease ledger at {ledger_path} could not be read ({err}); \
refusing the presented lease as expired rather than trusting an unreadable \
ledger"
);
refuse(
Some(token),
verdict::REFUSED_LEASE_EXPIRED,
Some(format!(
"lease ledger at {ledger_path} could not be read: {err}"
)),
);
return Err(LeaseGateRefusal::Expired);
}
};
let Some(record) = ledger.get(token) else {
refuse(Some(token), verdict::REFUSED_LEASE_EXPIRED, None);
return Err(LeaseGateRefusal::Expired);
};
if !record.is_live(chrono::Utc::now()) {
refuse(Some(token), verdict::REFUSED_LEASE_EXPIRED, None);
return Err(LeaseGateRefusal::Expired);
}
if record.file_id != file_id {
refuse(Some(token), verdict::REFUSED_LEASE_WRONG_FILE, None);
return Err(LeaseGateRefusal::WrongFile);
}
if live_version != Some(record.version.as_str()) {
refuse(Some(token), verdict::REFUSED_LEASE_STALE, None);
return Err(LeaseGateRefusal::Stale);
}
let intent = before(Some(token), verdict::PENDING);
if let Err(err) = crate::request_log::record_audit_event(intent) {
return Err(LeaseGateRefusal::Failed(format!(
"failed to write the write-ahead audit record: {err}"
)));
}
Ok(LeaseGrant {
lock,
backup: record.backup.clone(),
token: token.to_string(),
})
})
}
pub(crate) enum LeaseGateRefusal {
NoLease,
Expired,
WrongFile,
Stale,
Failed(String),
}
impl LeaseGateRefusal {
pub(crate) fn log_status(&self) -> &'static str {
match self {
Self::NoLease => verdict::REFUSED_NO_LEASE,
Self::Expired => verdict::REFUSED_LEASE_EXPIRED,
Self::WrongFile => verdict::REFUSED_LEASE_WRONG_FILE,
Self::Stale => verdict::REFUSED_LEASE_STALE,
Self::Failed(_) => verdict::FAILED,
}
}
pub(crate) fn describe_line(&self, id: &str, target: &str) -> Option<String> {
match self {
Self::NoLease => Some(format!(
"Refused: {target} requires a Drive write lease — run `omni-dev drive lease \
acquire {id}` and pass the printed token via `--lease`."
)),
Self::Expired => Some(format!(
"Refused: the presented lease is expired, released, or unknown to this ledger \
— run `omni-dev drive lease acquire {id}` again."
)),
Self::WrongFile => Some(format!(
"Refused: the presented lease was acquired for a different file — run \
`omni-dev drive lease acquire {id}` for this one."
)),
Self::Stale => Some(format!(
"Refused: {target} changed since the lease was acquired (or last written \
under) — re-run `omni-dev drive lease acquire {id}` to lease the current \
version."
)),
Self::Failed(_) => None,
}
}
}
pub(crate) trait FromLeaseRefusal {
fn from_no_lease() -> Self;
fn from_lease_expired() -> Self;
fn from_lease_wrong_file() -> Self;
fn from_lease_stale() -> Self;
fn from_lease_failed(detail: String) -> Self;
}
impl LeaseGateRefusal {
pub(crate) fn into_result<T: FromLeaseRefusal>(self) -> T {
match self {
Self::NoLease => T::from_no_lease(),
Self::Expired => T::from_lease_expired(),
Self::WrongFile => T::from_lease_wrong_file(),
Self::Stale => T::from_lease_stale(),
Self::Failed(detail) => T::from_lease_failed(detail),
}
}
}
pub(crate) async fn gate_leased_write(
write: LeasedWrite<'_>,
files_api: &FilesApi<'_>,
lease_token: Option<&str>,
) -> Result<LeaseGrant, LeaseGateRefusal> {
let (live_version, live_modified_time) = files_api
.get_metadata(write.file_id)
.await
.map(|fresh| (fresh.version, fresh.modified_time))
.map_err(|err| LeaseGateRefusal::Failed(err.to_string()))?;
check_and_lock_lease(
write,
lease_token,
live_version.as_deref(),
live_modified_time.as_deref(),
)
.await
}
pub(crate) async fn gate_optional_leased_write(
write: LeasedWrite<'_>,
files_api: &FilesApi<'_>,
requires_lease: bool,
lease_token: Option<&str>,
) -> Result<Option<LeaseGrant>, LeaseGateRefusal> {
if !requires_lease && lease_token.is_none() {
return Ok(None);
}
gate_leased_write(write, files_api, lease_token)
.await
.map(Some)
}
fn audit_outcome(write: LeasedWrite<'_>, lease_id: Option<&str>, verdict: &str) -> AuditOutcome {
AuditOutcome {
command: vec!["drive".to_string(), write.operation.to_string()],
integration: "drive",
file_id: write.file_id.to_string(),
lease_id: lease_id.map(str::to_string),
verdict: verdict.to_string(),
..Default::default()
}
}
fn write_audit_best_effort(log_prefix: &str, outcome: AuditOutcome) {
let verdict = outcome.verdict.clone();
if let Err(err) = offload_short_blocking_io(|| crate::request_log::record_audit_event(outcome))
{
tracing::warn!("{log_prefix}: failed to write the `{verdict}` audit record: {err}");
}
}
pub(crate) fn record_failed_leased_write(write: LeasedWrite<'_>, token: &str, error: &str) {
let mut outcome = audit_outcome(write, Some(token), verdict::FAILED);
outcome.error = Some(error.to_string());
write_audit_best_effort(write.log_prefix, outcome);
}
pub(crate) fn finish_leased_write(
write: LeasedWrite<'_>,
lock: &LedgerLock,
token: &str,
version: Option<String>,
modified_time: Option<String>,
) {
let LeasedWrite {
log_prefix,
ledger_path,
..
} = write;
offload_short_blocking_io(|| {
let mut outcome = audit_outcome(write, Some(token), verdict::ALLOWED);
outcome.version_after.clone_from(&version);
outcome.modified_time_after.clone_from(&modified_time);
write_audit_best_effort(log_prefix, outcome);
let Some(version) = version else {
tracing::debug!(
"{log_prefix}: write response carried no `version`; lease ledger not refreshed"
);
return;
};
let result = LeaseLedger::mutate(lock, ledger_path, |ledger| {
ledger.record_write(token, version, modified_time);
});
if let Err(err) = result {
tracing::debug!(
"{log_prefix}: failed to refresh lease ledger after a successful write: {err}"
);
}
});
}
pub(crate) async fn finish_leased_native_write(
write: LeasedWrite<'_>,
lock: &LedgerLock,
token: &str,
files_api: &FilesApi<'_>,
) {
let (version, modified_time) = match files_api.get_metadata(write.file_id).await {
Ok(fresh) => (fresh.version, fresh.modified_time),
Err(err) => {
let log_prefix = write.log_prefix;
tracing::debug!(
"{log_prefix}: failed to re-fetch version after a successful write; lease \
ledger not refreshed: {err}"
);
(None, None)
}
};
finish_leased_write(write, lock, token, version, modified_time);
}
pub(crate) fn conclude_leased_write<T, E>(
write: LeasedWrite<'_>,
lease_grant: &Option<LeaseGrant>,
outcome: Result<T, E>,
version_and_modified_time: impl FnOnce(&T) -> (Option<String>, Option<String>),
detail: impl FnOnce(&E) -> String,
) -> Result<T, E> {
match &outcome {
Ok(value) => {
if let Some(grant) = lease_grant {
let (version, modified_time) = version_and_modified_time(value);
finish_leased_write(write, &grant.lock, &grant.token, version, modified_time);
}
}
Err(err) => {
if let Some(grant) = lease_grant {
record_failed_leased_write(write, &grant.token, &detail(err));
}
}
}
outcome
}
pub(crate) async fn conclude_native_leased_write<T, E>(
write: LeasedWrite<'_>,
lease_grant: &Option<LeaseGrant>,
files_api: &FilesApi<'_>,
outcome: Result<T, E>,
detail: impl FnOnce(&E) -> String,
) -> Result<T, E> {
match &outcome {
Ok(_) => {
if let Some(grant) = lease_grant {
finish_leased_native_write(write, &grant.lock, &grant.token, files_api).await;
}
}
Err(err) => {
if let Some(grant) = lease_grant {
record_failed_leased_write(write, &grant.token, &detail(err));
}
}
}
outcome
}
pub(crate) fn recovery_note(backup: Option<&LeaseBackup>, is_delete_sheet: bool) -> String {
match backup {
Some(LeaseBackup::DriveCopy { file_id }) if is_delete_sheet => format!(
"this cannot be undone through omni-dev — the lease this write required backed the \
whole spreadsheet up when it was acquired (Drive copy {file_id}); run `omni-dev \
drive lease restore <TOKEN>` — it restores a single deleted sheet automatically, or \
otherwise locates the copy to restore from by hand in the Drive UI — or fall back \
to Google Drive's own version history"
),
Some(LeaseBackup::DriveCopy { file_id }) => format!(
"this cannot be undone through omni-dev — the lease this write required backed the \
whole spreadsheet up when it was acquired (Drive copy {file_id}); run `omni-dev \
drive lease restore <TOKEN>` to locate it, restore from that copy in the Drive UI, \
or fall back to Google Drive's own version history"
),
Some(LeaseBackup::Bytes { path, .. }) => format!(
"this cannot be undone through omni-dev — the lease this write required backed the \
file up when it was acquired ({}); run `omni-dev drive lease restore <TOKEN>` to \
restore it, or fall back to Google Drive's own version history",
path.display()
),
None => "this cannot be undone through omni-dev — no lease backup was taken (the \
deciding write-permission rule sets `require_lease: false`), so Google Drive's \
version history is the only recovery path"
.to_string(),
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use crate::drive::auth::{DriveCredentials, DriveGrantedScopes};
use crate::drive::client::DriveClient;
use crate::drive::lease::ledger::LeaseLedger;
use crate::test_support::AuditLogGuard;
use crate::utils::secret::Secret;
fn seed_lease(ledger_path: &Path, token: &str, file_id: &str, version: &str) {
crate::drive::test_support::seed_lease_full(
ledger_path,
token,
file_id,
version,
chrono::Duration::hours(1),
LeaseBackup::Bytes {
path: std::path::PathBuf::from("/tmp/test-backup"),
sha256: "deadbeef".to_string(),
size: 0,
},
);
}
fn leased<'a>(operation: &'a str, ledger_path: &'a Path, file_id: &'a str) -> LeasedWrite<'a> {
LeasedWrite {
log_prefix: "test",
operation,
ledger_path,
file_id,
}
}
fn test_credentials() -> DriveCredentials {
DriveCredentials {
client_id: "client-1".to_string(),
client_secret: Secret::new("secret-1"),
refresh_token: Secret::new("refresh-1"),
scope: DriveGrantedScopes::READONLY,
}
}
async fn client_with_bootstrapped_token(server: &wiremock::MockServer) -> DriveClient {
wiremock::Mock::given(wiremock::matchers::method("POST"))
.and(wiremock::matchers::path("/token"))
.respond_with(
wiremock::ResponseTemplate::new(200).set_body_json(serde_json::json!({
"access_token": "test-token",
"expires_in": 3600,
})),
)
.mount(server)
.await;
let mut client = DriveClient::new(&server.uri(), &test_credentials()).unwrap();
crate::drive::client::test_support::replace_session(
&mut client,
&test_credentials(),
&format!("{}/token", server.uri()),
);
client
}
#[tokio::test]
async fn refuses_as_expired_and_logs_the_path_when_the_ledger_is_unreadable() {
let dir = tempfile::tempdir().unwrap();
let audit = AuditLogGuard::redirect(dir.path());
let ledger_path = dir.path().join("lease-ledger.jsonl");
std::fs::create_dir(&ledger_path).unwrap();
let outcome = check_and_lock_lease(
leased("edit", &ledger_path, "file-1"),
Some("any-token"),
Some("1"),
None,
)
.await;
assert!(matches!(outcome, Err(LeaseGateRefusal::Expired)));
let records = audit.records();
assert_eq!(audit.verdicts(), [verdict::REFUSED_LEASE_EXPIRED]);
let error = records[0].error.as_deref().unwrap_or_default();
assert!(error.contains("could not be read"), "{error}");
}
#[tokio::test]
async fn native_refresh_logs_and_swallows_a_failed_refetch() {
let server = wiremock::MockServer::start().await;
let client = client_with_bootstrapped_token(&server).await;
wiremock::Mock::given(wiremock::matchers::method("GET"))
.and(wiremock::matchers::path("/drive/v3/files/file-1"))
.respond_with(wiremock::ResponseTemplate::new(500))
.mount(&server)
.await;
let files_api = FilesApi::new(&client);
let dir = tempfile::tempdir().unwrap();
let audit = AuditLogGuard::redirect(dir.path());
let ledger_path = dir.path().join("lease-ledger.jsonl");
let lock = LedgerLock::acquire(&ledger_path).unwrap();
seed_lease(&ledger_path, "tok", "file-1", "1");
finish_leased_native_write(
leased("edit", &ledger_path, "file-1"),
&lock,
"tok",
&files_api,
)
.await;
let reloaded = LeaseLedger::load(&ledger_path).unwrap();
assert_eq!(reloaded.get("tok").unwrap().version, "1");
let records = audit.records();
assert_eq!(audit.verdicts(), [verdict::ALLOWED]);
assert_eq!(records[0].context.get("version_after"), None);
}
#[tokio::test]
async fn native_finish_records_the_refetched_version_in_both_ledger_and_audit() {
let server = wiremock::MockServer::start().await;
let client = client_with_bootstrapped_token(&server).await;
wiremock::Mock::given(wiremock::matchers::method("GET"))
.and(wiremock::matchers::path("/drive/v3/files/file-1"))
.respond_with(
wiremock::ResponseTemplate::new(200).set_body_json(serde_json::json!({
"id": "file-1", "name": "Sheet", "version": "7",
"modifiedTime": "2026-09-12T00:00:00Z",
})),
)
.mount(&server)
.await;
let files_api = FilesApi::new(&client);
let dir = tempfile::tempdir().unwrap();
let audit = AuditLogGuard::redirect(dir.path());
let ledger_path = dir.path().join("lease-ledger.jsonl");
let lock = LedgerLock::acquire(&ledger_path).unwrap();
seed_lease(&ledger_path, "tok", "file-1", "1");
finish_leased_native_write(
leased("sheets-write", &ledger_path, "file-1"),
&lock,
"tok",
&files_api,
)
.await;
let reloaded = LeaseLedger::load(&ledger_path).unwrap();
assert_eq!(reloaded.get("tok").unwrap().version, "7");
let records = audit.records();
assert_eq!(audit.verdicts(), [verdict::ALLOWED]);
assert_eq!(records[0].command, ["drive", "sheets-write"]);
assert_eq!(
records[0].context.get("version_after").map(String::as_str),
Some("7")
);
assert_eq!(
records[0]
.context
.get("modified_time_after")
.map(String::as_str),
Some("2026-09-12T00:00:00Z")
);
}
#[tokio::test]
async fn an_unrequired_write_with_no_token_skips_the_gate_entirely() {
let server = wiremock::MockServer::start().await;
let client = client_with_bootstrapped_token(&server).await;
let files_api = FilesApi::new(&client);
let dir = tempfile::tempdir().unwrap();
let _audit = AuditLogGuard::redirect(dir.path());
let ledger_path = dir.path().join("lease-ledger.jsonl");
let outcome = gate_optional_leased_write(
leased("edit", &ledger_path, "file-1"),
&files_api,
false,
None,
)
.await;
assert!(matches!(outcome, Ok(None)));
}
#[tokio::test]
async fn an_unrequired_write_with_a_valid_token_is_gated_like_a_required_one() {
let server = wiremock::MockServer::start().await;
let client = client_with_bootstrapped_token(&server).await;
wiremock::Mock::given(wiremock::matchers::method("GET"))
.and(wiremock::matchers::path("/drive/v3/files/file-1"))
.respond_with(
wiremock::ResponseTemplate::new(200).set_body_json(serde_json::json!({
"id": "file-1", "name": "file-1", "version": "1",
})),
)
.mount(&server)
.await;
let files_api = FilesApi::new(&client);
let dir = tempfile::tempdir().unwrap();
let audit = AuditLogGuard::redirect(dir.path());
let ledger_path = dir.path().join("lease-ledger.jsonl");
seed_lease(&ledger_path, "tok-1", "file-1", "1");
let outcome = gate_optional_leased_write(
leased("edit", &ledger_path, "file-1"),
&files_api,
false,
Some("tok-1"),
)
.await;
assert!(matches!(outcome, Ok(Some(_))));
assert_eq!(audit.verdicts(), [verdict::PENDING]);
}
#[tokio::test]
async fn an_unrequired_write_still_refuses_a_stale_presented_token() {
let server = wiremock::MockServer::start().await;
let client = client_with_bootstrapped_token(&server).await;
wiremock::Mock::given(wiremock::matchers::method("GET"))
.and(wiremock::matchers::path("/drive/v3/files/file-1"))
.respond_with(
wiremock::ResponseTemplate::new(200).set_body_json(serde_json::json!({
"id": "file-1", "name": "file-1", "version": "1",
})),
)
.mount(&server)
.await;
let files_api = FilesApi::new(&client);
let dir = tempfile::tempdir().unwrap();
let audit = AuditLogGuard::redirect(dir.path());
let ledger_path = dir.path().join("lease-ledger.jsonl");
seed_lease(&ledger_path, "tok-1", "file-1", "0");
let outcome = gate_optional_leased_write(
leased("edit", &ledger_path, "file-1"),
&files_api,
false,
Some("tok-1"),
)
.await;
assert!(matches!(outcome, Err(LeaseGateRefusal::Stale)));
assert_eq!(audit.verdicts(), [verdict::REFUSED_LEASE_STALE]);
}
#[tokio::test]
async fn a_successful_check_writes_a_pending_intent_record() {
let dir = tempfile::tempdir().unwrap();
let audit = AuditLogGuard::redirect(dir.path());
let ledger_path = dir.path().join("lease-ledger.jsonl");
seed_lease(&ledger_path, "tok-1", "file-1", "1");
let outcome = check_and_lock_lease(
leased("edit", &ledger_path, "file-1"),
Some("tok-1"),
Some("1"),
Some("2026-09-12T00:00:00Z"),
)
.await;
assert!(outcome.is_ok());
let records = audit.records();
assert_eq!(audit.verdicts(), [verdict::PENDING], "{records:?}");
let record = &records[0];
assert_eq!(record.command, ["drive", "edit"]);
assert_eq!(
record.context.get("lease_id").map(String::as_str),
Some("tok-1")
);
assert_eq!(
record.context.get("file_id").map(String::as_str),
Some("file-1")
);
assert_eq!(
record.context.get("version_before").map(String::as_str),
Some("1")
);
assert_eq!(
record
.context
.get("modified_time_before")
.map(String::as_str),
Some("2026-09-12T00:00:00Z")
);
assert_eq!(record.error, None);
}
#[tokio::test]
async fn the_write_is_refused_when_the_intent_record_cannot_be_written() {
let dir = tempfile::tempdir().unwrap();
let _audit = AuditLogGuard::redirect(dir.path());
std::fs::create_dir(dir.path().join("audit.jsonl")).unwrap();
let ledger_path = dir.path().join("lease-ledger.jsonl");
seed_lease(&ledger_path, "tok-1", "file-1", "1");
let outcome = check_and_lock_lease(
leased("edit", &ledger_path, "file-1"),
Some("tok-1"),
Some("1"),
None,
)
.await;
let Err(LeaseGateRefusal::Failed(detail)) = outcome else {
panic!("expected Failed, got a lock/refusal instead");
};
assert!(detail.contains("write-ahead"), "{detail}");
assert!(
LedgerLock::acquire(&ledger_path).is_ok(),
"the ledger lock must not outlive a refused check"
);
}
#[tokio::test]
async fn each_refusal_writes_its_own_verdict_to_the_audit_log() {
let dir = tempfile::tempdir().unwrap();
let audit = AuditLogGuard::redirect(dir.path());
let ledger_path = dir.path().join("lease-ledger.jsonl");
seed_lease(&ledger_path, "tok-1", "file-1", "1");
assert!(matches!(
check_and_lock_lease(
leased("edit", &ledger_path, "file-1"),
None,
Some("1"),
None
)
.await,
Err(LeaseGateRefusal::NoLease)
));
assert!(matches!(
check_and_lock_lease(
leased("edit", &ledger_path, "file-1"),
Some("bogus"),
Some("1"),
None
)
.await,
Err(LeaseGateRefusal::Expired)
));
assert!(matches!(
check_and_lock_lease(
leased("edit", &ledger_path, "some-other-file"),
Some("tok-1"),
Some("1"),
None
)
.await,
Err(LeaseGateRefusal::WrongFile)
));
assert!(matches!(
check_and_lock_lease(
leased("edit", &ledger_path, "file-1"),
Some("tok-1"),
Some("2"),
None
)
.await,
Err(LeaseGateRefusal::Stale)
));
let records = audit.records();
assert_eq!(
audit.verdicts(),
[
verdict::REFUSED_NO_LEASE,
verdict::REFUSED_LEASE_EXPIRED,
verdict::REFUSED_LEASE_WRONG_FILE,
verdict::REFUSED_LEASE_STALE,
],
"{records:?}"
);
assert!(
records.iter().all(|record| record.error.is_none()),
"{records:?}"
);
assert_eq!(records[0].context.get("lease_id"), None);
assert_eq!(
records[1].context.get("lease_id").map(String::as_str),
Some("bogus")
);
assert!(records
.iter()
.all(|record| record.context.contains_key("version_before")));
}
#[test]
fn finish_leased_write_records_the_allowed_outcome_and_refreshes_the_ledger() {
let dir = tempfile::tempdir().unwrap();
let audit = AuditLogGuard::redirect(dir.path());
let ledger_path = dir.path().join("lease-ledger.jsonl");
let lock = LedgerLock::acquire(&ledger_path).unwrap();
seed_lease(&ledger_path, "tok-1", "file-1", "1");
finish_leased_write(
leased("edit", &ledger_path, "file-1"),
&lock,
"tok-1",
Some("2".to_string()),
Some("2026-09-12T00:00:00Z".to_string()),
);
let records = audit.records();
assert_eq!(audit.verdicts(), [verdict::ALLOWED], "{records:?}");
assert_eq!(
records[0].context.get("lease_id").map(String::as_str),
Some("tok-1")
);
assert_eq!(
records[0].context.get("version_after").map(String::as_str),
Some("2")
);
assert_eq!(
records[0]
.context
.get("modified_time_after")
.map(String::as_str),
Some("2026-09-12T00:00:00Z")
);
let reloaded = LeaseLedger::load(&ledger_path).unwrap();
assert_eq!(reloaded.get("tok-1").unwrap().version, "2");
}
#[test]
fn finish_leased_write_still_records_allowed_when_the_response_had_no_version() {
let dir = tempfile::tempdir().unwrap();
let audit = AuditLogGuard::redirect(dir.path());
let ledger_path = dir.path().join("lease-ledger.jsonl");
let lock = LedgerLock::acquire(&ledger_path).unwrap();
seed_lease(&ledger_path, "tok-1", "file-1", "1");
finish_leased_write(
leased("edit", &ledger_path, "file-1"),
&lock,
"tok-1",
None,
None,
);
let records = audit.records();
assert_eq!(audit.verdicts(), [verdict::ALLOWED], "{records:?}");
assert_eq!(records[0].context.get("version_after"), None);
let reloaded = LeaseLedger::load(&ledger_path).unwrap();
assert_eq!(reloaded.get("tok-1").unwrap().version, "1");
}
#[test]
fn record_failed_leased_write_carries_the_error() {
let dir = tempfile::tempdir().unwrap();
let audit = AuditLogGuard::redirect(dir.path());
let ledger_path = dir.path().join("lease-ledger.jsonl");
record_failed_leased_write(
leased("sheets-format-cells", &ledger_path, "file-1"),
"tok-1",
"HTTP 500 from Sheets",
);
let records = audit.records();
assert_eq!(audit.verdicts(), [verdict::FAILED], "{records:?}");
assert_eq!(records[0].command, ["drive", "sheets-format-cells"]);
assert_eq!(
records[0].context.get("lease_id").map(String::as_str),
Some("tok-1")
);
assert_eq!(records[0].error.as_deref(), Some("HTTP 500 from Sheets"));
}
#[tokio::test]
async fn a_best_effort_audit_failure_is_warned_and_swallowed() {
let dir = tempfile::tempdir().unwrap();
let _audit = AuditLogGuard::redirect(dir.path());
std::fs::create_dir(dir.path().join("audit.jsonl")).unwrap();
let ledger_path = dir.path().join("lease-ledger.jsonl");
seed_lease(&ledger_path, "tok-1", "file-1", "1");
assert!(matches!(
check_and_lock_lease(
leased("edit", &ledger_path, "file-1"),
None,
Some("1"),
None
)
.await,
Err(LeaseGateRefusal::NoLease)
));
record_failed_leased_write(leased("edit", &ledger_path, "file-1"), "tok-1", "boom");
let lock = LedgerLock::acquire(&ledger_path).unwrap();
finish_leased_write(
leased("edit", &ledger_path, "file-1"),
&lock,
"tok-1",
Some("2".to_string()),
None,
);
drop(lock);
let reloaded = LeaseLedger::load(&ledger_path).unwrap();
assert_eq!(reloaded.get("tok-1").unwrap().version, "2");
}
#[cfg(unix)]
#[tokio::test]
async fn a_ledger_lock_failure_writes_a_failed_record_carrying_the_error() {
use std::os::unix::fs::PermissionsExt;
let dir = tempfile::tempdir().unwrap();
let audit = AuditLogGuard::redirect(dir.path());
let ledger_dir = dir.path().join("locked");
std::fs::create_dir(&ledger_dir).unwrap();
let ledger_path = ledger_dir.join("lease-ledger.jsonl");
seed_lease(&ledger_path, "tok-1", "file-1", "1");
std::fs::set_permissions(&ledger_dir, std::fs::Permissions::from_mode(0o500)).unwrap();
let outcome = check_and_lock_lease(
leased("edit", &ledger_path, "file-1"),
Some("tok-1"),
Some("1"),
None,
)
.await;
std::fs::set_permissions(&ledger_dir, std::fs::Permissions::from_mode(0o700)).unwrap();
assert!(matches!(outcome, Err(LeaseGateRefusal::Failed(_))));
let records = audit.records();
assert_eq!(audit.verdicts(), [verdict::FAILED], "{records:?}");
let error = records[0].error.as_deref().unwrap_or_default();
assert!(error.contains("failed to lock"), "{error}");
}
#[tokio::test]
async fn a_ledger_lock_wait_timeout_writes_a_failed_record_carrying_the_error() {
let dir = tempfile::tempdir().unwrap();
let audit = AuditLogGuard::redirect(dir.path());
let ledger_path = dir.path().join("lease-ledger.jsonl");
seed_lease(&ledger_path, "tok-1", "file-1", "1");
let _held = LedgerLock::acquire(&ledger_path).unwrap();
let outcome = check_and_lock_lease_with_timeout(
leased("edit", &ledger_path, "file-1"),
Some("tok-1"),
Some("1"),
None,
Duration::from_millis(150),
)
.await;
assert!(
matches!(&outcome, Err(LeaseGateRefusal::Failed(detail)) if detail.contains("timed out")),
"expected a timed-out Failed refusal"
);
let records = audit.records();
assert_eq!(audit.verdicts(), [verdict::FAILED], "{records:?}");
let error = records[0].error.as_deref().unwrap_or_default();
assert!(error.contains("timed out"), "{error}");
}
#[tokio::test]
async fn check_and_lock_lease_waits_for_a_concurrent_holder_then_succeeds() {
let dir = tempfile::tempdir().unwrap();
let audit = AuditLogGuard::redirect(dir.path());
let ledger_path = dir.path().join("lease-ledger.jsonl");
seed_lease(&ledger_path, "tok-1", "file-1", "1");
let held = LedgerLock::acquire(&ledger_path).unwrap();
let releaser = std::thread::spawn(move || {
std::thread::sleep(std::time::Duration::from_millis(100));
drop(held);
});
let outcome = check_and_lock_lease(
leased("edit", &ledger_path, "file-1"),
Some("tok-1"),
Some("1"),
None,
)
.await;
releaser.join().unwrap();
assert!(
outcome.is_ok(),
"waited holder should have released the lock"
);
assert_eq!(audit.verdicts(), [verdict::PENDING]);
}
#[test]
fn log_status_matches_the_verdict_constants() {
assert_eq!(
LeaseGateRefusal::NoLease.log_status(),
verdict::REFUSED_NO_LEASE
);
assert_eq!(
LeaseGateRefusal::Expired.log_status(),
verdict::REFUSED_LEASE_EXPIRED
);
assert_eq!(
LeaseGateRefusal::WrongFile.log_status(),
verdict::REFUSED_LEASE_WRONG_FILE
);
assert_eq!(
LeaseGateRefusal::Stale.log_status(),
verdict::REFUSED_LEASE_STALE
);
assert_eq!(
LeaseGateRefusal::Failed("boom".to_string()).log_status(),
verdict::FAILED
);
}
#[test]
fn describe_line_renders_the_target_and_id_where_each_variant_names_one() {
assert_eq!(
LeaseGateRefusal::NoLease.describe_line("file-1", "'Budget'"),
Some(
"Refused: 'Budget' requires a Drive write lease — run `omni-dev drive lease \
acquire file-1` and pass the printed token via `--lease`."
.to_string()
)
);
assert_eq!(
LeaseGateRefusal::Expired.describe_line("file-1", "'Budget'"),
Some(
"Refused: the presented lease is expired, released, or unknown to this ledger \
— run `omni-dev drive lease acquire file-1` again."
.to_string()
)
);
assert_eq!(
LeaseGateRefusal::WrongFile.describe_line("file-1", "'Budget'"),
Some(
"Refused: the presented lease was acquired for a different file — run \
`omni-dev drive lease acquire file-1` for this one."
.to_string()
)
);
assert_eq!(
LeaseGateRefusal::Stale.describe_line("file-1", "'Budget'"),
Some(
"Refused: 'Budget' changed since the lease was acquired (or last written \
under) — re-run `omni-dev drive lease acquire file-1` to lease the current \
version."
.to_string()
)
);
assert_eq!(
LeaseGateRefusal::Failed("boom".to_string()).describe_line("file-1", "'Budget'"),
None
);
}
#[tokio::test(flavor = "multi_thread")]
async fn the_gate_writes_its_audit_records_on_the_calling_thread_on_a_multi_thread_runtime() {
let dir = tempfile::tempdir().unwrap();
let audit = AuditLogGuard::redirect(dir.path());
let ledger_path = dir.path().join("lease-ledger.jsonl");
seed_lease(&ledger_path, "tok-1", "file-1", "1");
let Ok(grant) = check_and_lock_lease(
leased("edit", &ledger_path, "file-1"),
Some("tok-1"),
Some("1"),
Some("2026-09-11T00:00:00Z"),
)
.await
else {
panic!("a live, correctly-bound, fresh lease must be granted");
};
finish_leased_write(
leased("edit", &ledger_path, "file-1"),
&grant.lock,
&grant.token,
Some("2".to_string()),
None,
);
drop(grant);
assert_eq!(
audit.verdicts(),
[verdict::PENDING, verdict::ALLOWED],
"{:?}",
audit.records()
);
let reloaded = LeaseLedger::load(&ledger_path).unwrap();
assert_eq!(reloaded.get("tok-1").unwrap().version, "2");
}
#[test]
fn finish_leased_write_preserves_a_ledger_change_made_under_the_same_lock() {
let dir = tempfile::tempdir().unwrap();
let _audit = AuditLogGuard::redirect(dir.path());
let ledger_path = dir.path().join("lease-ledger.jsonl");
let lock = LedgerLock::acquire(&ledger_path).unwrap();
seed_lease(&ledger_path, "tok-1", "file-1", "1");
LeaseLedger::mutate(&lock, &ledger_path, |ledger| {
ledger.mark_restored("tok-1", chrono::Utc::now(), Some(42));
})
.unwrap();
finish_leased_write(
leased("edit", &ledger_path, "file-1"),
&lock,
"tok-1",
Some("2".to_string()),
None,
);
let reloaded = LeaseLedger::load(&ledger_path).unwrap();
let record = reloaded.get("tok-1").unwrap();
assert_eq!(record.version, "2", "the write's own refresh must land");
assert_eq!(
record.restored_sheet_id,
Some(42),
"the change made under the same lock must survive the refresh"
);
assert!(record.restored_at.is_some());
}
}