Skip to content

test/replaytest: add Session/Memory multi-backend replay consistency framework#2264

Open
MUYI-luyu wants to merge 13 commits into
trpc-group:mainfrom
MUYI-luyu:feat/replay-consistency
Open

test/replaytest: add Session/Memory multi-backend replay consistency framework#2264
MUYI-luyu wants to merge 13 commits into
trpc-group:mainfrom
MUYI-luyu:feat/replay-consistency

Conversation

@MUYI-luyu

Copy link
Copy Markdown

What changed

Introduced the replaytest package — a replay consistency testing
framework for Session and Memory services. The lightweight matrix
compares InMemory against SQLite with zero external dependencies.

Three design decisions worth calling out

SQLite Summary correctness: buildEvent populates Timestamp on
both event.Event.Timestamp and Response.Timestamp because
SummarizeSession reads the top-level field. Omitting it causes
SQLite's getSummariesList to silently discard summaries via its
createdAt filter. All four summary failure modes (loss, content
corruption, filter-key error, wrong session attribution) are verified
at 100% detection.

True concurrent event testing: runConcurrentSteps pre-builds
all events outside the critical section (pure functions, no
shared-state mutation), then locks only the GetSession+AppendEvent
window. All concurrent events share a single base timestamp to model
semantically simultaneous operations. Verified clean under -race.

Order-independent event comparison: instead of broad wildcard
exemptions that can mask field-level corruption, concurrent event
batches are sorted by deterministic JSON serialization before
comparison. This achieves field-level precision with zero
allowed_diffs rules across all 10 cases, while the mechanism itself
is retained as a safety hatch for future backend-specific gaps.

Scope

  • 10 replay cases: single-turn, multi-turn, tool calls, state CRUD,
    memory CRUD, summary generation, summary truncation with boundary,
    track events, concurrent writes, error recovery
  • 9 artificial inconsistency types, 100% detection rate
  • 6-section recursive deep-diff with JSONPath field-level paths and
    context locators (event_index, memory_id, summary_filter_key,
    track_name)
  • Diff report with TRPC_AGENT_REPLAY_REPORT_PATH override
  • EN and CN README: design rationale, normalization strategy,
    lightweight/integration modes, case format, report format
  • 60+ unit and integration tests

No runtime public API is changed.

Why

Different Session/Memory backends must behave identically when replaying
the same agent trajectory. Divergence in event order, state values,
memory contents, or summary metadata causes replay corruption that is
hard to debug without a dedicated harness.

Fixes #2001

Testing

cd test
CGO_ENABLED=1 go test ./replaytest/... -count=1       # ~300ms
CGO_ENABLED=1 go test ./replaytest/... -race -count=1  # ~1.5s

Notes for reviewers

events_order_independent sorting uses encoding/json's deterministic map-key ordering, stable across Go versions.
allowed_diffs is intentionally unused in this PR; the mechanism exists for future backend-specific gaps (e.g., Redis TTL semantics).

Introduce the replaytest package — a reusable harness that drives
Session and Memory backends through identical operation sequences
and detects cross-backend inconsistencies with field-level diffs.

Framework (test/replaytest/):
  - scenario: ReplayCase types + JSON loader with validation
  - backends: ReplayBackend, DeterministicSummarizer, factory
  - driver: RunReplayCase engine supporting all 12 step types
  - normalizer: CaptureSnapshot with field-level normalization
  - comparator: CompareSnapshots with recursive path-precise diff
  - report: WriteDiffReport + HasUnexpectedDiffs

Test data (test/testdata/):
  - case_01_single_turn: single user + assistant event
  - case_02_multi_turn: six sequential events

Tests (test/consistency_test.go):
  - Load and validate both JSON scenarios
  - Run InMemory vs SQLite for each case (zero diffs)
  - Verify snapshot fields in detail

Zero LLM dependency — DeterministicSummarizer returns preset
text directly, requiring no API keys.
…d events_order_independent

- buildEvent: write Timestamp to both event.Event.Timestamp and
  Response.Timestamp so SummarizeSession→laterBoundary reads non-zero
  time, preventing SQLite getSummariesList from filtering all summaries
- runConcurrentSteps: pre-build events outside mutex so goroutines
  interleave; lock only GetSession+AppendEvent critical section
- normalizeSummaries: omit CutoffAt from replayBoundary (wall-clock)
- VerifySpec: add EventsOrderIndependent bool for content-based
  comparison of non-deterministically ordered events
- RunReplayCase: sort normalized events by JSON serialization when
  EventsOrderIndependent is set

- case_01: single-turn user + assistant text events
- case_02: multi-turn (3 rounds, 6 events)
- case_03: tool call + tool response with args/extensions
- case_04: app/user/session state write/overwrite/delete
- case_05: memory add→update→delete with aliases
- case_06: summary generation with force update
- case_07: summary truncation with boundary verification
- case_08: track events with payloads
- case_09: concurrent event appends (events_order_independent)
- case_10: error recovery (duplicate writes, session re-create)
…report


- consistency_test.go: AllCases (10-case InMemory vs SQLite),
 InjectedInconsistencies (9 types, 100% detection),
 LightweightMode (≤30s), VerifySpec, round-trip validation
- comparator_test.go: recursiveDiff, wildcardMatch, allowedRules,
 buildDiffContext (event_index, summary_filter_key, track_name)
- normalizer_test.go: event ID/timestamp stripping, state
 normalization, memory sorting, summary boundary, track sorting
- report_test.go: WriteDiffReport, DiffReportPath, HasUnexpectedDiffs
- README.md: design overview, quick start, lightweight/integration
 modes, case JSON format, diff report spec, feature coverage table
- README_CN.md: Chinese version of the full documentation
- sample_diff_report.json: format reference with all context fields
- session_memory_summary_track_diff_report.json: full-suite output
 ([] = zero unexpected diffs across all 10 cases)
These files were left from the initial skeleton commit. Their correct
counterparts now live under test/replaytest/ with full coverage.
…iles

- session_memory_summary_track_diff_report.json: replace empty [] with
  5 illustrative entries covering all sections (events, memory,
  summary, tracks) and context fields (event_index, memory_id,
  summary_filter_key, track_name), including one allowed_diff example
- Remove test/consistency_test.go and test/testdata/: stale skeleton
  duplicates now superseded by test/replaytest/
- Remove sample_diff_report.json: merged into the main report above
@github-actions

github-actions Bot commented Jul 19, 2026

Copy link
Copy Markdown

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

@coderabbitai

coderabbitai Bot commented Jul 19, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a replay-consistency harness comparing InMemory and SQLite Session/Memory backends through deterministic scenarios, normalized snapshots, recursive diffs, reports, and ten replay cases.
中文:新增 InMemory 与 SQLite 多后端回放一致性测试框架。

Changes

Replay consistency harness

Layer / File(s) Summary
Scenario contracts and replay cases
test/go.mod, test/replaytest/consistency_scenario.go, test/replaytest/testdata/*, test/replaytest/README*, test/replaytest/sample_diff_report.json, .gitignore
Defines scenario schemas, validation/loading, SQLite wiring, documentation, ten replay fixtures, and report artifacts.
Backend setup and replay execution
test/replaytest/consistency_backends.go, test/replaytest/consistency_driver.go
Creates deterministic InMemory and SQLite services and executes session, event, state, memory, summary, track, and concurrent operations.
Snapshot normalization
test/replaytest/consistency_normalizer.go
Normalizes events, state, memories, summaries, tracks, timestamps, ordering, and payload representations.
Snapshot comparison and reporting
test/replaytest/consistency_comparator.go, test/replaytest/consistency_report.go
Computes recursive diffs, adds context, applies allowed-diff rules, and writes JSON reports.
Validation and regression coverage
test/replaytest/*_test.go
Tests normalization, comparison, rule matching, reporting, replay verification, injected inconsistencies, and fixture loading.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

Suggested reviewers: sandyskies

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 41.18% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly names the replay consistency framework and the Session/Memory multi-backend focus.
Description check ✅ Passed The description directly explains the replay consistency framework, backend comparison, and testing scope.
Linked Issues check ✅ Passed The changes cover InMemory and SQLite backends, 10 replay cases, normalization, diffs, reports, docs, and tests, matching #2001's core goals.
Out of Scope Changes check ✅ Passed I don't see unrelated feature work; the edits stay within replaytest, docs, tests, and SQLite support needed for the framework.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@MUYI-luyu

Copy link
Copy Markdown
Author

I have read the CLA Document and I hereby sign the CLA

@codecov

codecov Bot commented Jul 19, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 89.85483%. Comparing base (12c70ff) to head (88a9a91).
⚠️ Report is 4 commits behind head on main.

Additional details and impacted files
@@                 Coverage Diff                 @@
##                main       #2264         +/-   ##
===================================================
+ Coverage   89.85102%   89.85483%   +0.00381%     
===================================================
  Files           1149        1150          +1     
  Lines         203321      203417         +96     
===================================================
+ Hits          182686      182780         +94     
- Misses         12918       12922          +4     
+ Partials        7717        7715          -2     
Flag Coverage Δ
unittests 89.85483% <ø> (+0.00381%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (2)
test/replaytest/consistency_driver.go (1)

26-32: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Global mutable stepBaseTime is a flakiness/global-state smell.

Re-deriving stepBaseTime from wall-clock time independently on every RunReplayCase call (line 52) means the two backend runs for the same case can get different base times if they straddle a one-second boundary — and normalizeTracks (consistency_normalizer.go) keeps absolute track timestamps rather than stripping them like events do, so this could produce a rare, timing-dependent spurious diff for track-based cases. It's also a package-level global that would silently become a real data race if a future contributor parallelizes case subtests with t.Parallel().

Consider computing the reference time once per case (e.g., in the test before invoking both backends) and passing it explicitly into RunReplayCase/buildEvent instead of a package var.

全局可变的 stepBaseTime 存在不稳定/全局状态问题。

stepBaseTime 在每次 RunReplayCase 调用时都独立从当前时钟重新计算(第52行),意味着同一 case 的两次 backend 运行如果跨越秒边界会得到不同的基准时间——而 normalizeTracks 保留了绝对时间戳(不像事件那样被剥离),可能导致 track 类用例出现偶发的、与时序相关的虚假差异。这也是一个包级全局变量,若未来有人为 case 子测试添加 t.Parallel(),会悄然变成真正的数据竞争。

建议每个用例只计算一次参考时间(例如在调用两个 backend 之前),并显式传入 RunReplayCase/buildEvent,而不是使用包级 var

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/replaytest/consistency_driver.go` around lines 26 - 32, Remove the
package-level mutable stepBaseTime and compute one reference timestamp per test
case before running either backend. Pass that timestamp explicitly through
RunReplayCase to buildEvent, ensuring both backend runs for the same case reuse
the identical value and avoiding shared global state.
test/replaytest/consistency_comparator.go (1)

166-188: 🎯 Functional Correctness | 🔵 Trivial | 🏗️ Heavy lift

Positional list diffing can cascade misleading diffs on mid-list insert/delete.

diffList compares elements strictly by index. If backend A and B disagree only on presence of one memory/track/event mid-list (not the last one), every subsequent index will be reported as "missing/extra/mismatched" rather than pinpointing the single real difference — undermining the precise-location goal of this harness. Notably, consistency_test.go's own missing_memory injection test only removes the last element, which reads as an implicit acknowledgment of this limitation rather than exercising the general case.

Consider a content-keyed diff (e.g., match memory entries by Key/track events by content fingerprint before diffing, similar to how normalizeMemories already sorts by Key) so a single insert/delete produces a single diff entry.

位置式列表比较在中间插入/删除时会产生连锁式误报差异。

diffList 严格按索引比较元素。如果两个 backend 只是在列表中间(而非末尾)存在一条记录的差异,后续所有索引都会被报告为"缺失/多余/不一致",而不是精确定位那一条真正的差异——这与本框架"精确定位差异"的目标相悖。值得注意的是,consistency_test.go 中的 missing_memory 注入测试只删除了最后一个元素,这暗示作者已意识到该局限性,只是回避了一般场景。

建议在比较前先按内容做键值对齐(例如按 Key/内容指纹匹配 memory/track 条目,类似 normalizeMemories 已经按 Key 排序的做法),使单次插入/删除只产生一条差异记录。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/replaytest/consistency_comparator.go` around lines 166 - 188, The
positional matching in diffList causes cascading diffs when a memory, track, or
event is inserted or removed mid-list. Update diffList and its callers to align
entries by a stable content key or fingerprint before invoking recursiveDiff,
reusing the existing normalizeMemories keying approach where applicable, so
unmatched entries produce only their own valueDiff while matched entries are
compared regardless of index.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test/replaytest/consistency_normalizer.go`:
- Around line 213-236: Update normalizeSummaries when constructing
entry.Boundary from sum.CutoffBoundary() to also copy the boundary’s CutoffAt
value into replayBoundary, preserving the existing Version, FilterKey, and
LastEventID fields.

In `@test/replaytest/consistency_report.go`:
- Around line 19-53: Change defaultReportName and DiffReportPath in
test/replaytest/consistency_report.go (lines 19-53) so unconfigured tests write
to a temporary/build-specific path instead of the tracked fixture name. Update
any default-path assumptions in test/replaytest/consistency_test.go (lines 75-80
and 410-418); test/replaytest/session_memory_summary_track_diff_report.json
(lines 1-81) requires no direct change.

In `@test/replaytest/consistency_scenario.go`:
- Around line 201-205: Extend the Validate method’s step-type validation to also
iterate through each nested step in Concurrent and reject any type absent from
validStepTypes with the same fail-fast error behavior. Preserve the existing
top-level validation and error reporting context.

In `@test/replaytest/consistency_test.go`:
- Around line 142-166: Update TestReplayConsistency_LightweightMode to keep
logging the elapsed replay duration but remove the hard 30-second t.Errorf
failure, so slow or loaded CI runners do not cause nondeterministic test
failures.

---

Nitpick comments:
In `@test/replaytest/consistency_comparator.go`:
- Around line 166-188: The positional matching in diffList causes cascading
diffs when a memory, track, or event is inserted or removed mid-list. Update
diffList and its callers to align entries by a stable content key or fingerprint
before invoking recursiveDiff, reusing the existing normalizeMemories keying
approach where applicable, so unmatched entries produce only their own valueDiff
while matched entries are compared regardless of index.

In `@test/replaytest/consistency_driver.go`:
- Around line 26-32: Remove the package-level mutable stepBaseTime and compute
one reference timestamp per test case before running either backend. Pass that
timestamp explicitly through RunReplayCase to buildEvent, ensuring both backend
runs for the same case reuse the identical value and avoiding shared global
state.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 05c850e9-267d-499f-bbd1-3838818915ed

📥 Commits

Reviewing files that changed from the base of the PR and between 12c70ff and fa6697e.

⛔ Files ignored due to path filters (1)
  • test/go.sum is excluded by !**/*.sum
📒 Files selected for processing (25)
  • test/go.mod
  • test/replaytest/README.md
  • test/replaytest/README_CN.md
  • test/replaytest/comparator_test.go
  • test/replaytest/consistency_backends.go
  • test/replaytest/consistency_comparator.go
  • test/replaytest/consistency_driver.go
  • test/replaytest/consistency_normalizer.go
  • test/replaytest/consistency_report.go
  • test/replaytest/consistency_scenario.go
  • test/replaytest/consistency_test.go
  • test/replaytest/normalizer_test.go
  • test/replaytest/report_test.go
  • test/replaytest/sample_diff_report.json
  • test/replaytest/session_memory_summary_track_diff_report.json
  • test/replaytest/testdata/case_01_single_turn.json
  • test/replaytest/testdata/case_02_multi_turn.json
  • test/replaytest/testdata/case_03_tool_calls.json
  • test/replaytest/testdata/case_04_state_updates.json
  • test/replaytest/testdata/case_05_memory_crud.json
  • test/replaytest/testdata/case_06_summary.json
  • test/replaytest/testdata/case_07_summary_truncation.json
  • test/replaytest/testdata/case_08_tracks.json
  • test/replaytest/testdata/case_09_concurrent.json
  • test/replaytest/testdata/case_10_error_recovery.json

Comment thread test/replaytest/consistency_normalizer.go
Comment thread test/replaytest/consistency_report.go
Comment thread test/replaytest/consistency_scenario.go
Comment thread test/replaytest/consistency_test.go
Fix golangci-lint failures: the project requires 'any' over
'interface{}' (Go 1.18+) on Go 1.24.4 code.
Comment thread test/replaytest/consistency_test.go
Comment thread test/replaytest/testdata/case_04_state_updates.json Outdated
- Use unique app_name per case to prevent cross-case state leakage
- Validate nested step types inside Concurrent blocks recursively
- Soften 30s hard assertion to diagnostic warning
- Rename default output to replay_diff_report.json and add to .gitignore
- Use sample_diff_report.json as the permanent reference artifact
- Remove old session_memory_summary_track_diff_report.json from tracking
- Drop init() cleanup hack — the gitignored output no longer dirties the tree
- Change defaultReportName to replay_diff_report.json
- Remove init() cleanup hack — no longer needed with gitignored output

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.gitignore:
- Around line 44-45: Update the .gitignore entry for the replay consistency
report to cover both generated locations: the existing
test/replaytest/replay_diff_report.json path and the repository-root
replay_diff_report.json path produced by DiffReportPath().
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 56f56d47-56c2-40f1-aa37-dfca755e4dd0

📥 Commits

Reviewing files that changed from the base of the PR and between dd4bf37 and d4e96f5.

📒 Files selected for processing (3)
  • .gitignore
  • test/replaytest/consistency_report.go
  • test/replaytest/consistency_test.go
💤 Files with no reviewable changes (1)
  • test/replaytest/consistency_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • test/replaytest/consistency_report.go

Comment thread .gitignore Outdated

@Rememorio Rememorio left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reviewed the changed lines and left 6 inline comments below. The comments focus on issues worth addressing before merge.

Fresh review found one reliability issue and five test-harness gaps. Shared timestamp state is race-prone; concurrent writes are actually serialized; summary boundaries and event order are incompletely verified; malformed scenarios can panic; and the error-recovery case never exercises an error.

中文

我看过这次变更的相关代码,在下面留下 6 条行内评论。评论聚焦在合并前值得处理的问题。

本次审阅发现 1 个可靠性问题和 5 个测试框架缺口:共享时间状态存在竞态风险;所谓并发写入实际被串行化;摘要边界和事件顺序验证不完整;错误场景可能触发 panic;异常恢复用例也没有真正制造异常。

Comment thread test/replaytest/consistency_driver.go Outdated
// session creation (which uses wall-clock time.Now() inside the
// first step). This prevents SQLite's getSummariesList from
// discarding summaries.
stepBaseTime = time.Now().UTC().Truncate(time.Second)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Keep replay timestamps out of package-global state

stepBaseTime is overwritten by every RunReplayCase call. Parallel subtests or backend runs will race and can change timestamps while another replay is still building events. Even sequential comparisons sample a separate base for each backend, so track timestamps can differ when the calls cross a one-second boundary.

Keep the clock local to a replay case, or inject one deterministic base shared by all compared backends, and pass it through event, track, and concurrent-step construction. A parallel race test and a clock-boundary test would cover both regressions.

中文

避免使用包级全局回放时间

stepBaseTime 是包级可变全局变量,每次调用 RunReplayCase 都会覆盖它。并行子测试或后端执行会产生数据竞争,还可能在另一个回放仍在构造事件时改变时间戳。即使顺序执行,两个后端也会分别取基准时间,跨过整秒边界时 track 时间戳会不同,导致用例偶发失败。

请将时钟作为单次回放的局部状态,或注入所有待比较后端共享的确定性基准时间,并传给事件、track 和并发步骤的构造逻辑。建议补充并行竞态测试和跨秒边界测试。

Comment thread test/replaytest/consistency_driver.go Outdated
case StepAppendEvent:
// Serialise GetSession + AppendEvent to avoid
// lost updates under CopyOnWrite semantics.
mu.Lock()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The concurrent scenario serializes every backend write

This mutex covers the complete GetSession plus AppendEvent operation, and the same mutex also surrounds every memory operation. Only event preconstruction overlaps; all observable backend mutations are serialized. A backend that loses updates or races under concurrent AppendEvent calls would still pass, despite the claimed concurrent-write coverage.

Exercise genuinely overlapping service calls with a barrier and assert that every expected event survives. If this scenario is intentionally limited to nondeterministic ordering, rename it and adjust the documentation instead of presenting it as backend concurrency validation.

中文

并发场景实际串行化了所有后端写入

该互斥锁覆盖了完整的 GetSessionAppendEvent 操作,同一个锁还包住了所有 memory 操作。只有事件对象的预构造存在并行,所有可观察的后端写入仍是串行的。因此,即使某个后端在真正并发调用 AppendEvent 时丢失更新或发生竞态,这个用例仍会通过。

建议通过屏障让多个服务调用真正重叠,并断言所有预期事件都被保留。如果该场景只想验证非确定性顺序,应相应调整名称和文档,不要将其描述为后端并发写入验证。

entry.Boundary = &replayBoundary{
Version: boundary.Version,
FilterKey: boundary.FilterKey,
LastEventID: boundary.LastEventID,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Summary cutoff corruption is omitted from comparison

replayBoundary defines CutoffAt, but this snapshot only copies Version, FilterKey, and LastEventID. A backend that persists the wrong cutoff therefore compares equal even though that timestamp controls which history is replayed. In addition, buildEvent leaves event IDs empty, so LastEventID never exercises real boundary anchoring.

Populate normalized CutoffAt using a deterministic shared clock, assign stable scenario event IDs, and assert all boundary fields in the summary-truncation case. That would detect the replay-corrupting boundary failures this case is intended to cover.

中文

摘要截止边界未纳入比较

replayBoundary 定义了 CutoffAt,但快照这里只复制了 VersionFilterKeyLastEventID。因此,后端即使错误持久化了截止时间,比较结果仍会相同,而该时间正是决定回放历史范围的关键字段。此外,buildEvent 没有设置事件 ID,导致 LastEventID 始终无法验证真实的边界锚点。

请使用共享的确定性时钟填充归一化后的 CutoffAt,为场景事件分配稳定 ID,并在摘要截断用例中断言全部边界字段。这样才能捕获该用例声称覆盖的回放边界错误。

MemoriesCount *int `json:"memories_count,omitempty"`
NoDuplicateEvents bool `json:"no_duplicate_events,omitempty"`
NoDuplicateMemories bool `json:"no_duplicate_memories,omitempty"`
EventsOrderPreserved bool `json:"events_order_preserved,omitempty"`

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: EventsOrderPreserved is never enforced

Four scenarios set events_order_preserved, but the field is never read. verifySnapshot checks only counts and duplicates, while cross-backend comparison proves only that the two backends agree. If both backends reorder the trajectory identically, the suite still passes.

Assert the snapshot against the scenario's append order using stable IDs or tags, and add a regression where both snapshots contain the same swapped order. Otherwise remove this verification option and the associated coverage claim until it is implemented.

中文

EventsOrderPreserved 从未被校验

四个场景设置了 events_order_preserved,但代码从未读取该字段。verifySnapshot 只检查数量和重复项,跨后端比较也只能证明两个后端结果一致。如果两个后端以相同方式错误地重排事件轨迹,测试仍会通过。

请利用稳定的事件 ID 或 tag,将快照顺序与场景中的追加顺序进行断言,并增加“两边都以相同方式交换顺序”的回归用例。否则应暂时移除该校验选项及相关覆盖声明。

// validateSteps recursively validates step types, including steps nested
// inside Concurrent blocks.
func validateSteps(step ReplayStep, index int) error {
if !validStepTypes[step.Type] {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Validate step payloads before execution

Validation checks only the step type. An append_event without event, a summary or track step without its payload, or a memory step with an invalid operation passes LoadReplayCase and later nil-dereferences or fails deep inside execution instead of returning a scenario error.

Validate the required payload and allowed operation for each step type, including permitted children of concurrent blocks. Table-driven malformed-case tests should assert descriptive load-time errors rather than panics.

中文

执行前应校验步骤载荷

当前校验只检查步骤类型。缺少 eventappend_event、缺少载荷的 summary 或 track 步骤,以及操作值非法的 memory 步骤,都会通过 LoadReplayCase,随后在执行阶段发生空指针 panic 或产生难以定位的深层错误。

请针对每种步骤类型校验必需载荷和允许的操作值,同时限制并发块中的子步骤类型。建议使用表驱动测试确认错误场景在加载阶段返回清晰错误,而不是触发 panic。

}
},
{
"type": "append_event",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The error-recovery case never injects an error

This is another successful append; the entire case performs four successful writes and only expects four events. No operation fails, no retry occurs, and no recovery behavior is asserted. A backend that commits and then returns an error, or duplicates an event during retry, would still pass this claimed coverage.

Use a fault-injecting service wrapper to fail before or after persistence, retry the operation, and assert the documented exactly-once or at-least-once behavior together with unchanged state.

中文

异常恢复用例没有制造异常

这里仍然只是一次成功的事件追加。整个用例执行了四次成功写入,并仅断言最终有四个事件;没有操作失败、没有重试,也没有验证任何恢复行为。如果后端写入成功后返回错误,或重试时重复插入事件,该用例依然无法发现。

建议使用可注入故障的服务包装器,分别模拟持久化前后失败,再执行重试,并根据约定断言 exactly-once 或 at-least-once 语义以及状态未被污染。


- P1: Keep replay timestamps out of package-global state — add BaseTime field to ReplayCase;
 RunReplayCase uses it when set (otherwise falls back to time.Now()); tests share a single
 baseTime across both backends so track timestamps are identical and never cross a second boundary

- P2: True concurrent backend writes — replace global mutex with two-phase barrier (ready + start)
 in runConcurrentSteps; all goroutines released simultaneously call GetSession + AppendEvent
 independently; only aliases map uses a narrow lock; case_09 upgraded to 8 concurrent operations

- P2: Summary cutoff corruption detection — add CutoffAtNonZero to summarySnapshot; remove CutoffAt
 from replayBoundary so wall-clock timestamps are verified for existence without false-diffing;
 assign explicit event IDs in scenarios so LastEventID is deterministic and cross-backend comparable

- P2: Enforce EventsOrderPreserved — verifySnapshot now calls verifyEventsOrder using stable tags as
 event identifiers; greedy subsequence scan asserts snapshot events follow scenario append order;
 four scenarios that declare events_order_preserved are now actually verified

- P2: Validate step payloads before execution — validateSteps checks required payload per step type
 (event, memory, summary, track); validates memory op values and non-empty concurrent blocks;
 table-driven TestReplayCase_Validate_RejectsMalformed with 15 cases catches malformation at load time

- P2: Inject real errors in error-recovery — faultSessionService wrapper intercepts AppendEvent;
 FaultConfig supports fail_before (error before commit) and fail_after (commit then error); driver
 sets nextFault, tolerates expected errors and continues to retry step; case_10 rewritten accordingly

- chore: blank line fix in consistency_report.go
@MUYI-luyu

Copy link
Copy Markdown
Author

Thanks for the thorough review! All issues have been addressed in 69d2b20 (and 88a9a91 for gofmt):

P1 (Timestamp Stability): Moved replay timestamps out of package-global state by adding a BaseTime field to ReplayCase. RunReplayCase uses it when set (falling back to time.Now()), ensuring tests share a single base time across both backends so track timestamps remain identical without crossing second boundaries.

P2 (Concurrent Writes): Replaced the global mutex with a two-phase barrier (ready + start) in runConcurrentSteps. All goroutines are released simultaneously, calling GetSession + AppendEvent independently, with the lock narrowed to only the aliases map. Upgraded case_09 to 8 concurrent operations.

P2 (Summary Cutoff Detection): Added CutoffAtNonZero to summarySnapshot and removed CutoffAt from replayBoundary, verifying wall-clock timestamps for existence without false-diffing. Assigned explicit event IDs in scenarios so LastEventID is deterministic and cross-backend comparable.

P2 (Events Order Enforcement): verifySnapshot now calls verifyEventsOrder, using stable tags as event identifiers with a greedy subsequence scan to assert snapshot events follow the scenario append order. All 4 scenarios that declare events_order_preserved are now actually verified.

P2 (Payload Validation): Added validateSteps to check required payloads per step type at load time (event, memory, summary, track; memory op validity; non-empty concurrent blocks). Added TestReplayCase_Validate_RejectsMalformed with 15 table-driven cases to catch malformations before execution.

P2 (Error Recovery & Fault Injection): Added a faultSessionService wrapper that intercepts AppendEvent, with FaultConfig supporting fail_before (error returned, backend never called) and fail_after (backend called successfully, then error returned). The driver sets nextFault before each step, tolerates expected errors, and continues to the retry step. Rewrote case_10 accordingly.

Chore: Fixed formatting and blank line in consistency_report.go; corrected import ordering in consistency_scenario.go (88a9a91).

All CI checks (lint, unit, race, and integration) have passed. Ready for another look when you have time!

@MUYI-luyu
MUYI-luyu requested a review from Rememorio July 21, 2026 01:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

构建 Session / Memory 多后端回放一致性测试框架

3 participants