diff --git a/internal/admin/handler_raw_samples.go b/internal/admin/handler_raw_samples.go index 4bf85a8..c9ad58e 100644 --- a/internal/admin/handler_raw_samples.go +++ b/internal/admin/handler_raw_samples.go @@ -324,11 +324,13 @@ func buildCaptureChains(snapshot []devcapture.Entry) []captureChain { return nil } ordered := make([]devcapture.Entry, len(snapshot)) - copy(ordered, snapshot) + // devcapture snapshots are newest-first because the store prepends entries. + // Reverse once so equal-second timestamps can preserve the actual capture + // order (completion before continue) under the stable CreatedAt sort below. + for i := range snapshot { + ordered[len(snapshot)-1-i] = snapshot[i] + } sort.SliceStable(ordered, func(i, j int) bool { - if ordered[i].CreatedAt == ordered[j].CreatedAt { - return ordered[i].ID < ordered[j].ID - } return ordered[i].CreatedAt < ordered[j].CreatedAt }) diff --git a/internal/admin/handler_raw_samples_test.go b/internal/admin/handler_raw_samples_test.go index fa15e45..de4fa03 100644 --- a/internal/admin/handler_raw_samples_test.go +++ b/internal/admin/handler_raw_samples_test.go @@ -285,6 +285,39 @@ func TestQueryRawSampleCapturesGroupsBySessionAndMatchesQuestion(t *testing.T) { } } +func TestBuildCaptureChainsPreservesCaptureOrderWhenTimestampsCollide(t *testing.T) { + snapshot := []devcapture.Entry{ + { + ID: "cap_continue", + CreatedAt: 1712365200, + Label: "deepseek_continue", + RequestBody: `{"chat_session_id":"session-collision","message_id":2}`, + ResponseBody: "data: {\"v\":\"第二段\"}\n\n", + }, + { + ID: "cap_completion", + CreatedAt: 1712365200, + Label: "deepseek_completion", + RequestBody: `{"chat_session_id":"session-collision","prompt":"题目"}`, + ResponseBody: "data: {\"v\":\"第一段\"}\n\n", + }, + } + + chains := buildCaptureChains(snapshot) + if len(chains) != 1 { + t.Fatalf("expected 1 chain, got %d", len(chains)) + } + if len(chains[0].Entries) != 2 { + t.Fatalf("expected 2 entries, got %d", len(chains[0].Entries)) + } + if chains[0].Entries[0].Label != "deepseek_completion" { + t.Fatalf("expected completion first, got %#v", chains[0].Entries) + } + if chains[0].Entries[1].Label != "deepseek_continue" { + t.Fatalf("expected continue second, got %#v", chains[0].Entries) + } +} + func TestSaveRawSampleFromCapturesPersistsSelectedChain(t *testing.T) { root := t.TempDir() t.Setenv("DS2API_RAW_STREAM_SAMPLE_ROOT", root)