Files
esengine--deepseek-reasonix/desktop/memory_suggestions_compiler_test.go
2026-07-13 13:00:08 +08:00

241 lines
7.9 KiB
Go

package main
import (
"context"
"strings"
"testing"
"reasonix/internal/config"
"reasonix/internal/control"
"reasonix/internal/memory"
"reasonix/internal/memorycompiler"
)
func seedCompilerFailures(t *testing.T, workspaceRoot string, turns int, errText string) {
t.Helper()
rt := memorycompiler.New(config.MemoryCompilerDir(workspaceRoot))
if rt == nil {
t.Fatal("memory compiler dir did not resolve under isolated dirs")
}
for i := 0; i < turns; i++ {
_, turn := rt.StartTurn(context.Background(), "fix a bug", nil)
turn.RecordToolResults([]memorycompiler.ToolRecord{
{Name: "bash", Error: errText},
{Name: "bash", Error: errText},
})
turn.Finish(nil)
}
}
func TestMemorySuggestionsIncludeCompilerCandidates(t *testing.T) {
isolateDesktopUserDirs(t)
userDir := t.TempDir()
cwd := t.TempDir()
sessionDir := t.TempDir()
store := memory.StoreFor(userDir, cwd)
seedCompilerFailures(t, cwd, 2, "cannot find module providing package foo")
app := NewApp()
app.setTestCtrl(control.New(control.Options{
Memory: &memory.Set{Store: store, CWD: cwd, UserDir: userDir},
SessionDir: sessionDir,
}), "test-model")
app.tabs["test"].WorkspaceRoot = cwd
view := app.MemorySuggestions()
var candidate MemorySuggestion
for _, item := range view.Memories {
if strings.HasPrefix(item.Name, "memory-v5-") {
candidate = item
break
}
}
if candidate.ID == "" {
t.Fatalf("memories = %+v, want a memory-v5 candidate", view.Memories)
}
if candidate.Type != string(memory.TypeProject) {
t.Fatalf("candidate type = %q, want project", candidate.Type)
}
if !strings.Contains(candidate.Description, "cannot find module") {
t.Fatalf("candidate description missing pattern: %q", candidate.Description)
}
if !strings.Contains(candidate.Reason, "Memory v5") {
t.Fatalf("candidate reason missing provenance: %q", candidate.Reason)
}
path, err := app.AcceptMemorySuggestion(candidate)
if err != nil {
t.Fatalf("AcceptMemorySuggestion: %v", err)
}
if path == "" {
t.Fatal("AcceptMemorySuggestion returned empty path")
}
saved := store.List()
if len(saved) != 1 || !strings.Contains(saved[0].Body, "cannot find module") {
t.Fatalf("saved memories = %+v, want compiler candidate body", saved)
}
// Once accepted, the same pattern is covered by an existing memory and
// must not be suggested again.
again := app.MemorySuggestions()
for _, item := range again.Memories {
if strings.HasPrefix(item.Name, "memory-v5-") {
t.Fatalf("accepted pattern suggested again: %+v", item)
}
}
}
func TestCompilerCandidatesRequireStablePattern(t *testing.T) {
isolateDesktopUserDirs(t)
userDir := t.TempDir()
cwd := t.TempDir()
sessionDir := t.TempDir()
store := memory.StoreFor(userDir, cwd)
// A pattern seen in only one turn is not stable enough to suggest.
seedCompilerFailures(t, cwd, 1, "cannot find module providing package foo")
app := NewApp()
app.setTestCtrl(control.New(control.Options{
Memory: &memory.Set{Store: store, CWD: cwd, UserDir: userDir},
SessionDir: sessionDir,
}), "test-model")
app.tabs["test"].WorkspaceRoot = cwd
view := app.MemorySuggestions()
for _, item := range view.Memories {
if strings.HasPrefix(item.Name, "memory-v5-") {
t.Fatalf("unstable pattern was suggested: %+v", item)
}
}
}
// TestCompilerCandidateNamesUniqueForSimilarPatterns covers the review
// finding: asciiSlug drops non-ASCII runes and truncates to 56 chars, so
// patterns differing only in CJK text (or sharing a long English prefix) used
// to collide on Name/ID — colliding IDs cross-wire the frontend's accepted
// state and colliding Names make Store.Save overwrite the earlier memory.
func TestCompilerCandidateNamesUniqueForSimilarPatterns(t *testing.T) {
isolateDesktopUserDirs(t)
userDir := t.TempDir()
cwd := t.TempDir()
sessionDir := t.TempDir()
store := memory.StoreFor(userDir, cwd)
// Same tool, errors differ only in CJK text: identical after asciiSlug.
seedCompilerFailures(t, cwd, 2, "编译失败:找不到模块甲")
seedCompilerFailures(t, cwd, 2, "编译失败:找不到模块乙")
app := NewApp()
app.setTestCtrl(control.New(control.Options{
Memory: &memory.Set{Store: store, CWD: cwd, UserDir: userDir},
SessionDir: sessionDir,
}), "test-model")
app.tabs["test"].WorkspaceRoot = cwd
view := app.MemorySuggestions()
var candidates []MemorySuggestion
for _, item := range view.Memories {
if strings.HasPrefix(item.Name, "memory-v5-") {
candidates = append(candidates, item)
}
}
if len(candidates) != 2 {
t.Fatalf("candidates = %+v, want both CJK-only-diff patterns", candidates)
}
if candidates[0].Name == candidates[1].Name || candidates[0].ID == candidates[1].ID {
t.Fatalf("similar patterns collided on Name/ID: %q vs %q", candidates[0].Name, candidates[1].Name)
}
// Accepting both must persist two distinct memories, not overwrite one.
for _, c := range candidates {
if _, err := app.AcceptMemorySuggestion(c); err != nil {
t.Fatalf("AcceptMemorySuggestion(%s): %v", c.Name, err)
}
}
saved := store.List()
if len(saved) != 2 {
t.Fatalf("saved memories = %d (%+v), want 2 distinct files", len(saved), saved)
}
}
// TestCompilerCandidateNamesStableAcrossRefreshes: the hash suffix must be
// derived from the pattern, not from ordering or randomness, so a refresh
// keeps the same ID and the frontend's accepted-state map stays valid.
func TestCompilerCandidateNamesStableAcrossRefreshes(t *testing.T) {
isolateDesktopUserDirs(t)
userDir := t.TempDir()
cwd := t.TempDir()
sessionDir := t.TempDir()
store := memory.StoreFor(userDir, cwd)
seedCompilerFailures(t, cwd, 2, "cannot find module providing package foo")
app := NewApp()
app.setTestCtrl(control.New(control.Options{
Memory: &memory.Set{Store: store, CWD: cwd, UserDir: userDir},
SessionDir: sessionDir,
}), "test-model")
app.tabs["test"].WorkspaceRoot = cwd
first := app.MemorySuggestions()
second := app.MemorySuggestions()
firstIDs := compilerCandidateIDs(first.Memories)
secondIDs := compilerCandidateIDs(second.Memories)
if len(firstIDs) == 0 || strings.Join(firstIDs, ",") != strings.Join(secondIDs, ",") {
t.Fatalf("candidate IDs changed across refreshes: %v vs %v", firstIDs, secondIDs)
}
}
func compilerCandidateIDs(memories []MemorySuggestion) []string {
var out []string
for _, item := range memories {
if strings.HasPrefix(item.Name, "memory-v5-") {
out = append(out, item.ID)
}
}
return out
}
// TestCompilerCandidateLongPrefixNamesPersistDistinctly reproduces the review
// finding: two patterns sharing a >56-char ASCII prefix get distinct generated
// Name/ID (hash suffix), but the accept path used to re-run asciiSlug, which
// truncates to 56 chars and strips the hash — so both saved to one file.
func TestCompilerCandidateLongPrefixNamesPersistDistinctly(t *testing.T) {
isolateDesktopUserDirs(t)
userDir := t.TempDir()
cwd := t.TempDir()
sessionDir := t.TempDir()
store := memory.StoreFor(userDir, cwd)
prefix := "cannot find module providing package example dot com slash very long prefix "
seedCompilerFailures(t, cwd, 2, prefix+"alpha")
seedCompilerFailures(t, cwd, 2, prefix+"beta")
app := NewApp()
app.setTestCtrl(control.New(control.Options{
Memory: &memory.Set{Store: store, CWD: cwd, UserDir: userDir},
SessionDir: sessionDir,
}), "test-model")
app.tabs["test"].WorkspaceRoot = cwd
view := app.MemorySuggestions()
var candidates []MemorySuggestion
for _, item := range view.Memories {
if strings.HasPrefix(item.Name, "memory-v5-") {
candidates = append(candidates, item)
}
}
if len(candidates) != 2 {
t.Fatalf("candidates = %+v, want both long-prefix patterns", candidates)
}
for _, c := range candidates {
if _, err := app.AcceptMemorySuggestion(c); err != nil {
t.Fatalf("AcceptMemorySuggestion(%s): %v", c.Name, err)
}
}
saved := store.List()
if len(saved) != 2 {
t.Fatalf("saved memories = %d, want 2 distinct files (accept path truncated the hash suffix)", len(saved))
}
if saved[0].Name == saved[1].Name {
t.Fatalf("saved names collided: %q", saved[0].Name)
}
}