feat(brain): surface an aging review queue in brain_context (#82)
brain_pending existed the whole time four real items sat unreviewed from 2026-04-23 to 2026-07-15. The tool was never the gap — nothing ever prompted anyone to call it. The system should be what remembers the queue exists. Option 2 from the issue: ride along with brain_context, which sessions already call, so noticing an aging note costs nobody a decision to go looking. No scheduled job, no ntfy wiring, nothing new to keep running. Threshold-gated at 14 days, and silent below it — a notice on every context call is one people learn to skip, which reproduces the original failure with extra steps. It reports the whole queue once it speaks: the threshold decides whether to talk, not what to count. It never promotes, rejects or reorders. Age is a reason to ask a human, not a verdict; an old note may be exactly right to promote once someone looks. A test asserts the queue is untouched and that the payload carries a hint rather than an action. An unreadable raw/ dir degrades to silence — brain_context is a hot path and must not fail because the review queue is broken. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BeAp5LTscnz2W7Ubt4eJ6m
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
package mcp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// writePending drops a raw/ note whose created_at is age old.
|
||||
func writePending(t *testing.T, brainDir, name string, age time.Duration, now time.Time) {
|
||||
t.Helper()
|
||||
raw := filepath.Join(brainDir, "raw")
|
||||
require.NoError(t, os.MkdirAll(raw, 0o755))
|
||||
body := "---\ncreated_at: " + now.Add(-age).UTC().Format(time.RFC3339) + "\n---\n\nnote body\n"
|
||||
require.NoError(t, os.WriteFile(filepath.Join(raw, name), []byte(body), 0o644))
|
||||
}
|
||||
|
||||
func TestPendingNudge_SilentWhenNothingIsPending(t *testing.T) {
|
||||
// No queue, no nudge. A notice that fires every session is one people
|
||||
// learn to skip, which is how the queue got ignored for three months in
|
||||
// the first place (#82).
|
||||
got := pendingNudge(t.TempDir(), time.Now(), 14*24*time.Hour)
|
||||
assert.Nil(t, got)
|
||||
}
|
||||
|
||||
func TestPendingNudge_SilentWhileEverythingIsBelowTheThreshold(t *testing.T) {
|
||||
now := time.Now()
|
||||
dir := t.TempDir()
|
||||
writePending(t, dir, "fresh.md", 2*24*time.Hour, now)
|
||||
|
||||
got := pendingNudge(dir, now, 14*24*time.Hour)
|
||||
assert.Nil(t, got, "a two-day-old note is not aging")
|
||||
}
|
||||
|
||||
func TestPendingNudge_ReportsCountAndOldestAgeOnceSomethingAges(t *testing.T) {
|
||||
now := time.Now()
|
||||
dir := t.TempDir()
|
||||
writePending(t, dir, "old.md", 100*24*time.Hour, now)
|
||||
writePending(t, dir, "older.md", 120*24*time.Hour, now)
|
||||
writePending(t, dir, "fresh.md", 1*24*time.Hour, now)
|
||||
|
||||
got := pendingNudge(dir, now, 14*24*time.Hour)
|
||||
require.NotNil(t, got)
|
||||
|
||||
// All three are pending; the threshold decides whether to speak, not
|
||||
// what to count. Someone opening the queue sees the whole queue.
|
||||
assert.Equal(t, 3, got.Pending)
|
||||
assert.Equal(t, 2, got.Aging)
|
||||
assert.Equal(t, 120, got.OldestDays)
|
||||
assert.Equal(t, "older.md", got.Oldest)
|
||||
}
|
||||
|
||||
func TestPendingNudge_NeverDecidesAnything(t *testing.T) {
|
||||
// Aging is a trigger to ask a human, never a verdict. The nudge must
|
||||
// carry no action and must leave the queue exactly as it found it.
|
||||
now := time.Now()
|
||||
dir := t.TempDir()
|
||||
writePending(t, dir, "ancient.md", 365*24*time.Hour, now)
|
||||
|
||||
before, err := os.ReadDir(filepath.Join(dir, "raw"))
|
||||
require.NoError(t, err)
|
||||
|
||||
got := pendingNudge(dir, now, 14*24*time.Hour)
|
||||
require.NotNil(t, got)
|
||||
|
||||
after, err := os.ReadDir(filepath.Join(dir, "raw"))
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, after, len(before), "the nudge must not promote or reject anything")
|
||||
assert.Contains(t, got.Hint, "brain_pending", "the nudge should say how to look, not act")
|
||||
}
|
||||
|
||||
func TestPendingNudge_SurvivesAnUnreadableQueue(t *testing.T) {
|
||||
// brain_context is a hot path; a broken raw/ dir must degrade to silence
|
||||
// rather than failing the whole context call.
|
||||
dir := t.TempDir()
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "raw"), []byte("not a directory"), 0o644))
|
||||
|
||||
assert.NotPanics(t, func() {
|
||||
assert.Nil(t, pendingNudge(dir, time.Now(), 14*24*time.Hour))
|
||||
})
|
||||
}
|
||||
|
||||
func TestBrainContext_CarriesTheNudgeWithoutFailingWhenTheQueueIsEmpty(t *testing.T) {
|
||||
// The wiring, not the summary: a nudge that never reaches a caller is the
|
||||
// same as no nudge, and brain_context must still work with no queue.
|
||||
dir := t.TempDir()
|
||||
srv := NewServer(dir, nil, nil, nil)
|
||||
|
||||
raw, err := srv.brainContext(context.Background(),
|
||||
json.RawMessage(`{"project_root":"/home/mathias/dev/AGENTS/hyperguild"}`))
|
||||
require.NoError(t, err)
|
||||
|
||||
var empty map[string]any
|
||||
require.NoError(t, json.Unmarshal(raw, &empty))
|
||||
assert.NotContains(t, empty, "pending_review", "silence when nothing is aging")
|
||||
|
||||
writePending(t, dir, "ancient.md", 200*24*time.Hour, time.Now())
|
||||
|
||||
raw, err = srv.brainContext(context.Background(),
|
||||
json.RawMessage(`{"project_root":"/home/mathias/dev/AGENTS/hyperguild"}`))
|
||||
require.NoError(t, err)
|
||||
|
||||
var withQueue map[string]any
|
||||
require.NoError(t, json.Unmarshal(raw, &withQueue))
|
||||
nudge, ok := withQueue["pending_review"].(map[string]any)
|
||||
require.True(t, ok, "the nudge must reach the caller: %v", withQueue)
|
||||
assert.Equal(t, float64(1), nudge["aging"])
|
||||
assert.Equal(t, "ancient.md", nudge["oldest"])
|
||||
}
|
||||
Reference in New Issue
Block a user