From 50aaa25a40705699149b6b227475a3a98c75e065 Mon Sep 17 00:00:00 2001 From: Luke Marsden Date: Thu, 2 Apr 2026 13:35:04 +0100 Subject: [PATCH] Add response entries isolation validation to E2E test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Validates that follow-up interactions in the same session don't contain message_ids from previous interactions — detects the bug where Zed's flush_streaming_throttle resends ALL thread entries and the Helix accumulator re-accumulates old ones. Checks all sessions with multiple completed interactions, groups by session, and verifies no message_id appears in more than one interaction. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../e2e-test/helix-ws-test-server/go.mod | 4 +- .../e2e-test/helix-ws-test-server/go.sum | 8 +-- .../e2e-test/helix-ws-test-server/main.go | 67 +++++++++++++++++++ 3 files changed, 73 insertions(+), 6 deletions(-) diff --git a/crates/external_websocket_sync/e2e-test/helix-ws-test-server/go.mod b/crates/external_websocket_sync/e2e-test/helix-ws-test-server/go.mod index 085e777507cb32..7ff5814c32c342 100644 --- a/crates/external_websocket_sync/e2e-test/helix-ws-test-server/go.mod +++ b/crates/external_websocket_sync/e2e-test/helix-ws-test-server/go.mod @@ -136,7 +136,7 @@ require ( github.com/hashicorp/go-retryablehttp v0.7.8 // indirect github.com/hashicorp/go-version v1.8.0 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect - github.com/helixml/kodit v1.2.1 // indirect + github.com/helixml/kodit v1.2.4 // indirect github.com/huandu/xstrings v1.5.0 // indirect github.com/infracloudio/msbotbuilder-go v0.2.5 // indirect github.com/invopop/jsonschema v0.13.0 // indirect @@ -289,7 +289,7 @@ require ( golang.org/x/sync v0.20.0 // indirect golang.org/x/sys v0.41.0 // indirect golang.org/x/term v0.40.0 // indirect - golang.org/x/text v0.34.0 // indirect + golang.org/x/text v0.35.0 // indirect golang.org/x/time v0.12.0 // indirect golang.org/x/tools v0.42.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect diff --git a/crates/external_websocket_sync/e2e-test/helix-ws-test-server/go.sum b/crates/external_websocket_sync/e2e-test/helix-ws-test-server/go.sum index 0885944e487633..3c78a3e2d08b54 100644 --- a/crates/external_websocket_sync/e2e-test/helix-ws-test-server/go.sum +++ b/crates/external_websocket_sync/e2e-test/helix-ws-test-server/go.sum @@ -520,8 +520,8 @@ github.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09 github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/helixml/kodit v1.2.1 h1:m4bwhEuhtYrx8+SoHryushOVR1oF8rKTStwH21vZbtU= -github.com/helixml/kodit v1.2.1/go.mod h1:bwT0baMTiauunMRF1dAG4cNh6Gw5d8bos28WwajJYWA= +github.com/helixml/kodit v1.2.4 h1:GjAD+68VlL0CpAzvif7pNVFvY6GtxRQATkcVljfD35o= +github.com/helixml/kodit v1.2.4/go.mod h1:bwT0baMTiauunMRF1dAG4cNh6Gw5d8bos28WwajJYWA= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= @@ -1166,8 +1166,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= -golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/crates/external_websocket_sync/e2e-test/helix-ws-test-server/main.go b/crates/external_websocket_sync/e2e-test/helix-ws-test-server/main.go index f3edf66a09252d..1d4bfeccceb28e 100644 --- a/crates/external_websocket_sync/e2e-test/helix-ws-test-server/main.go +++ b/crates/external_websocket_sync/e2e-test/helix-ws-test-server/main.go @@ -1465,6 +1465,73 @@ func (d *testDriver) validateStore() bool { } } + // --- RESPONSE ENTRIES ISOLATION VALIDATION --- + // Verify that follow-up interactions don't accumulate response_entries from + // previous interactions in the same session. This detects the bug where Zed's + // flush_streaming_throttle resends ALL entries in the ACP thread and the + // accumulator treats old entries as new, ballooning response_entries. + log.Println("\n--------------------------------------------------") + log.Println(" RESPONSE ENTRIES ISOLATION VALIDATION") + log.Println("--------------------------------------------------") + + // Group interactions by session + sessionInteractions := make(map[string][]*types.Interaction) + for _, i := range interactions { + if i.State == types.InteractionStateComplete && len(i.ResponseEntries) > 0 { + sessionInteractions[i.SessionID] = append(sessionInteractions[i.SessionID], i) + } + } + + isolationChecked := 0 + for sessionID, ints := range sessionInteractions { + if len(ints) < 2 { + continue // Need at least 2 interactions to check isolation + } + + // Sort by creation time + sort.Slice(ints, func(a, b int) bool { + return ints[a].Created.Before(ints[b].Created) + }) + + // Collect message_ids from each interaction + type parsedEntry struct { + MessageID string `json:"message_id"` + } + + // For each follow-up interaction, check it doesn't contain message_ids from earlier ones + previousMessageIDs := make(map[string]string) // message_id → interaction_id that owns it + for _, inter := range ints { + var entries []parsedEntry + if err := json.Unmarshal(inter.ResponseEntries, &entries); err != nil { + continue + } + + // Check for leakage: does this interaction contain message_ids from a previous one? + for _, e := range entries { + if e.MessageID == "" { + continue + } + if ownerID, leaked := previousMessageIDs[e.MessageID]; leaked { + errors = append(errors, fmt.Sprintf( + "ISOLATION VIOLATION: Interaction %s (session %s) contains message_id %q which belongs to earlier interaction %s — response_entries leaked across interactions", + truncate(inter.ID, 12), truncate(sessionID, 12), e.MessageID, truncate(ownerID, 12))) + } + } + + // Register this interaction's message_ids + for _, e := range entries { + if e.MessageID != "" { + previousMessageIDs[e.MessageID] = inter.ID + } + } + isolationChecked++ + } + } + if isolationChecked > 0 { + log.Printf("[store] Response entries isolation: checked %d interactions across %d sessions with follow-ups", + isolationChecked, len(sessionInteractions)) + } + // --- THREAD TITLE VALIDATION --- log.Println("\n--------------------------------------------------") log.Println(" THREAD TITLE VALIDATION")