4b6817381b
Benchmark image — build + push to ECR (any adapter) / build + push (push) Waiting to run
CI / quality (ubuntu-latest) (push) Waiting to run
CI / test (tools-runtime) (push) Waiting to run
CI / test (e2e-general) (push) Waiting to run
CI / test (cli-runtime) (push) Waiting to run
CI / test (e2e-provider-and-openclaw) (push) Waiting to run
CI / test (integrations-and-misc) (push) Waiting to run
CI / coverage-report (push) Blocked by required conditions
CI / test-kubernetes (push) Waiting to run
CI / should-run-thorough (push) Waiting to run
CI / test-thorough (cloudwatch-demo) (push) Blocked by required conditions
CI / test-thorough (flink-ecs) (push) Blocked by required conditions
CI / test-thorough (upstream-lambda) (push) Blocked by required conditions
CI / test-thorough (prefect-ecs-fargate) (push) Blocked by required conditions
CodeQL / Analyze (python) (push) Waiting to run
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Blocked by required conditions
Release / publish-release (push) Blocked by required conditions
Release / publish-main-release (push) Blocked by required conditions
Release / prepare (push) Waiting to run
Release / verify (push) Blocked by required conditions
Release / build-python-dist (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Blocked by required conditions
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Waiting to run
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Waiting to run
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Waiting to run
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
319 lines
12 KiB
Python
319 lines
12 KiB
Python
"""End-to-end integration tests for the ``/fleet`` token wiring (#2023).
|
||
|
||
These tests drive the *real* pipeline — provider resolver, real
|
||
:class:`ClaudeCodeJsonlSource` / :class:`CodexRolloutSource`, real
|
||
:class:`ClaudeCodeMeter` / :class:`CodexMeter`, real
|
||
:class:`TokenRateTracker`, real ``_resolved_model_for_pid``, real
|
||
``_format_tokens_per_min`` / ``_format_usd_per_hour`` — against a
|
||
fixture on disk.
|
||
|
||
The only stubs are :func:`tools.system.fleet_monitoring.probe.cwd_for_pid` and
|
||
:func:`started_at_for_pid` (no real PIDs in a unit test) and the
|
||
``AgentRegistry`` lookup (no real ``~/.opensre/agents.jsonl``).
|
||
Everything else exercises production code.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import io
|
||
import time
|
||
from datetime import UTC, datetime
|
||
from pathlib import Path
|
||
|
||
import pytest
|
||
from rich.console import Console
|
||
|
||
from surfaces.interactive_shell.ui.agents.agents_view import _build_agents_table
|
||
from tools.system.fleet_monitoring import sampler as sampler_mod
|
||
from tools.system.fleet_monitoring.probe import ProcessSnapshot
|
||
from tools.system.fleet_monitoring.registry import AgentRecord
|
||
from tools.system.fleet_monitoring.token_rate import TOKEN_RATE_TRACKER
|
||
from tools.system.fleet_monitoring.token_sources import claude_code as claude_source_mod
|
||
from tools.system.fleet_monitoring.token_sources import codex as codex_source_mod
|
||
|
||
|
||
@pytest.fixture(autouse=True)
|
||
def _isolate_sampler_globals() -> None:
|
||
"""Same isolation pattern as the unit tests in this directory."""
|
||
sampler_mod._latest.clear()
|
||
sampler_mod._TickCache.registry_snapshot = {}
|
||
sampler_mod._TickCache.agents_config = None
|
||
for pid in list(TOKEN_RATE_TRACKER.known_pids()):
|
||
TOKEN_RATE_TRACKER.forget(pid)
|
||
|
||
|
||
def _render(records: list[AgentRecord]) -> str:
|
||
table = _build_agents_table(records)
|
||
buf = io.StringIO()
|
||
Console(file=buf, force_terminal=False, highlight=False, width=140).print(table)
|
||
return buf.getvalue()
|
||
|
||
|
||
# ---------- Claude Code end-to-end ----------------------------------
|
||
|
||
|
||
def test_claude_code_end_to_end_renders_real_tokens_and_cost(
|
||
monkeypatch: pytest.MonkeyPatch,
|
||
tmp_path: Path,
|
||
) -> None:
|
||
"""Real pipeline: psutil-stubbed cwd → real JSONL on disk →
|
||
real source → real meter → real tracker → real view formatter.
|
||
The view shows live ``tokens/min`` and ``$/hr`` cells, not ``-``.
|
||
"""
|
||
# Build a Claude Code project directory and session JSONL.
|
||
fake_cwd = tmp_path / "repo"
|
||
fake_cwd.mkdir()
|
||
projects_root = tmp_path / "claude_projects"
|
||
project_dir = projects_root / str(fake_cwd).replace("/", "-")
|
||
project_dir.mkdir(parents=True)
|
||
session = project_dir / "session-abc.jsonl"
|
||
# Start with one historical event — the source seeks past it on
|
||
# first read (no retro-pricing) so it should not contribute.
|
||
session.write_text(
|
||
'{"type":"system","subtype":"init","session_id":"abc","model":"claude-sonnet-4-5"}\n',
|
||
encoding="utf-8",
|
||
)
|
||
|
||
# Stub psutil-fenced helpers — no real PID lookups in unit tests.
|
||
monkeypatch.setattr(
|
||
"tools.system.fleet_monitoring.token_sources.claude_code.cwd_for_pid",
|
||
lambda _pid: fake_cwd,
|
||
)
|
||
# After the Greptile-driven fix, the claude source requires
|
||
# fd-level evidence to attribute a JSONL to a PID (no more
|
||
# silent fallback to newest-by-mtime — mirrors codex).
|
||
monkeypatch.setattr(
|
||
"tools.system.fleet_monitoring.token_sources.claude_code.open_files_for_pid",
|
||
lambda _pid: (session,),
|
||
)
|
||
|
||
# Wire a fresh source against the test projects root so the
|
||
# production singleton (which would hit ``~/.claude/projects/``)
|
||
# never enters this test's resolution path.
|
||
isolated_source = claude_source_mod.ClaudeCodeJsonlSource(projects_root=projects_root)
|
||
monkeypatch.setattr(
|
||
"tools.system.fleet_monitoring.sampler.get_token_source",
|
||
lambda provider: isolated_source if provider == "claude-code" else None,
|
||
)
|
||
|
||
# Registered agent: Claude Code at PID 8421.
|
||
record = AgentRecord(
|
||
name="claude-code-8421",
|
||
pid=8421,
|
||
command="claude --output-format stream-json",
|
||
provider="claude-code",
|
||
)
|
||
|
||
class _Registry:
|
||
def list(self) -> list[AgentRecord]:
|
||
return [record]
|
||
|
||
def get(self, pid: int) -> AgentRecord | None:
|
||
return record if pid == 8421 else None
|
||
|
||
monkeypatch.setattr("tools.system.fleet_monitoring.sampler.AgentRegistry", _Registry)
|
||
|
||
# Stub probe so the snapshot path is also alive (uptime/cpu%).
|
||
fake_snapshot = ProcessSnapshot(
|
||
pid=8421,
|
||
cpu_percent=18.4,
|
||
rss_mb=128.0,
|
||
num_fds=42,
|
||
num_connections=3,
|
||
status="running",
|
||
started_at=datetime(2026, 5, 14, 10, 0, 0, tzinfo=UTC),
|
||
)
|
||
monkeypatch.setattr("tools.system.fleet_monitoring.sampler.probe", lambda _pid: fake_snapshot)
|
||
|
||
# First sampler tick: cold-start the source (seeks to EOF) and
|
||
# populate registry caches. No tokens yet — historical content
|
||
# below EOF is correctly ignored.
|
||
sampler_mod._TickCache.registry_snapshot[record.pid] = record
|
||
sampler_mod._TickCache.agents_config = None
|
||
sampler_mod._sample_tokens(record)
|
||
|
||
# Append a real Claude assistant turn with usage. This is the
|
||
# delta the next sample call should pick up.
|
||
with session.open("a", encoding="utf-8") as fh:
|
||
fh.write(
|
||
'{"type":"assistant","message":{"model":"claude-sonnet-4-5",'
|
||
'"usage":{"input_tokens":200,"output_tokens":50}}}\n'
|
||
)
|
||
# Brief wait so mtime > started_at - 5s on every filesystem
|
||
# (APFS sub-second mtime quirks rarely matter, but cheap).
|
||
time.sleep(0.01)
|
||
|
||
sampler_mod._sample_tokens(record)
|
||
|
||
# Render through the real view.
|
||
out = _render([record])
|
||
|
||
# ``tokens/min`` populated: 250 tokens in a 60 s window → 250.
|
||
assert "250" in out
|
||
|
||
# ``$/hr`` populated with a real dollar figure for sonnet-4-5
|
||
# (3 USD/M input × 0.7 + 15 USD/M output × 0.3 blend ≈ 6.6e-6
|
||
# per token; 250 tok/min × 60 × 6.6e-6 ≈ $0.099). The actual
|
||
# rendering rounds to two decimals, so look for the ``$`` prefix.
|
||
assert "$0.0" in out or "$0.1" in out
|
||
|
||
# And the placeholder is gone for the metric columns.
|
||
assert "claude-code-8421" in out
|
||
|
||
|
||
def test_claude_code_end_to_end_shows_dash_when_source_resolves_nothing(
|
||
monkeypatch: pytest.MonkeyPatch,
|
||
tmp_path: Path,
|
||
) -> None:
|
||
"""When psutil cannot read the cwd (macOS hardened-runtime
|
||
denials, cross-user processes), the dashboard stays honest:
|
||
``tokens/min`` and ``$/hr`` render ``-`` rather than ``0`` or a
|
||
bogus number.
|
||
"""
|
||
monkeypatch.setattr(
|
||
"tools.system.fleet_monitoring.token_sources.claude_code.cwd_for_pid",
|
||
lambda _pid: None,
|
||
)
|
||
isolated_source = claude_source_mod.ClaudeCodeJsonlSource(
|
||
projects_root=tmp_path / "claude_projects",
|
||
)
|
||
monkeypatch.setattr(
|
||
"tools.system.fleet_monitoring.sampler.get_token_source",
|
||
lambda provider: isolated_source if provider == "claude-code" else None,
|
||
)
|
||
|
||
record = AgentRecord(
|
||
name="claude-code-8421",
|
||
pid=8421,
|
||
command="claude",
|
||
provider="claude-code",
|
||
)
|
||
|
||
class _Registry:
|
||
def list(self) -> list[AgentRecord]:
|
||
return [record]
|
||
|
||
def get(self, pid: int) -> AgentRecord | None:
|
||
return record if pid == 8421 else None
|
||
|
||
monkeypatch.setattr("tools.system.fleet_monitoring.sampler.AgentRegistry", _Registry)
|
||
monkeypatch.setattr("tools.system.fleet_monitoring.sampler.probe", lambda _pid: None)
|
||
|
||
sampler_mod._TickCache.registry_snapshot[record.pid] = record
|
||
sampler_mod._sample_tokens(record)
|
||
sampler_mod._sample_tokens(record)
|
||
|
||
# Drive the real render and inspect the actual table cells rather
|
||
# than substring-matching the printed form (which contains the
|
||
# ``$/hr`` header literal).
|
||
table = _build_agents_table([record])
|
||
assert table.row_count == 1
|
||
rendered_cells = [list(col.cells)[0] for col in table.columns]
|
||
# cells[0]=agent, [1]=pid, [2..6]=uptime/cpu/tokens/min/$/hr/status.
|
||
# Every metric cell — including ``$/hr`` — falls back to ``-``.
|
||
assert rendered_cells[2:] == ["-", "-", "-", "-", "-"]
|
||
# The printed form should still include the agent name (so
|
||
# rendering didn't silently bail).
|
||
out = _render([record])
|
||
assert "claude-code-8421" in out
|
||
|
||
|
||
# ---------- Codex end-to-end ----------------------------------------
|
||
|
||
|
||
def test_codex_end_to_end_renders_real_tokens_and_cost(
|
||
monkeypatch: pytest.MonkeyPatch,
|
||
tmp_path: Path,
|
||
) -> None:
|
||
"""Mirrors the Claude Code test for the Codex rollout source."""
|
||
codex_home = tmp_path / "codex_home"
|
||
started_at_epoch = time.time() - 30 # process started 30 s ago
|
||
started_dt = datetime.fromtimestamp(started_at_epoch, tz=UTC)
|
||
rollout_dir = (
|
||
codex_home
|
||
/ "sessions"
|
||
/ started_dt.strftime("%Y")
|
||
/ started_dt.strftime("%m")
|
||
/ started_dt.strftime("%d")
|
||
)
|
||
rollout_dir.mkdir(parents=True)
|
||
rollout = rollout_dir / "rollout-001-abc.jsonl"
|
||
rollout.write_text(
|
||
'{"type":"thread.started","model":"gpt-5-codex"}\n{"type":"turn.started"}\n',
|
||
encoding="utf-8",
|
||
)
|
||
import os as _os
|
||
|
||
_os.utime(rollout, (started_at_epoch + 1, started_at_epoch + 1))
|
||
|
||
monkeypatch.setattr(
|
||
"tools.system.fleet_monitoring.token_sources.codex.started_at_for_pid",
|
||
lambda _pid: started_at_epoch,
|
||
)
|
||
# Source now requires fd-level confirmation that the PID owns
|
||
# the rollout (production: each codex holds its rollout fd open).
|
||
monkeypatch.setattr(
|
||
"tools.system.fleet_monitoring.token_sources.codex.open_files_for_pid",
|
||
lambda _pid: (rollout,),
|
||
)
|
||
isolated_source = codex_source_mod.CodexRolloutSource(codex_home=codex_home)
|
||
monkeypatch.setattr(
|
||
"tools.system.fleet_monitoring.sampler.get_token_source",
|
||
lambda provider: isolated_source if provider == "codex" else None,
|
||
)
|
||
|
||
record = AgentRecord(
|
||
name="codex-9999",
|
||
pid=9999,
|
||
command="codex exec",
|
||
provider="codex",
|
||
)
|
||
|
||
class _Registry:
|
||
def list(self) -> list[AgentRecord]:
|
||
return [record]
|
||
|
||
def get(self, pid: int) -> AgentRecord | None:
|
||
return record if pid == 9999 else None
|
||
|
||
monkeypatch.setattr("tools.system.fleet_monitoring.sampler.AgentRegistry", _Registry)
|
||
|
||
fake_snapshot = ProcessSnapshot(
|
||
pid=9999,
|
||
cpu_percent=4.2,
|
||
rss_mb=64.0,
|
||
num_fds=20,
|
||
num_connections=1,
|
||
status="running",
|
||
started_at=started_dt,
|
||
)
|
||
monkeypatch.setattr("tools.system.fleet_monitoring.sampler.probe", lambda _pid: fake_snapshot)
|
||
|
||
sampler_mod._TickCache.registry_snapshot[record.pid] = record
|
||
sampler_mod._TickCache.agents_config = None
|
||
sampler_mod._sample_tokens(record)
|
||
|
||
# Append a real per-turn token_count event (the on-disk format
|
||
# codex-cli 0.130.0 writes — distinct from ``codex exec --json``
|
||
# stdout). ``turn_context`` carries the model; ``event_msg`` with
|
||
# ``payload.type == "token_count"`` carries ``last_token_usage``.
|
||
with rollout.open("a", encoding="utf-8") as fh:
|
||
fh.write(
|
||
'{"type":"turn_context","payload":{"turn_id":"t_1","model":"gpt-5-codex"}}\n'
|
||
'{"type":"event_msg","payload":{"type":"token_count","info":'
|
||
'{"last_token_usage":'
|
||
'{"input_tokens":175,"cached_input_tokens":0,'
|
||
'"output_tokens":50,"reasoning_output_tokens":0,"total_tokens":225}}}}\n'
|
||
)
|
||
time.sleep(0.01)
|
||
|
||
sampler_mod._sample_tokens(record)
|
||
|
||
out = _render([record])
|
||
|
||
# 225 tokens (input + output) over a 60 s window.
|
||
assert "225" in out
|
||
# Real cost figure from the gpt-5-codex price table.
|
||
assert "$0.0" in out or "$0.1" in out
|
||
assert "codex-9999" in out
|