Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 13:10:45 +08:00

319 lines
12 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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