241 lines
7.9 KiB
Go
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)
|
|
}
|
|
}
|