Single-commit clean baseline after security scrub of niche-tells, project codenames, internal jargon, and contributor-email leaks. Contents: - 100 Rust crates (_primitives/_rust/) - 37 agent manifests (_manifests/) + generated specs (_generated/) - 67 user-invocable skills (skills/) - 33 hooks (hooks/) - Composition blocks (_blocks/) - Documentation (docs/, README.md) - TS adapter packages (_ts_packages/) - Assembler (_assembler/) - Roles (_roles/) - Templates (_templates/) - Forgejo CI (.forgejo/) Author: Denis Parfionovich <info@greendragon.info> License: see LICENSE.
133 lines
4.9 KiB
Rust
133 lines
4.9 KiB
Rust
//! v7 micro-cents column tests (Wave 44c, 2026-04-24).
|
||
//!
|
||
//! Constructor Pattern: extracted from `v6_cost.rs` so each test file
|
||
//! stays under the 200-LOC ceiling. Loads source modules via `#[path]`
|
||
//! to avoid forcing all callers through the public lib API.
|
||
|
||
#[path = "../src/migrations_list.rs"]
|
||
mod migrations_list;
|
||
#[path = "../src/schema.rs"]
|
||
mod schema;
|
||
#[path = "../src/error.rs"]
|
||
mod error;
|
||
#[path = "../src/row.rs"]
|
||
mod row;
|
||
#[path = "../src/ledger.rs"]
|
||
mod ledger;
|
||
#[path = "../src/descendants.rs"]
|
||
mod descendants;
|
||
#[path = "../src/cost.rs"]
|
||
mod cost;
|
||
|
||
use rusqlite::Connection;
|
||
use tempfile::TempDir;
|
||
|
||
fn open_tmp() -> (TempDir, Connection) {
|
||
let dir = tempfile::tempdir().unwrap();
|
||
let db = dir.path().join("ledger.sqlite");
|
||
let conn = ledger::open(&db).unwrap();
|
||
(dir, conn)
|
||
}
|
||
|
||
/// v7-T8: schema reaches at least v7 from a fresh DB; cost_micro_cents
|
||
/// column exists with DEFAULT 0 for new rows and backfill SQL is harmless.
|
||
/// Uses `>= 7` rather than `== 7` so future migrations (currently at v8)
|
||
/// don't break this v7-specific assertion.
|
||
#[test]
|
||
fn migration_v7_adds_micro_cents_column() {
|
||
let (_d, conn) = open_tmp();
|
||
let v: i64 = conn
|
||
.query_row("PRAGMA user_version", [], |r| r.get(0))
|
||
.unwrap();
|
||
assert!(v >= 7, "schema must be at least v7, got v{v}");
|
||
ledger::fork(&conn, "v7", "br-v7", None, "sha", None, None, None, None).unwrap();
|
||
let micro: i64 = conn
|
||
.query_row(
|
||
"SELECT cost_micro_cents FROM agents WHERE id = 'v7'",
|
||
[],
|
||
|r| r.get(0),
|
||
)
|
||
.unwrap();
|
||
assert_eq!(micro, 0, "fresh row defaults to 0 micro-cents");
|
||
}
|
||
|
||
/// v7-T9: pre-v7 row carrying a non-zero cost_cents value gets backfilled
|
||
/// to (cost_cents × 1_000_000) micro-cents on migration.
|
||
#[test]
|
||
fn migration_v7_backfills_pre_existing_cost_cents() {
|
||
let dir = tempfile::tempdir().unwrap();
|
||
let db = dir.path().join("ledger.sqlite");
|
||
// Create at v6 manually by invoking only the first 6 migrations,
|
||
// then mass-insert a row with cost_cents set, THEN reopen so v7
|
||
// backfill runs.
|
||
{
|
||
let conn = rusqlite::Connection::open(&db).unwrap();
|
||
for sql in &schema::MIGRATIONS[..6] {
|
||
conn.execute_batch(sql).unwrap();
|
||
}
|
||
conn.pragma_update(None, "user_version", 6_i64).unwrap();
|
||
conn.execute(
|
||
"INSERT INTO agents (id, branch, spec_sha, status, started_ts, cost_cents)
|
||
VALUES ('legacy', 'br-legacy', 'sha', 'done', 1, 250)",
|
||
[],
|
||
)
|
||
.unwrap();
|
||
}
|
||
let conn = ledger::open(&db).unwrap();
|
||
let (cents, micro, _, _) = cost::read_cost_micro(&conn, "legacy").unwrap().unwrap();
|
||
assert_eq!(cents, 250);
|
||
assert_eq!(micro, 250 * 1_000_000, "backfilled to 250M micro-cents");
|
||
}
|
||
|
||
/// v7-T10: compose_micro_cents is exact under integer overflow guards.
|
||
/// 1.5M input + 0.5M output @ 100c/MTok / 500c/MTok = 400M micro-cents.
|
||
#[test]
|
||
fn compose_micro_cents_exact_arithmetic() {
|
||
let m = cost::compose_micro_cents(1_500_000, 500_000, 100, 500);
|
||
// 1.5M × 100 = 150M, 0.5M × 500 = 250M, total = 400M micro-cents.
|
||
assert_eq!(m, 400_000_000);
|
||
}
|
||
|
||
/// v7-T11: 100 micro-turns of 5 tokens each input @ 1c/MTok do NOT
|
||
/// round to 1 cent each; the cents accumulator stays 0 while micro
|
||
/// accumulates 500.
|
||
#[test]
|
||
fn compose_micro_cents_micro_turns_no_rounding_loss() {
|
||
let mut total_micro: u64 = 0;
|
||
for _ in 0..100 {
|
||
let m = cost::compose_micro_cents(5, 0, 1, 0);
|
||
total_micro = total_micro.saturating_add(m);
|
||
}
|
||
assert_eq!(total_micro, 500, "100 × 5 tokens × 1 cent/MTok = 500 micro-cents");
|
||
assert_eq!(
|
||
cost::display_cents_from_micro(total_micro),
|
||
0,
|
||
"rounds DOWN to 0 cents — under threshold"
|
||
);
|
||
}
|
||
|
||
/// v7-T12: display_cents_from_micro uses half-up rounding at boundaries.
|
||
#[test]
|
||
fn display_cents_from_micro_half_up_at_boundary() {
|
||
assert_eq!(cost::display_cents_from_micro(0), 0);
|
||
assert_eq!(cost::display_cents_from_micro(499_999), 0, "below half rounds down");
|
||
assert_eq!(cost::display_cents_from_micro(500_000), 1, "exactly half rounds up");
|
||
assert_eq!(cost::display_cents_from_micro(999_999), 1);
|
||
assert_eq!(cost::display_cents_from_micro(1_000_000), 1);
|
||
assert_eq!(cost::display_cents_from_micro(1_500_000), 2);
|
||
}
|
||
|
||
/// v7-T3c: micro-cents accumulator persists alongside cents. 100 calls
|
||
/// of 5 micro-cents each (= 500 micro-cents = 0.0005 cents) round-trip
|
||
/// without rounding loss in the micro column.
|
||
#[test]
|
||
fn record_cost_micro_accumulates_without_rounding_loss() {
|
||
let (_d, conn) = open_tmp();
|
||
ledger::fork(&conn, "mic", "br-mic", None, "sha", None, None, None, None).unwrap();
|
||
for _ in 0..100 {
|
||
cost::record_cost_micro(&conn, "mic", 0, 5, "anthropic", "claude").unwrap();
|
||
}
|
||
let (cents, micro, _, _) = cost::read_cost_micro(&conn, "mic").unwrap().unwrap();
|
||
assert_eq!(cents, 0, "100 × 0 cents truncates to 0");
|
||
assert_eq!(micro, 500, "but micro-cents accumulator is exact");
|
||
}
|