chore: import upstream snapshot with attribution
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

This commit is contained in:
wehub-resource-sync
2026-07-13 13:10:45 +08:00
commit 4b6817381b
3933 changed files with 525247 additions and 0 deletions
@@ -0,0 +1,443 @@
"""End-to-end /resume scenario tests: session identity, JSONL turns, slash recording."""
from __future__ import annotations
import io
import json
import os
from pathlib import Path
from unittest.mock import patch
import pytest
from pydantic import ValidationError
from rich.console import Console
from config.config import (
get_configured_llm_provider,
get_llm_provider_api_key_env,
resolve_llm_settings_verbose,
)
from config.llm_auth.credentials import status as credential_status
from core.agent_harness.session import JsonlSessionStorage
from surfaces.interactive_shell.command_registry import dispatch_slash
from surfaces.interactive_shell.session import Session
SessionStore = JsonlSessionStorage()
def _capture() -> tuple[Console, io.StringIO]:
buf = io.StringIO()
return Console(file=buf, force_terminal=False, highlight=False), buf
def _read_turns(path: Path) -> list[dict]:
if not path.exists():
return []
turns: list[dict] = []
for line in path.read_text(encoding="utf-8").splitlines():
if not line.strip():
continue
rec = json.loads(line)
if rec.get("type") == "custom_message" and rec.get("custom_type") == "turn_stub":
turns.append({"type": "turn", "kind": rec.get("kind"), "text": rec.get("text")})
return turns
def _write_finalized_session(
sessions_dir: Path,
session_id: str,
*,
chat_text: str = "why is redis slow?",
messages: list[tuple[str, str]] | None = None,
context: dict[str, str] | None = None,
) -> Path:
"""Create a closed session file that /resume can load."""
path = sessions_dir / f"{session_id}.jsonl"
msgs = messages or [("user", chat_text), ("assistant", "check connection pool")]
ctx = context or {"service": "redis"}
ids = [f"entry{i}" for i in range(1, 6)]
lines = [
json.dumps(
{
"type": "session",
"version": 2,
"id": session_id,
"created_at": "2026-05-29T10:00:00+00:00",
"cwd": "",
}
),
json.dumps(
{
"id": ids[0],
"parent_id": None,
"timestamp": "2026-05-29T10:00:01+00:00",
"type": "custom_message",
"custom_type": "turn_stub",
"kind": "chat",
"text": chat_text,
"display": False,
}
),
json.dumps(
{
"id": ids[1],
"parent_id": ids[0],
"timestamp": "2026-05-29T10:00:02+00:00",
"type": "message",
"role": "user",
"content": msgs[0][1],
"metadata": {"kind": "chat"},
}
),
json.dumps(
{
"id": ids[2],
"parent_id": ids[1],
"timestamp": "2026-05-29T10:00:03+00:00",
"type": "message",
"role": "assistant",
"content": msgs[1][1],
"metadata": {"kind": "chat"},
}
),
json.dumps(
{
"id": ids[3],
"parent_id": ids[2],
"timestamp": "2026-05-29T10:00:04+00:00",
"type": "custom_message",
"custom_type": "accumulated_context",
"content": ctx,
"display": False,
}
),
json.dumps(
{
"id": ids[4],
"parent_id": ids[3],
"timestamp": "2026-05-29T10:00:05+00:00",
"type": "leaf",
"total_turns": 1,
}
),
]
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
return path
@pytest.fixture
def isolated_sessions(tmp_path: Path) -> Path:
"""Sessions directory with SessionStore patched for the test."""
directory = tmp_path / "sessions"
directory.mkdir()
with patch("config.constants.OPENSRE_HOME_DIR", tmp_path):
yield directory
def _open_current(session: Session) -> None:
SessionStore.open_session(session)
def _require_live_llm_for_repl_planner() -> None:
explicit_pin = os.environ.get("LLM_PROVIDER", "").strip().lower()
resolution = None
try:
resolution = resolve_llm_settings_verbose()
except ValidationError as exc:
provider = get_configured_llm_provider()
env_var = get_llm_provider_api_key_env(provider)
msg = exc.errors()[0].get("msg", str(exc)) if exc.errors() else str(exc)
hint = f" configured provider={provider!r}"
if env_var is not None:
hint += f", required key={env_var}"
pytest.skip(f"Skipping live REPL planner smoke; missing LLM configuration:{hint}. {msg}")
if resolution is None:
pytest.skip("Skipping live REPL planner smoke; LLM configuration was not resolved.")
if explicit_pin and resolution.resolved_provider != explicit_pin:
pytest.skip(
f"Skipping live REPL planner smoke; LLM_PROVIDER={explicit_pin!r} resolved as "
f"{resolution.resolved_provider!r}."
)
auth = credential_status(resolution.resolved_provider)
if not auth.configured or auth.stale:
pytest.skip(
"Skipping live REPL planner smoke; missing LLM credentials:"
f" provider={resolution.resolved_provider!r}, auth={auth.source}, detail={auth.detail}"
)
env_var = get_llm_provider_api_key_env(resolution.resolved_provider)
if env_var is not None and not os.environ.get(env_var):
pytest.skip(
f"Skipping live REPL planner smoke; {env_var} is not exported to the child REPL."
)
class TestResumeScenarioMatrix:
"""Scenario coverage for /resume session adoption and JSONL persistence."""
def test_scenario_fresh_repl_resumes_prior_session(self, isolated_sessions: Path) -> None:
"""Fresh REPL session resumes a prior session: adopt ID, slash on target only."""
target_id = "aaaa1111-2222-3333-4444-555566667777"
_write_finalized_session(isolated_sessions, target_id, chat_text="why is redis slow?")
session = Session()
current_id = session.session_id
_open_current(session)
console, buf = _capture()
assert dispatch_slash(f"/resume {target_id[:8]}", session, console) is True
assert session.session_id == target_id
assert "resumed session" in buf.getvalue()
current_path = isolated_sessions / f"{current_id}.jsonl"
if current_path.exists():
assert not any(
t.get("kind") == "slash" and "/resume" in t.get("text", "")
for t in _read_turns(current_path)
)
target_turns = _read_turns(isolated_sessions / f"{target_id}.jsonl")
assert any(
t.get("kind") == "slash" and t.get("text", "").startswith("/resume")
for t in target_turns
)
assert session.agent.messages[0] == ("user", "why is redis slow?")
assert session.accumulated_context == {"service": "redis"}
def test_scenario_post_resume_slash_and_chat_append_to_target(
self,
isolated_sessions: Path,
) -> None:
"""After /resume, further slash commands and chat turns land on the same file."""
target_id = "bbbb2222-3333-4444-5555-666677778888"
_write_finalized_session(isolated_sessions, target_id)
session = Session()
_open_current(session)
console, _ = _capture()
dispatch_slash(f"/resume {target_id[:8]}", session, console)
dispatch_slash("/status", session, console)
session.record("chat", "follow-up question after resume")
target_path = isolated_sessions / f"{target_id}.jsonl"
kinds = [t["kind"] for t in _read_turns(target_path)]
assert "slash" in kinds
assert kinds.count("slash") >= 2
assert "chat" in kinds
assert "leaf" in target_path.read_text(encoding="utf-8")
def test_scenario_empty_starter_session_removed_on_resume(
self,
isolated_sessions: Path,
) -> None:
"""A brand-new REPL with no turns should not leave a junk file after /resume."""
target_id = "cccc3333-4444-5555-6666-777788889999"
_write_finalized_session(isolated_sessions, target_id)
session = Session()
starter_id = session.session_id
_open_current(session)
console, _ = _capture()
dispatch_slash(f"/resume {target_id[:8]}", session, console)
starter_path = isolated_sessions / f"{starter_id}.jsonl"
assert not starter_path.exists()
assert session.session_id == target_id
def test_scenario_resume_by_name_substring(self, isolated_sessions: Path) -> None:
target_id = "dddd4444-5555-6666-7777-888899990000"
_write_finalized_session(isolated_sessions, target_id, chat_text="investigate OOM killer")
session = Session()
_open_current(session)
console, buf = _capture()
dispatch_slash("/resume OOM", session, console)
assert session.session_id == target_id
assert "resumed session" in buf.getvalue()
target_turns = _read_turns(isolated_sessions / f"{target_id}.jsonl")
assert any(t.get("text") == "/resume OOM" for t in target_turns)
def test_resume_session_by_prefix_matches_name_substring(
self,
isolated_sessions: Path,
) -> None:
from surfaces.interactive_shell.command_registry.session_cmds.resume import (
resume_session_by_prefix,
)
target_id = "eeee5555-6666-7777-8888-999900001111"
_write_finalized_session(isolated_sessions, target_id, chat_text="investigate OOM killer")
session = Session()
_open_current(session)
console, buf = _capture()
assert resume_session_by_prefix("OOM", session, console)
assert session.session_id == target_id
assert "resumed session" in buf.getvalue()
def test_scenario_resume_not_found_records_on_current(
self,
isolated_sessions: Path,
) -> None:
session = Session()
current_id = session.session_id
_open_current(session)
console, buf = _capture()
dispatch_slash("/resume deadbeef", session, console)
assert session.session_id == current_id
assert "not found" in buf.getvalue()
turns = _read_turns(isolated_sessions / f"{current_id}.jsonl")
assert turns[-1]["kind"] == "slash"
assert turns[-1]["text"] == "/resume deadbeef"
assert session.history[-1]["ok"] is False
def test_scenario_resume_current_session_guard(
self,
isolated_sessions: Path,
) -> None:
session = Session()
_open_current(session)
session.record("chat", "still working here")
console, buf = _capture()
dispatch_slash(f"/resume {session.session_id[:8]}", session, console)
assert "current session" in buf.getvalue()
assert session.session_id # unchanged
turns = _read_turns(isolated_sessions / f"{session.session_id}.jsonl")
assert any(t.get("text", "").startswith("/resume") for t in turns)
def test_scenario_resume_empty_target_does_not_switch(
self,
isolated_sessions: Path,
) -> None:
empty_id = "eeee5555-6666-7777-8888-999900001111"
path = isolated_sessions / f"{empty_id}.jsonl"
path.write_text(
json.dumps(
{
"type": "session",
"version": 2,
"id": empty_id,
"created_at": "2026-05-29T10:00:00+00:00",
"cwd": "",
}
)
+ "\n",
encoding="utf-8",
)
session = Session()
current_id = session.session_id
_open_current(session)
console, buf = _capture()
dispatch_slash(f"/resume {empty_id[:8]}", session, console)
assert session.session_id == current_id
assert "no conversation to resume" in buf.getvalue()
def test_scenario_chain_resume_two_targets(
self,
isolated_sessions: Path,
) -> None:
"""Resume session A, then resume session B — each gets its own /resume slash turn."""
id_a = "ffff6666-7777-8888-9999-000011112222"
id_b = "11117777-8888-9999-0000-111122223333"
_write_finalized_session(isolated_sessions, id_a, chat_text="session A question")
_write_finalized_session(isolated_sessions, id_b, chat_text="session B question")
session = Session()
_open_current(session)
console, _ = _capture()
dispatch_slash(f"/resume {id_a[:8]}", session, console)
assert session.session_id == id_a
dispatch_slash(f"/resume {id_b[:8]}", session, console)
assert session.session_id == id_b
turns_a = _read_turns(isolated_sessions / f"{id_a}.jsonl")
turns_b = _read_turns(isolated_sessions / f"{id_b}.jsonl")
assert any("/resume" in t.get("text", "") for t in turns_a)
assert any("/resume" in t.get("text", "") for t in turns_b)
assert session.agent.messages[0] == ("user", "session B question")
def test_scenario_active_session_with_turns_flushed_without_resume_slash(
self,
isolated_sessions: Path,
) -> None:
"""Switching away from a session that had real turns preserves them without /resume."""
target_id = "22228888-9999-0000-1111-222233334444"
_write_finalized_session(isolated_sessions, target_id)
session = Session()
current_id = session.session_id
_open_current(session)
session.record("chat", "work in progress")
console, _ = _capture()
dispatch_slash(f"/resume {target_id[:8]}", session, console)
old_turns = _read_turns(isolated_sessions / f"{current_id}.jsonl")
assert any(t["kind"] == "chat" for t in old_turns)
assert not any(t.get("kind") == "slash" for t in old_turns)
assert (isolated_sessions / f"{current_id}.jsonl").read_text(encoding="utf-8").find(
'"type": "leaf"'
) != -1
@pytest.mark.integration
@pytest.mark.live_llm
class TestResumeLiveRepl:
"""Live REPL smoke test via ReplDriver with isolated HOME and real planner."""
@pytest.mark.skip(
reason="Flaky live-LLM REPL round-trip (subprocess + 60s waits); resume load "
"logic is covered offline. Run manually via ReplDriver."
)
def test_live_resume_round_trip(self, tmp_path: Path) -> None:
_require_live_llm_for_repl_planner()
from tests.utils.repl_driver import ReplDriver
home = tmp_path / "home"
home.mkdir()
sessions_dir = home / ".opensre" / "sessions"
sessions_dir.mkdir(parents=True)
target_id = "live9999-aaaa-bbbb-cccc-ddddeeeeffff"
_write_finalized_session(sessions_dir, target_id, chat_text="live redis investigation")
with ReplDriver(home=home, startup_wait=10.0) as repl:
if repl.contains("Press Enter to continue"):
repl.send("", wait=3.0)
repl.reset_output()
repl.send("/sessions", wait=1.0)
assert repl.wait_until_contains("live9999", "live redis", timeout=60.0)
repl.reset_output()
repl.send(f"/resume {target_id[:8]}", wait=1.0)
assert repl.wait_until_contains("resumed session", timeout=60.0)
assert repl.wait_until_contains("live redis investigation", timeout=10.0)
repl.reset_output()
repl.send("/status", wait=1.0)
assert repl.wait_until_contains("interactions", timeout=60.0)
target_path = sessions_dir / f"{target_id}.jsonl"
assert target_path.exists()
turns = _read_turns(target_path)
assert any(t.get("kind") == "slash" and "/resume" in t.get("text", "") for t in turns)
assert any(t.get("kind") == "slash" and "/status" in t.get("text", "") for t in turns)
@@ -0,0 +1,966 @@
"""Tests for SessionStore: incremental writes (open_session, append_turn, flush, load_recent)."""
from __future__ import annotations
import json
import time
import uuid
from pathlib import Path
from unittest.mock import patch
import pytest
from core.agent_harness.session import (
JsonlSessionRepo,
JsonlSessionStorage,
)
from core.agent_harness.session.persistence.paths import sessions_dir as _sessions_dir
from surfaces.interactive_shell.session import (
Session,
)
class _SessionStoreFacade(JsonlSessionStorage, JsonlSessionRepo):
"""Test facade exposing both the storage and repo APIs on one object."""
SessionStore = _SessionStoreFacade()
# ── helpers ───────────────────────────────────────────────────────────────────
def _make_session() -> Session:
return Session()
def _read_lines(path: Path) -> list[dict]:
return [json.loads(line) for line in path.read_text().splitlines() if line.strip()]
def _turn_stubs(records: list[dict]) -> list[dict]:
return [
record
for record in records
if record.get("type") == "custom_message" and record.get("custom_type") == "turn_stub"
]
def _write_v2_session(path: Path, sid: str, *, started_at: str, text: str = "hi") -> None:
path.write_text(
"\n".join(
[
json.dumps(
{
"type": "session",
"version": 2,
"id": sid,
"created_at": started_at,
"cwd": "",
}
),
json.dumps(
{
"id": "entry1",
"parent_id": None,
"timestamp": started_at,
"type": "custom_message",
"custom_type": "turn_stub",
"kind": "chat",
"text": text,
"display": False,
}
),
]
)
+ "\n"
)
def _patch_dir(tmp_path: Path):
return patch("core.agent_harness.session.persistence.paths.sessions_dir", return_value=tmp_path)
@pytest.fixture
def tmp_home(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
"""Redirect OpenSRE's home to a real temp dir so SessionStore reads/writes real files.
No mocking: ``_sessions_dir()`` resolves the real ``OPENSRE_HOME_DIR`` constant
on every call, so pointing it at a temp directory exercises the genuine
filesystem path end to end.
"""
monkeypatch.setattr("config.constants.OPENSRE_HOME_DIR", tmp_path)
return tmp_path
# ── open_session ──────────────────────────────────────────────────────────────
def test_open_session_creates_file_with_session_start(tmp_path: Path) -> None:
session = _make_session()
with _patch_dir(tmp_path):
SessionStore.open_session(session)
files = list(tmp_path.glob("*.jsonl"))
assert len(files) == 1
records = _read_lines(files[0])
assert records[0]["type"] == "session"
assert records[0]["version"] == 2
assert records[0]["id"] == session.session_id
def test_open_session_uses_session_id_as_filename(tmp_path: Path) -> None:
session = _make_session()
with _patch_dir(tmp_path):
SessionStore.open_session(session)
assert (tmp_path / f"{session.session_id}.jsonl").exists()
def test_open_session_never_raises_on_bad_path() -> None:
session = _make_session()
with patch(
"core.agent_harness.session.persistence.paths.sessions_dir",
return_value=Path("/nonexistent/cannot/write"),
):
SessionStore.open_session(session) # must not raise
# ── append_turn ───────────────────────────────────────────────────────────────
def test_append_turn_adds_record_to_existing_file(tmp_path: Path) -> None:
session = _make_session()
with _patch_dir(tmp_path):
SessionStore.open_session(session)
SessionStore.append_turn(session, "chat", "hello world")
SessionStore.append_turn(session, "alert", "HighCPU on prod")
records = _read_lines(tmp_path / f"{session.session_id}.jsonl")
turns = _turn_stubs(records)
assert len(turns) == 2
assert turns[0]["kind"] == "chat"
assert turns[0]["text"] == "hello world"
assert turns[1]["kind"] == "alert"
assert turns[1]["text"] == "HighCPU on prod"
def test_append_turn_stores_full_text_without_truncation(tmp_path: Path) -> None:
session = _make_session()
long_text = "x" * 500
with _patch_dir(tmp_path):
SessionStore.open_session(session)
SessionStore.append_turn(session, "chat", long_text)
records = _read_lines(tmp_path / f"{session.session_id}.jsonl")
turn = next(r for r in records if r["type"] == "custom_message")
assert len(turn["text"]) == 500
def test_append_turn_noop_when_file_missing(tmp_path: Path) -> None:
session = _make_session()
with _patch_dir(tmp_path):
# Do NOT call open_session — file doesn't exist
SessionStore.append_turn(session, "chat", "hello")
assert not list(tmp_path.glob("*.jsonl")), "no file should be created"
# ── append_turn_detail ────────────────────────────────────────────────────────
def test_append_turn_detail_writes_full_prompt_and_response(tmp_path: Path) -> None:
session = _make_session()
with _patch_dir(tmp_path):
SessionStore.open_session(session)
SessionStore.append_turn_detail(
session.session_id,
"chat",
"how do I debug high CPU?",
response="Root cause is a memory leak.",
turn_id="abc-123",
model="claude-3-5",
provider="anthropic",
latency_ms=1500,
)
records = _read_lines(tmp_path / f"{session.session_id}.jsonl")
messages = [r for r in records if r["type"] == "message"]
assert [(m["role"], m["content"]) for m in messages] == [
("user", "how do I debug high CPU?"),
("assistant", "Root cause is a memory leak."),
]
assert messages[0]["metadata"]["kind"] == "chat"
assert messages[0]["metadata"]["turn_id"] == "abc-123"
assert messages[0]["metadata"]["model"] == "claude-3-5"
assert messages[0]["metadata"]["provider"] == "anthropic"
assert messages[0]["metadata"]["latency_ms"] == 1500
def test_append_turn_detail_stores_system_prompt_metadata(tmp_path: Path) -> None:
session = _make_session()
with _patch_dir(tmp_path):
SessionStore.open_session(session)
SessionStore.append_turn_detail(
session.session_id,
"chat",
"question",
response="answer",
system_prompt="assistant system block",
)
records = _read_lines(tmp_path / f"{session.session_id}.jsonl")
messages = [r for r in records if r["type"] == "message"]
assert messages[0]["metadata"]["system_prompt"] == "assistant system block"
def test_append_turn_detail_omits_none_fields(tmp_path: Path) -> None:
session = _make_session()
with _patch_dir(tmp_path):
SessionStore.open_session(session)
SessionStore.append_turn_detail(session.session_id, "chat", "hi")
records = _read_lines(tmp_path / f"{session.session_id}.jsonl")
detail = next(r for r in records if r["type"] == "message")
assert detail["role"] == "user"
assert detail["content"] == "hi"
assert "turn_id" not in detail["metadata"]
assert "model" not in detail["metadata"]
def test_append_turn_detail_noop_when_file_missing(tmp_path: Path) -> None:
with _patch_dir(tmp_path):
SessionStore.append_turn_detail("nonexistent-id", "chat", "hi")
assert not list(tmp_path.glob("*.jsonl"))
# ── append_tool_call ──────────────────────────────────────────────────────────
def test_append_tool_call_writes_record(tmp_home: Path) -> None:
session = _make_session()
SessionStore.open_session(session)
SessionStore.append_tool_call(
session.session_id,
tool="call_posthog_tool",
arguments={"tool": "execute-sql", "args": {"query": "select 1"}},
result='{"rows": []}',
ok=True,
source="posthog_mcp",
)
records = _read_lines(_sessions_dir() / f"{session.session_id}.jsonl")
call = next(r for r in records if r["type"] == "tool_call")
result = next(r for r in records if r["type"] == "tool_result")
assert call["tool"] == "call_posthog_tool"
assert call["arguments"] == {"tool": "execute-sql", "args": {"query": "select 1"}}
assert result["content"] == '{"rows": []}'
assert result["ok"] is True
assert call["source"] == "posthog_mcp"
assert "timestamp" in call
def test_append_tool_call_omits_source_when_none(tmp_home: Path) -> None:
session = _make_session()
SessionStore.open_session(session)
SessionStore.append_tool_call(
session.session_id,
tool="list_posthog_tools",
arguments={},
result="error: boom",
ok=False,
)
records = _read_lines(_sessions_dir() / f"{session.session_id}.jsonl")
result = next(r for r in records if r["type"] == "tool_result")
assert result["ok"] is False
call = next(r for r in records if r["type"] == "tool_call")
assert "source" not in call
def test_append_tool_call_noop_when_file_missing(tmp_home: Path) -> None:
SessionStore.append_tool_call("nonexistent-id", tool="t", arguments={}, result="r", ok=True)
assert not list(_sessions_dir().glob("*.jsonl"))
def test_append_tool_call_reopens_finalized_session(tmp_home: Path) -> None:
session = _make_session()
SessionStore.open_session(session)
SessionStore.append_turn(session, "cli_agent", "events for davincios in posthog")
SessionStore.flush(session)
# A late tool-call write (e.g. background gather) must reopen the file.
SessionStore.append_tool_call(
session.session_id, tool="call_posthog_tool", arguments={}, result="{}", ok=True
)
records = _read_lines(_sessions_dir() / f"{session.session_id}.jsonl")
assert any(r["type"] == "tool_call" for r in records)
# ── flush ─────────────────────────────────────────────────────────────────────
def test_flush_writes_session_end(tmp_path: Path) -> None:
session = _make_session()
with _patch_dir(tmp_path):
SessionStore.open_session(session)
SessionStore.append_turn(session, "chat", "q1")
SessionStore.append_turn(session, "alert", "alert1")
SessionStore.flush(session)
records = _read_lines(tmp_path / f"{session.session_id}.jsonl")
leaf = records[-1]
assert leaf["type"] == "leaf"
assert leaf["total_turns"] == 2
assert leaf["chat_turns"] == 1
assert leaf["investigation_turns"] == 1
def test_flush_counts_cli_agent_turns_as_chat(tmp_path: Path) -> None:
"""Chat turns recorded as kind='cli_agent' must count as chat_turns."""
session = _make_session()
with _patch_dir(tmp_path):
SessionStore.open_session(session)
SessionStore.append_turn(session, "cli_agent", "why is redis slow?")
SessionStore.append_turn(session, "chat", "how do I use /resume?")
SessionStore.append_turn(session, "follow_up", "what else?")
SessionStore.flush(session)
records = _read_lines(tmp_path / f"{session.session_id}.jsonl")
leaf = records[-1]
assert leaf["chat_turns"] == 3
assert leaf["investigation_turns"] == 0
def test_flush_writes_conversation_snapshot_when_messages_present(tmp_path: Path) -> None:
session = _make_session()
session.agent.messages = [("user", "hello"), ("assistant", "hi there")]
session.accumulated_context = {"service": "api", "cluster": "prod"}
with _patch_dir(tmp_path):
SessionStore.open_session(session)
SessionStore.append_turn(session, "chat", "hello")
SessionStore.flush(session)
records = _read_lines(tmp_path / f"{session.session_id}.jsonl")
messages = [r for r in records if r["type"] == "message"]
context = next(
r
for r in records
if r.get("type") == "custom_message" and r.get("custom_type") == "accumulated_context"
)
assert [(m["role"], m["content"]) for m in messages] == [
("user", "hello"),
("assistant", "hi there"),
]
assert context["content"] == {"service": "api", "cluster": "prod"}
# persisted branch entries must come before leaf
types = [r["type"] for r in records]
assert types.index("message") < types.index("leaf")
def test_flush_skips_snapshot_when_no_messages(tmp_path: Path) -> None:
session = _make_session()
with _patch_dir(tmp_path):
SessionStore.open_session(session)
SessionStore.append_turn(session, "chat", "hi")
SessionStore.flush(session)
records = _read_lines(tmp_path / f"{session.session_id}.jsonl")
assert not any(r["type"] == "message" for r in records)
def test_flush_deletes_file_when_no_turns(tmp_path: Path) -> None:
session = _make_session()
with _patch_dir(tmp_path):
SessionStore.open_session(session)
SessionStore.flush(session)
assert not (tmp_path / f"{session.session_id}.jsonl").exists()
def test_flush_keeps_file_when_only_turn_details(tmp_path: Path) -> None:
session = _make_session()
with _patch_dir(tmp_path):
SessionStore.open_session(session)
# turn_detail only (no stub) — should NOT delete the file
SessionStore.append_turn_detail(session.session_id, "chat", "hello", response="hi")
SessionStore.flush(session)
assert (tmp_path / f"{session.session_id}.jsonl").exists()
def test_flush_noop_when_file_missing(tmp_path: Path) -> None:
session = _make_session()
with _patch_dir(tmp_path):
SessionStore.flush(session) # no open_session called — must not raise
def test_flush_is_idempotent(tmp_path: Path) -> None:
session = _make_session()
with _patch_dir(tmp_path):
SessionStore.open_session(session)
SessionStore.append_turn(session, "chat", "hi")
SessionStore.flush(session)
SessionStore.flush(session) # second call must not append another session_end
records = _read_lines(tmp_path / f"{session.session_id}.jsonl")
leaf_records = [r for r in records if r["type"] == "leaf"]
assert len(leaf_records) == 1, "flush() must be idempotent — only one leaf"
def test_append_turn_reopens_finalized_session(tmp_path: Path) -> None:
session = _make_session()
with _patch_dir(tmp_path):
SessionStore.open_session(session)
session.record("chat", "hello")
SessionStore.flush(session)
session.record("slash", "/status")
records = _read_lines(tmp_path / f"{session.session_id}.jsonl")
types = [r["type"] for r in records]
assert types.count("leaf") == 1
assert records[-1]["type"] == "custom_message"
assert records[-1]["kind"] == "slash"
assert records[-1]["text"] == "/status"
def test_reopen_session_strips_trailing_end_and_snapshot(tmp_path: Path) -> None:
session = _make_session()
with _patch_dir(tmp_path):
SessionStore.open_session(session)
session.record("chat", "hello")
SessionStore.flush(session)
SessionStore.reopen_session(session.session_id)
session.record("chat", "continued")
records = _read_lines(tmp_path / f"{session.session_id}.jsonl")
types = [r["type"] for r in records]
assert types.count("leaf") == 1
assert "conversation_snapshot" not in types
assert types[-1] == "custom_message"
assert records[-1]["text"] == "continued"
def test_reopen_session_noop_for_open_session(tmp_path: Path) -> None:
session = _make_session()
with _patch_dir(tmp_path):
SessionStore.open_session(session)
session.record("chat", "hello")
SessionStore.reopen_session(session.session_id)
session.record("chat", "still open")
records = _read_lines(tmp_path / f"{session.session_id}.jsonl")
assert records[-1]["text"] == "still open"
assert all(r["type"] != "leaf" for r in records)
# ── session.record() wiring ───────────────────────────────────────────────────
def test_session_record_calls_append_turn(tmp_path: Path) -> None:
session = _make_session()
with _patch_dir(tmp_path):
SessionStore.open_session(session)
session.record("chat", "what's wrong with prod?")
records = _read_lines(tmp_path / f"{session.session_id}.jsonl")
turns = _turn_stubs(records)
assert len(turns) == 1
assert turns[0]["kind"] == "chat"
assert turns[0]["text"] == "what's wrong with prod?"
# ── load_recent ───────────────────────────────────────────────────────────────
def test_load_recent_returns_empty_when_no_dir(tmp_path: Path) -> None:
with _patch_dir(tmp_path / "missing"):
result = SessionStore.load_recent()
assert result == []
def test_load_recent_counts_turns_for_in_progress_session(tmp_path: Path) -> None:
session = _make_session()
with _patch_dir(tmp_path):
SessionStore.open_session(session)
SessionStore.append_turn(session, "cli_agent", "hi")
SessionStore.append_turn(session, "chat", "follow-up")
SessionStore.append_turn(session, "alert", "OOM")
# No flush — session still in progress
results = SessionStore.load_recent()
assert len(results) == 1
assert results[0]["total_turns"] == 3
assert results[0]["chat_turns"] == 2
assert results[0]["investigation_turns"] == 1
assert results[0]["duration_secs"] is None
assert results[0]["is_ended"] is False
def test_load_recent_uses_session_end_stats_when_available(tmp_path: Path) -> None:
session = _make_session()
with _patch_dir(tmp_path):
SessionStore.open_session(session)
SessionStore.append_turn(session, "chat", "hi")
SessionStore.flush(session)
results = SessionStore.load_recent()
assert results[0]["is_ended"] is True
assert results[0]["total_turns"] == 1
assert results[0]["duration_secs"] is not None
def test_load_recent_reports_has_snapshot_true(tmp_path: Path) -> None:
session = _make_session()
session.agent.messages = [("user", "hi"), ("assistant", "hello")]
with _patch_dir(tmp_path):
SessionStore.open_session(session)
SessionStore.append_turn(session, "chat", "hi")
SessionStore.flush(session)
results = SessionStore.load_recent()
assert results[0]["has_snapshot"] is True
def test_load_recent_reports_has_snapshot_false_without_conversation(tmp_path: Path) -> None:
session = _make_session()
with _patch_dir(tmp_path):
SessionStore.open_session(session)
SessionStore.append_turn(session, "chat", "hi")
SessionStore.flush(session)
results = SessionStore.load_recent()
assert results[0]["has_snapshot"] is False
def test_load_recent_returns_newest_first(tmp_path: Path) -> None:
for started in ["2024-01-01T10:00:00+00:00", "2024-01-02T10:00:00+00:00"]:
sid = str(uuid.uuid4())
_write_v2_session(tmp_path / f"{sid}.jsonl", sid, started_at=started)
with _patch_dir(tmp_path):
results = SessionStore.load_recent()
assert results[0]["started_at"] > results[1]["started_at"]
def test_load_recent_skips_malformed_files(tmp_path: Path) -> None:
(tmp_path / "bad.jsonl").write_text("not json\n")
(tmp_path / "empty.jsonl").write_text("")
sid = str(uuid.uuid4())
_write_v2_session(
tmp_path / f"{sid}.jsonl",
sid,
started_at="2024-01-01T10:00:00+00:00",
text="ok",
)
with _patch_dir(tmp_path):
results = SessionStore.load_recent()
assert len(results) == 1
assert results[0]["session_id"] == sid
def test_load_recent_respects_n_limit(tmp_path: Path) -> None:
for _ in range(5):
sid = str(uuid.uuid4())
_write_v2_session(
tmp_path / f"{sid}.jsonl",
sid,
started_at="2024-01-01T10:00:00+00:00",
)
with _patch_dir(tmp_path):
assert len(SessionStore.load_recent(n=3)) == 3
# ── load_session ──────────────────────────────────────────────────────────────
def test_load_session_returns_none_for_missing_prefix(tmp_path: Path) -> None:
with _patch_dir(tmp_path):
assert SessionStore.load_session("nonexistent") is None
def test_load_session_returns_none_when_no_dir(tmp_path: Path) -> None:
with _patch_dir(tmp_path / "missing"):
assert SessionStore.load_session("abc") is None
def test_load_session_restores_from_conversation_snapshot(tmp_path: Path) -> None:
session = _make_session()
session.agent.messages = [("user", "how is prod?"), ("assistant", "prod is healthy")]
session.accumulated_context = {"service": "api"}
with _patch_dir(tmp_path):
SessionStore.open_session(session)
SessionStore.append_turn(session, "chat", "how is prod?")
SessionStore.flush(session)
data = SessionStore.load_session(session.session_id[:8])
assert data is not None
assert data["has_snapshot"] is False
assert data["cli_agent_messages"] == [
("user", "how is prod?"),
("assistant", "prod is healthy"),
]
assert data["accumulated_context"] == {"service": "api"}
def test_load_session_fallback_to_turn_details_when_no_snapshot(tmp_path: Path) -> None:
session = _make_session()
with _patch_dir(tmp_path):
SessionStore.open_session(session)
SessionStore.append_turn_detail(
session.session_id, "chat", "debug high CPU", response="It's a leak"
)
# Flush without cli_agent_messages — no snapshot written
SessionStore.flush(session)
data = SessionStore.load_session(session.session_id[:8])
assert data is not None
assert data["has_snapshot"] is False
messages = data["cli_agent_messages"]
assert ("user", "debug high CPU") in messages
assert ("assistant", "It's a leak") in messages
def test_load_session_ambiguous_prefix_returns_none(tmp_path: Path) -> None:
# Two sessions sharing the same prefix
for _ in range(2):
sid = "aaaabbbb" + str(uuid.uuid4())[8:]
_write_v2_session(
tmp_path / f"{sid}.jsonl",
sid,
started_at="2024-01-01T10:00:00+00:00",
)
with _patch_dir(tmp_path):
result = SessionStore.load_session("aaaa")
assert result is None
def test_load_session_includes_history_and_turn_details(tmp_path: Path) -> None:
session = _make_session()
with _patch_dir(tmp_path):
SessionStore.open_session(session)
SessionStore.append_turn(session, "chat", "hello")
SessionStore.append_turn_detail(session.session_id, "chat", "hello", response="hi")
SessionStore.flush(session)
data = SessionStore.load_session(session.session_id)
assert data is not None
assert len(data["history"]) == 1
assert data["history"][0]["kind"] == "chat"
assert len(data["turn_details"]) == 1
assert data["turn_details"][0]["response"] == "hi"
# ── session name derivation ───────────────────────────────────────────────────
def test_load_recent_derives_name_from_turn_detail(tmp_path: Path) -> None:
session = _make_session()
with _patch_dir(tmp_path):
SessionStore.open_session(session)
SessionStore.append_turn(session, "chat", "why is redis slow?")
SessionStore.append_turn_detail(
session.session_id, "chat", "why is redis slow?", response="It's a memory issue"
)
results = SessionStore.load_recent()
assert results[0]["name"] == "why is redis slow?"
def test_load_recent_derives_name_from_cli_agent_turn(tmp_path: Path) -> None:
"""Real chat turns recorded as kind='cli_agent' are reconstructed."""
session = _make_session()
with _patch_dir(tmp_path):
SessionStore.open_session(session)
SessionStore.append_turn(session, "cli_agent", "debug the OOM killer on prod")
results = SessionStore.load_recent()
assert results[0]["name"] == "debug the OOM killer on prod"
def test_load_recent_derives_name_from_turn_stub_when_no_detail(tmp_path: Path) -> None:
session = _make_session()
with _patch_dir(tmp_path):
SessionStore.open_session(session)
SessionStore.append_turn(session, "alert", "HighCPU on prod-api-1")
results = SessionStore.load_recent()
assert results[0]["name"] == "HighCPU on prod-api-1"
def test_load_recent_name_truncated_at_50_chars(tmp_path: Path) -> None:
session = _make_session()
long_prompt = "a" * 60
with _patch_dir(tmp_path):
SessionStore.open_session(session)
SessionStore.append_turn(session, "chat", long_prompt)
results = SessionStore.load_recent()
assert results[0]["name"] == "a" * 50 + ""
def test_load_recent_name_empty_for_slash_only_session(tmp_path: Path) -> None:
sid = str(uuid.uuid4())
(tmp_path / f"{sid}.jsonl").write_text(
"\n".join(
[
json.dumps(
{
"type": "session",
"version": 2,
"id": sid,
"created_at": "2024-01-01T10:00:00+00:00",
"cwd": "",
}
),
json.dumps(
{
"id": "entry1",
"parent_id": None,
"timestamp": "2024-01-01T10:00:01+00:00",
"type": "custom_message",
"custom_type": "turn_stub",
"kind": "slash",
"text": "/status",
"display": False,
}
),
]
)
+ "\n"
)
with _patch_dir(tmp_path):
results = SessionStore.load_recent()
assert results[0]["name"] == ""
def test_load_session_includes_name(tmp_path: Path) -> None:
session = _make_session()
with _patch_dir(tmp_path):
SessionStore.open_session(session)
SessionStore.append_turn(session, "chat", "debug the OOM killer")
SessionStore.flush(session)
data = SessionStore.load_session(session.session_id[:8])
assert data is not None
assert data["name"] == "debug the OOM killer"
def test_count_prefix_matches_returns_correct_count(tmp_path: Path) -> None:
for _ in range(3):
sid = str(uuid.uuid4())
_write_v2_session(
tmp_path / f"{sid}.jsonl",
sid,
started_at="2024-01-01T10:00:00+00:00",
)
with _patch_dir(tmp_path):
# Full UUID prefix matches exactly one
first_sid = list(tmp_path.glob("*.jsonl"))[0].stem
assert SessionStore.count_prefix_matches(first_sid[:8]) == 1
# Very short prefix may match multiple — no assertion on count, just that it doesn't raise
count = SessionStore.count_prefix_matches("")
assert count == 3
def test_count_prefix_matches_returns_zero_for_missing_dir(tmp_path: Path) -> None:
with _patch_dir(tmp_path / "missing"):
assert SessionStore.count_prefix_matches("abc") == 0
def test_load_session_matches_full_id(tmp_path: Path) -> None:
session = _make_session()
with _patch_dir(tmp_path):
SessionStore.open_session(session)
SessionStore.append_turn(session, "chat", "hi")
SessionStore.flush(session)
data = SessionStore.load_session(session.session_id)
assert data is not None
assert data["session_id"] == session.session_id
# ── flush resilience ─────────────────────────────────────────────────────────
def test_flush_writes_session_end_even_when_snapshot_serialization_fails(
tmp_path: Path,
) -> None:
"""P1: a snapshot serialization failure must not prevent session_end from being written."""
session = _make_session()
with _patch_dir(tmp_path):
SessionStore.open_session(session)
session.record("chat", "hello")
# Inject a non-JSON-serializable value into accumulated_context so
# json.dumps(snapshot) raises TypeError inside the inner suppress block.
session.accumulated_context["bad"] = object() # type: ignore[assignment]
SessionStore.flush(session)
records = _read_lines(tmp_path / f"{session.session_id}.jsonl")
types = [r["type"] for r in records]
# context entry may degrade through default=str, but leaf must be present.
assert "leaf" in types
assert records[-1]["type"] == "leaf"
# ── /new lifecycle ────────────────────────────────────────────────────────────
def test_new_closes_old_session_and_opens_new(tmp_path: Path) -> None:
session = _make_session()
with _patch_dir(tmp_path):
SessionStore.open_session(session)
session.record("chat", "pre-new question")
sid1 = session.session_id
# Simulate /new (flush → clear → open_session)
SessionStore.flush(session)
session.clear()
SessionStore.open_session(session)
sid2 = session.session_id
session.record("chat", "post-new question")
assert sid1 != sid2
# Old session file has a leaf marker
old_records = _read_lines(tmp_path / f"{sid1}.jsonl")
assert old_records[-1]["type"] == "leaf"
# New session file exists with turn but no leaf yet
new_records = _read_lines(tmp_path / f"{sid2}.jsonl")
assert new_records[0]["type"] == "session"
assert any(r["type"] == "custom_message" for r in new_records)
assert new_records[-1]["type"] != "leaf"
# ── Session field behaviour ───────────────────────────────────────────────
def test_repl_session_has_stable_session_id() -> None:
s = _make_session()
assert isinstance(s.session_id, str) and len(s.session_id) > 0
assert s.started_at <= time.time()
def test_session_rotates_id_on_clear() -> None:
s = _make_session()
original_id = s.session_id
s.history.append({"type": "chat", "text": "hi", "ok": True})
time.sleep(0.01)
s.clear()
assert s.session_id != original_id
assert s.started_at <= time.time()
# ── investigation_result / RCA history ───────────────────────────────────────
def test_append_investigation_result_writes_record(tmp_path: Path) -> None:
session = _make_session()
state = {
"root_cause": "connection pool exhausted",
"problem_md": "## Summary\nPool leak in checkout-api",
"root_cause_category": "resource",
"alert_name": "checkout latency",
}
with _patch_dir(tmp_path):
SessionStore.open_session(session)
inv_id = SessionStore.append_investigation_result(
session.session_id,
state,
trigger="/investigate generic",
)
records = _read_lines(tmp_path / f"{session.session_id}.jsonl")
inv = next(r for r in records if r["type"] == "investigation_result")
assert inv["investigation_id"] == inv_id
assert inv["root_cause"] == "connection pool exhausted"
assert "Pool leak" in inv["report"]
assert inv["trigger"] == "/investigate generic"
def test_append_investigation_result_uses_report_fallback(tmp_path: Path) -> None:
session = _make_session()
with _patch_dir(tmp_path):
SessionStore.open_session(session)
SessionStore.append_investigation_result(
session.session_id,
{"root_cause": "api error", "report": "report-only payload"},
trigger="/investigate generic",
)
records = _read_lines(tmp_path / f"{session.session_id}.jsonl")
inv = next(r for r in records if r["type"] == "investigation_result")
assert inv["report"] == "report-only payload"
def test_load_investigation_history_returns_newest_first(tmp_path: Path) -> None:
session_a = _make_session()
session_b = Session()
with _patch_dir(tmp_path):
SessionStore.open_session(session_a)
SessionStore.append_investigation_result(
session_a.session_id,
{"root_cause": "older issue", "problem_md": "old report"},
trigger="/investigate generic",
)
SessionStore.open_session(session_b)
SessionStore.append_investigation_result(
session_b.session_id,
{"root_cause": "newer issue", "problem_md": "new report"},
trigger="/investigate datadog",
)
history = SessionStore.load_investigation_history()
assert len(history) == 2
assert history[0]["root_cause"] == "newer issue"
assert history[1]["root_cause"] == "older issue"
def test_load_investigation_by_prefix(tmp_path: Path) -> None:
session = _make_session()
with _patch_dir(tmp_path):
SessionStore.open_session(session)
inv_id = SessionStore.append_investigation_result(
session.session_id,
{"root_cause": "disk full", "problem_md": "report body"},
trigger="/investigate alert.json",
)
loaded = SessionStore.load_investigation(inv_id[:4])
assert loaded is not None
assert loaded["investigation_id"] == inv_id
assert loaded["root_cause"] == "disk full"
def test_apply_investigation_result_persists_record(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setattr("config.constants.OPENSRE_HOME_DIR", tmp_path)
session = Session()
SessionStore.open_session(session)
session.apply_investigation_result(
{"root_cause": "OOM killer", "problem_md": "memory spike"},
trigger="sample:generic",
)
history = SessionStore.load_investigation_history()
assert len(history) == 1
assert history[0]["root_cause"] == "OOM killer"
assert history[0]["trigger"] == "sample:generic"