Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions crates/tracedecay-global-db/src/registered_accounting.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,14 @@ impl RegisteredGlobalDb {
after_tokens: u64,
timestamp: i64,
) -> tracedecay_domain::errors::Result<()> {
// SQLite's signed INTEGER cannot represent every u64. Refuse before
// writing rather than wrapping a valid token count into a negative.
let before_tokens = i64::try_from(before_tokens).map_err(|error| {
global_db_operation_error("encode savings ledger before tokens", error)
})?;
let after_tokens = i64::try_from(after_tokens).map_err(|error| {
global_db_operation_error("encode savings ledger after tokens", error)
})?;
let project_path = RegisteredGlobalDb::canonical_project_key(Path::new(project_path));
let tool_name = tool_name.to_owned();
self.transact(async move |transaction| {
Expand All @@ -99,8 +107,8 @@ impl RegisteredGlobalDb {
timestamp,
project_path.as_str(),
tool_name.as_str(),
before_tokens as i64,
after_tokens as i64
before_tokens,
after_tokens
],
)
.await
Expand Down
39 changes: 39 additions & 0 deletions crates/tracedecay-global-db/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1644,6 +1644,45 @@ async fn registered_runtime_does_not_recreate_a_missing_database() {
);
}

#[asupersync::test]
async fn savings_ledger_refuses_unrepresentable_token_counts() {
let harness = RegisteredGlobalDbHarness::open("savings-ledger-token-encoding").await;
let database = &harness.registered;
let out_of_range = i64::MAX as u64 + 1;

for (before, after) in [
(out_of_range, 0),
(0, out_of_range),
(u64::MAX, 3),
(3, u64::MAX),
] {
let error = database
.try_record_savings("/project", "tool", before, after, 1)
.await
.expect_err("unrepresentable token count must be refused");
assert!(
error.to_string().contains("encode savings ledger"),
"unexpected refusal: {error}"
);
}
assert_eq!(
row_count(database, "savings_ledger").await,
0,
"a rejected count must not append a corrupted or partial row"
);

database
.try_record_savings("/project", "tool", i64::MAX as u64, 0, 1)
.await
.expect("the largest representable count must be accepted");
let total = database
.sum_savings(Some("/project"), 0)
.await
.expect("read the committed savings row");
assert_eq!(total.saved_tokens, i64::MAX as u64);
assert_eq!(total.calls, 1);
}

#[asupersync::test]
async fn project_token_projection_rejects_unrepresentable_totals() {
let harness = RegisteredGlobalDbHarness::open("project-tokens-overflow").await;
Expand Down
Loading