feat(brain): ListPending + PromoteNote — raw→wiki curation primitives (#38)
Closes the human curation loop: list brain/raw/ notes awaiting review (oldest-first, with excerpt) and promote one into brain/wiki/<wing>/<hall>/. PromoteNote rewrites frontmatter (sets wing/hall/promoted_at, preserves created_at + custom fields via the existing frontmatter editor), rebuilds the wing _index, and runs auto-tunnel. Atomic from the caller's view: hall/wing/slug/collision validation happens before any fs change, and the source is deleted only after the destination write succeeds (write-then- delete, never move) — a collision or write failure leaves raw/ intact. Default slug = filename minus the YYYY-MM-DD- prefix. Also exposes GET /pending + POST /promote for shell scripts (bad hall/collision/missing-source → 400, not 500). Scope note: operates on raw/ (the retrospective-skill review queue) per this issue's spec. The knowledge/ legacy pile is #22's bulk-migration concern, not this ongoing queue. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
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")
|
||||
}
|
||||
Reference in New Issue
Block a user