Files
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

372 lines
14 KiB
Python

"""Tests for the Claude Code JSONL token source (#2023)."""
from __future__ import annotations
import time
from pathlib import Path
import psutil
import pytest
from tools.system.fleet_monitoring.token_sources.claude_code import ClaudeCodeJsonlSource
def _mangled_dir(cwd: Path) -> str:
return str(cwd).replace("/", "-")
@pytest.fixture
def projects_root(tmp_path: Path) -> Path:
"""Isolated ``~/.claude/projects`` per test."""
root = tmp_path / "claude_projects"
root.mkdir()
return root
@pytest.fixture
def source(projects_root: Path) -> ClaudeCodeJsonlSource:
return ClaudeCodeJsonlSource(projects_root=projects_root)
@pytest.fixture(autouse=True)
def _no_open_files(monkeypatch: pytest.MonkeyPatch) -> None:
"""Default: pretend the test PID has no open files.
Without this stub, CI runners may have a live process with the
fake PID (1234) holding fds we can't predict, which would let
the source's ``_session_file_for_pid`` pick a wrong JSONL.
Tests that need a specific ``open_files`` set override this.
"""
monkeypatch.setattr(
"tools.system.fleet_monitoring.token_sources.claude_code.open_files_for_pid",
lambda _pid: (),
)
def _patch_cwd(monkeypatch: pytest.MonkeyPatch, cwd: Path) -> None:
"""Make ``cwd_for_pid`` return ``cwd`` for any PID.
The source no longer imports ``psutil`` directly (per the #1489
acceptance criterion that confines psutil to ``tools/system/fleet_monitoring/probe.py``);
it calls :func:`tools.system.fleet_monitoring.probe.cwd_for_pid`. The test patches
the helper at the source's import site so the unit test stays
decoupled from psutil internals.
"""
monkeypatch.setattr(
"tools.system.fleet_monitoring.token_sources.claude_code.cwd_for_pid",
lambda _pid: cwd,
)
def _patch_open_files(monkeypatch: pytest.MonkeyPatch, *paths: Path) -> None:
"""Make ``open_files_for_pid`` return ``paths`` for any PID.
The source only attributes a JSONL to a PID when the PID has its
fd open — the misattribution-prone fallback to ``newest-by-mtime``
was removed in response to Greptile's review. Tests that expect
resolution to succeed must declare which session the fake PID is
"holding".
"""
monkeypatch.setattr(
"tools.system.fleet_monitoring.token_sources.claude_code.open_files_for_pid",
lambda _pid: paths,
)
def _patch_cwd_raises(monkeypatch: pytest.MonkeyPatch, exc: type[BaseException]) -> None:
"""Simulate the psutil failure path: ``cwd_for_pid`` returns ``None``.
The ``exc`` parameter is kept for caller-side semantics
(each test names the kind of failure it intends to model) even
though ``cwd_for_pid`` collapses them all to ``None`` — exposes
the exception type in the test name so the failure mode under
test is greppable.
"""
del exc # All psutil failure paths collapse to None via cwd_for_pid.
monkeypatch.setattr(
"tools.system.fleet_monitoring.token_sources.claude_code.cwd_for_pid",
lambda _pid: None,
)
class TestFirstCallResolution:
def test_returns_none_when_psutil_denies_cwd(
self,
source: ClaudeCodeJsonlSource,
monkeypatch: pytest.MonkeyPatch,
) -> None:
# macOS hardened-runtime apps can deny ``Process.cwd()`` even
# cross-user-same-user. The source must degrade silently — the
# dashboard already renders ``-`` for unobservable PIDs.
_patch_cwd_raises(monkeypatch, psutil.AccessDenied)
assert source.read_new_chunk(1234) is None
def test_returns_none_when_no_such_process(
self,
source: ClaudeCodeJsonlSource,
monkeypatch: pytest.MonkeyPatch,
) -> None:
_patch_cwd_raises(monkeypatch, psutil.NoSuchProcess)
assert source.read_new_chunk(1234) is None
def test_returns_none_when_project_dir_missing(
self,
source: ClaudeCodeJsonlSource,
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
# The cwd resolves but the project dir does not exist yet
# (claude-code has not written anything). The source must NOT
# cache an empty resolution — it will resolve next tick when
# the directory shows up.
_patch_cwd(monkeypatch, tmp_path / "fresh-project")
assert source.read_new_chunk(1234) is None
# Subsequent calls retry; the previous None must not have
# poisoned the cache.
assert source.read_new_chunk(1234) is None
def test_first_call_seeks_to_eof_and_returns_empty_string(
self,
source: ClaudeCodeJsonlSource,
monkeypatch: pytest.MonkeyPatch,
projects_root: Path,
tmp_path: Path,
) -> None:
# Attaching to a long-running session should NOT retro-price
# historical content; the first call seeks to EOF and the
# next read picks up only new appends.
cwd = tmp_path / "myrepo"
cwd.mkdir()
project_dir = projects_root / _mangled_dir(cwd)
project_dir.mkdir()
session = project_dir / "session-abc.jsonl"
session.write_text("historical line\n" * 50, encoding="utf-8")
_patch_cwd(monkeypatch, cwd)
_patch_open_files(monkeypatch, session)
first = source.read_new_chunk(1234)
# ``""`` (not None): the PID is now resolved but there is
# nothing new since cold start.
assert first == ""
def test_picks_newest_jsonl_when_pid_holds_multiple(
self,
source: ClaudeCodeJsonlSource,
monkeypatch: pytest.MonkeyPatch,
projects_root: Path,
tmp_path: Path,
) -> None:
# A PID can briefly hold fds on two JSONLs (an old session it
# is finishing flushing, plus the new one it is writing).
# When multiple fd-matching candidates exist, the source must
# pick the newest by mtime — the active session — never the
# stale one.
cwd = tmp_path / "myrepo"
cwd.mkdir()
project_dir = projects_root / _mangled_dir(cwd)
project_dir.mkdir()
older = project_dir / "old.jsonl"
older.write_text("old\n", encoding="utf-8")
# Force a clearly different mtime so ``max(key=mtime)`` is
# deterministic regardless of filesystem timestamp granularity.
old_time = time.time() - 100
import os as _os
_os.utime(older, (old_time, old_time))
newer = project_dir / "new.jsonl"
newer.write_text("new\n", encoding="utf-8")
_patch_cwd(monkeypatch, cwd)
_patch_open_files(monkeypatch, older, newer)
# First call seeks to EOF on the newest file. We then append
# to *that* file and confirm the next read returns that
# appended content (proving the newer file was picked).
source.read_new_chunk(1234)
with newer.open("a", encoding="utf-8") as fh:
fh.write("appended\n")
assert source.read_new_chunk(1234) == "appended\n"
def test_returns_none_when_pid_holds_no_jsonl_fd(
self,
source: ClaudeCodeJsonlSource,
monkeypatch: pytest.MonkeyPatch,
projects_root: Path,
tmp_path: Path,
) -> None:
# Mirror of the codex source contract: when there is no
# fd-level evidence that this PID owns a JSONL under the
# project dir, return ``None`` rather than falling back to a
# global ``newest-by-mtime`` pick. The old behaviour
# silently misattributed another session's tokens; the
# dashboard now honestly renders ``-`` instead.
cwd = tmp_path / "myrepo"
cwd.mkdir()
project_dir = projects_root / _mangled_dir(cwd)
project_dir.mkdir()
leftover = project_dir / "someone-elses.jsonl"
leftover.write_text("not mine\n", encoding="utf-8")
_patch_cwd(monkeypatch, cwd)
# ``open_files_for_pid`` returns ``()`` via the autouse
# fixture — no fds for this PID.
assert source.read_new_chunk(1234) is None
# Retry next tick must also stay ``None`` (no poisoned cache).
assert source.read_new_chunk(1234) is None
def test_prefers_pid_open_file_when_cwd_has_multiple_sessions(
self,
source: ClaudeCodeJsonlSource,
monkeypatch: pytest.MonkeyPatch,
projects_root: Path,
tmp_path: Path,
) -> None:
cwd = tmp_path / "myrepo"
cwd.mkdir()
project_dir = projects_root / _mangled_dir(cwd)
project_dir.mkdir()
older = project_dir / "older-session.jsonl"
older.write_text("older historical\n", encoding="utf-8")
old_time = time.time() - 100
import os as _os
_os.utime(older, (old_time, old_time))
newer = project_dir / "newer-session.jsonl"
newer.write_text("newer historical\n", encoding="utf-8")
_patch_cwd(monkeypatch, cwd)
monkeypatch.setattr(
"tools.system.fleet_monitoring.token_sources.claude_code.open_files_for_pid",
lambda pid: (older,) if pid == 1111 else (newer,),
)
assert source.read_new_chunk(1111) == ""
assert source.read_new_chunk(2222) == ""
with older.open("a", encoding="utf-8") as fh:
fh.write("older append\n")
with newer.open("a", encoding="utf-8") as fh:
fh.write("newer append\n")
assert source.read_new_chunk(1111) == "older append\n"
assert source.read_new_chunk(2222) == "newer append\n"
class TestIncrementalReads:
def test_returns_empty_string_when_nothing_appended(
self,
source: ClaudeCodeJsonlSource,
monkeypatch: pytest.MonkeyPatch,
projects_root: Path,
tmp_path: Path,
) -> None:
cwd = tmp_path / "myrepo"
cwd.mkdir()
project_dir = projects_root / _mangled_dir(cwd)
project_dir.mkdir()
session = project_dir / "session.jsonl"
session.write_text("initial\n", encoding="utf-8")
_patch_cwd(monkeypatch, cwd)
_patch_open_files(monkeypatch, session)
source.read_new_chunk(1234)
# Distinct from ``None``: source is observing, nothing new.
# The wiring layer forwards ``""`` to the meter (returns 0)
# which records an idle observation.
assert source.read_new_chunk(1234) == ""
def test_returns_only_appended_bytes(
self,
source: ClaudeCodeJsonlSource,
monkeypatch: pytest.MonkeyPatch,
projects_root: Path,
tmp_path: Path,
) -> None:
cwd = tmp_path / "myrepo"
cwd.mkdir()
project_dir = projects_root / _mangled_dir(cwd)
project_dir.mkdir()
session = project_dir / "session.jsonl"
session.write_text("turn1\n", encoding="utf-8")
_patch_cwd(monkeypatch, cwd)
_patch_open_files(monkeypatch, session)
source.read_new_chunk(1234) # seeks to EOF
with session.open("a", encoding="utf-8") as fh:
fh.write("turn2\n")
assert source.read_new_chunk(1234) == "turn2\n"
# Second read with no further appends → empty string, not a re-read.
assert source.read_new_chunk(1234) == ""
class TestRotation:
def test_inode_change_resets_offset(
self,
source: ClaudeCodeJsonlSource,
monkeypatch: pytest.MonkeyPatch,
projects_root: Path,
tmp_path: Path,
) -> None:
# A new claude-code session under the same cwd writes a new
# JSONL; depending on the user's workflow they may also
# delete and recreate the same filename. The inode-change
# heuristic catches both.
cwd = tmp_path / "myrepo"
cwd.mkdir()
project_dir = projects_root / _mangled_dir(cwd)
project_dir.mkdir()
session = project_dir / "session.jsonl"
session.write_text("first session\n", encoding="utf-8")
_patch_cwd(monkeypatch, cwd)
_patch_open_files(monkeypatch, session)
source.read_new_chunk(1234)
# Delete + recreate with new content. The new inode + smaller
# file means our cached offset points past EOF; the source
# must detect this and start over.
session.unlink()
session.write_text("rebooted\n", encoding="utf-8")
# First call after rotation: returns "" because the rotation
# detector resets state. Next call returns appended bytes.
assert source.read_new_chunk(1234) == ""
with session.open("a", encoding="utf-8") as fh:
fh.write("after-reboot\n")
assert source.read_new_chunk(1234) == "after-reboot\n"
class TestForget:
def test_forget_clears_per_pid_state(
self,
source: ClaudeCodeJsonlSource,
monkeypatch: pytest.MonkeyPatch,
projects_root: Path,
tmp_path: Path,
) -> None:
cwd = tmp_path / "myrepo"
cwd.mkdir()
project_dir = projects_root / _mangled_dir(cwd)
project_dir.mkdir()
session = project_dir / "session.jsonl"
session.write_text("turn1\nturn2\n", encoding="utf-8")
_patch_cwd(monkeypatch, cwd)
_patch_open_files(monkeypatch, session)
source.read_new_chunk(1234) # cache state, EOF = end of two lines
source.forget(1234)
# After forget, the next call resolves fresh. Append before
# the next call so we can assert resolution worked.
with session.open("a", encoding="utf-8") as fh:
fh.write("turn3\n")
# First call after forget seeks to EOF again (so ``turn3`` is
# part of the historical content from this PID's view). The
# contract guarantees no retro-pricing.
assert source.read_new_chunk(1234) == ""
def test_forget_unknown_pid_is_silent(self, source: ClaudeCodeJsonlSource) -> None:
# The sampler GC pass calls ``forget`` on every disappeared
# PID; it must not raise for PIDs the source never resolved.
source.forget(99999)