kenn-io--agentsview
f99010fae1
CI / lint (push) Failing after 1s
CI / frontend (push) Failing after 1s
CI / scripts (push) Failing after 1s
CI / Go Test (ubuntu-latest) (push) Failing after 0s
CI / frontend-node-25 (push) Failing after 1s
CI / docs (push) Failing after 0s
CI / coverage (push) Failing after 0s
CI / e2e (push) Failing after 0s
Docker / build-and-push (push) Failing after 1s
CI / integration (push) Failing after 4m43s
CI / Go Test (windows-latest) (push) Has been cancelled
CI / Desktop Unit Tests (Windows) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux (arm64)) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Windows) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (aarch64)) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (x86_64)) (push) Has been cancelled
297 行
9.1 KiB
Go
297 行
9.1 KiB
Go
package db
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
corerecall "go.kenn.io/agentsview/internal/recall"
|
|
)
|
|
|
|
func TestFlattenTrajectoryText(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
in string
|
|
want string
|
|
}{
|
|
{
|
|
name: "object keys sorted, arrays in order, non-strings skipped",
|
|
in: `{"b":"second","a":"first","arr":["x","y"],"n":42,"ok":true,"blank":" ","obj":{"z":"deep"}}`,
|
|
want: "first\nx\ny\nsecond\ndeep",
|
|
},
|
|
{name: "bare string", in: `"hello world"`, want: "hello world"},
|
|
{name: "array root", in: `["a","b"]`, want: "a\nb"},
|
|
{name: "no strings", in: `{"n":1,"b":true,"z":null}`, want: ""},
|
|
{
|
|
name: "nested objects inside arrays",
|
|
in: `{"msgs":[{"role":"user","text":"q"},{"role":"asst","text":"a"}]}`,
|
|
want: "user\nq\nasst\na",
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got, err := flattenTrajectoryText(json.RawMessage(tt.in))
|
|
require.NoError(t, err)
|
|
assert.Equal(t, tt.want, got)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFlattenTrajectoryTextInvalidJSON(t *testing.T) {
|
|
_, err := flattenTrajectoryText(json.RawMessage(`{not json`))
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestFlattenTrajectoryTextDeterministic(t *testing.T) {
|
|
in := json.RawMessage(`{"c":"3","a":"1","b":"2"}`)
|
|
first, err := flattenTrajectoryText(in)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "1\n2\n3", first)
|
|
for range 5 {
|
|
again, err := flattenTrajectoryText(in)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, first, again)
|
|
}
|
|
}
|
|
|
|
func TestChunkText(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
in string
|
|
size int
|
|
want []string
|
|
}{
|
|
{"splits with partial last", "abcdefg", 3, []string{"abc", "def", "g"}},
|
|
{"exact multiple", "abcdef", 3, []string{"abc", "def"}},
|
|
{"single chunk", "abc", 10, []string{"abc"}},
|
|
{"empty", "", 3, nil},
|
|
{"non-positive size falls back to default", "abc", 0, []string{"abc"}},
|
|
{"multibyte runes are not split", "a√bc", 2, []string{"a√", "bc"}},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
assert.Equal(t, tt.want, chunkText(tt.in, tt.size))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestEvalIngestIDDeterministicAndCollisionSafe(t *testing.T) {
|
|
a, err := evalIngestID("eval-trajectory", "run1", "traj1", 0)
|
|
require.NoError(t, err)
|
|
b, err := evalIngestID("eval-trajectory", "run1", "traj1", 0)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, a, b, "same parts must produce the same id")
|
|
assert.Len(t, a, 64, "sha-256 hex is 64 chars")
|
|
|
|
other, err := evalIngestID("eval-trajectory", "run1", "traj1", 1)
|
|
require.NoError(t, err)
|
|
assert.NotEqual(t, a, other, "a different chunk index must produce a different id")
|
|
|
|
// Raw concatenation of ("a","b:c") and ("a:b","c") would collide; the
|
|
// JSON-tuple hash must not.
|
|
x, err := evalIngestID("a", "b:c")
|
|
require.NoError(t, err)
|
|
y, err := evalIngestID("a:b", "c")
|
|
require.NoError(t, err)
|
|
assert.NotEqual(t, x, y, "delimiter-ambiguous tuples must not collide")
|
|
}
|
|
|
|
func TestIngestEvalTrajectory(t *testing.T) {
|
|
d := testDB(t)
|
|
ctx := context.Background()
|
|
|
|
// Flattened text spans three 2000-rune chunks (4050 runes total).
|
|
long := strings.Repeat("x", defaultEvalChunkChars*2+50)
|
|
in := EvalTrajectoryIngest{
|
|
RunID: "run1",
|
|
TrajectoryID: "traj1",
|
|
Trajectory: json.RawMessage(`{"text":"` + long + `"}`),
|
|
ExtractorMethod: "eval-harness-raw-trajectory",
|
|
SourceVersion: "test-harness-v1",
|
|
}
|
|
|
|
res, err := d.IngestEvalTrajectory(ctx, in)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "run1", res.RunID)
|
|
assert.Equal(t, "traj1", res.TrajectoryID)
|
|
assert.Equal(t, 3, res.EntriesIndexed)
|
|
|
|
id0, err := evalTrajectoryChunkID(
|
|
in, evalTrajectoryContentDigest(long), 0,
|
|
)
|
|
require.NoError(t, err)
|
|
m0, err := d.GetRecallEntry(ctx, id0)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, m0)
|
|
assert.Equal(t, corerecall.TypeFact, m0.Type)
|
|
assert.Equal(t, corerecall.ScopeRepository, m0.Scope)
|
|
assert.Equal(t, corerecall.StatusAccepted, m0.Status)
|
|
assert.Equal(t, corerecall.ReviewStateEvalRaw, m0.ReviewState)
|
|
assert.Equal(t, in.ExtractorMethod, m0.ExtractorMethod)
|
|
assert.Equal(t, "run1", m0.SourceRunID)
|
|
assert.Equal(t, "traj1:chunk:0", m0.SourceEpisodeID)
|
|
assert.True(t, m0.Transferable)
|
|
assert.True(t, m0.ProvenanceOK)
|
|
assert.Nil(t, m0.Confidence, "raw chunks carry no confidence")
|
|
assert.Equal(t, defaultEvalChunkChars, len([]rune(m0.Body)))
|
|
assert.Equal(t, defaultEvalTrajectoryProject, m0.Project)
|
|
assert.Equal(t, defaultEvalTrajectoryAgent, m0.Agent)
|
|
|
|
sess, err := d.GetSession(ctx, m0.SourceSessionID)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, sess)
|
|
assert.Equal(t, in.SourceVersion, sess.SourceVersion)
|
|
|
|
res2, err := d.IngestEvalTrajectory(ctx, in)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 0, res2.EntriesIndexed, "re-ingest must insert nothing new")
|
|
}
|
|
|
|
func TestIngestEvalTrajectoryConcurrentReingestIsIdempotent(t *testing.T) {
|
|
d := testDB(t)
|
|
in := EvalTrajectoryIngest{
|
|
RunID: "run-concurrent",
|
|
TrajectoryID: "traj-concurrent",
|
|
Trajectory: json.RawMessage(`{"text":"` + strings.Repeat("x", defaultEvalChunkChars*2+50) + `"}`),
|
|
ExtractorMethod: "eval-harness-raw-trajectory",
|
|
SourceVersion: "test-harness-v1",
|
|
}
|
|
const workers = 16
|
|
start := make(chan struct{})
|
|
type outcome struct {
|
|
result EvalTrajectoryIngestResult
|
|
err error
|
|
}
|
|
outcomes := make(chan outcome, workers)
|
|
var ready sync.WaitGroup
|
|
ready.Add(workers)
|
|
for range workers {
|
|
go func() {
|
|
ready.Done()
|
|
<-start
|
|
result, err := d.IngestEvalTrajectory(context.Background(), in)
|
|
outcomes <- outcome{result: result, err: err}
|
|
}()
|
|
}
|
|
ready.Wait()
|
|
close(start)
|
|
|
|
totalIndexed := 0
|
|
for range workers {
|
|
outcome := <-outcomes
|
|
require.NoError(t, outcome.err)
|
|
totalIndexed += outcome.result.EntriesIndexed
|
|
}
|
|
assert.Equal(t, 3, totalIndexed)
|
|
entries, err := d.ListRecallEntries(context.Background(), RecallQuery{
|
|
SourceRunID: in.RunID,
|
|
Status: corerecall.StatusAccepted,
|
|
Limit: 10,
|
|
})
|
|
require.NoError(t, err)
|
|
assert.Len(t, entries, 3)
|
|
}
|
|
|
|
func TestIngestEvalTrajectoryVersionsIdentityByExtractorMetadataAndContent(t *testing.T) {
|
|
d := testDB(t)
|
|
ctx := context.Background()
|
|
base := EvalTrajectoryIngest{
|
|
RunID: "run1",
|
|
TrajectoryID: "traj1",
|
|
Trajectory: json.RawMessage(`{"text":"first trajectory"}`),
|
|
ExtractorMethod: "extractor-v1",
|
|
SourceVersion: "harness-v1",
|
|
}
|
|
|
|
first, err := d.IngestEvalTrajectory(ctx, base)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 1, first.EntriesIndexed)
|
|
|
|
changedExtractor := base
|
|
changedExtractor.ExtractorMethod = "extractor-v2"
|
|
second, err := d.IngestEvalTrajectory(ctx, changedExtractor)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 1, second.EntriesIndexed)
|
|
|
|
changedSourceVersion := base
|
|
changedSourceVersion.SourceVersion = "harness-v2"
|
|
third, err := d.IngestEvalTrajectory(ctx, changedSourceVersion)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 1, third.EntriesIndexed)
|
|
|
|
changedContent := base
|
|
changedContent.Trajectory = json.RawMessage(`{"text":"revised trajectory"}`)
|
|
fourth, err := d.IngestEvalTrajectory(ctx, changedContent)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 1, fourth.EntriesIndexed)
|
|
|
|
entries, err := d.ListRecallEntries(ctx, RecallQuery{
|
|
SourceRunID: "run1",
|
|
Status: corerecall.StatusAccepted,
|
|
Limit: 10,
|
|
})
|
|
require.NoError(t, err)
|
|
require.Len(t, entries, 4)
|
|
assert.ElementsMatch(t, []string{
|
|
"first trajectory",
|
|
"first trajectory",
|
|
"first trajectory",
|
|
"revised trajectory",
|
|
}, []string{entries[0].Body, entries[1].Body, entries[2].Body, entries[3].Body})
|
|
}
|
|
|
|
func TestIngestEvalTrajectoryNoStringsIndexesNothing(t *testing.T) {
|
|
d := testDB(t)
|
|
res, err := d.IngestEvalTrajectory(context.Background(), EvalTrajectoryIngest{
|
|
RunID: "run1",
|
|
TrajectoryID: "traj-empty",
|
|
Trajectory: json.RawMessage(`{"n":1,"ok":true}`),
|
|
ExtractorMethod: "eval-harness-raw-trajectory",
|
|
SourceVersion: "test-harness-v1",
|
|
})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 0, res.EntriesIndexed)
|
|
}
|
|
|
|
func TestIngestEvalTrajectoryRequiresIDs(t *testing.T) {
|
|
d := testDB(t)
|
|
_, err := d.IngestEvalTrajectory(context.Background(), EvalTrajectoryIngest{
|
|
TrajectoryID: "traj1",
|
|
Trajectory: json.RawMessage(`{"text":"hi"}`),
|
|
ExtractorMethod: "eval-harness-raw-trajectory",
|
|
SourceVersion: "test-harness-v1",
|
|
})
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestIngestEvalTrajectoryRequiresExtractorMethodAndSourceVersion(t *testing.T) {
|
|
d := testDB(t)
|
|
ctx := context.Background()
|
|
base := EvalTrajectoryIngest{
|
|
RunID: "run1",
|
|
TrajectoryID: "traj1",
|
|
Trajectory: json.RawMessage(`{"text":"hi"}`),
|
|
}
|
|
|
|
missingExtractor := base
|
|
missingExtractor.SourceVersion = "test-harness-v1"
|
|
_, err := d.IngestEvalTrajectory(ctx, missingExtractor)
|
|
require.ErrorContains(t, err, "extractor_method is required")
|
|
|
|
missingSourceVersion := base
|
|
missingSourceVersion.ExtractorMethod = "eval-harness-raw-trajectory"
|
|
_, err = d.IngestEvalTrajectory(ctx, missingSourceVersion)
|
|
require.ErrorContains(t, err, "source_version is required")
|
|
|
|
tooLong := base
|
|
tooLong.ExtractorMethod = strings.Repeat("x", maxEvalFieldRunes+1)
|
|
tooLong.SourceVersion = "test-harness-v1"
|
|
_, err = d.IngestEvalTrajectory(ctx, tooLong)
|
|
require.ErrorContains(t, err, "extractor_method exceeds maximum length")
|
|
}
|