Add repo-memory integration tests

This commit is contained in:
2026-03-20 16:01:48 +08:00
parent dd6b9c2c1f
commit 9915e12a30
16 changed files with 1394 additions and 79 deletions
@@ -0,0 +1,127 @@
package main
import (
"path/filepath"
"testing"
)
func TestAddRegistersRepoAndEntry(t *testing.T) {
fixture := newRepoMemoryFixture(t)
initGitRepo(t, fixture.RepoPath)
sourcePath := filepath.Join(fixture.RepoPath, "app/app/src/main/java/foo/AITask.java")
writeFile(t, sourcePath, "class AITask {}\n")
commitAll(t, fixture.RepoPath, "init")
addOut := runRepoMemoryCommand(
t,
"add",
"--db", fixture.DBPath,
"--repo", fixture.RepoPath,
"--kind", "term",
"--key", "AITask",
"--summary", "Plan 内嵌任务结构,不是独立表",
"--status", "confirmed",
"--source-path", sourcePath,
"--source-line", "42",
"--alias", "AI Task",
"--dep", "file:"+sourcePath+":hard",
)
listOut := runRepoMemoryCommand(
t,
"list",
"--db", fixture.DBPath,
"--repo", "repo",
"--kind", "term",
"--status", "confirmed",
)
if got, want := stringsTrim(addOut), "upserted entry 1 (term:AITask)"; got != want {
t.Fatalf("add output = %q, want %q", got, want)
}
assertContains(t, listOut, "#1 [repo] term:AITask [confirmed]")
assertContains(t, listOut, "Plan 内嵌任务结构,不是独立表")
}
func TestAddUpdatesExistingEntryOnSameKindAndKey(t *testing.T) {
fixture := newRepoMemoryFixture(t)
runRepoMemoryCommand(t, "init", "--db", fixture.DBPath)
initGitRepo(t, fixture.RepoPath)
writeFile(t, filepath.Join(fixture.RepoPath, "foo.txt"), "hello\n")
commitAll(t, fixture.RepoPath, "init")
runRepoMemoryCommand(
t,
"add",
"--db", fixture.DBPath,
"--repo", fixture.RepoPath,
"--kind", "term",
"--key", "AITask",
"--summary", "初版摘要",
"--status", "draft",
)
addOut := runRepoMemoryCommand(
t,
"add",
"--db", fixture.DBPath,
"--repo", fixture.RepoPath,
"--kind", "term",
"--key", "AITask",
"--summary", "修订后的摘要",
"--status", "confirmed",
"--alias", "AI Task",
)
eventsOut := runRepoMemoryCommand(
t,
"events",
"--db", fixture.DBPath,
"--id", "1",
)
listOut := runRepoMemoryCommand(
t,
"list",
"--db", fixture.DBPath,
"--repo", "repo",
"--kind", "term",
)
if got, want := stringsTrim(addOut), "upserted entry 1 (term:AITask)"; got != want {
t.Fatalf("second add output = %q, want %q", got, want)
}
assertContains(t, eventsOut, "updated (draft -> confirmed)")
assertContains(t, eventsOut, "created (- -> draft)")
assertContains(t, listOut, "修订后的摘要")
}
func TestAddFailedValidationStillRegistersRepo(t *testing.T) {
fixture := newRepoMemoryFixture(t)
initGitRepo(t, fixture.RepoPath)
writeFile(t, filepath.Join(fixture.RepoPath, "foo.txt"), "hello\n")
commitAll(t, fixture.RepoPath, "init")
_, stderr, exitCode := executeRepoMemoryCommand(
"add",
"--db", fixture.DBPath,
"--repo", fixture.RepoPath,
"--summary", "missing kind",
)
reposOut := runRepoMemoryCommand(
t,
"repos",
"--db", fixture.DBPath,
)
listOut := runRepoMemoryCommand(
t,
"list",
"--db", fixture.DBPath,
"--repo", "repo",
)
if exitCode != 1 {
t.Fatalf("add exit code = %d, want 1\nstderr:\n%s", exitCode, stderr)
}
mustContain(t, stderr, "kind is required")
mustContain(t, reposOut, fixture.RepoPath+" (0 entries, updated ")
if got, want := stringsTrim(listOut), "no entries"; got != want {
t.Fatalf("list output = %q, want %q", got, want)
}
}
@@ -0,0 +1,85 @@
package main
import (
"strings"
"testing"
)
func TestEventsReadsHistoryByID(t *testing.T) {
fixture := newRepoMemoryFixture(t)
runRepoMemoryCommand(t, "init", "--db", fixture.DBPath)
initGitRepo(t, fixture.RepoPath)
runRepoMemoryCommand(t,
"add",
"--db", fixture.DBPath,
"--repo", fixture.RepoPath,
"--kind", "term",
"--key", "AITask",
"--summary", "initial summary",
"--status", "draft",
)
runRepoMemoryCommand(t,
"add",
"--db", fixture.DBPath,
"--repo", fixture.RepoPath,
"--kind", "term",
"--key", "AITask",
"--summary", "revised summary",
"--status", "confirmed",
)
eventsOut := runRepoMemoryCommand(t,
"events",
"--db", fixture.DBPath,
"--id", "1",
)
assertContains(t, eventsOut, "term:AITask [confirmed] #1")
assertContains(t, eventsOut, "updated (draft -> confirmed)")
assertContains(t, eventsOut, "created (- -> draft)")
if strings.Index(eventsOut, "updated (draft -> confirmed)") > strings.Index(eventsOut, "created (- -> draft)") {
t.Fatalf("expected newest-first event order, got %q", eventsOut)
}
}
func TestEventsResolvesEntryByRepoKindKey(t *testing.T) {
fixture := newRepoMemoryFixture(t)
runRepoMemoryCommand(t, "init", "--db", fixture.DBPath)
initGitRepo(t, fixture.RepoPath)
runRepoMemoryCommand(t,
"add",
"--db", fixture.DBPath,
"--repo", fixture.RepoPath,
"--kind", "term",
"--key", "AITask",
"--summary", "Plan embedded task structure",
"--status", "confirmed",
)
eventsOut := runRepoMemoryCommand(t,
"events",
"--db", fixture.DBPath,
"--repo", fixture.RepoPath,
"--kind", "term",
"--key", "AITask",
)
assertContains(t, eventsOut, "term:AITask [confirmed] #1")
assertContains(t, eventsOut, "created (- -> confirmed)")
}
func TestEventsRejectsMissingEntrySelector(t *testing.T) {
fixture := newRepoMemoryFixture(t)
runRepoMemoryCommand(t, "init", "--db", fixture.DBPath)
_, stderr, exitCode := executeRepoMemoryCommand(
"events",
"--db", fixture.DBPath,
)
if exitCode != 1 {
t.Fatalf("expected exit code 1, got %d with stderr %q", exitCode, stderr)
}
assertContains(t, stderr, "either --id or --repo+--kind+--key is required")
}
@@ -0,0 +1,66 @@
package main
import (
"fmt"
"io"
"os"
"sync"
)
var (
executeMu sync.Mutex
commandStdout io.Writer = os.Stdout
commandStderr io.Writer = os.Stderr
)
func Execute(args []string, stdout, stderr io.Writer) int {
executeMu.Lock()
defer executeMu.Unlock()
prevStdout := commandStdout
prevStderr := commandStderr
commandStdout = stdout
commandStderr = stderr
defer func() {
commandStdout = prevStdout
commandStderr = prevStderr
}()
if len(args) < 1 {
usage()
return 2
}
if err := runCommand(args); err != nil {
_, _ = fmt.Fprintln(commandStderr, err)
return 1
}
return 0
}
func runCommand(args []string) error {
switch args[0] {
case "init":
return runInit(args[1:])
case "add":
return runAdd(args[1:])
case "ingest":
return runIngest(args[1:])
case "search":
return runSearch(args[1:])
case "list":
return runList(args[1:])
case "events":
return runEvents(args[1:])
case "link":
return runLink(args[1:])
case "verify":
return runVerify(args[1:])
case "repos":
return runRepos(args[1:])
default:
usage()
return fmt.Errorf("unknown command %s", args[0])
}
}
@@ -0,0 +1,85 @@
package main
import (
"os"
"path/filepath"
"testing"
)
func TestIngestImportsDocsAIMarkdown(t *testing.T) {
fixture := newRepoMemoryFixture(t)
runRepoMemoryCommand(t, "init", "--db", fixture.DBPath)
initGitRepo(t, fixture.RepoPath)
writeFile(t, filepath.Join(fixture.RepoPath, "docs/ai/repo-memory.md"), "# Repo Memory\n\n## Module Map\n\n- gateway\n- app/app\n\n## Danger Zones\n\n- shared libs first\n")
commitAll(t, fixture.RepoPath, "init")
ingestOut := runRepoMemoryCommand(
t,
"ingest",
"--db", fixture.DBPath,
"--repo", fixture.RepoPath,
)
listOut := runRepoMemoryCommand(
t,
"list",
"--db", fixture.DBPath,
"--repo", "repo",
)
mustContain(t, ingestOut, "ingested 1 docs from "+fixture.RepoPath)
mustContain(t, listOut, "module:repo-memory:module-map [confirmed]")
mustContain(t, listOut, "danger:repo-memory:danger-zones [confirmed]")
}
func TestIngestRejectsWhenNoMarkdownFound(t *testing.T) {
fixture := newRepoMemoryFixture(t)
runRepoMemoryCommand(t, "init", "--db", fixture.DBPath)
initGitRepo(t, fixture.RepoPath)
runGit(t, fixture.RepoPath, "commit", "--allow-empty", "-m", "init")
if err := os.MkdirAll(filepath.Join(fixture.RepoPath, "docs", "ai"), 0o755); err != nil {
t.Fatalf("mkdir docs/ai: %v", err)
}
_, stderr, exitCode := executeRepoMemoryCommand(
"ingest",
"--db", fixture.DBPath,
"--repo", fixture.RepoPath,
)
if exitCode != 1 {
t.Fatalf("ingest exit code = %d, want 1\nstderr:\n%s", exitCode, stderr)
}
mustContain(t, stderr, "no markdown files found under "+filepath.Join(fixture.RepoPath, "docs", "ai"))
}
func TestIngestImportsHeadinglessMarkdownAsSingleEntry(t *testing.T) {
fixture := newRepoMemoryFixture(t)
runRepoMemoryCommand(t, "init", "--db", fixture.DBPath)
initGitRepo(t, fixture.RepoPath)
writeFile(t, filepath.Join(fixture.RepoPath, "docs/ai/repo-memory.md"), "This repository keeps AI memory notes near docs/ai.\nGateway owns ingress and app/app owns orchestration.\n")
commitAll(t, fixture.RepoPath, "init")
ingestOut := runRepoMemoryCommand(
t,
"ingest",
"--db", fixture.DBPath,
"--repo", fixture.RepoPath,
)
listOut := runRepoMemoryCommand(
t,
"list",
"--db", fixture.DBPath,
"--repo", "repo",
)
searchOut := runRepoMemoryCommand(
t,
"search",
"--db", fixture.DBPath,
"--repo", "repo",
"--query", "Gateway orchestration",
)
mustContain(t, ingestOut, "ingested 1 docs from "+fixture.RepoPath)
mustContain(t, listOut, "decision:repo-memory:overview [confirmed]")
mustContain(t, searchOut, "decision:repo-memory:overview")
}
@@ -0,0 +1,34 @@
package main
import (
"os"
"path/filepath"
"testing"
)
func TestInitCreatesSchemaOnEmptyDB(t *testing.T) {
dbPath := filepath.Join(t.TempDir(), "repo-memory.db")
stdout := runRepoMemoryCommand(t, "init", "--db", dbPath)
if got, want := stringsTrim(stdout), "initialized "+dbPath; got != want {
t.Fatalf("init output = %q, want %q", got, want)
}
if _, err := os.Stat(dbPath); err != nil {
t.Fatalf("stat db %q: %v", dbPath, err)
}
}
func TestInitIsIdempotentOnExistingDB(t *testing.T) {
dbPath := filepath.Join(t.TempDir(), "repo-memory.db")
first := runRepoMemoryCommand(t, "init", "--db", dbPath)
second := runRepoMemoryCommand(t, "init", "--db", dbPath)
if got, want := stringsTrim(first), "initialized "+dbPath; got != want {
t.Fatalf("first init output = %q, want %q", got, want)
}
if got, want := stringsTrim(second), "initialized "+dbPath; got != want {
t.Fatalf("second init output = %q, want %q", got, want)
}
}
@@ -0,0 +1,202 @@
package main
import (
"bytes"
"database/sql"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
_ "github.com/mattn/go-sqlite3"
)
type repoMemoryFixture struct {
TempDir string
DBPath string
RepoPath string
}
func newRepoMemoryFixture(t *testing.T) repoMemoryFixture {
t.Helper()
tempDir := t.TempDir()
return repoMemoryFixture{
TempDir: tempDir,
DBPath: filepath.Join(tempDir, "repo-memory.db"),
RepoPath: filepath.Join(tempDir, "repo"),
}
}
func runRepoMemoryCommand(t *testing.T, args ...string) string {
t.Helper()
stdout, stderr, exitCode := executeRepoMemoryCommand(args...)
if exitCode != 0 {
t.Fatalf("execute repo-memory command %v: exit=%d\nstderr:\n%s\nstdout:\n%s", args, exitCode, stderr, stdout)
}
return stdout
}
func executeRepoMemoryCommand(args ...string) (string, string, int) {
var stdout bytes.Buffer
var stderr bytes.Buffer
exitCode := Execute(args, &stdout, &stderr)
return stdout.String(), stderr.String(), exitCode
}
func initRepoMemoryTestDB(t *testing.T) repoMemoryFixture {
t.Helper()
fixture := newRepoMemoryFixture(t)
runRepoMemoryCommand(t, "init", "--db", fixture.DBPath)
return fixture
}
func writeFile(t *testing.T, path, content string) {
t.Helper()
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
t.Fatalf("mkdir %s: %v", filepath.Dir(path), err)
}
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
t.Fatalf("write file %s: %v", path, err)
}
}
func initGitRepo(t *testing.T, repoPath string) {
t.Helper()
if err := os.MkdirAll(repoPath, 0o755); err != nil {
t.Fatalf("mkdir repo %s: %v", repoPath, err)
}
runGit(t, repoPath, "init")
runGit(t, repoPath, "config", "user.email", "test@example.com")
runGit(t, repoPath, "config", "user.name", "Tester")
}
func createNamedGitRepo(t *testing.T, name string, files map[string]string) string {
t.Helper()
repoPath := filepath.Join(t.TempDir(), name)
initGitRepo(t, repoPath)
for relPath, content := range files {
writeFile(t, filepath.Join(repoPath, relPath), content)
}
if len(files) > 0 {
commitAll(t, repoPath, "init")
} else {
runGit(t, repoPath, "commit", "--allow-empty", "-m", "init")
}
return repoPath
}
func commitAll(t *testing.T, repoPath, message string) {
t.Helper()
runGit(t, repoPath, "add", ".")
runGit(t, repoPath, "commit", "-m", message)
}
func runGit(t *testing.T, repoPath string, args ...string) string {
t.Helper()
cmd := exec.Command("git", append([]string{"-C", repoPath}, args...)...)
out, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("git %v failed: %v\n%s", args, err, string(out))
}
return string(out)
}
func mustAbsPath(t *testing.T, path string) string {
t.Helper()
absPath, err := filepath.Abs(path)
if err != nil {
t.Fatalf("abs path %s: %v", path, err)
}
return absPath
}
func assertContains(t *testing.T, got, want string) {
t.Helper()
if !strings.Contains(got, want) {
t.Fatalf("expected %q to contain %q", got, want)
}
}
func assertNotContains(t *testing.T, got, want string) {
t.Helper()
if strings.Contains(got, want) {
t.Fatalf("expected %q to not contain %q", got, want)
}
}
func mustContain(t *testing.T, got, want string) {
t.Helper()
assertContains(t, got, want)
}
func mustNotContain(t *testing.T, got, want string) {
t.Helper()
assertNotContains(t, got, want)
}
func mustWriteRepoFile(t *testing.T, repoPath, relPath, content string) string {
t.Helper()
path := filepath.Join(repoPath, relPath)
writeFile(t, path, content)
return path
}
func queryStrings(t *testing.T, dbPath, query string, args ...any) []string {
t.Helper()
db, err := sql.Open("sqlite3", dbPath)
if err != nil {
t.Fatalf("open sqlite %s: %v", dbPath, err)
}
defer db.Close()
rows, err := db.Query(query, args...)
if err != nil {
t.Fatalf("query sqlite %q: %v", query, err)
}
defer rows.Close()
var items []string
for rows.Next() {
var value string
if err := rows.Scan(&value); err != nil {
t.Fatalf("scan sqlite row for %q: %v", query, err)
}
items = append(items, value)
}
if err := rows.Err(); err != nil {
t.Fatalf("iterate sqlite rows for %q: %v", query, err)
}
return items
}
func queryInt(t *testing.T, dbPath, query string, args ...any) int {
t.Helper()
db, err := sql.Open("sqlite3", dbPath)
if err != nil {
t.Fatalf("open sqlite %s: %v", dbPath, err)
}
defer db.Close()
var value int
if err := db.QueryRow(query, args...).Scan(&value); err != nil {
t.Fatalf("query row sqlite %q: %v", query, err)
}
return value
}
@@ -0,0 +1,107 @@
package main
import (
"testing"
)
func TestLinkCreatesRelationBetweenEntries(t *testing.T) {
fixture := newRepoMemoryFixture(t)
runRepoMemoryCommand(t, "init", "--db", fixture.DBPath)
initGitRepo(t, fixture.RepoPath)
runRepoMemoryCommand(t,
"add",
"--db", fixture.DBPath,
"--repo", fixture.RepoPath,
"--kind", "term",
"--key", "AITask",
"--summary", "Plan embedded task structure",
"--status", "confirmed",
)
runRepoMemoryCommand(t,
"add",
"--db", fixture.DBPath,
"--repo", fixture.RepoPath,
"--kind", "chain",
"--key", "ai-insight.get",
"--summary", "gateway -> app service -> cache/db",
"--status", "confirmed",
)
linkOut := runRepoMemoryCommand(t,
"link",
"--db", fixture.DBPath,
"--from-id", "1",
"--to-id", "2",
"--relation", "related_to",
)
assertContains(t, linkOut, "linked #1 -[related_to]-> #2")
relations := queryStrings(t, fixture.DBPath, `SELECT relation FROM knowledge_links WHERE from_entry_id = 1 AND to_entry_id = 2`)
if len(relations) != 1 || relations[0] != "related_to" {
t.Fatalf("relations = %#v, want [related_to]", relations)
}
}
func TestLinkRejectsMissingRelation(t *testing.T) {
fixture := newRepoMemoryFixture(t)
runRepoMemoryCommand(t, "init", "--db", fixture.DBPath)
initGitRepo(t, fixture.RepoPath)
runRepoMemoryCommand(t,
"add",
"--db", fixture.DBPath,
"--repo", fixture.RepoPath,
"--kind", "term",
"--key", "AITask",
"--summary", "Plan embedded task structure",
"--status", "confirmed",
)
runRepoMemoryCommand(t,
"add",
"--db", fixture.DBPath,
"--repo", fixture.RepoPath,
"--kind", "chain",
"--key", "ai-insight.get",
"--summary", "gateway -> app service -> cache/db",
"--status", "confirmed",
)
_, stderr, exitCode := executeRepoMemoryCommand(
"link",
"--db", fixture.DBPath,
"--from-id", "1",
"--to-id", "2",
)
if exitCode != 1 {
t.Fatalf("expected exit code 1, got %d with stderr %q", exitCode, stderr)
}
assertContains(t, stderr, "relation is required")
}
func TestLinkRejectsWhenEntryIDMissing(t *testing.T) {
fixture := newRepoMemoryFixture(t)
runRepoMemoryCommand(t, "init", "--db", fixture.DBPath)
initGitRepo(t, fixture.RepoPath)
runRepoMemoryCommand(t,
"add",
"--db", fixture.DBPath,
"--repo", fixture.RepoPath,
"--kind", "term",
"--key", "AITask",
"--summary", "Plan embedded task structure",
"--status", "confirmed",
)
_, stderr, exitCode := executeRepoMemoryCommand(
"link",
"--db", fixture.DBPath,
"--from-id", "1",
"--relation", "related_to",
)
if exitCode != 1 {
t.Fatalf("expected exit code 1, got %d with stderr %q", exitCode, stderr)
}
assertContains(t, stderr, "both entry ids are required")
}
@@ -0,0 +1,75 @@
package main
import (
"path/filepath"
"testing"
)
func TestListFiltersByKindAndStatus(t *testing.T) {
fixture := newRepoMemoryFixture(t)
runRepoMemoryCommand(t, "init", "--db", fixture.DBPath)
initGitRepo(t, fixture.RepoPath)
writeFile(t, filepath.Join(fixture.RepoPath, "foo.txt"), "hello\n")
commitAll(t, fixture.RepoPath, "init")
runRepoMemoryCommand(
t,
"add",
"--db", fixture.DBPath,
"--repo", fixture.RepoPath,
"--kind", "term",
"--key", "AITask",
"--summary", "Plan 内嵌任务结构",
"--status", "confirmed",
)
runRepoMemoryCommand(
t,
"add",
"--db", fixture.DBPath,
"--repo", fixture.RepoPath,
"--kind", "term",
"--key", "AIJob",
"--summary", "后台任务封装",
"--status", "draft",
)
runRepoMemoryCommand(
t,
"add",
"--db", fixture.DBPath,
"--repo", fixture.RepoPath,
"--kind", "chain",
"--key", "ai-insight.get",
"--summary", "gateway -> app service -> cache/db",
"--status", "confirmed",
)
listOut := runRepoMemoryCommand(
t,
"list",
"--db", fixture.DBPath,
"--repo", "repo",
"--kind", "term",
"--status", "confirmed",
)
mustContain(t, listOut, "term:AITask [confirmed]")
mustNotContain(t, listOut, "AIJob")
mustNotContain(t, listOut, "chain:ai-insight.get")
}
func TestListReturnsNoEntriesWhenEmpty(t *testing.T) {
fixture := initRepoMemoryTestDB(t)
listOut := runRepoMemoryCommand(
t,
"list",
"--db", fixture.DBPath,
"--repo", "repo",
"--kind", "term",
"--status", "confirmed",
)
if got, want := stringsTrim(listOut), "no entries"; got != want {
t.Fatalf("list output = %q, want %q", got, want)
}
}
@@ -15,52 +15,7 @@ import (
)
func main() {
if len(os.Args) < 2 {
usage()
os.Exit(2)
}
switch os.Args[1] {
case "init":
if err := runInit(os.Args[2:]); err != nil {
fatal(err)
}
case "add":
if err := runAdd(os.Args[2:]); err != nil {
fatal(err)
}
case "ingest":
if err := runIngest(os.Args[2:]); err != nil {
fatal(err)
}
case "search":
if err := runSearch(os.Args[2:]); err != nil {
fatal(err)
}
case "list":
if err := runList(os.Args[2:]); err != nil {
fatal(err)
}
case "events":
if err := runEvents(os.Args[2:]); err != nil {
fatal(err)
}
case "link":
if err := runLink(os.Args[2:]); err != nil {
fatal(err)
}
case "verify":
if err := runVerify(os.Args[2:]); err != nil {
fatal(err)
}
case "repos":
if err := runRepos(os.Args[2:]); err != nil {
fatal(err)
}
default:
usage()
os.Exit(2)
}
os.Exit(Execute(os.Args[1:], os.Stdout, os.Stderr))
}
type stringSliceFlag []string
@@ -76,6 +31,7 @@ func (s *stringSliceFlag) Set(value string) error {
func runInit(args []string) error {
fs := flag.NewFlagSet("init", flag.ContinueOnError)
fs.SetOutput(commandStderr)
dbPath := fs.String("db", "repo-memory.db", "SQLite database path")
if err := fs.Parse(args); err != nil {
return err
@@ -91,12 +47,13 @@ func runInit(args []string) error {
return err
}
fmt.Printf("initialized %s\n", *dbPath)
_, _ = fmt.Fprintf(commandStdout, "initialized %s\n", *dbPath)
return nil
}
func runIngest(args []string) error {
fs := flag.NewFlagSet("ingest", flag.ContinueOnError)
fs.SetOutput(commandStderr)
dbPath := fs.String("db", "repo-memory.db", "SQLite database path")
repoPath := fs.String("repo", "", "Repository root")
scanPath := fs.String("path", "docs/ai", "Relative path under repo to scan for markdown")
@@ -156,12 +113,13 @@ func runIngest(args []string) error {
}
}
fmt.Printf("ingested %d docs from %s\n", len(docs), absRepo)
_, _ = fmt.Fprintf(commandStdout, "ingested %d docs from %s\n", len(docs), absRepo)
return nil
}
func runAdd(args []string) error {
fs := flag.NewFlagSet("add", flag.ContinueOnError)
fs.SetOutput(commandStderr)
dbPath := fs.String("db", "repo-memory.db", "SQLite database path")
repoPath := fs.String("repo", "", "Repository root")
kind := fs.String("kind", "", "Knowledge kind, e.g. term|chain|danger")
@@ -237,12 +195,13 @@ func runAdd(args []string) error {
return err
}
fmt.Printf("upserted entry %d (%s:%s)\n", entryID, input.Kind, input.Key)
_, _ = fmt.Fprintf(commandStdout, "upserted entry %d (%s:%s)\n", entryID, input.Kind, input.Key)
return nil
}
func runSearch(args []string) error {
fs := flag.NewFlagSet("search", flag.ContinueOnError)
fs.SetOutput(commandStderr)
dbPath := fs.String("db", "repo-memory.db", "SQLite database path")
query := fs.String("query", "", "Search query")
repo := fs.String("repo", "", "Optional repo path filter (substring match)")
@@ -269,20 +228,21 @@ func runSearch(args []string) error {
return err
}
if len(results) == 0 {
fmt.Println("no results")
_, _ = fmt.Fprintln(commandStdout, "no results")
return nil
}
for i, r := range results {
fmt.Printf("%d. [%s] %s:%s [%s]\n", i+1, filepath.Base(r.RepoPath), r.Kind, r.Key, r.Status)
fmt.Printf(" %s\n", r.Title)
fmt.Printf(" %s\n", r.Snippet)
_, _ = fmt.Fprintf(commandStdout, "%d. [%s] %s:%s [%s]\n", i+1, filepath.Base(r.RepoPath), r.Kind, r.Key, r.Status)
_, _ = fmt.Fprintf(commandStdout, " %s\n", r.Title)
_, _ = fmt.Fprintf(commandStdout, " %s\n", r.Snippet)
}
return nil
}
func runRepos(args []string) error {
fs := flag.NewFlagSet("repos", flag.ContinueOnError)
fs.SetOutput(commandStderr)
dbPath := fs.String("db", "repo-memory.db", "SQLite database path")
if err := fs.Parse(args); err != nil {
return err
@@ -299,18 +259,19 @@ func runRepos(args []string) error {
return err
}
if len(repos) == 0 {
fmt.Println("no repos")
_, _ = fmt.Fprintln(commandStdout, "no repos")
return nil
}
for _, repo := range repos {
fmt.Printf("- %s (%d entries, updated %s)\n", repo.Path, repo.EntryCount, repo.UpdatedAt.Format(time.RFC3339))
_, _ = fmt.Fprintf(commandStdout, "- %s (%d entries, updated %s)\n", repo.Path, repo.EntryCount, repo.UpdatedAt.Format(time.RFC3339))
}
return nil
}
func runList(args []string) error {
fs := flag.NewFlagSet("list", flag.ContinueOnError)
fs.SetOutput(commandStderr)
dbPath := fs.String("db", "repo-memory.db", "SQLite database path")
repo := fs.String("repo", "", "Optional repo path filter (substring match)")
kind := fs.String("kind", "", "Optional knowledge kind filter")
@@ -336,19 +297,20 @@ func runList(args []string) error {
return err
}
if len(items) == 0 {
fmt.Println("no entries")
_, _ = fmt.Fprintln(commandStdout, "no entries")
return nil
}
for _, item := range items {
fmt.Printf("- #%d [%s] %s:%s [%s]\n", item.ID, filepath.Base(item.RepoPath), item.Kind, item.Key, item.Status)
fmt.Printf(" %s\n", item.Summary)
_, _ = fmt.Fprintf(commandStdout, "- #%d [%s] %s:%s [%s]\n", item.ID, filepath.Base(item.RepoPath), item.Kind, item.Key, item.Status)
_, _ = fmt.Fprintf(commandStdout, " %s\n", item.Summary)
}
return nil
}
func runEvents(args []string) error {
fs := flag.NewFlagSet("events", flag.ContinueOnError)
fs.SetOutput(commandStderr)
dbPath := fs.String("db", "repo-memory.db", "SQLite database path")
id := fs.Int64("id", 0, "Entry id")
repo := fs.String("repo", "", "Repo root when resolving by kind/key")
@@ -378,19 +340,19 @@ func runEvents(args []string) error {
return err
}
if len(events) == 0 {
fmt.Println("no events")
_, _ = fmt.Fprintln(commandStdout, "no events")
return nil
}
fmt.Printf("%s:%s [%s] #%d\n", entry.Kind, entry.Key, entry.Status, entry.ID)
_, _ = fmt.Fprintf(commandStdout, "%s:%s [%s] #%d\n", entry.Kind, entry.Key, entry.Status, entry.ID)
for _, ev := range events {
line := fmt.Sprintf("- %s %s", ev.CreatedAt.Format(time.RFC3339), ev.EventType)
if ev.FromStatus != "" || ev.ToStatus != "" {
line += fmt.Sprintf(" (%s -> %s)", emptyDash(ev.FromStatus), emptyDash(ev.ToStatus))
}
fmt.Println(line)
_, _ = fmt.Fprintln(commandStdout, line)
if ev.Reason != "" {
fmt.Printf(" reason: %s\n", ev.Reason)
_, _ = fmt.Fprintf(commandStdout, " reason: %s\n", ev.Reason)
}
}
return nil
@@ -398,6 +360,7 @@ func runEvents(args []string) error {
func runLink(args []string) error {
fs := flag.NewFlagSet("link", flag.ContinueOnError)
fs.SetOutput(commandStderr)
dbPath := fs.String("db", "repo-memory.db", "SQLite database path")
fromID := fs.Int64("from-id", 0, "From entry id")
toID := fs.Int64("to-id", 0, "To entry id")
@@ -416,12 +379,13 @@ func runLink(args []string) error {
return err
}
fmt.Printf("linked #%d -[%s]-> #%d\n", *fromID, *relation, *toID)
_, _ = fmt.Fprintf(commandStdout, "linked #%d -[%s]-> #%d\n", *fromID, *relation, *toID)
return nil
}
func runVerify(args []string) error {
fs := flag.NewFlagSet("verify", flag.ContinueOnError)
fs.SetOutput(commandStderr)
dbPath := fs.String("db", "repo-memory.db", "SQLite database path")
repo := fs.String("repo", "", "Optional repo root to verify; if omitted, verify all known repos")
if err := fs.Parse(args); err != nil {
@@ -451,14 +415,14 @@ func runVerify(args []string) error {
}
}
if len(repoRoots) == 0 {
fmt.Println("no repos")
_, _ = fmt.Fprintln(commandStdout, "no repos")
return nil
}
for _, repoRoot := range repoRoots {
gitState := detectGitState(repoRoot)
if gitState.commit == "" {
fmt.Printf("%s: skipped (not a git repo or no HEAD)\n", repoRoot)
_, _ = fmt.Fprintf(commandStdout, "%s: skipped (not a git repo or no HEAD)\n", repoRoot)
continue
}
if _, err := st.UpsertRepo(context.Background(), store.RepoState{
@@ -494,14 +458,14 @@ func runVerify(args []string) error {
return err
}
}
fmt.Printf("%s: verified %d entries, %d downgraded, %d stale\n", repoRoot, len(candidates), changedCount, staleCount)
_, _ = fmt.Fprintf(commandStdout, "%s: verified %d entries, %d downgraded, %d stale\n", repoRoot, len(candidates), changedCount, staleCount)
}
return nil
}
func usage() {
fmt.Fprintf(os.Stderr, `repo-memory: repo memory CLI
_, _ = fmt.Fprintf(commandStderr, `repo-memory: repo memory CLI
Usage:
repo-memory init --db repo-memory.db
@@ -516,11 +480,6 @@ Usage:
`)
}
func fatal(err error) {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
type gitState struct {
branch string
commit string
@@ -0,0 +1,54 @@
package main
import (
"path/filepath"
"testing"
)
func TestReposListsTrackedRepositories(t *testing.T) {
fixture := newRepoMemoryFixture(t)
runRepoMemoryCommand(t, "init", "--db", fixture.DBPath)
cupidRepo := filepath.Join(t.TempDir(), "cupid-service")
marsRepo := filepath.Join(t.TempDir(), "mars-service")
initGitRepo(t, cupidRepo)
initGitRepo(t, marsRepo)
runRepoMemoryCommand(t,
"add",
"--db", fixture.DBPath,
"--repo", cupidRepo,
"--kind", "term",
"--key", "AITask",
"--summary", "Plan embedded task structure",
"--status", "confirmed",
)
runRepoMemoryCommand(t,
"add",
"--db", fixture.DBPath,
"--repo", marsRepo,
"--kind", "term",
"--key", "DeployPlan",
"--summary", "Release plan",
"--status", "confirmed",
)
reposOut := runRepoMemoryCommand(t,
"repos",
"--db", fixture.DBPath,
)
assertContains(t, reposOut, cupidRepo+" (1 entries, updated ")
assertContains(t, reposOut, marsRepo+" (1 entries, updated ")
}
func TestReposPrintsNoReposWhenEmpty(t *testing.T) {
fixture := newRepoMemoryFixture(t)
runRepoMemoryCommand(t, "init", "--db", fixture.DBPath)
reposOut := runRepoMemoryCommand(t,
"repos",
"--db", fixture.DBPath,
)
if stringsTrim(reposOut) != "no repos" {
t.Fatalf("expected no repos, got %q", reposOut)
}
}
@@ -0,0 +1,113 @@
package main
import (
"path/filepath"
"testing"
)
func TestSearchReturnsMatchingEntrySnippet(t *testing.T) {
fixture := newRepoMemoryFixture(t)
runRepoMemoryCommand(t, "init", "--db", fixture.DBPath)
initGitRepo(t, fixture.RepoPath)
writeFile(t, fixture.RepoPath+"/foo.txt", "hello\n")
commitAll(t, fixture.RepoPath, "init")
runRepoMemoryCommand(
t,
"add",
"--db", fixture.DBPath,
"--repo", fixture.RepoPath,
"--kind", "chain",
"--key", "ai-insight.get",
"--summary", "gateway -> app service -> cache/db",
"--detail", "The AI insight read path goes through gateway before app service reaches cache and database.",
"--status", "confirmed",
)
searchOut := runRepoMemoryCommand(
t,
"search",
"--db", fixture.DBPath,
"--repo", "repo",
"--query", "insight gateway",
)
mustContain(t, searchOut, "1. [repo] chain:ai-insight.get [confirmed]")
mustContain(t, searchOut, "gateway")
}
func TestSearchMatchesAliasWithRepoFilter(t *testing.T) {
fixture := newRepoMemoryFixture(t)
runRepoMemoryCommand(t, "init", "--db", fixture.DBPath)
cupid := newRepoMemoryFixture(t)
cupid.RepoPath = filepath.Join(cupid.TempDir, "cupid-service")
mars := newRepoMemoryFixture(t)
mars.RepoPath = filepath.Join(mars.TempDir, "mars-service")
initGitRepo(t, cupid.RepoPath)
initGitRepo(t, mars.RepoPath)
writeFile(t, filepath.Join(cupid.RepoPath, "foo.txt"), "hello\n")
writeFile(t, filepath.Join(mars.RepoPath, "bar.txt"), "hello\n")
commitAll(t, cupid.RepoPath, "init")
commitAll(t, mars.RepoPath, "init")
runRepoMemoryCommand(
t,
"add",
"--db", fixture.DBPath,
"--repo", cupid.RepoPath,
"--kind", "term",
"--key", "AITask",
"--summary", "Plan 内嵌任务结构",
"--status", "confirmed",
"--alias", "plan task",
)
runRepoMemoryCommand(
t,
"add",
"--db", fixture.DBPath,
"--repo", mars.RepoPath,
"--kind", "term",
"--key", "DeployPlan",
"--summary", "发布计划",
"--status", "confirmed",
"--alias", "release plan",
)
searchOut := runRepoMemoryCommand(
t,
"search",
"--db", fixture.DBPath,
"--repo", "cupid",
"--query", "plan task",
)
mustContain(t, searchOut, "[cupid-service] term:AITask [confirmed]")
mustNotContain(t, searchOut, "[mars-service]")
}
func TestSearchReturnsNoResultsWhenEmpty(t *testing.T) {
fixture := initRepoMemoryTestDB(t)
searchOut := runRepoMemoryCommand(
t,
"search",
"--db", fixture.DBPath,
"--query", "missing term",
)
if got, want := stringsTrim(searchOut), "no results"; got != want {
t.Fatalf("search output = %q, want %q", got, want)
}
}
func TestSearchRejectsMissingQuery(t *testing.T) {
fixture := initRepoMemoryTestDB(t)
_, stderr, exitCode := executeRepoMemoryCommand(
"search",
"--db", fixture.DBPath,
)
if exitCode != 1 {
t.Fatalf("search exit code = %d, want 1\nstderr:\n%s", exitCode, stderr)
}
mustContain(t, stderr, "--query is required")
}
@@ -0,0 +1,181 @@
package main
import (
"os"
"path/filepath"
"testing"
)
func TestVerifyDowngradesChangedFileDependency(t *testing.T) {
fixture := newRepoMemoryFixture(t)
runRepoMemoryCommand(t, "init", "--db", fixture.DBPath)
initGitRepo(t, fixture.RepoPath)
filePath := filepath.Join(fixture.RepoPath, "foo.txt")
writeFile(t, filePath, "hello\n")
commitAll(t, fixture.RepoPath, "add foo")
runRepoMemoryCommand(t,
"add",
"--db", fixture.DBPath,
"--repo", fixture.RepoPath,
"--kind", "term",
"--key", "AITask",
"--summary", "Plan embedded task structure",
"--status", "confirmed",
"--dep", "file:"+filePath+":hard",
)
if err := os.WriteFile(filePath, []byte("changed\n"), 0o644); err != nil {
t.Fatalf("mutate dependency file: %v", err)
}
verifyOut := runRepoMemoryCommand(t,
"verify",
"--db", fixture.DBPath,
"--repo", fixture.RepoPath,
)
assertContains(t, verifyOut, "verified 1 entries, 1 downgraded, 0 stale")
listOut := runRepoMemoryCommand(t,
"list",
"--db", fixture.DBPath,
"--repo", "repo",
"--status", "needs_review",
)
assertContains(t, listOut, "term:AITask [needs_review]")
eventsOut := runRepoMemoryCommand(t,
"events",
"--db", fixture.DBPath,
"--id", "1",
)
assertContains(t, eventsOut, "downgraded (confirmed -> needs_review)")
}
func TestVerifyMarksMissingHardDependencyStale(t *testing.T) {
fixture := newRepoMemoryFixture(t)
runRepoMemoryCommand(t, "init", "--db", fixture.DBPath)
initGitRepo(t, fixture.RepoPath)
runGit(t, fixture.RepoPath, "commit", "--allow-empty", "-m", "init")
missingPath := filepath.Join(fixture.RepoPath, "missing.txt")
runRepoMemoryCommand(t,
"add",
"--db", fixture.DBPath,
"--repo", fixture.RepoPath,
"--kind", "term",
"--key", "AITask",
"--summary", "Plan embedded task structure",
"--status", "confirmed",
"--dep", "file:"+missingPath+":hard",
)
verifyOut := runRepoMemoryCommand(t,
"verify",
"--db", fixture.DBPath,
"--repo", fixture.RepoPath,
)
assertContains(t, verifyOut, "verified 1 entries, 0 downgraded, 1 stale")
listOut := runRepoMemoryCommand(t,
"list",
"--db", fixture.DBPath,
"--repo", "repo",
"--status", "stale",
)
assertContains(t, listOut, "term:AITask [stale]")
eventsOut := runRepoMemoryCommand(t,
"events",
"--db", fixture.DBPath,
"--id", "1",
)
assertContains(t, eventsOut, "marked_stale (confirmed -> stale)")
}
func TestVerifyPrintsNoReposWhenEmpty(t *testing.T) {
fixture := newRepoMemoryFixture(t)
runRepoMemoryCommand(t, "init", "--db", fixture.DBPath)
verifyOut := runRepoMemoryCommand(t,
"verify",
"--db", fixture.DBPath,
)
if stringsTrim(verifyOut) != "no repos" {
t.Fatalf("expected no repos, got %q", verifyOut)
}
}
func TestVerifySkipsExplicitRepoWithoutGitHead(t *testing.T) {
fixture := newRepoMemoryFixture(t)
runRepoMemoryCommand(t, "init", "--db", fixture.DBPath)
repoPath := filepath.Join(t.TempDir(), "repo")
writeFile(t, filepath.Join(repoPath, ".keep"), "")
verifyOut := runRepoMemoryCommand(t,
"verify",
"--db", fixture.DBPath,
"--repo", repoPath,
)
assertContains(t, verifyOut, repoPath+": skipped (not a git repo or no HEAD)")
reposOut := runRepoMemoryCommand(t,
"repos",
"--db", fixture.DBPath,
)
if stringsTrim(reposOut) != "no repos" {
t.Fatalf("expected no repos after skipped verify, got %q", reposOut)
}
}
func TestVerifyDowngradesEntryMissingVerifiedOnCommit(t *testing.T) {
fixture := newRepoMemoryFixture(t)
runRepoMemoryCommand(t, "init", "--db", fixture.DBPath)
repoPath := filepath.Join(t.TempDir(), "repo")
if err := os.MkdirAll(repoPath, 0o755); err != nil {
t.Fatalf("mkdir repo: %v", err)
}
filePath := filepath.Join(repoPath, "foo.txt")
if err := os.WriteFile(filePath, []byte("hello\n"), 0o644); err != nil {
t.Fatalf("write dependency file: %v", err)
}
runRepoMemoryCommand(t,
"add",
"--db", fixture.DBPath,
"--repo", repoPath,
"--kind", "term",
"--key", "AITask",
"--summary", "Recorded before git init",
"--status", "confirmed",
"--dep", "file:"+filePath+":hard",
)
runCmd(t, repoPath, "git", "init")
runCmd(t, repoPath, "git", "config", "user.email", "test@example.com")
runCmd(t, repoPath, "git", "config", "user.name", "Tester")
runCmd(t, repoPath, "git", "add", ".")
runCmd(t, repoPath, "git", "commit", "-m", "init")
verifyOut := runRepoMemoryCommand(t,
"verify",
"--db", fixture.DBPath,
"--repo", repoPath,
)
assertContains(t, verifyOut, "verified 1 entries, 1 downgraded, 0 stale")
listOut := runRepoMemoryCommand(t,
"list",
"--db", fixture.DBPath,
"--repo", "repo",
"--status", "needs_review",
)
assertContains(t, listOut, "term:AITask [needs_review]")
eventsOut := runRepoMemoryCommand(t,
"events",
"--db", fixture.DBPath,
"--id", "1",
)
assertContains(t, eventsOut, "downgraded (confirmed -> needs_review)")
assertContains(t, eventsOut, "reason: missing verified_on_commit")
}
@@ -0,0 +1,195 @@
package main
import (
"path/filepath"
"strings"
"testing"
)
func TestWorkflowAddSearchEventsRoundtrip(t *testing.T) {
fixture := newRepoMemoryFixture(t)
evidencePath := filepath.Join(fixture.RepoPath, "app", "app", "src", "main", "java", "foo", "AITask.java")
writeFile(t, evidencePath, "class AITask {}\n")
initGitRepo(t, fixture.RepoPath)
commitAll(t, fixture.RepoPath, "init")
runRepoMemoryCommand(t, "init", "--db", fixture.DBPath)
addOut := runRepoMemoryCommand(
t,
"add",
"--db", fixture.DBPath,
"--repo", fixture.RepoPath,
"--kind", "term",
"--key", "AITask",
"--summary", "Plan 内嵌任务结构,不是独立表",
"--status", "confirmed",
"--source-path", evidencePath,
"--source-line", "42",
"--alias", "AI Task",
"--dep", "file:"+evidencePath+":hard",
)
searchOut := runRepoMemoryCommand(
t,
"search",
"--db", fixture.DBPath,
"--repo", "repo",
"--query", "AI Task",
)
eventsOut := runRepoMemoryCommand(
t,
"events",
"--db", fixture.DBPath,
"--id", "1",
)
assertContains(t, addOut, "upserted entry 1 (term:AITask)")
assertContains(t, searchOut, "[repo] term:AITask [confirmed]")
assertContains(t, eventsOut, "term:AITask [confirmed] #1")
assertContains(t, eventsOut, "created")
}
func TestWorkflowIngestSearchListAcrossSections(t *testing.T) {
fixture := newRepoMemoryFixture(t)
docPath := filepath.Join(fixture.RepoPath, "docs", "ai", "repo-memory.md")
writeFile(t, docPath, strings.TrimSpace(`
# Repo Memory
## Module Map
- gateway
- app/app
## Danger Zones
- shared libs first
`)+"\n")
initGitRepo(t, fixture.RepoPath)
commitAll(t, fixture.RepoPath, "init")
ingestOut := runRepoMemoryCommand(
t,
"ingest",
"--db", fixture.DBPath,
"--repo", fixture.RepoPath,
)
searchOut := runRepoMemoryCommand(
t,
"search",
"--db", fixture.DBPath,
"--repo", "repo",
"--query", "gateway",
)
listOut := runRepoMemoryCommand(
t,
"list",
"--db", fixture.DBPath,
"--repo", "repo",
)
assertContains(t, ingestOut, "ingested 1 docs from "+fixture.RepoPath)
assertContains(t, searchOut, "[repo] module:repo-memory:module-map [confirmed]")
assertContains(t, listOut, "module:repo-memory:module-map [confirmed]")
assertContains(t, listOut, "danger:repo-memory:danger-zones [confirmed]")
}
func TestWorkflowAddLinkAndResolveRelatedEntry(t *testing.T) {
fixture := newRepoMemoryFixture(t)
evidencePath := filepath.Join(fixture.RepoPath, "docs", "term.md")
writeFile(t, evidencePath, "AITask reference\n")
initGitRepo(t, fixture.RepoPath)
commitAll(t, fixture.RepoPath, "init")
runRepoMemoryCommand(t, "init", "--db", fixture.DBPath)
runRepoMemoryCommand(
t,
"add",
"--db", fixture.DBPath,
"--repo", fixture.RepoPath,
"--kind", "term",
"--key", "AITask",
"--summary", "Plan 内嵌任务结构,不是独立表",
"--status", "confirmed",
"--source-path", evidencePath,
)
runRepoMemoryCommand(
t,
"add",
"--db", fixture.DBPath,
"--repo", fixture.RepoPath,
"--kind", "chain",
"--key", "ai-insight.get",
"--summary", "gateway -> app service -> cache/db",
"--status", "confirmed",
)
linkOut := runRepoMemoryCommand(
t,
"link",
"--db", fixture.DBPath,
"--from-id", "1",
"--to-id", "2",
"--relation", "related_to",
)
relations := queryStrings(
t,
fixture.DBPath,
`SELECT relation FROM knowledge_links WHERE from_entry_id = ? AND to_entry_id = ?`,
1,
2,
)
eventsOne := runRepoMemoryCommand(t, "events", "--db", fixture.DBPath, "--id", "1")
eventsTwo := runRepoMemoryCommand(t, "events", "--db", fixture.DBPath, "--id", "2")
assertContains(t, linkOut, "linked #1 -[related_to]-> #2")
if len(relations) != 1 || relations[0] != "related_to" {
t.Fatalf("expected one stored relation related_to, got %#v", relations)
}
assertContains(t, eventsOne, "term:AITask [confirmed] #1")
assertContains(t, eventsTwo, "chain:ai-insight.get [confirmed] #2")
}
func TestWorkflowVerifyDowngradesAfterRepoChange(t *testing.T) {
fixture := newRepoMemoryFixture(t)
evidencePath := filepath.Join(fixture.RepoPath, "foo.txt")
writeFile(t, evidencePath, "hello\n")
initGitRepo(t, fixture.RepoPath)
commitAll(t, fixture.RepoPath, "init")
runRepoMemoryCommand(t, "init", "--db", fixture.DBPath)
runRepoMemoryCommand(
t,
"add",
"--db", fixture.DBPath,
"--repo", fixture.RepoPath,
"--kind", "term",
"--key", "AITask",
"--summary", "Plan 内嵌任务结构",
"--status", "confirmed",
"--dep", "file:"+evidencePath+":hard",
)
writeFile(t, evidencePath, "changed\n")
verifyOut := runRepoMemoryCommand(
t,
"verify",
"--db", fixture.DBPath,
"--repo", fixture.RepoPath,
)
listOut := runRepoMemoryCommand(
t,
"list",
"--db", fixture.DBPath,
"--repo", "repo",
)
eventsOut := runRepoMemoryCommand(
t,
"events",
"--db", fixture.DBPath,
"--id", "1",
)
assertContains(t, verifyOut, "verified 1 entries, 1 downgraded, 0 stale")
assertContains(t, listOut, "term:AITask [needs_review]")
assertContains(t, eventsOut, "downgraded (confirmed -> needs_review)")
}
@@ -674,22 +674,32 @@ func (s *Store) ListVerifyCandidates(ctx context.Context, repoRoot string) ([]Ve
if err != nil {
return nil, err
}
defer rows.Close()
var candidates []VerifyCandidate
for rows.Next() {
var c VerifyCandidate
if err := rows.Scan(&c.ID, &c.RepoPath, &c.Kind, &c.Key, &c.Title, &c.Status, &c.VerifiedOnCommit); err != nil {
rows.Close()
return nil, err
}
deps, err := s.listDependencies(ctx, c.ID)
candidates = append(candidates, c)
}
if err := rows.Err(); err != nil {
rows.Close()
return nil, err
}
if err := rows.Close(); err != nil {
return nil, err
}
for i := range candidates {
deps, err := s.listDependencies(ctx, candidates[i].ID)
if err != nil {
return nil, err
}
c.Dependencies = deps
candidates = append(candidates, c)
candidates[i].Dependencies = deps
}
return candidates, rows.Err()
return candidates, nil
}
func (s *Store) ApplyVerificationResult(ctx context.Context, entryID int64, currentCommit, nextStatus, reason string) error {