Files
synapbus/internal/messaging/memory_injections_test.go
Algis DumbrisandClaude Opus 4.7 8a5d5e1f59 feat(020): US1 — proactive injection on MCP tool responses
Wraps eligible MCP tool handlers (my_status, send_message, search,
execute; get_replies excluded as pure metadata) with a middleware that
appends relevant_context to the JSON response. Retrieval reuses the
existing search.Service hybrid pipeline; owner scoping filters out
memories from other owners' agents (SC-008). Pin overlay is a marked
TODO for US3.

Components:
- internal/search/injection.go (+ test): BuildContextPacket with token
  budget greedy fill, score floor, truncation flag, CoreMemoryProvider
  interface stubbed for US2.
- internal/mcp/injection_wrap.go (+ test): WrapInjection middleware,
  registered via SetInjection on the existing handler.
- internal/mcp/injection_e2e_test.go: adversarial cross-owner test
  asserts H1 cannot see H2's memories on any wrapped tool.
- internal/messaging/memory_injections.go (+ test): 24h audit ring,
  hourly cleanup tick wired into stalemate worker.

Discovery during impl: claim_messages/read_inbox/read_channel live as
actions inside the execute bridge, not as registered top-level MCP
tools. They inherit injection through the execute wrapper.

This commit also bundles pre-existing working-tree changes for the
027 "remove approval noise" cleanup (migration 027, design doc,
removal of reminder/escalate logic from stalemate worker, related
trims in goals_tools.go and tools_hybrid.go). The two changes touch
the same files (stalemate.go, tools_hybrid.go) and bundling them
keeps history readable.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 15:09:38 +03:00

167 lines
4.2 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package messaging
import (
"context"
"testing"
"time"
_ "modernc.org/sqlite"
)
func TestMemoryInjections_RecordAndList(t *testing.T) {
db := newTestDB(t)
store := NewMemoryInjections(db)
ctx := context.Background()
rec := InjectionRecord{
OwnerID: "1",
AgentName: "research-mcpproxy",
ToolName: "my_status",
PacketSizeChars: 412,
PacketItemsCount: 3,
MessageIDs: []int64{1, 2, 3},
CoreBlobIncluded: true,
}
if err := store.Record(ctx, rec); err != nil {
t.Fatalf("Record: %v", err)
}
got, err := store.ListRecent(ctx, "1", 10)
if err != nil {
t.Fatalf("ListRecent: %v", err)
}
if len(got) != 1 {
t.Fatalf("ListRecent returned %d rows, want 1", len(got))
}
if got[0].PacketSizeChars != 412 || got[0].PacketItemsCount != 3 {
t.Errorf("packet counters mismatch: %+v", got[0])
}
if len(got[0].MessageIDs) != 3 || got[0].MessageIDs[0] != 1 {
t.Errorf("MessageIDs round-trip failed: %v", got[0].MessageIDs)
}
if !got[0].CoreBlobIncluded {
t.Error("CoreBlobIncluded round-trip failed")
}
}
func TestMemoryInjections_CleanupOnlyOldRows(t *testing.T) {
db := newTestDB(t)
store := NewMemoryInjections(db)
ctx := context.Background()
now := time.Now().UTC()
tests := []struct {
name string
offset time.Duration
wantKept bool
}{
{"3 days old", -72 * time.Hour, false},
{"36 hours old", -36 * time.Hour, false},
{"23 hours old", -23 * time.Hour, true},
{"30 minutes old", -30 * time.Minute, true},
{"current", 0, true},
}
// Seed 50 rows: 10 per bucket so we exercise the DELETE plan.
for _, tc := range tests {
for i := 0; i < 10; i++ {
rec := InjectionRecord{
OwnerID: "1",
AgentName: "a",
ToolName: "my_status",
PacketSizeChars: 100,
PacketItemsCount: 1,
MessageIDs: []int64{int64(i)},
CreatedAt: now.Add(tc.offset),
}
if err := store.Record(ctx, rec); err != nil {
t.Fatalf("Record %s: %v", tc.name, err)
}
}
}
deleted, err := store.Cleanup(ctx, 24*time.Hour)
if err != nil {
t.Fatalf("Cleanup: %v", err)
}
// 2 buckets older than 24h × 10 rows = 20 expected deletions.
if deleted != 20 {
t.Errorf("Cleanup removed %d rows, want 20", deleted)
}
remaining, err := store.ListRecent(ctx, "1", 100)
if err != nil {
t.Fatalf("ListRecent: %v", err)
}
if len(remaining) != 30 {
t.Errorf("after cleanup got %d rows, want 30", len(remaining))
}
}
func TestMemoryInjections_ListRecent_OwnerScoped(t *testing.T) {
db := newTestDB(t)
store := NewMemoryInjections(db)
ctx := context.Background()
for _, owner := range []string{"1", "2", "3"} {
for i := 0; i < 5; i++ {
if err := store.Record(ctx, InjectionRecord{
OwnerID: owner,
AgentName: "a",
ToolName: "my_status",
PacketSizeChars: 100,
PacketItemsCount: 1,
MessageIDs: []int64{int64(i)},
}); err != nil {
t.Fatalf("Record owner=%s i=%d: %v", owner, i, err)
}
}
}
got, err := store.ListRecent(ctx, "2", 100)
if err != nil {
t.Fatalf("ListRecent: %v", err)
}
if len(got) != 5 {
t.Fatalf("owner=2 got %d rows, want 5", len(got))
}
for _, r := range got {
if r.OwnerID != "2" {
t.Errorf("found leaked row OwnerID=%q in owner=2 listing", r.OwnerID)
}
}
}
func TestMemoryInjections_ListRecent_LimitOrdering(t *testing.T) {
db := newTestDB(t)
store := NewMemoryInjections(db)
ctx := context.Background()
base := time.Now().UTC().Add(-1 * time.Hour)
for i := 0; i < 10; i++ {
if err := store.Record(ctx, InjectionRecord{
OwnerID: "1",
AgentName: "a",
ToolName: "my_status",
PacketSizeChars: 100,
PacketItemsCount: 1,
MessageIDs: []int64{int64(i)},
CreatedAt: base.Add(time.Duration(i) * time.Minute),
}); err != nil {
t.Fatalf("Record %d: %v", i, err)
}
}
got, err := store.ListRecent(ctx, "1", 3)
if err != nil {
t.Fatalf("ListRecent: %v", err)
}
if len(got) != 3 {
t.Fatalf("ListRecent limit=3 returned %d", len(got))
}
// Newest first: i=9, 8, 7. Validate MessageIDs[0] descends.
if got[0].MessageIDs[0] != 9 || got[1].MessageIDs[0] != 8 || got[2].MessageIDs[0] != 7 {
t.Errorf("ordering wrong: %d, %d, %d", got[0].MessageIDs[0], got[1].MessageIDs[0], got[2].MessageIDs[0])
}
}