feat(02-02): capture, scrub, and strictly diff stateful flows

Resolve named placeholders from a private variable store, mask
dates and ids after shape checks, and keep comparing independent steps.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jakub Zych
2026-09-17 12:32:49 +02:00
parent bb6a5a91c9
commit aa165fe3d0
13 changed files with 1390 additions and 203 deletions

View File

@@ -19,22 +19,44 @@ func ReplayFlow(ctx context.Context, flow Flow, cfg ReplayConfig) (Result, error
client = defaultClient()
}
limit := maxBody(cfg.MaxBody)
store := cfg.Store
if store == nil {
store = mustMemoryStore()
}
result := Result{OK: true, Steps: make([]StepResult, 0, len(flow.Steps))}
skipRest := false
for _, step := range flow.Steps {
got, err := doStep(ctx, client, cfg.Target, step.Request, limit)
if skipRest {
result.Steps = append(result.Steps, StepResult{ID: step.ID, Skipped: true})
continue
}
sr := StepResult{ID: step.ID, OK: true}
req, err := expandRequest(step.Request, store)
if err != nil {
sr.OK = false
result.OK = false
sr.Diffs = append(sr.Diffs, Diff{Path: "request", Expected: "resolved placeholders", Actual: err.Error()})
result.Steps = append(result.Steps, sr)
skipRest = true
continue
}
got, err := doStep(ctx, client, cfg.Target, req, limit)
if err != nil {
return result, fmt.Errorf("tide: replay step %s: %w", step.ID, err)
}
sr := StepResult{ID: step.ID, OK: true}
if step.Response.Status != 0 && got.Status != step.Response.Status {
live := step
live.Request = req
live.Response = got
if err := CaptureStep(store, &live); err != nil {
sr.OK = false
sr.Diffs = append(sr.Diffs, Diff{
Path: "status",
Expected: fmt.Sprintf("%d", step.Response.Status),
Actual: fmt.Sprintf("%d", got.Status),
})
result.OK = false
sr.Diffs = append(sr.Diffs, Diff{Path: "capture", Expected: "captured value", Actual: err.Error()})
result.Steps = append(result.Steps, sr)
skipRest = true
continue
}
sr.Diffs = append(sr.Diffs, compareBodies(step.Response, got)...)
_ = ScrubStep(store, &live)
sr.Diffs = append(sr.Diffs, compareStep(step, live.Response)...)
if len(sr.Diffs) > 0 {
sr.OK = false
result.OK = false
@@ -46,3 +68,22 @@ func ReplayFlow(ctx context.Context, flow Flow, cfg ReplayConfig) (Result, error
}
return result, nil
}
func mustMemoryStore() *Store {
s, _ := OpenStore("")
return s
}
func compareStep(want Step, got Response) []Diff {
var diffs []Diff
if want.Response.Status != 0 && got.Status != want.Response.Status {
diffs = append(diffs, Diff{
Path: "status",
Expected: fmt.Sprintf("%d", want.Response.Status),
Actual: fmt.Sprintf("%d", got.Status),
})
}
diffs = append(diffs, compareHeaders(want.Response.Headers, got.Headers, want.Headers)...)
diffs = append(diffs, compareBodies(want.Response, got, want)...)
return diffs
}