Files
wehub-resource-sync a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:33:42 +08:00

258 lines
7.4 KiB
Go

package svc
import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"
"testing"
"github.com/zzet/gortex/internal/llm"
)
// --- completeChunked unit tests --------------------------------------
// overflowAbove returns a call func that "overflows" (returns a
// context-overflow error) whenever the chunk holds more than limit
// items, and otherwise returns the chunk verbatim as its result.
func overflowAbove(limit int, calls *int) func([]int) ([]int, error) {
return func(sub []int) ([]int, error) {
*calls++
if len(sub) > limit {
return nil, errors.New("fake: maximum context length exceeded")
}
return sub, nil
}
}
func concatInts(parts [][]int) []int {
var out []int
for _, p := range parts {
out = append(out, p...)
}
return out
}
func TestCompleteChunked_NoOverflow(t *testing.T) {
calls := 0
got, chunked, err := completeChunked([]int{1, 2, 3}, overflowAbove(100, &calls), concatInts)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if chunked {
t.Error("chunked should be false when the first call succeeds")
}
if calls != 1 {
t.Errorf("calls = %d, want 1 (no bisection)", calls)
}
if fmt.Sprint(got) != fmt.Sprint([]int{1, 2, 3}) {
t.Errorf("got %v, want [1 2 3]", got)
}
}
func TestCompleteChunked_BisectsOnOverflow(t *testing.T) {
calls := 0
cands := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
got, chunked, err := completeChunked(cands, overflowAbove(3, &calls), concatInts)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !chunked {
t.Error("chunked should be true after bisection")
}
// Bisection must preserve every candidate, in order.
if fmt.Sprint(got) != fmt.Sprint(cands) {
t.Errorf("got %v, want %v — bisection must preserve order and membership", got, cands)
}
}
func TestCompleteChunked_NonOverflowErrorAborts(t *testing.T) {
calls := 0
call := func(sub []int) ([]int, error) {
calls++
return nil, errors.New("fake: 401 invalid api key")
}
_, chunked, err := completeChunked([]int{1, 2, 3, 4}, call, concatInts)
if err == nil {
t.Fatal("expected the non-overflow error to be returned")
}
if chunked {
t.Error("a non-overflow error must not trigger bisection")
}
if calls != 1 {
t.Errorf("calls = %d, want 1 — non-overflow error aborts immediately", calls)
}
}
func TestCompleteChunked_SucceedsAtEightLeaves(t *testing.T) {
calls := 0
// 8 items, overflow above 1 → must bisect into 8 single-item leaves.
cands := []int{0, 1, 2, 3, 4, 5, 6, 7}
got, chunked, err := completeChunked(cands, overflowAbove(1, &calls), concatInts)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !chunked {
t.Error("chunked should be true")
}
if fmt.Sprint(got) != fmt.Sprint(cands) {
t.Errorf("got %v, want %v", got, cands)
}
}
func TestCompleteChunked_FailsWhenLeafStillOverflows(t *testing.T) {
calls := 0
// 16 items, overflow above 1: the deepest allowed leaf still holds
// 2 items (16 -> 8 -> 4 -> 2), so the 8x ceiling is hit and the
// overflow is surfaced as a hard error.
cands := make([]int, 16)
_, chunked, err := completeChunked(cands, overflowAbove(1, &calls), concatInts)
if err == nil {
t.Fatal("expected a hard error once the 8x bisection ceiling is exhausted")
}
if !llm.IsContextOverflow(err) {
t.Errorf("surfaced error should still read as a context overflow: %v", err)
}
if chunked {
t.Error("chunked must be false when bisection ultimately fails")
}
}
func TestConcatDedupe(t *testing.T) {
got := concatDedupe([][]string{{"a", "b"}, {"b", "c"}, {"c", "d"}})
if strings.Join(got, ",") != "a,b,c,d" {
t.Errorf("concatDedupe = %v, want [a b c d]", got)
}
}
// --- integration: adaptive chunking through the assist passes --------
// fakeOverflowProvider implements llm.Provider. It counts the "id-"
// markers in the prompt and fails with a context-overflow error when
// that exceeds maxCands; otherwise it echoes every id back under the
// JSON key the request's shape expects.
type fakeOverflowProvider struct {
maxCands int
calls int
}
func (p *fakeOverflowProvider) Name() string { return "fake" }
func (p *fakeOverflowProvider) Close() error { return nil }
func (p *fakeOverflowProvider) Complete(_ context.Context, req llm.CompletionRequest) (llm.CompletionResponse, error) {
p.calls++
var user string
for _, m := range req.Messages {
if m.Role == llm.RoleUser {
user = m.Content
}
}
if strings.Count(user, "id-") > p.maxCands {
return llm.CompletionResponse{}, errors.New("fake: prompt is too long for this model's context window")
}
ids := scanFakeIDs(user)
key := ""
switch req.Shape {
case llm.ShapeRerankOrder:
key = "order"
case llm.ShapeVerifyKeep:
key = "keep"
default:
return llm.CompletionResponse{Text: "ok"}, nil
}
b, _ := json.Marshal(map[string][]string{key: ids})
return llm.CompletionResponse{Text: string(b)}, nil
}
// scanFakeIDs pulls every whitespace-delimited "id-N" token out of a
// prompt, in first-seen order without duplicates.
func scanFakeIDs(prompt string) []string {
seen := map[string]bool{}
var out []string
for tok := range strings.FieldsSeq(prompt) {
if strings.HasPrefix(tok, "id-") && !seen[tok] {
seen[tok] = true
out = append(out, tok)
}
}
return out
}
func newFakeService(p llm.Provider) *Service {
s := NewService(llm.Config{}, llm.MockBackend{})
s.provider = p
return s
}
func TestRerankSymbols_AdaptiveChunking(t *testing.T) {
cands := make([]llm.RerankCandidate, 20)
for i := range cands {
cands[i] = llm.RerankCandidate{ID: fmt.Sprintf("id-%d", i), Name: fmt.Sprintf("name%d", i)}
}
// maxCands 5 → the full set of 20 overflows; bisection must reach
// chunks of 5 before the fake provider accepts the prompt.
prov := &fakeOverflowProvider{maxCands: 5}
s := newFakeService(prov)
res, err := s.RerankSymbols(context.Background(), "find the thing", cands)
if err != nil {
t.Fatalf("RerankSymbols: %v", err)
}
if !res.Chunked {
t.Error("expected Chunked=true — the 20-candidate prompt overflows")
}
if len(res.Order) != 20 {
t.Fatalf("Order has %d ids, want 20 — bisection must preserve every candidate", len(res.Order))
}
seen := map[string]bool{}
for _, id := range res.Order {
seen[id] = true
}
for _, c := range cands {
if !seen[c.ID] {
t.Errorf("candidate %s lost during chunked rerank", c.ID)
}
}
}
func TestVerifyRelevance_AdaptiveChunking(t *testing.T) {
cands := make([]llm.VerifyCandidate, 20)
for i := range cands {
cands[i] = llm.VerifyCandidate{ID: fmt.Sprintf("id-%d", i), Name: fmt.Sprintf("name%d", i)}
}
prov := &fakeOverflowProvider{maxCands: 5}
s := newFakeService(prov)
res, err := s.VerifyRelevance(context.Background(), "is this relevant", cands)
if err != nil {
t.Fatalf("VerifyRelevance: %v", err)
}
if !res.Chunked {
t.Error("expected Chunked=true — the 20-candidate prompt overflows")
}
// The fake keeps every id, so chunked verification must return all 20.
if len(res.Keep) != 20 {
t.Fatalf("Keep has %d ids, want 20 — chunked verify must be exact", len(res.Keep))
}
}
func TestRerankSymbols_NoChunkingWhenItFits(t *testing.T) {
cands := []llm.RerankCandidate{
{ID: "id-0", Name: "a"}, {ID: "id-1", Name: "b"}, {ID: "id-2", Name: "c"},
}
prov := &fakeOverflowProvider{maxCands: 50}
s := newFakeService(prov)
res, err := s.RerankSymbols(context.Background(), "q", cands)
if err != nil {
t.Fatalf("RerankSymbols: %v", err)
}
if res.Chunked {
t.Error("Chunked should be false when the prompt fits")
}
if prov.calls != 1 {
t.Errorf("provider calls = %d, want 1 (no bisection)", prov.calls)
}
}