Skip to content
Draft
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
92 changes: 89 additions & 3 deletions apps/desktop-tauri/src-tauri/src/capacity_events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -920,14 +920,22 @@ fn normalize_window_id(value: &str) -> String {
}

pub(crate) fn observation_scope(snapshot: &ProviderUsageSnapshot) -> String {
// Scope by BOTH account identifiers, not either/or: the same email can
// Scope by every account identifier, not either/or: the same email can
// belong to different organizations (e.g. a personal vs a business
// workspace) with distinct limits, and those must never share a baseline.
// Directory seats that share a login email (Codex personal + Team) also
// stamp distinct `account_id`s; omitting that id collapsed them (SBS-1079).
let email = snapshot.account_email.as_deref().unwrap_or("");
let organization = snapshot.account_organization.as_deref().unwrap_or("");
let account_id = snapshot
.account_id
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
.unwrap_or("");
let raw = format!(
"{}|{}|{}|{}",
snapshot.provider_id, snapshot.source_label, email, organization
"{}|{}|{}|{}|{}",
snapshot.provider_id, snapshot.source_label, email, organization, account_id
);
format!("{}:{:016x}", snapshot.provider_id, fnv1a64(raw.as_bytes()))
}
Expand Down Expand Up @@ -1554,6 +1562,84 @@ mod tests {
assert!(observer.observe(&other).is_empty());
}

/// Two Codex directory seats can share a login email (personal + Team).
/// After #379 they stamp distinct `account_id`s; usage_history, threshold
/// toasts, and predictive pace already key on those. Capacity-event
/// observation used to hash provider|source|email|org only, so both seats
/// shared a baseline (SBS-1079).
fn shared_email_directory_seats(
at: DateTime<Utc>,
used: f64,
reset: DateTime<Utc>,
account_id: &str,
) -> ProviderUsageSnapshot {
let mut seat = snapshot(at, used, reset);
seat.account_email = Some("Same@Example.com".into());
seat.account_organization = Some("Team".into());
seat.account_id = Some(account_id.into());
seat
}

#[test]
fn observation_scope_separates_directory_seats_that_share_an_email() {
let now = Utc::now();
let reset = now + Duration::hours(4);
let personal = shared_email_directory_seats(now, 90.0, reset, "acct-personal");
let work = shared_email_directory_seats(now, 10.0, reset, "acct-work");

assert_ne!(observation_scope(&personal), observation_scope(&work));
assert_eq!(
observation_scope(&personal),
observation_scope(&shared_email_directory_seats(
now + Duration::minutes(1),
91.0,
reset,
"acct-personal",
))
);
}

#[test]
fn shared_email_directory_seats_do_not_confirm_each_others_resets() {
// Without account_id in the scope, the work seat's drop from the
// personal baseline looks like a confirmed surprise reset.
let start = Utc::now();
let old_reset = start + Duration::hours(4);
let new_reset = start + Duration::hours(9);
let mut observer = CapacityEventObserver::default();

assert!(
observer
.observe(&shared_email_directory_seats(
start,
85.0,
old_reset,
"acct-personal",
))
.is_empty()
);
assert!(
observer
.observe(&shared_email_directory_seats(
start + Duration::minutes(5),
10.0,
new_reset,
"acct-work",
))
.is_empty()
);
let events = observer.observe(&shared_email_directory_seats(
start + Duration::minutes(10),
12.0,
new_reset + Duration::minutes(2),
"acct-work",
));
assert!(
events.is_empty(),
"shared-email seats must not share a capacity baseline: {events:?}"
);
}

#[test]
fn reset_time_shift_requires_confirmation() {
let start = Utc::now();
Expand Down
36 changes: 30 additions & 6 deletions apps/desktop-tauri/src-tauri/src/enforcement.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@
//! quietly drop out. Surfaces can then show honest uncertainty instead of
//! silently losing a limit or fabricating a percentage for it.
//!
//! Scope is `provider | data source | account identity` (shared with the
//! capacity-event observer), so accounts, sources, and providers never bleed
//! into each other. State is process-local: the first read of each scope after
//! launch is a fresh baseline that never emits `unavailable`, mirroring the
//! observer's re-baseline-on-launch behaviour so changes that happened while
//! Ceiling was closed are not replayed as surprises.
//! Scope is `provider | data source | account identity` including directory
//! `account_id` (shared with the capacity-event observer), so accounts,
//! sources, and providers never bleed into each other. State is process-local:
//! the first read of each scope after launch is a fresh baseline that never
//! emits `unavailable`, mirroring the observer's re-baseline-on-launch
//! behaviour so changes that happened while Ceiling was closed are not
//! replayed as surprises.

use std::collections::HashMap;

Expand Down Expand Up @@ -370,6 +371,29 @@ mod tests {
assert!(other_org.inactive_rate_windows.is_empty());
}

#[test]
fn shared_email_directory_seats_do_not_flag_each_other() {
// Codex personal + Team seats share a login email and can share an
// organization label; directory `account_id` is what keeps their
// enforcement baselines apart (SBS-1079). Without it, the work seat's
// first read would inherit the personal seat's expected windows.
let mut tracker = EnforcementTracker::new();
let mut personal = codex_snapshot();
personal.account_email = Some("Same@Example.com".into());
personal.account_organization = Some("Team".into());
personal.account_id = Some("acct-personal".into());
tracker.annotate(&mut personal);

let mut work = codex_snapshot();
work.account_email = Some("Same@Example.com".into());
work.account_organization = Some("Team".into());
work.account_id = Some("acct-work".into());
work.secondary = None;
work.secondary_label = None;
assert!(tracker.annotate(&mut work).is_empty());
assert!(work.inactive_rate_windows.is_empty());
}

#[test]
fn conditional_bonus_windows_are_never_flagged_unavailable() {
// Bonus pools (Spark, promos, additional budget) legitimately end. Their
Expand Down
Loading