mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-11-05 10:37:48 +00:00
A claude job (environment app-sim-claude, only on workflow_dispatch or a head commit with [claude-test]) starts its own test backend, builds the patched app, runs the scripted onboarding, and gives claude -p (pinned 2.1.292, Claude Haiku 5.5, at most 60 turns and 4 USD) the same search in both layouts to do with the ui helper, reading screenshots. Claude gets Bash, Read and Write only: a PreToolUse hook lets through nothing but single ui commands, credentials are scrubbed from its subprocesses, files are limited to its output directory, no MCP. The job passes when Claude reports CLAUDE-TEST: PASS, and uploads its report, screenshots, transcript and usage (turns, tokens, cost estimated at API prices).
63 lines
2.3 KiB
Python
Executable File
63 lines
2.3 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
|
|
# Reports what a Claude Code run spent, from the result event at the end of its stream-json transcript: prints it,
|
|
# adds it to the job summary, and writes it as JSON. Without a result event (a timeout or a crash) it says so
|
|
# rather than reporting zero.
|
|
# Usage: scripts/ci/desktop-linux/claude-usage.py <transcript .jsonl> <output .json>
|
|
|
|
import json
|
|
import os
|
|
import sys
|
|
|
|
FIELDS = ["subtype", "is_error", "num_turns", "duration_ms", "total_cost_usd", "usage", "modelUsage"]
|
|
|
|
|
|
def last_result(path):
|
|
result = None
|
|
try:
|
|
with open(path) as f:
|
|
for line in f:
|
|
try:
|
|
event = json.loads(line)
|
|
except json.JSONDecodeError:
|
|
continue
|
|
if isinstance(event, dict) and event.get("type") == "result":
|
|
result = event
|
|
except FileNotFoundError:
|
|
pass
|
|
return result
|
|
|
|
|
|
def summary(result):
|
|
lines = ["### Claude run usage", ""]
|
|
if result is None:
|
|
return lines + ["No result event in the transcript (timeout or crash): usage unknown, not zero."]
|
|
usage = result.get("usage") or {}
|
|
lines += [
|
|
f"- result: {result.get('subtype')}{' (error)' if result.get('is_error') else ''}",
|
|
f"- turns: {result.get('num_turns')}",
|
|
f"- duration: {(result.get('duration_ms') or 0) / 1000:.0f} s",
|
|
f"- tokens: input {usage.get('input_tokens')}, output {usage.get('output_tokens')}, "
|
|
f"cache read {usage.get('cache_read_input_tokens')}, cache write {usage.get('cache_creation_input_tokens')}",
|
|
f"- total_cost_usd: {result.get('total_cost_usd')}, estimated at API prices "
|
|
"(computed by Claude Code; on a subscription it is not a bill)",
|
|
]
|
|
for model, model_usage in (result.get("modelUsage") or {}).items():
|
|
lines.append(f"- {model}: {json.dumps(model_usage)}")
|
|
return lines
|
|
|
|
|
|
def main():
|
|
transcript, out = sys.argv[1], sys.argv[2]
|
|
result = last_result(transcript)
|
|
with open(out, "w") as f:
|
|
json.dump({k: result.get(k) for k in FIELDS} if result else {"result": None}, f, indent=2)
|
|
text = "\n".join(summary(result))
|
|
print(text)
|
|
if os.environ.get("GITHUB_STEP_SUMMARY"):
|
|
with open(os.environ["GITHUB_STEP_SUMMARY"], "a") as f:
|
|
f.write(text + "\n")
|
|
|
|
|
|
main()
|