Skip to content

session/replaytest: add multi-backend replay consistency harness#2293

Open
lai-long wants to merge 15 commits into
trpc-group:mainfrom
lai-long:session/replaytest-harness
Open

session/replaytest: add multi-backend replay consistency harness#2293
lai-long wants to merge 15 commits into
trpc-group:mainfrom
lai-long:session/replaytest-harness

Conversation

@lai-long

Copy link
Copy Markdown

Refs #2001.

What changed

Backend authors and CI now have a reusable way to verify that Session and
Memory backends behave identically under the same agent trajectory. A
deterministic case suite is replayed into each backend, and any behavioral
drift — lost or reordered events, state divergence, polluted memories,
missing or mis-attributed summaries, wrong filter-keys, dropped tracks —
is reported with an exact location (session id, event index, summary
filter-key, track name, memory id, field path, and both backends' values)
instead of surfacing as replay corruption or context loss in production.
New backends onboard in three documented steps and are compared against
the InMemory reference with no changes to existing code.

Why

Teams routinely prototype on InMemory and later switch to SQLite, Redis,
or other persistent backends. If event order, state, memory, or summary
semantics diverge silently across backends, long conversations replay
incorrectly and summaries get lost or overwritten — issues #2001 calls
out specifically, including the Go-specific filter-key summary and track
dimensions.

Non-obvious design decisions:

  • The harness is a self-contained leaf Go module that depends on the root
    and backend modules via replace, so it can import session/sqlite,
    session/redis, etc. without creating import cycles, and touches zero
    existing files.
  • Comparison is done on normalized snapshots (symbolic IDs with reference
    rewriting, canonical JSON, scrubbed timing fields, exact-decimal float
    tolerance) so that legitimate representation differences never become
    false failures, while everything else fails strictly.
  • A capability model reports unsupported features as unsupported
    (allowed_diff) rather than letting them fake-pass or hard-fail.
  • A probe session is read back after every summary case to catch
    cross-session summary leaks that per-case snapshots cannot see.

Testing

All commands run from session/replaytest/ (CGO enabled):

  • go build ./... && go vet ./... — clean.
  • go test ./... — all green (~12s): comparator/normalizer/reporter unit
    tests, mutation detection matrix, e2e fault injection, miniredis smoke
    test.
  • Lightweight acceptance: go test ./sqlite/ -run TestReplayConsistencySQLite
    — 11/11 public cases pass InMemory vs real SQLite, timed ~5s with an
    explicit < 30s assertion.
  • Injected-inconsistency detection: mutate_test.go applies 9 mutation
    classes across every public case — including all four summary fault
    classes (loss, stale overwrite, wrong session, wrong filter-key) — and
    asserts located detection; e2e_fault_test.go injects silent drop,
    dirty half-write, and cross-session summary leak at the real
    session.Service boundary.
  • False positives: TestFalsePositiveRateWithinBudget replays the full
    suite 10x on dual InMemory targets and asserts FP rate <= 5%
    (observed 0/110); the concurrency case is repeated 100x without flakes.
  • The committed example session_memory_summary_track_diff_report.json
    regenerates byte-identically via TestGenerateExampleReport.
  • CI checks replicated locally and passing: golangci-lint v1.64.8,
    gofmt/goimports, license headers, typos, go mod tidy (all 77
    modules), check-go-mod-version, external-consumer (CGO off), sumdb,
    file-size, LFS.

Not run: a real Redis server (the binding is verified on miniredis only);
Postgres/MySQL/ClickHouse are not wired, which the issue lists as
optional.

Notes for reviewers

  • Zero changes to existing modules; the new module's go.mod requires the
    sibling backend modules at their real release tags (v1.9.0) with local
    replace directives, since the placeholder
    v0.0.0-00010101000000-000000000000 is forbidden by
    check-go-mod-version.
  • Fault-injection depth: the mutation matrix mutates canonical snapshots
    (proving the differ), while e2e_fault_test.go covers three fault
    classes at the real service boundary; it does not simulate real backend
    non-atomic writes — see the README "Limitations" section, which also
    documents the InMemory-reference caveat (consistency != correctness).
  • The concurrency case intentionally exempts global total order and
    compares multiset equality plus per-branch partial order; please check
    this matches expected backend semantics.
  • Redis safety: rotating key prefixes, never flushes the server, session
    keys carry a 1h TTL; memory-side keys have no TTL (documented).

Add a reusable replay-consistency test harness for Session/Memory
backends (refs trpc-group#2001). A deterministic case suite is replayed against
multiple backends, five-dimension snapshots (events, state, memory,
summary, tracks) are normalized and diffed field by field, and a
located JSON diff report is produced.

- 11 public replay cases covering single/multi-turn dialog, tool
  calls, state overwrite/delete/clear, memory CRUD and scope
  isolation, summary generate/update/filter-key/truncation, tracks,
  concurrent interleaved appends, and failure/retry recovery.
- Backends: InMemory reference + real SQLite (session and memory);
  Redis binding gated by TRPC_REPLAYTEST_REDIS_URL with a miniredis
  smoke test running by default.
- Normalization: symbolic IDs with reference rewriting, canonical
  JSON, duration-key scrubbing, big.Rat float tolerance.
- Executable acceptance evidence: 9-class mutation matrix across all
  cases, four summary fault classes, service-boundary fault injection
  (silent drop, dirty half-write, cross-session summary leak),
  explicit false-positive-rate (<=5%) and lightweight-mode (<30s)
  assertions.
- Ships a regenerable example
  session_memory_summary_track_diff_report.json.
@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 086f7ef1-ab58-42dc-b08d-5aeb26147d74

📥 Commits

Reviewing files that changed from the base of the PR and between 49109ce and a402a94.

📒 Files selected for processing (3)
  • session/replaytest/e2e_fault_test.go
  • session/replaytest/internal_test.go
  • session/replaytest/redis/target_test.go
🚧 Files skipped from review as they are similar to previous changes (3)
  • session/replaytest/redis/target_test.go
  • session/replaytest/e2e_fault_test.go
  • session/replaytest/internal_test.go

📝 Walkthrough

English

Overview

Adds a new standalone session/replaytest Go module that runs deterministic Session/Memory “cases” across multiple backends. The harness replays scripted operations, snapshots what each backend returns (events, session/app/user state, memories, summaries, tracks, and optional memory-search results), normalizes both outputs to a canonical form (ID/UUID symbolization, deterministic ordering, JSON canonicalization, duration/timing scrubbing, float/number tolerance, and backend-equivalent rewrites), then diffs reference vs candidate via canonical JSON diffs. Results are reported per-case with precise dimension/severity/path plus optional “allowed” notes for known non-semantic differences, including capability-gated unsupported outcomes and cross-session summary isolation probes. It also includes mutation-based fault detection, concurrency stability checks, a false-positive budget test, and an optional regenerated JSON example report.

Operationally, it provides:

  • SQLite acceptance coverage (in-memory reference vs SQLite candidate).
  • Redis integration coverage behind env/config, plus a miniredis smoke test for best-effort behavior.

Public API and compatibility

New exported API surface (leaf module) is intended for backend owners to implement a replaytest.Target, run replaytest cases, and extend/interpret canonical snapshots and diffs.

Case model (package replaytest, case.go)

  • type OpKind string and exported op constants: OpCreateSession, OpAppendEvent, OpUpdateState, OpUpdateAppState, OpDeleteAppState, OpUpdateUserState, OpDeleteUserState, OpAddMemory, OpUpdateMemory, OpDeleteMemory, OpClearMemories, OpSummary, OpAppendTrack, OpConcurrentEvents
  • type Case, type Step
  • type EventSpec, type ToolCallSpec
  • type MemorySpec, type MetadataSpec, type SummarySpec, type TrackSpec, type WriterSpec

Backend abstraction and execution (package replaytest)

  • type Capability and var CapAll
    • func (c Capability) Missing(want Capability) Capability
    • func (c Capability) Names() []string
  • type Target interface (includes Close() error)
  • Runner:
    • type Runner, func NewRunner() *Runner
    • func (r *Runner) RunCase(ctx context.Context, c Case, t Target) (*Snapshot, error)
    • Exported constants: CaseAppName, CaseUserID
  • Pair runner / reporting:
    • func RunPair(ctx context.Context, cases []Case, ref, cand Target, opts ...PairOption) (*Report, error)
    • func RunPairT(t *testing.T, cases []Case, ref, cand Target, opts ...PairOption) *Report
    • type PairOptions, type PairOption
    • func WithReportPath(path string) PairOption
    • func WriteReport(path string, rep *Report) error
    • type Report, type PairInfo, type CaseReport, type Totals
    • Status constants: StatusPass, StatusFail, StatusUnsupported

Canonical snapshots / normalization / diffs (package replaytest)

  • Snapshot types:
    • type Snapshot, type SessionSnap, type AppState/UserState maps (via fields), type MemorySnap, type ErrorRecord
  • Canonical types + normalization:
    • type Canonical, CSession, CEvent, CToolCall, CSummary, CMemory, CError
    • func Normalize(snap *Snapshot) *Canonical
  • Diffing:
    • type Diff
    • Dimension/severity constants: DimSession, DimEvent, DimState, DimMemory, DimSummary, DimTrack, DimError, SevMismatch, SevMissing, SevExtra, SevOrder
    • func DiffCanonical(a, b *Canonical, unordered bool) []Diff
    • func DiffCanonicalWithDelta(a, b *Canonical, unordered bool, floatDelta float64) []Diff

Extension/testing utilities (package replaytest)

  • type Mutation, func Mutations() []Mutation, func CloneCanonical(c *Canonical) *Canonical
  • type FakeSummarizer, func NewFakeSummarizer() *FakeSummarizer

Backend target constructors (subpackages)

  • session/replaytest/inmemory: func NewInMemoryTarget(name string) *InMemoryTarget
  • session/replaytest/sqlite: func NewTarget(name string) (*Target, error)
  • session/replaytest/redis: func NewTarget(name, url string) (*Target, error)

Questions / export-naming/semantics to review

  • Confirm intended stability contract for canonical/diff schema (Canonical JSON shape, Diff path/dimension semantics, and the meaning of “allowed” vs “unsupported”).
  • Validate whether OpKind string constants are a long-term case/backends contract (naming and versioning strategy).
  • Ensure public types meant for external consumers are clearly documented in README and/or doc.go (especially FloatDelta semantics, unordered event semantics, canonicalization equivalence rules, and timing-scrub key list).
  • Consider whether some mutation helpers (Mutations, CloneCanonical) are meant to be stable public API or should be internal.
  • Check extensibility path: adding new snapshot dimensions or diff severities without breaking existing report parsing/regeneration workflows.

Behavioral and operational risks

  • Normalization equivalence may mask real bugs: timing scrubbing, symbolic ID rewriting, memory sorting/canonicalization, and “order-only” allowed diffs can reduce sensitivity if equivalence rules are too broad.
  • Float tolerance correctness: FloatDelta depends on bounded exact-decimal comparison and strict numeric parsing limits; misuse or edge formatting (scientific notation, malformed numbers) can either miss diffs or fail unexpectedly.
  • Unordered/concurrency semantics: unordered-mode multiset matching plus partial-order verification and concurrent multi-writer replay can expose runner assumptions or flakiness.
  • Cross-session summary probe sensitivity: runner polling/windowing and summary isolation checks depend on backend propagation timing and locking behavior.
  • Redis operational differences: key-prefix isolation, TTL handling, and miniredis best-effort limitations can cause CI/production drift.
  • Mutation/fault-injection/report drift: if cases or normalization/diff logic change, regenerated example reports and mutation coverage assertions can become stale.

Recommended validation

  • Run session/replaytest default in-memory self-test(s) and the full public case suite (cases.All()).
  • Run and verify unit tests covering:
    • canonical normalization (Normalize + canonical JSON/duration scrubbing/symbolizer),
    • tolerant diff behavior (DiffCanonicalWithDelta and delta parsing),
    • diff location accuracy and unordered semantics.
  • Run acceptance/smoke:
    • SQLite replay test.
    • miniredis replay consistency test.
    • real Redis replay test when TRPC_REPLAYTEST_REDIS_URL is configured.
  • Run concurrency checks under -race.
  • Regenerate and inspect the JSON example report (via REPLAY_REPORT_OUT) to ensure only the documented “allowed” categories appear in notes.
  • For any new backend Target:
    • implement Target correctly (especially Reset/Close) and declare accurate Capability flags,
    • validate all public cases, paying special attention to unordered/concurrency, memory-scope isolation, summary isolation probe behavior, and memory-search “soft” dimension handling.
中文

变更概述

新增独立的 session/replaytest Go 模块:使用确定性脚本化“回放用例”在多个 Session/Memory 后端之间做一致性校验。回放后会对观测结果做快照与规范化(事件、session/app/user 状态、记忆、摘要、轨迹、以及可选的记忆搜索结果),再将参考后端与候选后端的规范化结果做差异对比:包括 ID/UUID 符号化、确定性排序、JSON canonical 化、时长/时间字段清洗、数字/浮点容差与语义等价改写。最终按 case 输出带精确维度/严重度/路径定位的差异结果,并支持基于 capability 的 unsupported、跨会话摘要隔离探测、变异/故障注入、并发稳定性检查、误报率预算测试,以及可选的重新生成 JSON 示例报告。
同时提供验收/集成覆盖:SQLite 接受性测试、miniredis smoke 测试,以及在配置/环境变量开启时的真实 Redis 回放一致性测试。

公共 API 与兼容性

该 PR 以“叶子模块”形式新增面向后端接入方的导出 API,用于实现 replaytest.Target、运行用例并消费规范化/差异结果。

回放模型(package replaytest, case.go

  • type OpKind string + 导出操作常量:OpCreateSessionOpAppendEventOpUpdateStateOpUpdateAppStateOpDeleteAppStateOpUpdateUserStateOpDeleteUserStateOpAddMemoryOpUpdateMemoryOpDeleteMemoryOpClearMemoriesOpSummaryOpAppendTrackOpConcurrentEvents
  • type Casetype Step
  • type EventSpectype ToolCallSpec
  • type MemorySpectype MetadataSpectype SummarySpectype TrackSpectype WriterSpec

后端抽象与执行(package replaytest

  • type Capabilityvar CapAll
    • func (c Capability) Missing(want Capability) Capability
    • func (c Capability) Names() []string
  • type Target 接口(包含 Close() error
  • Runner:
    • type Runnerfunc NewRunner() *Runner
    • func (r *Runner) RunCase(ctx context.Context, c Case, t Target) (*Snapshot, error)
    • 导出常量:CaseAppNameCaseUserID
  • Pair 运行与报告:
    • func RunPair(...) (*Report, error)
    • func RunPairT(...) *Report
    • type PairOptionstype PairOption
    • func WithReportPath(path string) PairOption
    • func WriteReport(path string, rep *Report) error
    • type Reporttype PairInfotype CaseReporttype Totals
    • 状态常量:StatusPassStatusFailStatusUnsupported

快照/规范化/差异(package replaytest

  • 快照类型:type Snapshottype SessionSnaptype MemorySnaptype ErrorRecord
  • 规范化 canonical 类型:type CanonicalCSessionCEventCToolCallCSummaryCMemoryCError
    • func Normalize(snap *Snapshot) *Canonical
  • 差异类型与函数:
    • type Diff
    • 维度/严重度常量:DimSessionDimEventDimStateDimMemoryDimSummaryDimTrackDimErrorSevMismatchSevMissingSevExtraSevOrder
    • func DiffCanonical(a, b *Canonical, unordered bool) []Diff
    • func DiffCanonicalWithDelta(a, b *Canonical, unordered bool, floatDelta float64) []Diff

扩展/测试工具(package replaytest

  • type Mutationfunc Mutations() []Mutationfunc CloneCanonical(c *Canonical) *Canonical
  • type FakeSummarizerfunc NewFakeSummarizer() *FakeSummarizer

后端 Target 构造(子包)

  • in-memory:func NewInMemoryTarget(name string) *InMemoryTarget
  • sqlite:func sqlite.NewTarget(name string) (*Target, error)
  • redis:func redis.NewTarget(name, url string) (*Target, error)

API 评审要点(建议重点确认)

  • canonical/diff 的 schema(Canonical JSON 形态、Diff 的路径/维度/“allowed/unsupported”的语义)是否要作为长期稳定契约。
  • OpKind 字符串常量是否会长期作为用例与后端实现之间的契约,需要版本/兼容策略。
  • README/doc.go 是否充分说明:FloatDelta 语义、unordered 事件语义、规范化等价规则、以及时间字段清洗规则清单。
  • 部分测试工具(如 MutationsCloneCanonical)是否应保持为稳定公共 API,还是更适合收敛为内部实现。
  • 扩展性:未来新增快照字段/差异维度时如何演进而不破坏现有 report/解析/再生成流程。

行为与运行风险

  • 规范化等价可能掩盖真实缺陷:时间字段清洗、符号化 ID 重写、记忆排序/规范化,以及“仅顺序差异”的 allowed 策略可能降低敏感度。
  • 浮点容差正确性:容差依赖有界精确十进制比较与严格数字解析限制;边界输入(科学计数、格式异常)可能导致漏报或误报。
  • unordered/并发语义:unordered 模式多集合匹配 + 部分序检查、以及并发多写入回放可能暴露 runner 隐含假设或带来波动。
  • 跨会话摘要探测敏感:runner 轮询/窗口与摘要隔离检测依赖后端传播时序与锁语义。
  • Redis 行为差异:TTL/key-prefix 隔离与 miniredis 与真实 Redis 的不完全等价可能导致 CI/生产偏移。
  • 变异/故障注入与示例报告可能漂移:cases 或规范化/差异规则演进后,如果未同步更新,诊断价值会下降。

建议验证

  • 运行模块默认 in-memory 自测与全量公开用例(cases.All())。
  • 覆盖并确认:
    • 规范化单测(canonical JSON、时长清洗、symbolizer);
    • 容差差异单测(DiffCanonicalWithDelta 与 delta 解析);
    • 差异定位准确性与 unordered 语义。
  • 验收/集成:
    • SQLite 回放一致性测试;
    • miniredis 回放 smoke 测试;
    • 真实 Redis 回放一致性测试(设置 TRPC_REPLAYTEST_REDIS_URL 时)。
  • 对并发相关测试启用 -race
  • 重新生成并审阅 JSON 示例报告(REPLAY_REPORT_OUT),确认“允许差异”只出现在文档定义的类别里。
  • 新后端接入时:正确实现 Target(尤其 Reset/Close),声明准确的 Capability,并对全部公开用例做验收,重点关注 unordered/concurrency、内存作用域隔离、摘要隔离探测与 memory-search 的“软依赖”行为。

Walkthrough

Introduces a standalone Go replay-consistency module with declarative cases, deterministic execution, snapshot normalization, canonical diffing, backend targets, JSON reporting, fault injection, and SQLite/Redis acceptance coverage.

Changes

Session Replaytest Harness

Layer / File(s) Summary
Documentation, contracts, and module setup
session/replaytest/README.md, session/replaytest/doc.go, session/replaytest/go.mod, session/replaytest/case.go, session/replaytest/target.go
Documents the framework and defines replay operations, case schemas, capabilities, and target interfaces.
Public replay case suite
session/replaytest/cases/*
Adds 11 deterministic cases covering events, concurrency, memory, recovery, state, summaries, tool calls, and tracks.
Replay execution and snapshot capture
session/replaytest/runner.go, session/replaytest/snapshot.go
Executes scripted operations, handles retries and concurrent writers, synchronizes reads, checks summary isolation, and captures backend state.
Snapshot normalization
session/replaytest/normalize.go, session/replaytest/normalize_test.go, session/replaytest/internal_test.go
Produces canonical snapshots with stable identifiers, canonical JSON, timing scrubbing, sorted memories, and helper coverage.
Canonical and delta diff engine
session/replaytest/delta.go, session/replaytest/diff.go, session/replaytest/*_test.go
Compares replay dimensions field by field, supports unordered events and bounded exact-decimal numeric tolerance, and records allowed differences.
Backend target implementations
session/replaytest/inmemory.go, session/replaytest/fakesum.go, session/replaytest/sqlite/*, session/replaytest/redis/*
Wires in-memory, SQLite, and Redis services with reset, cleanup, and integration behavior.
Pair reports and validation helpers
session/replaytest/report.go, session/replaytest/testing.go, session/replaytest/report_test.go, session/replaytest/runner_errors_test.go
Generates JSON pair reports, handles unsupported capabilities, and validates runner and report error paths.
Mutation and end-to-end validation
session/replaytest/mutate.go, session/replaytest/e2e_fault_test.go, session/replaytest/selftest_test.go
Checks mutation-to-diff coverage, injects backend faults, and exercises replay stability.
Generated report and acceptance coverage
session/replaytest/session_memory_summary_track_diff_report.json, session/replaytest/example_report_test.go, session/replaytest/sqlite/replay_test.go, session/replaytest/redis/*_test.go
Adds a committed example report and SQLite/Redis replay acceptance tests.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

Suggested reviewers: sandyskies

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the new replay consistency harness across multiple backends.
Description check ✅ Passed The description is directly aligned with the replay harness, tests, and backend comparison changes.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ 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.

@github-actions

github-actions Bot commented Jul 22, 2026

Copy link
Copy Markdown

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

@lai-long

Copy link
Copy Markdown
Author

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

Comment thread session/replaytest/redis/target.go Outdated
func (t *Target) Reset(ctx context.Context) error {
t.closeServices()
t.seq++
prefix := fmt.Sprintf("replaytest:%s:%d", t.name, t.seq)

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.

Redis prefixes repeat across test processes because they only use name and seq, so old memory keys are reused on the next run. Add a per-target random run ID to the prefix.

中文 Redis 前缀只使用 `name` 和 `seq`,跨测试进程会重复,所以下次运行会复用旧 memory key。给前缀增加每个 target 独立的随机 run ID。

Comment thread session/replaytest/snapshot.go Outdated
continue
}
ms := &MemorySnap{
UserID: userID,

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.

Wrong memory scope is masked because toMemorySnaps replaces returned Entry.UserID with the queried user. Use e.UserID so stored scope mismatches fail.

中文 `toMemorySnaps` 用查询用户替换返回的 `Entry.UserID`,会掩盖错误的 memory 归属。改用 `e.UserID`,让存储归属不一致时报错。

@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: 7

🧹 Nitpick comments (8)
session/replaytest/case.go (2)

82-174: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Define the public step-field contracts.

Step.Op does not define required versus ignored fields or nil behavior. SummarySpec.FilterKey, ToolCallSpec.ID, TrackSpec.Payload, and WriterSpec also leave defaults and constraints undefined. Document and validate these combinations so third-party backend authors can construct cases reliably.

中文

请明确公开步骤字段的契约。

Step.Op 未说明哪些字段必填、哪些会被忽略以及 nil 的行为。SummarySpec.FilterKeyToolCallSpec.IDTrackSpec.PayloadWriterSpec 也没有定义默认值与约束。请补充文档并在 runner 中校验这些组合,避免第三方后端接入方错误构造 case。

As per path instructions, public API defaults, nil inputs, constraints, and errors must be defined.

🤖 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 `@session/replaytest/case.go` around lines 82 - 174, Define and document the
public contracts for Step and its operation-specific fields, including required
versus ignored fields, nil behavior, defaults, and invalid combinations. Add
runner validation for each Op, including SummarySpec.FilterKey, ToolCallSpec.ID,
TrackSpec.Payload, and WriterSpec constraints, with clear errors and consistent
handling of omitted values. Ensure raw JSON fields are validated and concurrent
writer settings have explicit non-negative/count and required-field rules.

Source: Path instructions


10-175: 📐 Maintainability & Code Quality | 🔵 Trivial

Run golangci-lint with the repo-level config
session/replaytest should use ../../.golangci.yml; then run go test ./....

中文

使用仓库级配置运行 golangci-lint
session/replaytest 应使用 ../../.golangci.yml;然后再运行 go test ./...

🤖 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 `@session/replaytest/case.go` around lines 10 - 175, Run golangci-lint for the
session/replaytest package using the repository-level ../../.golangci.yml
configuration, address any reported issues in the affected symbols, then run go
test ./... to verify the changes.

Source: Coding guidelines

session/replaytest/diff.go (1)

490-495: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Drop the local min helper.
This module targets Go 1.21, where min is already a predeclared builtin, so the package-level helper is redundant and may trigger predeclared lint.

中文

删除本地的 min 辅助函数。
该模块目标是 Go 1.21,min 已是预声明内建函数,因此这个包级辅助函数是多余的,也可能触发 predeclared 规则。

🤖 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 `@session/replaytest/diff.go` around lines 490 - 495, Remove the package-level
min helper function and rely on Go 1.21’s predeclared min builtin at its
existing call sites; do not alter the surrounding diff logic.
session/replaytest/report.go (1)

46-52: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Confusable public field names: Notes vs Note. These two exported fields differ only by a trailing s yet carry unrelated meanings — Notes []Diff collects allowed-diff entries, while Note string explains an unsupported status. In a brand-new public schema this is the cheapest moment to disambiguate (e.g. rename Note to Reason / UnsupportedNote) before consumers depend on the JSON key.

♻️ Proposed rename
 	Unsupported []string `json:"unsupported,omitempty"`
-	// Note explains an unsupported status: a capability gap is an
-	// allowed_diff by definition and never counts as a failure.
-	Note string `json:"note,omitempty"`
+	// Reason explains an unsupported status: a capability gap is an
+	// allowed_diff by definition and never counts as a failure.
+	Reason string `json:"reason,omitempty"`
 }
中文

公开字段名易混淆:NotesNote 两个导出字段仅差一个 s,含义却完全不同:Notes []Diff 收集 allowed-diff 条目,Note string 解释 unsupported 状态。作为全新的公开 schema,现在是消费者尚未依赖该 JSON key 时最低成本的消歧时机(例如把 Note 改名为 Reason / UnsupportedNote)。

依据 path instructions:导出命名影响可发现性时属于框架设计问题。

🤖 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 `@session/replaytest/report.go` around lines 46 - 52, Rename the exported Note
field in the report schema to a distinct name such as UnsupportedNote or Reason,
while preserving its string type, JSON behavior, and unsupported-status
explanation. Update all references to Note consistently; leave the Notes []Diff
field unchanged.

Source: Path instructions

session/replaytest/sqlite/replay_test.go (1)

54-56: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider failing (not just logging) on unexpected Unsupported cases.

sqlite.Target.Caps() returns CapAll, so a nonzero rep.Totals.Unsupported here would indicate a genuine capability-wiring regression, not an expected condition. Currently it's only logged, so this acceptance test wouldn't fail on that regression.

♻️ Proposed strengthening
-	if rep.Totals.Unsupported > 0 {
-		t.Logf("unsupported cases: %d (see report)", rep.Totals.Unsupported)
-	}
+	require.Zero(t, rep.Totals.Unsupported,
+		"sqlite target claims full capability; unsupported cases indicate a capability-wiring regression")
中文

sqlite.Target.Caps() 返回 CapAll,因此 rep.Totals.Unsupported 非零意味着能力接入出现了真实回归,而当前只是记录日志,不会导致测试失败。建议改为断言其为零。

🤖 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 `@session/replaytest/sqlite/replay_test.go` around lines 54 - 56, Update the
replay test’s handling of rep.Totals.Unsupported to fail the test when it is
nonzero, using the test’s assertion or failure mechanism instead of only t.Logf.
Preserve the existing diagnostic context by including the unsupported count and
report reference in the failure message.
session/replaytest/runner.go (1)

400-411: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Redundant rs.seq++ before buildEvent (which increments again internally).

Each concurrent event consumes two sequence slots instead of one, since buildEvent (line 435) already increments rs.seq. Harmless (IDs stay unique/monotonic and both backends see the same script), but the pre-increment at line 402 does nothing useful and makes sequence math inconsistent with the single-increment convention used everywhere else in this file.

♻️ Proposed cleanup
 		for i := 0; i < w.Count; i++ {
-			rs.seq++
 			specs = append(specs, r.buildEvent(rs, st.SessionID, &EventSpec{
中文

第402行在调用 buildEvent 之前多做了一次 rs.seq++,而 buildEvent 内部(第435行)自身也会自增,导致并发事件每个消耗两个序号而非一个。功能上无害(序号仍单调唯一,且两端后端回放同一脚本),但与本文件其余位置“每个事件自增一次”的约定不一致,建议移除多余的自增。

🤖 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 `@session/replaytest/runner.go` around lines 400 - 411, Remove the redundant
rs.seq++ inside the loop that builds concurrent events, while keeping buildEvent
responsible for the single sequence increment per event. Leave the event
construction and expected-count updates unchanged.
session/replaytest/snapshot.go (1)

121-133: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

State capture is gated by Session capability, contradicting the independent-capability contract.

Capability documents State and Session as independent fields, and Capability.Missing treats them independently. But here, App/User state capture (lines 121-133) only runs inside if svc != nil && t.Caps().Session. A future target with State: true, Session: false would pass the Missing() check (state "supported") yet silently produce an empty state snapshot instead of being reported Unsupported, violating the documented "unsupported instead of failing" contract. Currently latent since all targets return CapAll, but worth tightening for extensibility.

中文

CapabilityStateSession 定义为独立字段,但此处状态读取被嵌套在 Session 能力判断内部。若未来出现 State: true, Session: false 的后端,Missing() 检查会通过,但状态快照会被静默留空,而不是按文档约定标记为 Unsupported。目前因所有后端都返回 CapAll 而未触发,但建议为扩展性提前修正。

🤖 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 `@session/replaytest/snapshot.go` around lines 121 - 133, Update the
state-capture logic in the snapshot-building flow so AppState and UserState are
gated by t.Caps().State independently of t.Caps().Session. Preserve the existing
service availability and error handling, and ensure targets with State
unsupported produce the documented Unsupported result rather than an empty state
snapshot.
session/replaytest/target.go (1)

89-100: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider adding Close() error (or requiring io.Closer) to the Target interface.

All three concrete implementations reviewed in this cohort (InMemoryTarget, sqlite.Target, redis.Target) already implement an identical Close() error method, but it isn't part of the Target contract, so generic code holding a Target value can't clean it up without a type assertion. Given every current implementer already complies, this is low cost to add now versus later once more backends exist. Worth checking whether report.go's RunPair/RunPairT (not in this cohort) currently type-switches or leaks target cleanup responsibility to callers only.

中文

本队列审查到的三个具体实现(InMemoryTargetsqlite.Targetredis.Target)都已经实现了同样签名的 Close() error,但 Target 接口本身并未声明该方法,导致持有 Target 接口值的通用代码无法直接调用 Close。由于现有实现均已满足该签名,现在补充成本很低。建议核实 report.goRunPair/RunPairT(不在本次审查范围)是否需要统一的生命周期管理。

🤖 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 `@session/replaytest/target.go` around lines 89 - 100, Update the Target
interface to require Close() error, matching the existing implementations
InMemoryTarget, sqlite.Target, and redis.Target, so generic Target values can be
cleaned up directly. Check RunPair and RunPairT for target lifecycle handling
and use the interface method where cleanup is responsible there.
🤖 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 `@session/replaytest/cases/cases.go`:
- Around line 21-35: Update the case ordering returned by All() to match the
documented public numbering, placing MemoryScopeIsolation at its designated
position 11 and preserving the documented order of subsequent cases. Keep
RunPair’s slice-order behavior unchanged and do not alter case implementations.

In `@session/replaytest/cases/memory.go`:
- Around line 101-116: Adjust the replay sequence around OpClearMemories so the
final snapshot still includes the updated preference and metadata-bearing
records, or add an intermediate snapshot before clearing them. Preserve clear
behavior coverage through the existing OpClearMemories step, while ensuring
update, topics, metadata, and serialization fidelity are observable in the
captured snapshot; MemoryScopeIsolation already covers clear semantics.
- Around line 75-78: Remove the MemorySearch requirement from the
memory/write_read case’s NeedCaps configuration, retaining only the baseline
Memory capability so backends without search still run add/update/delete/list
validation. Keep SearchQuery unchanged so search results remain conditionally
captured when supported.

In `@session/replaytest/cases/track.go`:
- Around line 21-25: Update the Description in the replaytest.Case named
"track/tool_and_subtask" to state that duration fields are normalized/scrubbed,
replacing the inaccurate allowed_diff wording while preserving the rest of the
description.

In `@session/replaytest/go.mod`:
- Line 47: Upgrade the root module’s indirect OpenTelemetry SDK and gRPC
dependencies so replaytest resolves go.opentelemetry.io/otel/sdk to at least
v1.43.0 and google.golang.org/grpc to at least v1.79.3, then regenerate the
session/replaytest go.mod dependency pins and associated sums.

In `@session/replaytest/normalize.go`:
- Around line 237-240: Update the LongRunningToolIDs loop in normalizeEvent to
append callSym.sym(id) instead of the raw id, while preserving the existing
sorting of ce.LongRunning so long-running tool-call IDs are normalized
consistently with ToolID and ToolCalls[*].ID.

In `@session/replaytest/snapshot.go`:
- Around line 95-118: Protect both sess.Summaries reads in the primary snapshot
path and the WindowEventNum snapshot block with the same SummariesMu
RLock/RUnlock pattern used by verifySummaryIsolation in runner.go. Keep the
existing snapshot construction unchanged while ensuring each summary access
occurs under the read lock.

---

Nitpick comments:
In `@session/replaytest/case.go`:
- Around line 82-174: Define and document the public contracts for Step and its
operation-specific fields, including required versus ignored fields, nil
behavior, defaults, and invalid combinations. Add runner validation for each Op,
including SummarySpec.FilterKey, ToolCallSpec.ID, TrackSpec.Payload, and
WriterSpec constraints, with clear errors and consistent handling of omitted
values. Ensure raw JSON fields are validated and concurrent writer settings have
explicit non-negative/count and required-field rules.
- Around line 10-175: Run golangci-lint for the session/replaytest package using
the repository-level ../../.golangci.yml configuration, address any reported
issues in the affected symbols, then run go test ./... to verify the changes.

In `@session/replaytest/diff.go`:
- Around line 490-495: Remove the package-level min helper function and rely on
Go 1.21’s predeclared min builtin at its existing call sites; do not alter the
surrounding diff logic.

In `@session/replaytest/report.go`:
- Around line 46-52: Rename the exported Note field in the report schema to a
distinct name such as UnsupportedNote or Reason, while preserving its string
type, JSON behavior, and unsupported-status explanation. Update all references
to Note consistently; leave the Notes []Diff field unchanged.

In `@session/replaytest/runner.go`:
- Around line 400-411: Remove the redundant rs.seq++ inside the loop that builds
concurrent events, while keeping buildEvent responsible for the single sequence
increment per event. Leave the event construction and expected-count updates
unchanged.

In `@session/replaytest/snapshot.go`:
- Around line 121-133: Update the state-capture logic in the snapshot-building
flow so AppState and UserState are gated by t.Caps().State independently of
t.Caps().Session. Preserve the existing service availability and error handling,
and ensure targets with State unsupported produce the documented Unsupported
result rather than an empty state snapshot.

In `@session/replaytest/sqlite/replay_test.go`:
- Around line 54-56: Update the replay test’s handling of rep.Totals.Unsupported
to fail the test when it is nonzero, using the test’s assertion or failure
mechanism instead of only t.Logf. Preserve the existing diagnostic context by
including the unsupported count and report reference in the failure message.

In `@session/replaytest/target.go`:
- Around line 89-100: Update the Target interface to require Close() error,
matching the existing implementations InMemoryTarget, sqlite.Target, and
redis.Target, so generic Target values can be cleaned up directly. Check RunPair
and RunPairT for target lifecycle handling and use the interface method where
cleanup is responsible there.
🪄 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: 99e1c2bc-d01b-4855-a435-1e932aab3579

📥 Commits

Reviewing files that changed from the base of the PR and between c3c648e and 281ceba.

⛔ Files ignored due to path filters (1)
  • session/replaytest/go.sum is excluded by !**/*.sum
📒 Files selected for processing (38)
  • session/replaytest/README.md
  • session/replaytest/case.go
  • session/replaytest/cases/basic.go
  • session/replaytest/cases/cases.go
  • session/replaytest/cases/concurrency.go
  • session/replaytest/cases/memory.go
  • session/replaytest/cases/recovery.go
  • session/replaytest/cases/state.go
  • session/replaytest/cases/summary.go
  • session/replaytest/cases/toolcall.go
  • session/replaytest/cases/track.go
  • session/replaytest/delta.go
  • session/replaytest/delta_test.go
  • session/replaytest/diff.go
  • session/replaytest/diff_test.go
  • session/replaytest/doc.go
  • session/replaytest/e2e_fault_test.go
  • session/replaytest/example_report_test.go
  • session/replaytest/fakesum.go
  • session/replaytest/go.mod
  • session/replaytest/inmemory.go
  • session/replaytest/mutate.go
  • session/replaytest/mutate_test.go
  • session/replaytest/normalize.go
  • session/replaytest/normalize_test.go
  • session/replaytest/redis/miniredis_test.go
  • session/replaytest/redis/replay_test.go
  • session/replaytest/redis/target.go
  • session/replaytest/report.go
  • session/replaytest/report_test.go
  • session/replaytest/runner.go
  • session/replaytest/selftest_test.go
  • session/replaytest/session_memory_summary_track_diff_report.json
  • session/replaytest/snapshot.go
  • session/replaytest/sqlite/replay_test.go
  • session/replaytest/sqlite/target.go
  • session/replaytest/target.go
  • session/replaytest/testing.go

Comment thread session/replaytest/cases/cases.go Outdated
Comment thread session/replaytest/cases/memory.go
Comment thread session/replaytest/cases/memory.go Outdated
Comment thread session/replaytest/cases/track.go
Comment thread session/replaytest/go.mod Outdated
Comment thread session/replaytest/normalize.go
Comment thread session/replaytest/snapshot.go
@codecov

codecov Bot commented Jul 22, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.77619% with 77 lines in your changes missing coverage. Please review.
✅ Project coverage is 89.91635%. Comparing base (d4f78f6) to head (a402a94).
⚠️ Report is 53 commits behind head on main.

Files with missing lines Patch % Lines
session/replaytest/sqlite/target.go 64.81481% 13 Missing and 6 partials ⚠️
session/replaytest/runner.go 95.37037% 9 Missing and 6 partials ⚠️
session/replaytest/snapshot.go 88.88889% 6 Missing and 6 partials ⚠️
session/replaytest/report.go 90.78947% 4 Missing and 3 partials ⚠️
session/replaytest/mutate.go 95.38462% 3 Missing and 3 partials ⚠️
session/replaytest/redis/target.go 89.13043% 3 Missing and 2 partials ⚠️
session/replaytest/testing.go 64.28571% 4 Missing and 1 partial ⚠️
session/replaytest/diff.go 98.92857% 2 Missing and 1 partial ⚠️
session/replaytest/normalize.go 98.61111% 2 Missing and 1 partial ⚠️
session/replaytest/delta.go 98.19820% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@                 Coverage Diff                 @@
##                main       #2293         +/-   ##
===================================================
+ Coverage   89.86563%   89.91635%   +0.05071%     
===================================================
  Files           1144        1172         +28     
  Lines         198483      205382       +6899     
===================================================
+ Hits          178368      184672       +6304     
- Misses         12602       12963        +361     
- Partials        7513        7747        +234     
Flag Coverage Δ
unittests 89.91635% <95.77619%> (+0.05071%) ⬆️

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

🤖 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 `@session/replaytest/e2e_fault_test.go`:
- Around line 254-266: Update the test around replaytest.RunPair to execute only
cases.MemoryScopeIsolation() instead of cases.All(), then assert that the
specific case has StatusFail and includes a replaytest.DimMemory diff. Preserve
the existing failure validation while ensuring unrelated cases cannot satisfy
the assertions.

In `@session/replaytest/internal_test.go`:
- Around line 293-295: Update the over-length input setup in the
parseBoundedDecimal test to use valid numeric content via strings.Repeat("1",
maxExactNumberCharacters+1), ensuring the rejection specifically exercises the
maximum-length boundary rather than malformed NUL bytes.
- Around line 12-27: Update the validation setup to use the supported structured
form for .golangci.yml output.formats. In session/replaytest/internal_test.go
(12-27) and session/replaytest/snapshot.go (73-79), isolate Redis- and
CGO-dependent SQLite replay tests behind appropriate integration/build
constraints so go test ./... remains self-contained without external services or
CGO.

In `@session/replaytest/redis/target_test.go`:
- Around line 44-47: Strengthen the second Reset test around tgt.Reset by
retaining the pre-reset SessionService and MemoryService, writing representative
data before reset, then asserting the post-reset accessors are replaced and the
prior data is not visible. Verify the externally observable isolation and prefix
rotation without relying solely on non-nil checks.
🪄 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: 671c0059-ae5b-43a0-b8d2-eeef1b08ee59

📥 Commits

Reviewing files that changed from the base of the PR and between 281ceba and 49109ce.

⛔ Files ignored due to path filters (1)
  • session/replaytest/go.sum is excluded by !**/*.sum
📒 Files selected for processing (25)
  • session/replaytest/README.md
  • session/replaytest/case.go
  • session/replaytest/cases/cases.go
  • session/replaytest/cases/cases_test.go
  • session/replaytest/cases/memory.go
  • session/replaytest/cases/track.go
  • session/replaytest/diff.go
  • session/replaytest/e2e_fault_test.go
  • session/replaytest/example_report_test.go
  • session/replaytest/fakesum_test.go
  • session/replaytest/go.mod
  • session/replaytest/internal_test.go
  • session/replaytest/normalize.go
  • session/replaytest/normalize_test.go
  • session/replaytest/redis/target.go
  • session/replaytest/redis/target_test.go
  • session/replaytest/report.go
  • session/replaytest/report_test.go
  • session/replaytest/runner.go
  • session/replaytest/runner_errors_test.go
  • session/replaytest/session_memory_summary_track_diff_report.json
  • session/replaytest/snapshot.go
  • session/replaytest/sqlite/replay_test.go
  • session/replaytest/target.go
  • session/replaytest/target_test.go
💤 Files with no reviewable changes (2)
  • session/replaytest/diff.go
  • session/replaytest/runner.go
🚧 Files skipped from review as they are similar to previous changes (12)
  • session/replaytest/cases/track.go
  • session/replaytest/sqlite/replay_test.go
  • session/replaytest/target.go
  • session/replaytest/cases/cases.go
  • session/replaytest/redis/target.go
  • session/replaytest/report.go
  • session/replaytest/example_report_test.go
  • session/replaytest/README.md
  • session/replaytest/case.go
  • session/replaytest/normalize_test.go
  • session/replaytest/session_memory_summary_track_diff_report.json
  • session/replaytest/normalize.go

Comment thread session/replaytest/e2e_fault_test.go Outdated
Comment thread session/replaytest/internal_test.go
Comment thread session/replaytest/internal_test.go Outdated
Comment thread session/replaytest/redis/target_test.go Outdated
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.

3 participants