Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ jobs:
test:
runs-on: [self-hosted, trpc-agent-python-ci]
timeout-minutes: 20
env:
REPLAY_REPORT_DIR: ${{ github.workspace }}/replay-reports
steps:
- name: Checkout
uses: actions/checkout@v4
Expand All @@ -75,7 +77,18 @@ jobs:

- name: Run tests with coverage
# run: pytest --cov=trpc_agent_sdk --cov-report=xml --cov-report=term tests/
run: pytest --cov=trpc_agent_sdk --cov-report=xml --cov-report=term --cov-fail-under=80 tests/
run: |
mkdir -p "$REPLAY_REPORT_DIR"
pytest --cov=trpc_agent_sdk --cov-report=xml --cov-report=term --cov-fail-under=80 tests/

- name: Upload replay consistency reports
if: always()
uses: actions/upload-artifact@v4
with:
name: replay-consistency-reports
path: ${{ env.REPLAY_REPORT_DIR }}
if-no-files-found: warn
retention-days: 14

- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v4
Expand Down
5 changes: 5 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -206,3 +206,8 @@ minversion = "6.0"
addopts = "-ra -q"
testpaths = ["tests"]
asyncio_mode = "auto"
markers = [
"replay_lightweight: deterministic Session/Memory/Summary replay consistency tests",
"replay_integration: environment-gated replay consistency tests for external backends",
"replay_property: optional property-based replay consistency tests",
]
19 changes: 15 additions & 4 deletions tests/memory/test_mempalace_memory_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,31 @@

from __future__ import annotations

import importlib.util
import time
from typing import Optional

from trpc_agent_sdk.abc import MemoryServiceConfig
import pytest

_HAS_MEMPALACE = importlib.util.find_spec("mempalace") is not None
pytestmark = pytest.mark.skipif(
not _HAS_MEMPALACE,
reason="MemPalace memory tests require the optional mempalace extra",
)

from trpc_agent_sdk.context import new_agent_context
from trpc_agent_sdk.events import Event
from trpc_agent_sdk.memory.mempalace_memory_service import MempalaceMemoryService
from trpc_agent_sdk.memory.mempalace_memory_service import get_mempalace_filters
from trpc_agent_sdk.memory.mempalace_memory_service import set_mempalace_filters
from trpc_agent_sdk.sessions import Session
from trpc_agent_sdk.types import Content
from trpc_agent_sdk.types import Part
from trpc_agent_sdk.types import SearchMemoryResponse

if _HAS_MEMPALACE:
from trpc_agent_sdk.abc import MemoryServiceConfig
from trpc_agent_sdk.memory.mempalace_memory_service import MempalaceMemoryService
from trpc_agent_sdk.memory.mempalace_memory_service import get_mempalace_filters
from trpc_agent_sdk.memory.mempalace_memory_service import set_mempalace_filters


def _make_config() -> MemoryServiceConfig:
cfg = MemoryServiceConfig(enabled=True)
Expand Down
69 changes: 69 additions & 0 deletions tests/sessions/replay_consistency/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Session / Memory / Summary Replay Consistency

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.

## Default Contract

- Default lightweight comparison is `InMemoryReplayAdapter` vs `SQLiteReplayAdapter`.
- 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.
- Integration tests are opt-in. They run only when their explicit environment switches and backend URLs are set.
- 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.
- `acceptance_matrix.json` separates the 10 required public cases from the extended all-entities contract case.

## Design Note

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.

## SDK API Path

```text
ReplayCase
-> ReplayBackendAdapter
-> trpc_agent_sdk public SessionService / MemoryService APIs
-> Backend storage
-> fresh service read via SnapshotReader
-> canonicalize_snapshot
-> semantic oracle + field diff + report
```

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.

## Summary Oracle

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:

- `version` is the per-session summary creation order observed during replay.
- `covered_event_ids` are the logical client event IDs selected by `find_events_for_summary` before summary creation.
- `active` is derived from the active summary event after replay.
- `session_id`, `user_id`, and `app_name` are strict ownership checks and must match the session being replayed.

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.

## Reports

Each run writes structured JSON reports with:

- `case_id`
- `backend_pair`
- aggregate metrics
- `session_id`
- `entity_type`
- `entity_id`
- `index`
- `field_path`
- `reference_value`
- `actual_value`
- `category`
- `allowed`
- `reason`

Set `REPLAY_REPORT_DIR` to retain reports outside pytest temporary directories. CI uses this to upload replay artifacts.

## Commands

```bash
python -m pytest tests/sessions/replay_consistency/test_replay_consistency.py -q -m replay_lightweight
python -m pytest tests/sessions/replay_consistency -q
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
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
```
7 changes: 7 additions & 0 deletions tests/sessions/replay_consistency/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Tencent is pleased to support the open source community by making tRPC-Agent-Python available.
#
# Copyright (C) 2026 Tencent. All rights reserved.
#
# tRPC-Agent-Python is licensed under Apache-2.0.

"""Replay consistency tests for session, memory, and summary backends."""
92 changes: 92 additions & 0 deletions tests/sessions/replay_consistency/acceptance_matrix.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
{
"schema_version": "1.0",
"required_cases": [
"single_turn_text",
"multi_turn_text",
"tool_call_response",
"state_shallow_update",
"memory_scope_user_session",
"summary_create_update",
"summary_event_truncation",
"failure_retry_append",
"cross_session_isolation",
"summary_defect_specials"
],
"extended_cases": [
"all_entities_contract"
],
"requirements": {
"p0_backends": {
"requirement": "Run lightweight replay against InMemory and a persistent or simulated persistent backend.",
"backends": ["in_memory", "sqlite"],
"test": "test_lightweight_replay_cases"
},
"p0_public_cases": {
"requirement": "Provide at least 10 deterministic public replay cases.",
"required_case_count": 10,
"extended_case_count": 1,
"test": "test_acceptance_matrix_matches_fixtures"
},
"p0_public_case_mutation_matrix": {
"requirement": "Each required public replay case detects an injected inconsistency.",
"detected": 10,
"total": 10,
"rate": 1.0,
"test": "test_required_public_cases_detect_injected_inconsistency"
},
"p0_entities": {
"requirement": "Cover event, state, memory, and summary entities.",
"entities": ["event", "state", "memory", "summary"],
"test": "test_snapshot_contains_all_entities"
},
"p0_summary_defects": {
"requirement": "Detect summary loss, stale overwrite, and wrong session ownership.",
"summary_loss_detection_rate": 1.0,
"summary_overwrite_detection_rate": 1.0,
"summary_owner_error_detection_rate": 1.0,
"test": "test_summary_defect_detection"
},
"p0_mutation_score": {
"requirement": "Detect all registered public mutations.",
"mutation_detection_rate": 1.0,
"survived_mutations": [],
"test": "test_mutation_detection"
},
"p1_runtime_fault_detection": {
"requirement": "Detect an after-commit client retry fault as a runtime replay inconsistency.",
"runtime_fault_detection_rate": 1.0,
"test": "test_runtime_fault_retry_duplicate_is_detected"
},
"p1_persistent_rebuild": {
"requirement": "Destroy and rebuild the persistent backend between replay phases, then read back through a fresh service.",
"cases": [
"summary_create_update",
"single_turn_text"
],
"test": "test_sqlite_destroy_rebuild_continue_and_readback"
},
"p0_reports": {
"requirement": "Generate structured JSON diff reports with precise location fields.",
"required_fields": [
"case_id",
"backend_pair",
"session_id",
"entity_type",
"entity_id",
"index",
"field_path",
"reference_value",
"actual_value",
"allowed",
"category",
"reason"
],
"test": "test_diff_report_schema_contains_required_location_fields"
},
"p0_lightweight_runtime": {
"requirement": "Run default lightweight mode in less than 30 seconds without external services.",
"lightweight_duration_seconds_max": 30,
"test": "test_lightweight_replay_cases"
}
}
}
Loading
Loading