use std::{path::PathBuf, sync::Arc};
use chrono::{TimeZone, Utc};
use kcode_kmap_size::{KmapSize, measure, render};
use kcode_kweb_db::{Config, KwebDb, NodeData, NoopGossip, Owner, Provenance, WriterId};
use uuid::Uuid;
struct Fixture {
root: PathBuf,
signing_key: [u8; 32],
}
impl Fixture {
fn new() -> Self {
Self {
root: std::env::temp_dir().join(format!("kcode-kmap-size-{}", Uuid::new_v4())),
signing_key: [19; 32],
}
}
fn config(&self) -> Config {
Config {
signing_key: self.signing_key,
writers_by_priority: vec![WriterId::from_signing_key(&self.signing_key)],
gossip: Arc::new(NoopGossip),
}
}
fn open(&self) -> KwebDb {
KwebDb::open(&self.root, self.config()).unwrap()
}
fn provenance(label: &str) -> Provenance {
Provenance {
author: "kcode-kmap-size-test".into(),
source: "integration-test".into(),
source_created_at: Utc.with_ymd_and_hms(2026, 7, 26, 0, 0, 0).unwrap(),
data: label.into(),
}
}
}
impl Drop for Fixture {
fn drop(&mut self) {
if self.root.exists() {
std::fs::remove_dir_all(&self.root).unwrap();
}
}
}
fn node(short_name: &str, short_description: &str, long_description: &str) -> NodeData {
NodeData {
short_name: short_name.into(),
short_description: short_description.into(),
long_description: long_description.into(),
owner: Owner::SelfNode,
fixed_connections: Vec::new(),
recent_connections: Vec::new(),
objects: Vec::new(),
}
}
#[test]
fn empty_kweb_has_a_zero_size_estimate() {
let fixture = Fixture::new();
drop(fixture.open());
assert_eq!(
measure(&fixture.root, fixture.config()).unwrap(),
KmapSize {
node_count: 0,
full_node_characters: 0,
full_node_words: 0,
full_node_tokens: 0,
long_description_characters: 0,
long_description_words: 0,
long_description_tokens: 0,
}
);
}
#[test]
fn measurement_counts_current_node_text_only() {
let fixture = Fixture::new();
let database = fixture.open();
let mut initial = database
.start_transaction(Fixture::provenance("initial nodes"))
.unwrap();
let updated_id = initial
.create_node(node(
"Obsolete",
"This revision is historical",
"A deliberately enormous historical description that must not be counted.",
))
.unwrap();
initial
.create_object(vec![b'x'; 1024 * 1024])
.expect("objects do not contribute to the estimate");
initial.finalize().unwrap();
let mut current = database
.start_transaction(Fixture::provenance("current nodes"))
.unwrap();
current
.update_node(
updated_id,
node("Updated", "Current only", "Newest long text"),
)
.unwrap();
current.create_node(node("🦀", "", "naïve café")).unwrap();
current.finalize().unwrap();
drop(database);
let size = measure(&fixture.root, fixture.config()).unwrap();
assert_eq!(
size,
KmapSize {
node_count: 2,
full_node_characters: 46,
full_node_words: 9,
full_node_tokens: 12,
long_description_characters: 26,
long_description_words: 5,
long_description_tokens: 7,
}
);
}
#[test]
fn measurement_ignores_unrelated_files_in_the_node_tree() {
let fixture = Fixture::new();
let database = fixture.open();
let mut transaction = database
.start_transaction(Fixture::provenance("one node"))
.unwrap();
transaction
.create_node(node("One", "Two", "Three"))
.unwrap();
transaction.finalize().unwrap();
drop(database);
std::fs::write(fixture.root.join("nodes").join("README"), "not a node").unwrap();
let unrelated_shard = fixture.root.join("nodes").join("zz");
std::fs::create_dir_all(&unrelated_shard).unwrap();
std::fs::write(unrelated_shard.join("ignored.txt"), "not a node").unwrap();
let size = measure(&fixture.root, fixture.config()).unwrap();
assert_eq!(size.node_count, 1);
assert_eq!(size.full_node_characters, 11);
assert_eq!(size.full_node_words, 3);
}
#[test]
fn missing_kweb_root_returns_contextual_error() {
let fixture = Fixture::new();
let error = measure(&fixture.root, fixture.config()).unwrap_err();
assert!(error.to_string().contains("does not exist"), "{error:#}");
}
#[test]
fn report_is_grouped_and_discloses_its_approximation() {
let report = render(&KmapSize {
node_count: 12_345,
full_node_characters: 4_938_268,
full_node_words: 987_654,
full_node_tokens: 1_234_567,
long_description_characters: 3_950_616,
long_description_words: 800_000,
long_description_tokens: 987_654,
});
assert!(report.contains("Nodes: 12,345"));
assert!(report.contains("Full node text: ~1,234,567 tokens"));
assert!(report.contains("Long descriptions only: ~987,654 tokens"));
assert!(report.contains("one token per 4 Unicode characters"));
assert!(report.contains("history, provenance, connections, and other tables are excluded"));
}