package api import ( "os" "path/filepath" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func writeRaw(t *testing.T, brainDir, name, content string) { t.Helper() dir := filepath.Join(brainDir, "raw") require.NoError(t, os.MkdirAll(dir, 0o755)) require.NoError(t, os.WriteFile(filepath.Join(dir, name), []byte(content), 0o644)) } func TestListPendingEmptyWhenAbsent(t *testing.T) { got, err := ListPending(t.TempDir()) require.NoError(t, err, "absent raw/ is not an error") assert.Empty(t, got) } func TestListPendingReturnsOldestFirstWithExcerpt(t *testing.T) { dir := t.TempDir() writeRaw(t, dir, "2026-06-02-newer.md", "---\ncreated_at: 2026-06-02T00:00:00Z\n---\nNewer body here.\n") writeRaw(t, dir, "2026-06-01-older.md", "---\ncreated_at: 2026-06-01T00:00:00Z\n---\nOlder body content.\n") // non-md ignored writeRaw(t, dir, "notes.txt", "ignore me") got, err := ListPending(dir) require.NoError(t, err) require.Len(t, got, 2) assert.Equal(t, "2026-06-01-older.md", got[0].Filename, "oldest first") assert.Equal(t, "2026-06-02-newer.md", got[1].Filename) assert.Contains(t, got[0].Excerpt, "Older body content") assert.NotContains(t, got[0].Excerpt, "---", "excerpt is body, not frontmatter") assert.Positive(t, got[0].SizeBytes) } func TestPromoteHappyPath(t *testing.T) { dir := t.TempDir() writeRaw(t, dir, "2026-06-01-lejpa-decision.md", "---\ncreated_at: 2026-06-01T09:00:00Z\ncustom_field: keep-me\n---\n# LeJEPA\n\nbody.\n") rel, err := PromoteNote(dir, PromoteOptions{ Filename: "2026-06-01-lejpa-decision.md", Wing: "jepa-fx", Hall: "decisions", }) require.NoError(t, err) assert.Equal(t, "wiki/jepa-fx/decisions/lejpa-decision.md", rel, "slug defaults to filename minus date prefix") // Source deleted. _, statErr := os.Stat(filepath.Join(dir, "raw", "2026-06-01-lejpa-decision.md")) assert.True(t, os.IsNotExist(statErr), "source removed after promote") got, err := os.ReadFile(filepath.Join(dir, filepath.FromSlash(rel))) require.NoError(t, err) s := string(got) assert.Contains(t, s, "wing: jepa-fx") assert.Contains(t, s, "hall: decisions") assert.Contains(t, s, "created_at: 2026-06-01T09:00:00Z", "original created_at preserved") assert.Contains(t, s, "promoted_at:") assert.Contains(t, s, "custom_field: keep-me", "custom frontmatter preserved") assert.Contains(t, s, "# LeJEPA") } func TestPromoteExplicitSlug(t *testing.T) { dir := t.TempDir() writeRaw(t, dir, "2026-06-01-x.md", "body\n") rel, err := PromoteNote(dir, PromoteOptions{Filename: "2026-06-01-x.md", Wing: "a", Hall: "facts", Slug: "custom-slug"}) require.NoError(t, err) assert.Equal(t, "wiki/a/facts/custom-slug.md", rel) } func TestPromoteInvalidHallErrorsBeforeTouchingFS(t *testing.T) { dir := t.TempDir() writeRaw(t, dir, "2026-06-01-x.md", "body\n") _, err := PromoteNote(dir, PromoteOptions{Filename: "2026-06-01-x.md", Wing: "a", Hall: "garbage"}) require.Error(t, err) // Source untouched. _, statErr := os.Stat(filepath.Join(dir, "raw", "2026-06-01-x.md")) assert.NoError(t, statErr, "invalid hall must not delete or move the source") } func TestPromoteMissingSourceErrors(t *testing.T) { _, err := PromoteNote(t.TempDir(), PromoteOptions{Filename: "ghost.md", Wing: "a", Hall: "facts"}) require.Error(t, err) } func TestPromoteSlugCollisionNoOverwrite(t *testing.T) { dir := t.TempDir() // Pre-existing target. dest := filepath.Join(dir, "wiki", "a", "facts", "x.md") require.NoError(t, os.MkdirAll(filepath.Dir(dest), 0o755)) require.NoError(t, os.WriteFile(dest, []byte("EXISTING\n"), 0o644)) writeRaw(t, dir, "2026-06-01-x.md", "NEW\n") _, err := PromoteNote(dir, PromoteOptions{Filename: "2026-06-01-x.md", Wing: "a", Hall: "facts"}) require.Error(t, err, "collision must error, not overwrite") got, _ := os.ReadFile(dest) assert.Equal(t, "EXISTING\n", string(got), "target not overwritten") _, statErr := os.Stat(filepath.Join(dir, "raw", "2026-06-01-x.md")) assert.NoError(t, statErr, "source preserved on collision (atomic: no delete without write)") } func TestPromoteRejectsTraversalFilename(t *testing.T) { _, err := PromoteNote(t.TempDir(), PromoteOptions{Filename: "../escape.md", Wing: "a", Hall: "facts"}) require.Error(t, err) } func TestPromoteRebuildsWingIndex(t *testing.T) { dir := t.TempDir() writeRaw(t, dir, "2026-06-01-x.md", "---\ntitle: X Note\n---\nbody\n") _, err := PromoteNote(dir, PromoteOptions{Filename: "2026-06-01-x.md", Wing: "a", Hall: "facts"}) require.NoError(t, err) idx, err := os.ReadFile(filepath.Join(dir, "wiki", "a", "_index.md")) require.NoError(t, err, "wing _index regenerated") assert.Contains(t, string(idx), "x", "promoted note appears in the index") } // --- RejectNote (hyperguild#81) --------------------------------------------- func TestRejectHappyPath(t *testing.T) { dir := t.TempDir() writeRaw(t, dir, "2026-04-23-fed-sentiment-scrape.md", "---\ncreated_at: 2026-04-23T13:20:07Z\n---\n# Sentiment scrape\n\nbody.\n") rel, err := RejectNote(dir, RejectOptions{ Filename: "2026-04-23-fed-sentiment-scrape.md", Reason: "generic scrape, not homelab-relevant", }) require.NoError(t, err) assert.Equal(t, "raw/rejected/2026-04-23-fed-sentiment-scrape.md", rel) // Source removed from the pending queue. _, statErr := os.Stat(filepath.Join(dir, "raw", "2026-04-23-fed-sentiment-scrape.md")) assert.True(t, os.IsNotExist(statErr), "source removed from raw/ after reject") got, err := os.ReadFile(filepath.Join(dir, filepath.FromSlash(rel))) require.NoError(t, err) s := string(got) assert.Contains(t, s, "created_at: 2026-04-23T13:20:07Z", "original created_at preserved") assert.Contains(t, s, "rejected_at:") assert.Contains(t, s, "reject_reason: generic scrape, not homelab-relevant") assert.Contains(t, s, "# Sentiment scrape", "body preserved for later reference") // Rejected notes never entered the wiki, and are no longer pending. pending, err := ListPending(dir) require.NoError(t, err) assert.Empty(t, pending) } func TestRejectWithoutReason(t *testing.T) { dir := t.TempDir() writeRaw(t, dir, "2026-06-01-x.md", "body\n") rel, err := RejectNote(dir, RejectOptions{Filename: "2026-06-01-x.md"}) require.NoError(t, err) got, _ := os.ReadFile(filepath.Join(dir, filepath.FromSlash(rel))) assert.NotContains(t, string(got), "reject_reason:", "no reason given, none stamped") } func TestRejectMissingSourceErrors(t *testing.T) { _, err := RejectNote(t.TempDir(), RejectOptions{Filename: "ghost.md"}) require.Error(t, err) } func TestRejectRejectsTraversalFilename(t *testing.T) { _, err := RejectNote(t.TempDir(), RejectOptions{Filename: "../escape.md"}) require.Error(t, err) } func TestRejectCollisionNoOverwrite(t *testing.T) { dir := t.TempDir() dest := filepath.Join(dir, "raw", "rejected", "x.md") require.NoError(t, os.MkdirAll(filepath.Dir(dest), 0o755)) require.NoError(t, os.WriteFile(dest, []byte("EXISTING\n"), 0o644)) writeRaw(t, dir, "x.md", "NEW\n") _, err := RejectNote(dir, RejectOptions{Filename: "x.md"}) require.Error(t, err, "collision must error, not overwrite") got, _ := os.ReadFile(dest) assert.Equal(t, "EXISTING\n", string(got), "target not overwritten") _, statErr := os.Stat(filepath.Join(dir, "raw", "x.md")) assert.NoError(t, statErr, "source preserved on collision (atomic: no delete without write)") } func TestRejectedNotesAreNotListedAsPendingAgain(t *testing.T) { // ListPending already skips tunnel-candidates-*.md; confirm raw/rejected/ // (a subdirectory) doesn't leak into the top-level pending listing either // — os.ReadDir on raw/ doesn't recurse, but pin it so a future change to // ListPending can't silently start surfacing rejected notes as pending // again. dir := t.TempDir() writeRaw(t, dir, "2026-06-01-x.md", "body\n") _, err := RejectNote(dir, RejectOptions{Filename: "2026-06-01-x.md"}) require.NoError(t, err) pending, err := ListPending(dir) require.NoError(t, err) assert.Empty(t, pending, "a rejected note must not still appear as pending") }