Skip to main content

csusage_test_support/
openhands.rs

1use std::path::Path;
2
3/// Creates the event log used by OpenHands report tests. The layout mirrors
4/// the OpenHands persistence store: one directory per conversation, each
5/// holding `event-<idx>-<id>.json` files whose stats snapshots carry
6/// accumulated token usage per LLM.
7pub fn create_fixture(conversations_root: impl AsRef<Path>) {
8    write_conversation(
9        conversations_root.as_ref().join("conv-1"),
10        &[
11            // An early snapshot must not double-count.
12            stats_event(
13                "2026-01-01T00:00:00.000000",
14                "default",
15                "openai/gpt-5",
16                100,
17                10,
18                0,
19                0,
20                Some(0.5),
21            ),
22            // A second LLM in the same conversation.
23            stats_event(
24                "2026-01-02T00:00:00.000000",
25                "condenser",
26                "gpt-5-mini",
27                0,
28                0,
29                0,
30                0,
31                None,
32            ),
33            // The final snapshot wins: totals are 200/20, not 100/10.
34            stats_event(
35                "2026-01-02T00:00:01.000000",
36                "default",
37                "openai/gpt-5",
38                200,
39                20,
40                30,
41                10,
42                Some(1.5),
43            ),
44            non_stats_event(),
45        ],
46    );
47    write_conversation(
48        conversations_root.as_ref().join("conv-2"),
49        &[stats_event(
50            "2026-01-03T00:00:00.000000",
51            "default",
52            "anthropic/claude-sonnet-4-5",
53            50,
54            5,
55            0,
56            0,
57            None,
58        )],
59    );
60}
61
62fn write_conversation(conversation: std::path::PathBuf, events: &[serde_json::Value]) {
63    let events_dir = conversation.join("events");
64    std::fs::create_dir_all(&events_dir).unwrap();
65    for (index, event) in events.iter().enumerate() {
66        let path = events_dir.join(format!("event-{index:05}-id{index}.json"));
67        std::fs::write(path, serde_json::to_string(event).unwrap()).unwrap();
68    }
69}
70
71#[allow(clippy::too_many_arguments)]
72fn stats_event(
73    timestamp: &str,
74    usage_id: &str,
75    model: &str,
76    prompt_tokens: i64,
77    completion_tokens: i64,
78    cache_read: i64,
79    cache_write: i64,
80    cost: Option<f64>,
81) -> serde_json::Value {
82    let metrics = serde_json::json!({
83        "model_name": model,
84        "accumulated_cost": cost.unwrap_or(0.0),
85        "max_budget_per_task": None::<serde_json::Value>,
86        "accumulated_token_usage": {
87            "model": model,
88            "prompt_tokens": prompt_tokens,
89            "completion_tokens": completion_tokens,
90            "cache_read_tokens": cache_read,
91            "cache_write_tokens": cache_write,
92            "reasoning_tokens": 0,
93            "context_window": 0,
94            "per_turn_token": prompt_tokens + completion_tokens,
95            "response_id": ""
96        }
97    });
98    serde_json::json!({
99        "id": format!("event-{usage_id}-{timestamp}"),
100        "timestamp": timestamp,
101        "source": "environment",
102        "parent_id": null,
103        "kind": "ConversationStateUpdateEvent",
104        "key": "stats",
105        "value": {"usage_to_metrics": {usage_id: metrics}}
106    })
107}
108
109fn non_stats_event() -> serde_json::Value {
110    serde_json::json!({
111        "id": "event-other",
112        "timestamp": "2026-01-02T00:00:02.000000",
113        "source": "environment",
114        "parent_id": null,
115        "kind": "MessageEvent",
116        "key": "",
117        "value": {}
118    })
119}