Skip to content

Commit 2f033a7

Browse files
committed
test(coverage): batch 9–10 — browser_open, update_memory_md, credentials profiles, cost tracker (tinyhumansai#530)
Add 53 new tests across 4 more modules: - browser_open: IPv4/IPv6 private ranges, host matching, normalize_domain edges - update_memory_md: empty file, section creation, unknown action, param validation - credentials/profiles: token expiry, CRUD operations, active profile management - cost/tracker: budget warnings, monthly exceeded, model stats aggregation All 4027 tests pass.
1 parent e4d7cc7 commit 2f033a7

4 files changed

Lines changed: 559 additions & 0 deletions

File tree

src/openhuman/cost/tracker.rs

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -533,4 +533,133 @@ mod tests {
533533
.to_string()
534534
.contains("Estimated cost must be a finite, non-negative value"));
535535
}
536+
537+
#[test]
538+
fn invalid_budget_negative_is_rejected() {
539+
let tmp = TempDir::new().unwrap();
540+
let tracker = CostTracker::new(enabled_config(), tmp.path()).unwrap();
541+
assert!(tracker.check_budget(-1.0).is_err());
542+
}
543+
544+
#[test]
545+
fn invalid_budget_infinity_is_rejected() {
546+
let tmp = TempDir::new().unwrap();
547+
let tracker = CostTracker::new(enabled_config(), tmp.path()).unwrap();
548+
assert!(tracker.check_budget(f64::INFINITY).is_err());
549+
}
550+
551+
#[test]
552+
fn record_usage_when_disabled_is_noop() {
553+
let tmp = TempDir::new().unwrap();
554+
let config = CostConfig {
555+
enabled: false,
556+
..Default::default()
557+
};
558+
let tracker = CostTracker::new(config, tmp.path()).unwrap();
559+
let usage = TokenUsage::new("test/model", 1000, 500, 1.0, 2.0);
560+
tracker.record_usage(usage).unwrap();
561+
let summary = tracker.get_summary().unwrap();
562+
assert_eq!(summary.request_count, 0);
563+
}
564+
565+
#[test]
566+
fn record_usage_rejects_negative_cost() {
567+
let tmp = TempDir::new().unwrap();
568+
let tracker = CostTracker::new(enabled_config(), tmp.path()).unwrap();
569+
let mut usage = TokenUsage::new("test/model", 1000, 500, 1.0, 2.0);
570+
usage.cost_usd = -1.0;
571+
assert!(tracker.record_usage(usage).is_err());
572+
}
573+
574+
#[test]
575+
fn record_usage_rejects_nan_cost() {
576+
let tmp = TempDir::new().unwrap();
577+
let tracker = CostTracker::new(enabled_config(), tmp.path()).unwrap();
578+
let mut usage = TokenUsage::new("test/model", 1000, 500, 1.0, 2.0);
579+
usage.cost_usd = f64::NAN;
580+
assert!(tracker.record_usage(usage).is_err());
581+
}
582+
583+
#[test]
584+
fn budget_warning_threshold() {
585+
let tmp = TempDir::new().unwrap();
586+
let config = CostConfig {
587+
enabled: true,
588+
daily_limit_usd: 10.0,
589+
warn_at_percent: 80,
590+
monthly_limit_usd: 1000.0,
591+
..Default::default()
592+
};
593+
let tracker = CostTracker::new(config, tmp.path()).unwrap();
594+
595+
// Record usage just under warning threshold (80% of 10 = 8.0)
596+
let usage = TokenUsage::new("test/model", 100000, 50000, 1.0, 2.0);
597+
// This has a cost, so let's just check the budget with a projected amount
598+
let check = tracker.check_budget(8.5).unwrap();
599+
assert!(
600+
matches!(check, BudgetCheck::Warning { .. }),
601+
"expected warning, got {check:?}"
602+
);
603+
}
604+
605+
#[test]
606+
fn budget_monthly_exceeded() {
607+
let tmp = TempDir::new().unwrap();
608+
let config = CostConfig {
609+
enabled: true,
610+
daily_limit_usd: 1000.0,
611+
monthly_limit_usd: 0.01,
612+
..Default::default()
613+
};
614+
let tracker = CostTracker::new(config, tmp.path()).unwrap();
615+
616+
let usage = TokenUsage::new("test/model", 10000, 5000, 1.0, 2.0);
617+
tracker.record_usage(usage).unwrap();
618+
619+
let check = tracker.check_budget(0.01).unwrap();
620+
assert!(matches!(
621+
check,
622+
BudgetCheck::Exceeded {
623+
period: UsagePeriod::Month,
624+
..
625+
}
626+
));
627+
}
628+
629+
#[test]
630+
fn get_daily_cost_for_today() {
631+
let tmp = TempDir::new().unwrap();
632+
let tracker = CostTracker::new(enabled_config(), tmp.path()).unwrap();
633+
let usage = TokenUsage::new("test/model", 1000, 500, 1.0, 2.0);
634+
tracker.record_usage(usage.clone()).unwrap();
635+
636+
let today_cost = tracker.get_daily_cost(Utc::now().date_naive()).unwrap();
637+
assert!((today_cost - usage.cost_usd).abs() < 0.001);
638+
}
639+
640+
#[test]
641+
fn get_monthly_cost_for_current_month() {
642+
let tmp = TempDir::new().unwrap();
643+
let tracker = CostTracker::new(enabled_config(), tmp.path()).unwrap();
644+
let usage = TokenUsage::new("test/model", 1000, 500, 1.0, 2.0);
645+
tracker.record_usage(usage.clone()).unwrap();
646+
647+
let now = Utc::now();
648+
let monthly_cost = tracker.get_monthly_cost(now.year(), now.month()).unwrap();
649+
assert!((monthly_cost - usage.cost_usd).abs() < 0.001);
650+
}
651+
652+
#[test]
653+
fn build_session_model_stats_aggregates_correctly() {
654+
let records = vec![
655+
CostRecord::new("s1", TokenUsage::new("model-a", 100, 50, 1.0, 1.0)),
656+
CostRecord::new("s1", TokenUsage::new("model-a", 200, 100, 1.0, 1.0)),
657+
CostRecord::new("s1", TokenUsage::new("model-b", 300, 150, 1.0, 1.0)),
658+
];
659+
let stats = build_session_model_stats(&records);
660+
assert_eq!(stats.len(), 2);
661+
assert_eq!(stats["model-a"].request_count, 2);
662+
assert_eq!(stats["model-a"].total_tokens, 450);
663+
assert_eq!(stats["model-b"].request_count, 1);
664+
}
536665
}

src/openhuman/credentials/profiles.rs

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -681,4 +681,167 @@ mod tests {
681681
let contents = tokio::fs::read_to_string(path).await.unwrap();
682682
assert!(contents.contains("\"schema_version\": 1"));
683683
}
684+
685+
#[test]
686+
fn token_set_not_expiring_when_no_expiry() {
687+
let token_set = TokenSet {
688+
access_token: "token".into(),
689+
refresh_token: None,
690+
id_token: None,
691+
expires_at: None,
692+
token_type: None,
693+
scope: None,
694+
};
695+
assert!(!token_set.is_expiring_within(Duration::from_secs(3600)));
696+
}
697+
698+
#[test]
699+
fn auth_profile_new_token() {
700+
let profile = AuthProfile::new_token("anthropic", "default", "sk-abc".into());
701+
assert_eq!(profile.provider, "anthropic");
702+
assert_eq!(profile.profile_name, "default");
703+
assert_eq!(profile.kind, AuthProfileKind::Token);
704+
assert_eq!(profile.token.as_deref(), Some("sk-abc"));
705+
assert!(profile.token_set.is_none());
706+
}
707+
708+
#[test]
709+
fn auth_profile_new_oauth() {
710+
let ts = TokenSet {
711+
access_token: "access".into(),
712+
refresh_token: Some("refresh".into()),
713+
id_token: None,
714+
expires_at: None,
715+
token_type: None,
716+
scope: None,
717+
};
718+
let profile = AuthProfile::new_oauth("openai", "work", ts);
719+
assert_eq!(profile.kind, AuthProfileKind::OAuth);
720+
assert!(profile.token_set.is_some());
721+
assert!(profile.token.is_none());
722+
}
723+
724+
#[test]
725+
fn auth_profiles_data_default() {
726+
let data = AuthProfilesData::default();
727+
assert_eq!(data.schema_version, CURRENT_SCHEMA_VERSION);
728+
assert!(data.profiles.is_empty());
729+
assert!(data.active_profiles.is_empty());
730+
}
731+
732+
#[test]
733+
fn remove_nonexistent_profile_returns_false() {
734+
let tmp = TempDir::new().unwrap();
735+
let store = AuthProfilesStore::new(tmp.path(), false);
736+
let result = store.remove_profile("nonexistent:id").unwrap();
737+
assert!(!result);
738+
}
739+
740+
#[test]
741+
fn remove_existing_profile_returns_true() {
742+
let tmp = TempDir::new().unwrap();
743+
let store = AuthProfilesStore::new(tmp.path(), false);
744+
let profile = AuthProfile::new_token("test", "default", "tok".into());
745+
let id = profile.id.clone();
746+
store.upsert_profile(profile, true).unwrap();
747+
748+
let removed = store.remove_profile(&id).unwrap();
749+
assert!(removed);
750+
751+
let data = store.load().unwrap();
752+
assert!(!data.profiles.contains_key(&id));
753+
assert!(!data.active_profiles.values().any(|v| v == &id));
754+
}
755+
756+
#[test]
757+
fn set_active_profile_errors_for_missing_profile() {
758+
let tmp = TempDir::new().unwrap();
759+
let store = AuthProfilesStore::new(tmp.path(), false);
760+
let err = store
761+
.set_active_profile("openai", "missing:id")
762+
.unwrap_err();
763+
assert!(err.to_string().contains("not found"));
764+
}
765+
766+
#[test]
767+
fn set_active_profile_succeeds_for_existing_profile() {
768+
let tmp = TempDir::new().unwrap();
769+
let store = AuthProfilesStore::new(tmp.path(), false);
770+
let profile = AuthProfile::new_token("openai", "prod", "tok".into());
771+
let id = profile.id.clone();
772+
store.upsert_profile(profile, false).unwrap();
773+
774+
store.set_active_profile("openai", &id).unwrap();
775+
let data = store.load().unwrap();
776+
assert_eq!(data.active_profiles.get("openai"), Some(&id));
777+
}
778+
779+
#[test]
780+
fn clear_active_profile() {
781+
let tmp = TempDir::new().unwrap();
782+
let store = AuthProfilesStore::new(tmp.path(), false);
783+
let profile = AuthProfile::new_token("openai", "prod", "tok".into());
784+
store.upsert_profile(profile, true).unwrap();
785+
786+
store.clear_active_profile("openai").unwrap();
787+
let data = store.load().unwrap();
788+
assert!(data.active_profiles.get("openai").is_none());
789+
}
790+
791+
#[test]
792+
fn update_profile_modifies_in_place() {
793+
let tmp = TempDir::new().unwrap();
794+
let store = AuthProfilesStore::new(tmp.path(), false);
795+
let profile = AuthProfile::new_token("openai", "prod", "tok".into());
796+
let id = profile.id.clone();
797+
store.upsert_profile(profile, false).unwrap();
798+
799+
let updated = store
800+
.update_profile(&id, |p| {
801+
p.metadata.insert("env".into(), "staging".into());
802+
Ok(())
803+
})
804+
.unwrap();
805+
assert_eq!(
806+
updated.metadata.get("env").map(|s| s.as_str()),
807+
Some("staging")
808+
);
809+
}
810+
811+
#[test]
812+
fn update_profile_errors_for_missing_id() {
813+
let tmp = TempDir::new().unwrap();
814+
let store = AuthProfilesStore::new(tmp.path(), false);
815+
let err = store.update_profile("missing:id", |_| Ok(())).unwrap_err();
816+
assert!(err.to_string().contains("not found"));
817+
}
818+
819+
#[test]
820+
fn upsert_preserves_created_at_on_update() {
821+
let tmp = TempDir::new().unwrap();
822+
let store = AuthProfilesStore::new(tmp.path(), false);
823+
let profile = AuthProfile::new_token("openai", "prod", "tok1".into());
824+
let id = profile.id.clone();
825+
let created = profile.created_at;
826+
store.upsert_profile(profile, false).unwrap();
827+
828+
std::thread::sleep(Duration::from_millis(10));
829+
let updated = AuthProfile::new_token("openai", "prod", "tok2".into());
830+
store.upsert_profile(updated, false).unwrap();
831+
832+
let data = store.load().unwrap();
833+
let loaded = data.profiles.get(&id).unwrap();
834+
assert_eq!(loaded.created_at, created);
835+
}
836+
837+
#[test]
838+
fn auth_profile_kind_serde_roundtrip() {
839+
let json = serde_json::to_string(&AuthProfileKind::OAuth).unwrap();
840+
assert_eq!(json, "\"o-auth\""); // kebab-case
841+
let back: AuthProfileKind = serde_json::from_str(&json).unwrap();
842+
assert_eq!(back, AuthProfileKind::OAuth);
843+
844+
let json = serde_json::to_string(&AuthProfileKind::Token).unwrap();
845+
assert_eq!(json, "\"token\"");
846+
}
684847
}

0 commit comments

Comments
 (0)