The footer showed invented percentages. It now shows what two harnesses actually report, or says it does not know. lumbridge-core gains an append-only per-profile UsageLedger and a projection that labels every derived value estimated, withholds a burn rate from a single sample, withholds a window fraction with no reported ceiling, withholds an exhaustion estimate that lands after the reset, and reports an expired window as rolled over rather than freezing its last percentage. A missing fact renders as missing, never as zero. (0012) lumbridge-harness is the impure side: processes, clocks, and untrusted wire text in, observations out. Three adapters: - Codex's account/rateLimits/read over the app-server's JSON-RPC stdio. The client cannot express a request outside a two-variant enum and answers every server-to-client request with -32601, so a harness asking Lumbridge for a credential is refused by construction. (0013) - Claude Code's session transcripts, as a byte-offset tail follower that reports nothing until the backlog is read to EOF — a partially-read backlog is indistinguishable from a burst of spend, and the first run against 20 MB reported forty-six billion tokens an hour. The parser models four counters, so the conversations in those files are not representable. (0014) - Claude Code's five-hour and seven-day subscription windows, via a bridge installed as its statusLine command. 0014 had claimed no such surface existed; it does, and the record is corrected in place rather than quietly edited. Lumbridge does not read the OAuth credential to call the account usage endpoint, which is what comparable tools do — AGENTS.md forbids it, and 0015 says so rather than leaving the gap unexplained. Also in here: a capability-check ordering fix in the workspace reducer, where the applied-request replay table was consulted before the capability check and so answered questions the caller had no right to ask; the GPUI spike wired to the live probes with per-harness gauges and provenance chips; and a launcher that matches its own window by PID, because GPUI sets WM_NAME but not _NET_WM_NAME and a title match never succeeded. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
176 lines
6.1 KiB
Rust
176 lines
6.1 KiB
Rust
//! End-to-end: a Codex transcript through the real ledger and footer.
|
|
//!
|
|
//! The unit tests prove the adapter builds the right observations. These prove
|
|
//! the observations still obey every decision 0012 rule once they reach
|
|
//! `lumbridge-core`, which is the property that actually matters to a user
|
|
//! reading the footer.
|
|
//!
|
|
//! Every transcript below is hand-written from the documented wire shape in
|
|
//! openai/codex (Apache-2.0), commit
|
|
//! `17e8101699c5062117d0d37f504313e8af53b043`, `codex-rs/app-server/README.md`
|
|
//! section "7) Rate limits (`ChatGPT`)". No real account, transcript, or
|
|
//! credential is used, and all timestamps are small synthetic integers.
|
|
|
|
use lumbridge_core::{
|
|
AccountProfileId, FooterUsage, UsageConfidence, UsageLedger, UsageProvenance,
|
|
};
|
|
use lumbridge_harness::codex::replay_transcript;
|
|
|
|
const HOUR_MS: u64 = 3_600_000;
|
|
/// A synthetic reset five hours after the epoch, in seconds.
|
|
const RESETS_AT_SECONDS: u64 = 5 * 3_600;
|
|
const RESETS_AT_MS: u64 = 5 * HOUR_MS;
|
|
|
|
fn initialize_response() -> &'static str {
|
|
r#"{"jsonrpc":"2.0","id":1,"result":{"userAgent":"codex"}}"#
|
|
}
|
|
|
|
fn read_response(id: u64, percent: u64) -> String {
|
|
format!(
|
|
r#"{{"jsonrpc":"2.0","id":{id},"result":{{"rateLimits":{{"primary":{{"usedPercent":{percent},"windowDurationMins":300,"resetsAt":{RESETS_AT_SECONDS}}},"secondary":null,"rateLimitReachedType":null}}}}}}"#
|
|
)
|
|
}
|
|
|
|
fn primary() -> AccountProfileId {
|
|
AccountProfileId::new("codex-app-server-primary").expect("the probe's static ID is valid")
|
|
}
|
|
|
|
fn ledger_from(frames: &[(&str, u64)]) -> UsageLedger {
|
|
let mut ledger = UsageLedger::new();
|
|
for observation in replay_transcript(frames) {
|
|
ledger
|
|
.record(observation)
|
|
.expect("the adapter emits observations in time order");
|
|
}
|
|
ledger
|
|
}
|
|
|
|
#[test]
|
|
fn a_two_reading_transcript_produces_a_provider_reported_footer() {
|
|
let first = read_response(2, 20);
|
|
let second = read_response(3, 35);
|
|
let ledger = ledger_from(&[
|
|
(initialize_response(), 0),
|
|
(first.as_str(), HOUR_MS),
|
|
(second.as_str(), 2 * HOUR_MS),
|
|
]);
|
|
|
|
let projection = ledger.project(&primary(), 2 * HOUR_MS);
|
|
assert_eq!(projection.provenance(), UsageProvenance::ProviderReported);
|
|
assert_eq!(projection.confidence(), UsageConfidence::Exact);
|
|
assert_eq!(projection.consumed_permille(), Some(350));
|
|
assert_eq!(projection.resets_in_ms(), Some(3 * HOUR_MS));
|
|
// 200 permille burned over one hour, derived rather than reported.
|
|
assert_eq!(projection.burn_per_hour(), Some(150));
|
|
assert_eq!(projection.burn_provenance(), UsageProvenance::Estimated);
|
|
}
|
|
|
|
#[test]
|
|
fn a_single_reading_yields_a_percentage_but_no_rate() {
|
|
let only = read_response(2, 20);
|
|
let ledger = ledger_from(&[(initialize_response(), 0), (only.as_str(), HOUR_MS)]);
|
|
|
|
let projection = ledger.project(&primary(), HOUR_MS);
|
|
assert_eq!(projection.consumed_permille(), Some(200));
|
|
assert_eq!(
|
|
projection.burn_per_hour(),
|
|
None,
|
|
"one provider reading is not a rate"
|
|
);
|
|
assert_eq!(projection.exhaustion_in_ms(), None);
|
|
}
|
|
|
|
#[test]
|
|
fn an_api_key_account_renders_the_unavailable_path_rather_than_a_zero() {
|
|
// Codex answers -32600 with "chatgpt authentication required to read rate
|
|
// limits" for an account that has no subscription quota.
|
|
let denied = r#"{"jsonrpc":"2.0","id":2,"error":{"code":-32600,"message":"chatgpt authentication required to read rate limits"}}"#;
|
|
let ledger = ledger_from(&[(initialize_response(), 0), (denied, HOUR_MS)]);
|
|
|
|
let projection = ledger.project(&primary(), HOUR_MS);
|
|
assert!(!projection.is_available());
|
|
assert_eq!(projection.consumed_permille(), None);
|
|
|
|
let profile = lumbridge_core::AccountProfile::new(
|
|
"codex-app-server-primary",
|
|
"Codex",
|
|
"ChatGPT",
|
|
"primary window",
|
|
"subscription",
|
|
)
|
|
.expect("a valid profile");
|
|
let footer = FooterUsage::new(&profile, &projection);
|
|
assert_eq!(footer.window(), "usage unavailable");
|
|
assert_eq!(footer.burn(), "burn rate unavailable");
|
|
assert_eq!(footer.trust(), "no usage source");
|
|
assert!(
|
|
!footer.to_string().contains('%'),
|
|
"an account with no quota must not render a percentage"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn a_reading_taken_after_the_reset_never_renders_a_frozen_percentage() {
|
|
let stale = read_response(2, 90);
|
|
let ledger = ledger_from(&[
|
|
(initialize_response(), 0),
|
|
// Observed one millisecond after the window it describes ended.
|
|
(stale.as_str(), RESETS_AT_MS + 1),
|
|
]);
|
|
|
|
let projection = ledger.project(&primary(), RESETS_AT_MS + 1);
|
|
assert!(!projection.is_available());
|
|
assert_eq!(
|
|
projection.consumed_permille(),
|
|
None,
|
|
"an ended window must not keep rendering its last percentage"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn a_credential_refresh_request_produces_no_observation_at_all() {
|
|
let refresh = r#"{"jsonrpc":"2.0","id":77,"method":"account/chatgptAuthTokens/refresh"}"#;
|
|
let reading = read_response(2, 20);
|
|
let observations = replay_transcript(&[
|
|
(initialize_response(), 0),
|
|
(refresh, 1_000),
|
|
(reading.as_str(), HOUR_MS),
|
|
]);
|
|
assert_eq!(
|
|
observations.len(),
|
|
1,
|
|
"the refused credential request must not enter the usage stream"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn a_repeated_identical_reading_does_not_flood_the_ledger() {
|
|
let a = read_response(2, 20);
|
|
let b = read_response(3, 20);
|
|
let c = read_response(4, 20);
|
|
let ledger = ledger_from(&[
|
|
(initialize_response(), 0),
|
|
(a.as_str(), HOUR_MS),
|
|
(b.as_str(), HOUR_MS + 60_000),
|
|
(c.as_str(), HOUR_MS + 120_000),
|
|
]);
|
|
assert_eq!(
|
|
ledger.observation_count(&primary()),
|
|
1,
|
|
"identical readings inside the re-emit floor must not evict history"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn a_malformed_transcript_records_nothing_rather_than_guessing() {
|
|
let observations = replay_transcript(&[
|
|
(initialize_response(), 0),
|
|
("}{ not json", 1_000),
|
|
(
|
|
r#"{"jsonrpc":"2.0","id":2,"result":{"rateLimits":"a string"}}"#,
|
|
2_000,
|
|
),
|
|
]);
|
|
assert!(observations.is_empty());
|
|
}
|