Skip to content

Commit cdb203c

Browse files
committed
sessions: add replay consistency harness for session memory summary
This change adds a test-side replay consistency harness for Session, Memory, and Summary behavior across InMemory, SQLite, SQL URL, and Redis backends. It includes deterministic replay cases, canonical snapshots, semantic oracle checks, mutation detection, restart validation, structured JSON reports, and env-gated integration tests. Fixes #89 RELEASE NOTES: NONE
1 parent e113610 commit cdb203c

24 files changed

Lines changed: 2956 additions & 5 deletions

.github/workflows/ci.yml

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ jobs:
6565
test:
6666
runs-on: [self-hosted, trpc-agent-python-ci]
6767
timeout-minutes: 20
68+
env:
69+
REPLAY_REPORT_DIR: ${{ github.workspace }}/replay-reports
6870
steps:
6971
- name: Checkout
7072
uses: actions/checkout@v4
@@ -75,7 +77,18 @@ jobs:
7577
7678
- name: Run tests with coverage
7779
# run: pytest --cov=trpc_agent_sdk --cov-report=xml --cov-report=term tests/
78-
run: pytest --cov=trpc_agent_sdk --cov-report=xml --cov-report=term --cov-fail-under=80 tests/
80+
run: |
81+
mkdir -p "$REPLAY_REPORT_DIR"
82+
pytest --cov=trpc_agent_sdk --cov-report=xml --cov-report=term --cov-fail-under=80 tests/
83+
84+
- name: Upload replay consistency reports
85+
if: always()
86+
uses: actions/upload-artifact@v4
87+
with:
88+
name: replay-consistency-reports
89+
path: ${{ env.REPLAY_REPORT_DIR }}
90+
if-no-files-found: warn
91+
retention-days: 14
7992

8093
- name: Upload coverage reports to Codecov
8194
uses: codecov/codecov-action@v4

pyproject.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,3 +206,8 @@ minversion = "6.0"
206206
addopts = "-ra -q"
207207
testpaths = ["tests"]
208208
asyncio_mode = "auto"
209+
markers = [
210+
"replay_lightweight: deterministic Session/Memory/Summary replay consistency tests",
211+
"replay_integration: environment-gated replay consistency tests for external backends",
212+
"replay_property: optional property-based replay consistency tests",
213+
]

tests/memory/test_mempalace_memory_service.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,31 @@
77

88
from __future__ import annotations
99

10+
import importlib.util
1011
import time
1112
from typing import Optional
1213

13-
from trpc_agent_sdk.abc import MemoryServiceConfig
14+
import pytest
15+
16+
_HAS_MEMPALACE = importlib.util.find_spec("mempalace") is not None
17+
pytestmark = pytest.mark.skipif(
18+
not _HAS_MEMPALACE,
19+
reason="MemPalace memory tests require the optional mempalace extra",
20+
)
21+
1422
from trpc_agent_sdk.context import new_agent_context
1523
from trpc_agent_sdk.events import Event
16-
from trpc_agent_sdk.memory.mempalace_memory_service import MempalaceMemoryService
17-
from trpc_agent_sdk.memory.mempalace_memory_service import get_mempalace_filters
18-
from trpc_agent_sdk.memory.mempalace_memory_service import set_mempalace_filters
1924
from trpc_agent_sdk.sessions import Session
2025
from trpc_agent_sdk.types import Content
2126
from trpc_agent_sdk.types import Part
2227
from trpc_agent_sdk.types import SearchMemoryResponse
2328

29+
if _HAS_MEMPALACE:
30+
from trpc_agent_sdk.abc import MemoryServiceConfig
31+
from trpc_agent_sdk.memory.mempalace_memory_service import MempalaceMemoryService
32+
from trpc_agent_sdk.memory.mempalace_memory_service import get_mempalace_filters
33+
from trpc_agent_sdk.memory.mempalace_memory_service import set_mempalace_filters
34+
2435

2536
def _make_config() -> MemoryServiceConfig:
2637
cfg = MemoryServiceConfig(enabled=True)
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# Session / Memory / Summary Replay Consistency
2+
3+
This package implements the lightweight replay harness for Issue #89. It is intentionally test-side only: no production API, storage schema, or runtime dependency is changed.
4+
5+
## Default Contract
6+
7+
- Default lightweight comparison is `InMemoryReplayAdapter` vs `SQLiteReplayAdapter`.
8+
- The lightweight suite uses temporary SQLite files and a deterministic fake summarizer, so it does not require Redis, MySQL, PostgreSQL, network access, or a real LLM.
9+
- Integration tests are opt-in. They run only when their explicit environment switches and backend URLs are set.
10+
- Replay operations are Python fixtures because the SDK event model uses typed `Content`, `Part`, `FunctionCall`, and `FunctionResponse` objects. The checked-in `replay_cases_manifest.json` summarizes the public coverage in a review-friendly format.
11+
- `acceptance_matrix.json` separates the 10 required public cases from the extended all-entities contract case.
12+
13+
## Design Note
14+
15+
The harness replays deterministic operation sequences through the SDK service APIs, then reads observable state back through those same services before comparing snapshots. Canonicalization is intentionally minimal: generated event IDs are mapped to logical client IDs, dictionary keys are sorted, Unicode text is normalized, and backend metadata is excluded; event order, state values, tool call/response linkage, memory scope, summary ownership, and summary coverage remain strict. Summary comparison is split between stored facts and derived semantics because the SDK does not expose persisted summary revision or lineage fields. Allowed differences must be explicit, localized, and justified, so backend drift cannot be hidden by broad ignores. Default execution uses InMemory and temporary SQLite plus a deterministic fake summarizer, keeping CI lightweight while integration adapters allow real SQL and Redis checks when environment variables are provided.
16+
17+
## SDK API Path
18+
19+
```text
20+
ReplayCase
21+
-> ReplayBackendAdapter
22+
-> trpc_agent_sdk public SessionService / MemoryService APIs
23+
-> Backend storage
24+
-> fresh service read via SnapshotReader
25+
-> canonicalize_snapshot
26+
-> semantic oracle + field diff + report
27+
```
28+
29+
Adapters do not implement their own storage model. They call `create_session`, `append_event`, `create_session_summary`, `get_session`, `store_session`, and `search_memory` on real SDK services.
30+
31+
## Summary Oracle
32+
33+
The SDK currently exposes summary behavior through summary events, historical events, and `SummarizerSessionManager`; it does not persist a public `version`, `supersedes`, or coverage model. The harness therefore records derived summary metadata on the test side:
34+
35+
- `version` is the per-session summary creation order observed during replay.
36+
- `covered_event_ids` are the logical client event IDs selected by `find_events_for_summary` before summary creation.
37+
- `active` is derived from the active summary event after replay.
38+
- `session_id`, `user_id`, and `app_name` are strict ownership checks and must match the session being replayed.
39+
40+
These derived fields are not production API claims. They are a semantic oracle for detecting summary loss, stale overwrite, wrong-session ownership, and coverage drift without changing SDK schema.
41+
42+
## Reports
43+
44+
Each run writes structured JSON reports with:
45+
46+
- `case_id`
47+
- `backend_pair`
48+
- aggregate metrics
49+
- `session_id`
50+
- `entity_type`
51+
- `entity_id`
52+
- `index`
53+
- `field_path`
54+
- `reference_value`
55+
- `actual_value`
56+
- `category`
57+
- `allowed`
58+
- `reason`
59+
60+
Set `REPLAY_REPORT_DIR` to retain reports outside pytest temporary directories. CI uses this to upload replay artifacts.
61+
62+
## Commands
63+
64+
```bash
65+
python -m pytest tests/sessions/replay_consistency/test_replay_consistency.py -q -m replay_lightweight
66+
python -m pytest tests/sessions/replay_consistency -q
67+
RUN_REPLAY_SQL_INTEGRATION=1 DATABASE_URL=sqlite:////tmp/replay.db python -m pytest tests/sessions/replay_consistency/test_replay_integration.py -q -m replay_integration
68+
RUN_REPLAY_REDIS_INTEGRATION=1 REDIS_URL=redis://localhost:6379/15 python -m pytest tests/sessions/replay_consistency/test_replay_integration.py -q -m replay_integration
69+
```
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Tencent is pleased to support the open source community by making tRPC-Agent-Python available.
2+
#
3+
# Copyright (C) 2026 Tencent. All rights reserved.
4+
#
5+
# tRPC-Agent-Python is licensed under Apache-2.0.
6+
7+
"""Replay consistency tests for session, memory, and summary backends."""
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
{
2+
"schema_version": "1.0",
3+
"required_cases": [
4+
"single_turn_text",
5+
"multi_turn_text",
6+
"tool_call_response",
7+
"state_shallow_update",
8+
"memory_scope_user_session",
9+
"summary_create_update",
10+
"summary_event_truncation",
11+
"failure_retry_append",
12+
"cross_session_isolation",
13+
"summary_defect_specials"
14+
],
15+
"extended_cases": [
16+
"all_entities_contract"
17+
],
18+
"requirements": {
19+
"p0_backends": {
20+
"requirement": "Run lightweight replay against InMemory and a persistent or simulated persistent backend.",
21+
"backends": ["in_memory", "sqlite"],
22+
"test": "test_lightweight_replay_cases"
23+
},
24+
"p0_public_cases": {
25+
"requirement": "Provide at least 10 deterministic public replay cases.",
26+
"required_case_count": 10,
27+
"extended_case_count": 1,
28+
"test": "test_acceptance_matrix_matches_fixtures"
29+
},
30+
"p0_public_case_mutation_matrix": {
31+
"requirement": "Each required public replay case detects an injected inconsistency.",
32+
"detected": 10,
33+
"total": 10,
34+
"rate": 1.0,
35+
"test": "test_required_public_cases_detect_injected_inconsistency"
36+
},
37+
"p0_entities": {
38+
"requirement": "Cover event, state, memory, and summary entities.",
39+
"entities": ["event", "state", "memory", "summary"],
40+
"test": "test_snapshot_contains_all_entities"
41+
},
42+
"p0_summary_defects": {
43+
"requirement": "Detect summary loss, stale overwrite, and wrong session ownership.",
44+
"summary_loss_detection_rate": 1.0,
45+
"summary_overwrite_detection_rate": 1.0,
46+
"summary_owner_error_detection_rate": 1.0,
47+
"test": "test_summary_defect_detection"
48+
},
49+
"p0_mutation_score": {
50+
"requirement": "Detect all registered public mutations.",
51+
"mutation_detection_rate": 1.0,
52+
"survived_mutations": [],
53+
"test": "test_mutation_detection"
54+
},
55+
"p1_runtime_fault_detection": {
56+
"requirement": "Detect an after-commit client retry fault as a runtime replay inconsistency.",
57+
"runtime_fault_detection_rate": 1.0,
58+
"test": "test_runtime_fault_retry_duplicate_is_detected"
59+
},
60+
"p1_persistent_rebuild": {
61+
"requirement": "Destroy and rebuild the persistent backend between replay phases, then read back through a fresh service.",
62+
"cases": [
63+
"summary_create_update",
64+
"single_turn_text"
65+
],
66+
"test": "test_sqlite_destroy_rebuild_continue_and_readback"
67+
},
68+
"p0_reports": {
69+
"requirement": "Generate structured JSON diff reports with precise location fields.",
70+
"required_fields": [
71+
"case_id",
72+
"backend_pair",
73+
"session_id",
74+
"entity_type",
75+
"entity_id",
76+
"index",
77+
"field_path",
78+
"reference_value",
79+
"actual_value",
80+
"allowed",
81+
"category",
82+
"reason"
83+
],
84+
"test": "test_diff_report_schema_contains_required_location_fields"
85+
},
86+
"p0_lightweight_runtime": {
87+
"requirement": "Run default lightweight mode in less than 30 seconds without external services.",
88+
"lightweight_duration_seconds_max": 30,
89+
"test": "test_lightweight_replay_cases"
90+
}
91+
}
92+
}

0 commit comments

Comments
 (0)