first commit
This commit is contained in:
@@ -0,0 +1,195 @@
|
||||
package dream
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// canned builds an allowed-set + Plan pair for parser tests from a list of
|
||||
// absolute paths.
|
||||
func allowedSet(paths ...string) map[string]struct{} {
|
||||
m := make(map[string]struct{}, len(paths))
|
||||
for _, p := range paths {
|
||||
m[filepath.Clean(p)] = struct{}{}
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func TestBuildConsolidatePromptListsEntriesAndHints(t *testing.T) {
|
||||
root := "/mem"
|
||||
a := "/mem/global/user/a.md"
|
||||
b := "/mem/global/user/b.md"
|
||||
idx := "/mem/global/MEMORY.md"
|
||||
plan := Plan{
|
||||
Files: []MemoryFile{
|
||||
{Path: a, Scope: "global", Type: "user", Size: 5, Body: "alpha body"},
|
||||
{Path: b, Scope: "global", Type: "user", Size: 5, Body: "beta body"},
|
||||
{Path: idx, Scope: "global", Type: "", Size: 3, Body: "- [a](user/a.md)"},
|
||||
},
|
||||
NearDupPairs: []NearDupPair{{A: a, B: b, Similarity: 0.82}},
|
||||
InvalidPathRefs: []InvalidPathRef{{File: a, Ref: "./gone.go"}},
|
||||
}
|
||||
eligible := eligibleFiles(plan)
|
||||
if len(eligible) != 2 {
|
||||
t.Fatalf("eligibleFiles = %d, want 2 (MEMORY.md excluded)", len(eligible))
|
||||
}
|
||||
prompt := buildConsolidatePrompt(ConsolidateInput{Plan: plan, MemoryRoot: root, ProjectDir: ""}, eligible, defaultBodyBudget)
|
||||
|
||||
for _, want := range []string{a, b, "alpha body", "beta body", "similarity 0.82", "./gone.go", "global only"} {
|
||||
if !strings.Contains(prompt, want) {
|
||||
t.Errorf("prompt missing %q\n---\n%s", want, prompt)
|
||||
}
|
||||
}
|
||||
if strings.Contains(prompt, "MEMORY.md") {
|
||||
t.Errorf("prompt must not offer the MEMORY.md index as an entry:\n%s", prompt)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTruncateBody(t *testing.T) {
|
||||
if got := truncateBody("short", 100); got != "short" {
|
||||
t.Fatalf("no truncation expected, got %q", got)
|
||||
}
|
||||
long := strings.Repeat("x", 50)
|
||||
got := truncateBody(long, 10)
|
||||
if !strings.HasPrefix(got, strings.Repeat("x", 10)) || !strings.Contains(got, "truncated") {
|
||||
t.Fatalf("truncateBody = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseConsolidateResponseMergeAndPrune(t *testing.T) {
|
||||
a := "/mem/global/user/a.md"
|
||||
b := "/mem/global/user/b.md"
|
||||
c := "/mem/global/user/c.md"
|
||||
allowed := allowedSet(a, b, c)
|
||||
|
||||
raw := "here you go:\n```json\n" + `{
|
||||
"merges": [{"keep": "` + a + `", "body": "merged body", "remove": ["` + b + `"]}],
|
||||
"prunes": [{"path": "` + c + `", "reason": "superseded by newer note"}],
|
||||
"notes": ["did the thing"]
|
||||
}` + "\n```\n"
|
||||
|
||||
res := parseConsolidateResponse(raw, allowed)
|
||||
if res.Merged != 1 {
|
||||
t.Errorf("Merged = %d, want 1", res.Merged)
|
||||
}
|
||||
if res.Pruned != 1 {
|
||||
t.Errorf("Pruned = %d, want 1", res.Pruned)
|
||||
}
|
||||
if got := res.MergedBodies[a]; got != "merged body" {
|
||||
t.Errorf("MergedBodies[a] = %q", got)
|
||||
}
|
||||
wantDel := map[string]bool{b: true, c: true}
|
||||
if len(res.Deletions) != 2 {
|
||||
t.Fatalf("Deletions = %v, want b and c", res.Deletions)
|
||||
}
|
||||
for _, d := range res.Deletions {
|
||||
if !wantDel[d] {
|
||||
t.Errorf("unexpected deletion %q", d)
|
||||
}
|
||||
}
|
||||
joined := strings.Join(res.Notes, "|")
|
||||
if !strings.Contains(joined, "superseded by newer note") || !strings.Contains(joined, "did the thing") {
|
||||
t.Errorf("notes missing content: %v", res.Notes)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseConsolidateResponseRejectsUnknownAndUnsafePaths(t *testing.T) {
|
||||
a := "/mem/global/user/a.md"
|
||||
allowed := allowedSet(a)
|
||||
raw := `{
|
||||
"merges": [{"keep": "/etc/passwd", "body": "x", "remove": ["` + a + `"]}],
|
||||
"prunes": [{"path": "/mem/global/MEMORY.md", "reason": "index"}]
|
||||
}`
|
||||
res := parseConsolidateResponse(raw, allowed)
|
||||
if res.Merged != 0 || res.Pruned != 0 || len(res.Deletions) != 0 {
|
||||
t.Fatalf("unsafe/unknown paths must be ignored, got %+v", res)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseConsolidateResponseConservativeOnEmptyReasonAndBadJSON(t *testing.T) {
|
||||
a := "/mem/global/user/a.md"
|
||||
allowed := allowedSet(a)
|
||||
|
||||
// Empty prune reason → KEEP.
|
||||
res := parseConsolidateResponse(`{"prunes":[{"path":"`+a+`","reason":""}]}`, allowed)
|
||||
if res.Pruned != 0 || len(res.Deletions) != 0 {
|
||||
t.Fatalf("empty reason must KEEP, got %+v", res)
|
||||
}
|
||||
|
||||
// Unparseable → empty result with a note, never a deletion.
|
||||
res = parseConsolidateResponse("the model rambled with no json", allowed)
|
||||
if res.Merged != 0 || res.Pruned != 0 || len(res.Deletions) != 0 {
|
||||
t.Fatalf("bad JSON must KEEP everything, got %+v", res)
|
||||
}
|
||||
if len(res.Notes) == 0 {
|
||||
t.Fatal("expected an explanatory note on unparseable response")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseConsolidateResponseNoOpMergeIgnored(t *testing.T) {
|
||||
a := "/mem/global/user/a.md"
|
||||
allowed := allowedSet(a)
|
||||
// A merge that removes nothing must not touch the file.
|
||||
res := parseConsolidateResponse(`{"merges":[{"keep":"`+a+`","body":"rewritten","remove":[]}]}`, allowed)
|
||||
if len(res.MergedBodies) != 0 || res.Merged != 0 {
|
||||
t.Fatalf("no-op merge must be ignored, got %+v", res)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLLMConsolidatorUsesCompleter(t *testing.T) {
|
||||
root := "/mem"
|
||||
a := "/mem/global/user/a.md"
|
||||
b := "/mem/global/user/b.md"
|
||||
plan := Plan{Files: []MemoryFile{
|
||||
{Path: a, Scope: "global", Type: "user", Body: "one"},
|
||||
{Path: b, Scope: "global", Type: "user", Body: "two"},
|
||||
}}
|
||||
|
||||
var gotSystem, gotUser string
|
||||
c := &llmConsolidator{complete: func(_ context.Context, sys, user string) (string, error) {
|
||||
gotSystem, gotUser = sys, user
|
||||
return `{"merges":[{"keep":"` + a + `","body":"merged","remove":["` + b + `"]}]}`, nil
|
||||
}}
|
||||
res, err := c.Consolidate(context.Background(), ConsolidateInput{Plan: plan, MemoryRoot: root})
|
||||
if err != nil {
|
||||
t.Fatalf("Consolidate: %v", err)
|
||||
}
|
||||
if gotSystem != dreamSystemPrompt {
|
||||
t.Error("system prompt not passed through")
|
||||
}
|
||||
if !strings.Contains(gotUser, a) {
|
||||
t.Error("user prompt missing entry path")
|
||||
}
|
||||
if res.Merged != 1 || res.MergedBodies[a] != "merged" {
|
||||
t.Fatalf("merge not applied: %+v", res)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLLMConsolidatorPropagatesHardError(t *testing.T) {
|
||||
plan := Plan{Files: []MemoryFile{{Path: "/mem/global/user/a.md", Scope: "global", Type: "user", Body: "x"}}}
|
||||
c := &llmConsolidator{complete: func(context.Context, string, string) (string, error) {
|
||||
return "", errors.New("upstream 500")
|
||||
}}
|
||||
if _, err := c.Consolidate(context.Background(), ConsolidateInput{Plan: plan, MemoryRoot: "/mem"}); err == nil {
|
||||
t.Fatal("expected hard completion error to propagate")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLLMConsolidatorSkipsWhenNoEligibleFiles(t *testing.T) {
|
||||
called := false
|
||||
c := &llmConsolidator{complete: func(context.Context, string, string) (string, error) {
|
||||
called = true
|
||||
return "{}", nil
|
||||
}}
|
||||
// Only a MEMORY.md index → nothing eligible → no model call.
|
||||
plan := Plan{Files: []MemoryFile{{Path: "/mem/global/MEMORY.md", Scope: "global", Body: "idx"}}}
|
||||
if _, err := c.Consolidate(context.Background(), ConsolidateInput{Plan: plan, MemoryRoot: "/mem"}); err != nil {
|
||||
t.Fatalf("Consolidate: %v", err)
|
||||
}
|
||||
if called {
|
||||
t.Fatal("model should not be called when no eligible files exist")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user