From 33f6d99ba7fc1b46218f3b31dd2bf0aef1c884a3 Mon Sep 17 00:00:00 2001 From: Tyler Koenig Date: Wed, 20 May 2026 20:52:57 -0400 Subject: [PATCH] test(cmd): add tests for add, delete, promote, demote, absorb, ls Covers happy paths, error cases (not found, already promoted, already fluid, crystallized target, same-entity absorb), and empty result sets. Uses NIB_DB env var for test isolation. --- cmd/cmd_test.go | 221 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 221 insertions(+) create mode 100644 cmd/cmd_test.go diff --git a/cmd/cmd_test.go b/cmd/cmd_test.go new file mode 100644 index 0000000..f4b2364 --- /dev/null +++ b/cmd/cmd_test.go @@ -0,0 +1,221 @@ +package cmd + +import ( + "bytes" + "context" + "path/filepath" + "strings" + "testing" + + "github.com/lerko/nib/internal/db" + "github.com/spf13/cobra" +) + +func testStore(t *testing.T) *db.Store { + t.Helper() + dbPath := filepath.Join(t.TempDir(), "test.db") + t.Setenv("NIB_DB", dbPath) + store, err := db.Open(dbPath) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { store.Close() }) + return store +} + +func newCmd() *cobra.Command { + c := &cobra.Command{} + c.SetContext(context.Background()) + return c +} + +func captureOutput(t *testing.T, fn func()) string { + t.Helper() + var buf bytes.Buffer + rootCmd.SetOut(&buf) + defer rootCmd.SetOut(nil) + fn() + return buf.String() +} + +func seedEntity(t *testing.T, store *db.Store, body string, glyph db.Glyph) *db.Entity { + t.Helper() + e := &db.Entity{Body: body, Glyph: glyph, Tags: []string{}} + if err := store.Create(context.Background(), e); err != nil { + t.Fatal(err) + } + return e +} + +func TestRunAdd(t *testing.T) { + testStore(t) + + err := runAdd(newCmd(), []string{"hello", "world"}) + if err != nil { + t.Fatalf("runAdd: %v", err) + } +} + +func TestRunAddWithGlyph(t *testing.T) { + testStore(t) + + err := runAdd(newCmd(), []string{"-", "buy", "milk", "#errands"}) + if err != nil { + t.Fatalf("runAdd todo: %v", err) + } +} + +func TestRunAddWithTimeAnchor(t *testing.T) { + testStore(t) + + err := runAdd(newCmd(), []string{"@", "dentist", "@14:00"}) + if err != nil { + t.Fatalf("runAdd event: %v", err) + } +} + +func TestRunDelete(t *testing.T) { + store := testStore(t) + e := seedEntity(t, store, "to delete", db.GlyphNote) + store.Close() + + err := runDelete(newCmd(), []string{e.ID}) + if err != nil { + t.Fatalf("runDelete soft: %v", err) + } + + err = runDelete(newCmd(), []string{e.ID}) + if err != nil { + t.Fatalf("runDelete hard: %v", err) + } +} + +func TestRunDeleteNotFound(t *testing.T) { + testStore(t) + + err := runDelete(newCmd(), []string{"nonexistent"}) + if err == nil { + t.Fatal("expected error for nonexistent id") + } + if !strings.Contains(err.Error(), "not_found") { + t.Fatalf("expected not_found error, got: %v", err) + } +} + +func TestRunPromote(t *testing.T) { + store := testStore(t) + e := seedEntity(t, store, "reusable snippet", db.GlyphNote) + store.Close() + + err := runPromote(newCmd(), []string{e.ID, "snippet"}) + if err != nil { + t.Fatalf("runPromote: %v", err) + } +} + +func TestRunPromoteAlreadyPromoted(t *testing.T) { + store := testStore(t) + e := seedEntity(t, store, "already a card", db.GlyphNote) + store.Promote(context.Background(), e.ID, db.CardSnippet, nil) + store.Close() + + err := runPromote(newCmd(), []string{e.ID, "snippet"}) + if err == nil { + t.Fatal("expected error for already promoted") + } +} + +func TestRunDemote(t *testing.T) { + store := testStore(t) + e := seedEntity(t, store, "demote me", db.GlyphNote) + store.Promote(context.Background(), e.ID, db.CardSnippet, nil) + store.Close() + + err := runDemote(newCmd(), []string{e.ID}) + if err != nil { + t.Fatalf("runDemote: %v", err) + } +} + +func TestRunDemoteAlreadyFluid(t *testing.T) { + store := testStore(t) + e := seedEntity(t, store, "already fluid", db.GlyphNote) + store.Close() + + err := runDemote(newCmd(), []string{e.ID}) + if err == nil { + t.Fatal("expected error for already fluid") + } +} + +func TestRunAbsorb(t *testing.T) { + store := testStore(t) + target := seedEntity(t, store, "target body", db.GlyphNote) + source := seedEntity(t, store, "source body", db.GlyphNote) + store.Close() + + err := runAbsorb(newCmd(), []string{target.ID, source.ID}) + if err != nil { + t.Fatalf("runAbsorb: %v", err) + } +} + +func TestRunAbsorbSameEntity(t *testing.T) { + store := testStore(t) + e := seedEntity(t, store, "same entity", db.GlyphNote) + store.Close() + + err := runAbsorb(newCmd(), []string{e.ID, e.ID}) + if err == nil { + t.Fatal("expected error for same entity absorb") + } +} + +func TestRunAbsorbCrystallizedTarget(t *testing.T) { + store := testStore(t) + target := seedEntity(t, store, "crystallized", db.GlyphNote) + source := seedEntity(t, store, "source", db.GlyphNote) + store.Promote(context.Background(), target.ID, db.CardSnippet, nil) + store.Close() + + err := runAbsorb(newCmd(), []string{target.ID, source.ID}) + if err == nil { + t.Fatal("expected error for crystallized target") + } +} + +func TestRunLs(t *testing.T) { + store := testStore(t) + seedEntity(t, store, "recent note", db.GlyphNote) + store.Close() + + lsTag = "" + lsDate = "" + lsMonth = "" + lsFrom = "" + lsTo = "" + lsLimit = 0 + lsAll = false + + err := runLs(newCmd(), nil) + if err != nil { + t.Fatalf("runLs: %v", err) + } +} + +func TestRunLsEmpty(t *testing.T) { + testStore(t) + + lsTag = "" + lsDate = "" + lsMonth = "" + lsFrom = "" + lsTo = "" + lsLimit = 0 + lsAll = false + + err := runLs(newCmd(), nil) + if err != nil { + t.Fatalf("runLs empty: %v", err) + } +}