项目文件夹

文件
wehub-resource-sync 4b6817381b
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
CI / coverage-report (push) Has been cancelled
CI / test-kubernetes (push) Has been cancelled
CI / should-run-thorough (push) Has been cancelled
CI / test-thorough (cloudwatch-demo) (push) Has been cancelled
CI / test-thorough (flink-ecs) (push) Has been cancelled
CI / test-thorough (upstream-lambda) (push) Has been cancelled
CI / test-thorough (prefect-ecs-fargate) (push) Has been cancelled
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Has been cancelled
Benchmark image — build + push to ECR (any adapter) / build + push (push) Has been cancelled
CI / quality (ubuntu-latest) (push) Has been cancelled
CI / test (tools-runtime) (push) Has been cancelled
CI / test (e2e-general) (push) Has been cancelled
CI / test (cli-runtime) (push) Has been cancelled
CI / test (e2e-provider-and-openclaw) (push) Has been cancelled
CI / test (integrations-and-misc) (push) Has been cancelled
Release / verify (push) Has been cancelled
Release / build-python-dist (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Has been cancelled
Release / publish-release (push) Has been cancelled
Release / publish-main-release (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Has been cancelled
Release / prepare (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:10:45 +08:00

319 行
12 KiB
Python

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
"""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