Skip to content

Commit bc38795

Browse files
test(fallback): pin scanned-window pagination semantics
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
1 parent 6088f58 commit bc38795

2 files changed

Lines changed: 92 additions & 1 deletion

File tree

internal/analysis/fallback/service.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ func (s *Service) FindSuspects(ctx context.Context, opts Options) ([]SuspectEdge
6161

6262
// FindSuspectsPage reports suspect fallback call edges and returns one bounded page.
6363
// @intent bound fallback suspect analysis before annotation lookups so large graphs cannot expand unbounded responses.
64-
// @domainRule fetch limit+offset+1 fallback edges before suspect filtering; pagination applies to analyzed edge order, not global suspect total.
64+
// @domainRule fetch limit+offset+1 fallback edges before suspect filtering; paging bounds the scanned fallback-edge window, so returned suspect items may be fewer than limit while HasMore still reflects additional fallback edges beyond that window.
6565
func (s *Service) FindSuspectsPage(ctx context.Context, opts Options) (Result, error) {
6666
req, err := paging.Normalize(opts.Page)
6767
if err != nil {

internal/analysis/fallback/service_test.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,3 +132,94 @@ func TestSuspectFallbackEdges_FindPageBoundsFallbackEdgeAnalysis(t *testing.T) {
132132
t.Fatalf("unexpected pagination: %+v", got.Pagination)
133133
}
134134
}
135+
136+
func TestSuspectFallbackEdges_FindPageCanReturnFewerSuspectsThanLimitWhileHasMoreRemains(t *testing.T) {
137+
db := setupFallbackDB(t)
138+
ctx := context.Background()
139+
store := gormstore.New(db)
140+
141+
// First fallback edge is suspect.
142+
{
143+
source := model.Node{QualifiedName: "pkg.SourceA", Kind: model.NodeKindFunction, Name: "SourceA", FilePath: "src.go", StartLine: 1, EndLine: 2, Language: "go"}
144+
target := model.Node{QualifiedName: "pkg.TargetA", Kind: model.NodeKindFunction, Name: "TargetA", FilePath: "target.go", StartLine: 1, EndLine: 2, Language: "go"}
145+
if err := store.UpsertNodes(ctx, []model.Node{source, target}); err != nil {
146+
t.Fatal(err)
147+
}
148+
sourceNode, _ := store.GetNode(ctx, source.QualifiedName)
149+
targetNode, _ := store.GetNode(ctx, target.QualifiedName)
150+
if err := store.UpsertEdges(ctx, []model.Edge{{FromNodeID: sourceNode.ID, ToNodeID: targetNode.ID, Kind: model.EdgeKindFallbackCalls, Fingerprint: "fallback-a"}}); err != nil {
151+
t.Fatal(err)
152+
}
153+
if err := store.UpsertAnnotation(ctx, &model.Annotation{NodeID: sourceNode.ID, Tags: []model.DocTag{{Kind: model.TagIntent, Value: "source auth", Ordinal: 0}}}); err != nil {
154+
t.Fatal(err)
155+
}
156+
if err := store.UpsertAnnotation(ctx, &model.Annotation{NodeID: targetNode.ID, Tags: []model.DocTag{{Kind: model.TagIntent, Value: "invoice render", Ordinal: 0}}}); err != nil {
157+
t.Fatal(err)
158+
}
159+
}
160+
161+
// Second fallback edge is suppressed by overlapping annotation context.
162+
{
163+
source := model.Node{QualifiedName: "pkg.SourceB", Kind: model.NodeKindFunction, Name: "SourceB", FilePath: "src.go", StartLine: 1, EndLine: 2, Language: "go"}
164+
target := model.Node{QualifiedName: "pkg.TargetB", Kind: model.NodeKindFunction, Name: "TargetB", FilePath: "target.go", StartLine: 1, EndLine: 2, Language: "go"}
165+
if err := store.UpsertNodes(ctx, []model.Node{source, target}); err != nil {
166+
t.Fatal(err)
167+
}
168+
sourceNode, _ := store.GetNode(ctx, source.QualifiedName)
169+
targetNode, _ := store.GetNode(ctx, target.QualifiedName)
170+
if err := store.UpsertEdges(ctx, []model.Edge{{FromNodeID: sourceNode.ID, ToNodeID: targetNode.ID, Kind: model.EdgeKindFallbackCalls, Fingerprint: "fallback-b"}}); err != nil {
171+
t.Fatal(err)
172+
}
173+
if err := store.UpsertAnnotation(ctx, &model.Annotation{NodeID: sourceNode.ID, Tags: []model.DocTag{{Kind: model.TagIntent, Value: "session token", Ordinal: 0}}}); err != nil {
174+
t.Fatal(err)
175+
}
176+
if err := store.UpsertAnnotation(ctx, &model.Annotation{NodeID: targetNode.ID, Tags: []model.DocTag{{Kind: model.TagIntent, Value: "session token refresh", Ordinal: 0}}}); err != nil {
177+
t.Fatal(err)
178+
}
179+
}
180+
181+
// Third fallback edge is suspect and keeps HasMore true even when the page returns only one item.
182+
{
183+
source := model.Node{QualifiedName: "pkg.SourceC", Kind: model.NodeKindFunction, Name: "SourceC", FilePath: "src.go", StartLine: 1, EndLine: 2, Language: "go"}
184+
target := model.Node{QualifiedName: "pkg.TargetC", Kind: model.NodeKindFunction, Name: "TargetC", FilePath: "target.go", StartLine: 1, EndLine: 2, Language: "go"}
185+
if err := store.UpsertNodes(ctx, []model.Node{source, target}); err != nil {
186+
t.Fatal(err)
187+
}
188+
sourceNode, _ := store.GetNode(ctx, source.QualifiedName)
189+
targetNode, _ := store.GetNode(ctx, target.QualifiedName)
190+
if err := store.UpsertEdges(ctx, []model.Edge{{FromNodeID: sourceNode.ID, ToNodeID: targetNode.ID, Kind: model.EdgeKindFallbackCalls, Fingerprint: "fallback-c"}}); err != nil {
191+
t.Fatal(err)
192+
}
193+
if err := store.UpsertAnnotation(ctx, &model.Annotation{NodeID: sourceNode.ID, Tags: []model.DocTag{{Kind: model.TagIntent, Value: "source auth c", Ordinal: 0}}}); err != nil {
194+
t.Fatal(err)
195+
}
196+
if err := store.UpsertAnnotation(ctx, &model.Annotation{NodeID: targetNode.ID, Tags: []model.DocTag{{Kind: model.TagIntent, Value: "invoice render c", Ordinal: 0}}}); err != nil {
197+
t.Fatal(err)
198+
}
199+
}
200+
201+
got, err := New(db, store).FindSuspectsPage(ctx, Options{Page: paging.Request{Limit: 1, Offset: 0}})
202+
if err != nil {
203+
t.Fatal(err)
204+
}
205+
if len(got.Items) != 1 {
206+
t.Fatalf("expected 1 suspect from the first scanned fallback edge, got %d", len(got.Items))
207+
}
208+
if got.Items[0].Edge.Fingerprint != "fallback-a" {
209+
t.Fatalf("unexpected suspect edge: %s", got.Items[0].Edge.Fingerprint)
210+
}
211+
if got.Pagination.Limit != 1 || got.Pagination.Offset != 0 || got.Pagination.Returned != 1 || !got.Pagination.HasMore {
212+
t.Fatalf("unexpected pagination: %+v", got.Pagination)
213+
}
214+
215+
got, err = New(db, store).FindSuspectsPage(ctx, Options{Page: paging.Request{Limit: 1, Offset: 1}})
216+
if err != nil {
217+
t.Fatal(err)
218+
}
219+
if len(got.Items) != 0 {
220+
t.Fatalf("expected 0 suspects from a page containing only suppressed fallback edges, got %d", len(got.Items))
221+
}
222+
if got.Pagination.Limit != 1 || got.Pagination.Offset != 1 || got.Pagination.Returned != 0 || !got.Pagination.HasMore {
223+
t.Fatalf("unexpected pagination for empty suspect page: %+v", got.Pagination)
224+
}
225+
}

0 commit comments

Comments
 (0)