File size: 5,177 Bytes
afa0cbf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
//! Full and partial profile decoding through the authenticated HTTP client.
use super::*;
use pretty_assertions::assert_eq;
use serde_json::json;
use wiremock::Mock;
use wiremock::MockServer;
use wiremock::ResponseTemplate;
use wiremock::matchers::method;
use wiremock::matchers::path;

#[tokio::test]
async fn profile_keeps_missing_zero_and_future_invocations_distinct() {
    let server = MockServer::start().await;
    let client = Client::new(
        server.uri(),
        codex_http_client::HttpClientFactory::new(
            codex_http_client::OutboundProxyPolicy::ReqwestDefault,
        ),
    );
    let empty = AccountProfile {
        profile: None,
        metadata: None,
        stats: ProfileStats {
            tokens: TokenUsageProfileStats {
                lifetime_tokens: None,
                peak_daily_tokens: None,
                longest_running_turn_sec: None,
                current_streak_days: None,
                longest_streak_days: None,
                daily_usage_buckets: None,
            },
            fast_mode_usage_percentage: None,
            most_used_reasoning_effort: None,
            most_used_reasoning_effort_percentage: None,
            unique_skills_used: None,
            total_skills_used: None,
            total_threads: None,
            top_invocations: None,
        },
    };
    let full = AccountProfile {
        profile: Some(ProfileIdentity {
            display_name: Some("Test user".into()),
            username: Some("test.user".into()),
        }),
        metadata: Some(ProfileMetadata {
            stats_as_of: Some("2026-09-09".into()),
            stats_error: Some("partial upstream data".into()),
        }),
        stats: ProfileStats {
            tokens: TokenUsageProfileStats {
                lifetime_tokens: Some(142_300_000_000),
                peak_daily_tokens: Some(7_000_000_000),
                longest_running_turn_sec: Some(158_760),
                current_streak_days: Some(158),
                longest_streak_days: Some(160),
                daily_usage_buckets: Some(vec![crate::TokenUsageProfileDailyBucket {
                    start_date: "2026-09-09".into(),
                    tokens: 42,
                }]),
            },
            fast_mode_usage_percentage: serde_json::Number::from_f64(/*f*/ 37.5),
            most_used_reasoning_effort: Some("high".into()),
            most_used_reasoning_effort_percentage: Some(26.into()),
            unique_skills_used: Some(147),
            total_skills_used: Some(13_190),
            total_threads: Some(0),
            top_invocations: Some(vec![
                ProfileInvocation {
                    kind: ProfileInvocationKind::Unknown,
                    plugin_name: None,
                    skill_name: None,
                    usage_count: None,
                },
                ProfileInvocation {
                    kind: ProfileInvocationKind::Skill,
                    plugin_name: None,
                    skill_name: Some("review".into()),
                    usage_count: Some(12),
                },
                ProfileInvocation {
                    kind: ProfileInvocationKind::Plugin,
                    plugin_name: Some("github".into()),
                    skill_name: None,
                    usage_count: Some(34),
                },
            ]),
        },
    };
    for (body, expected) in [
        (json!({"stats":{}}), empty),
        (
            json!({
                "profile":{"display_name":"Test user","username":"test.user"},
                "metadata":{"stats_as_of":"2026-09-09","stats_error":"partial upstream data"},
                "stats":{
                    "lifetime_tokens":142_300_000_000_i64,"peak_daily_tokens":7_000_000_000_i64,
                    "longest_running_turn_sec":158_760,"current_streak_days":158,"longest_streak_days":160,
                    "daily_usage_buckets":[{"start_date":"2026-09-09","tokens":42}],
                    "fast_mode_usage_percentage":37.5,"most_used_reasoning_effort":"high",
                    "most_used_reasoning_effort_percentage":26,"unique_skills_used":147,
                    "total_skills_used":13_190,"total_threads":0,
                    "top_invocations":[{"type":"future_kind"},{"type":"skill","skill_name":"review","usage_count":12},
                        {"type":"plugin","plugin_name":"github","usage_count":34}]
                }
            }),
            full,
        ),
    ] {
        Mock::given(method("GET"))
            .and(path("/api/codex/profiles/me"))
            .respond_with(ResponseTemplate::new(/*s*/ 200).set_body_json(body))
            .expect(/*r*/ 1)
            .mount(&server)
            .await;
        assert_eq!(client.get_account_profile().await.unwrap(), expected);
        server.verify().await;
        server.reset().await;
    }
    Mock::given(method("GET"))
        .respond_with(ResponseTemplate::new(/*s*/ 200).set_body_string("private invalid payload"))
        .mount(&server)
        .await;
    let error = client.get_account_profile().await.unwrap_err().to_string();
    assert_eq!(error, "Invalid account profile response.");
}