Add release infrastructure and complete implementation

- Dockerfile: distroless container for MCP server
- GoReleaser: multi-platform binary and Docker builds with cosign signing
- GitHub Actions: release workflow using shared actions
- Semver config for automatic version calculation
- Persistence layer, content indexing, and improved tool handlers
This commit is contained in:
2026-03-07 18:31:00 +00:00
parent 0ddd0e4598
commit dded4ec04c
17 changed files with 2511 additions and 133 deletions
+332 -24
View File
@@ -26,7 +26,10 @@ func TestEstimateTokens(t *testing.T) {
func TestStoreAddAndQuery(t *testing.T) {
s := NewStore(100000)
item := s.Add("cilium uses netkit on kernel 6.8", "cilium netkit needs 6.8", []string{"cilium", "networking"}, 8)
item, err := s.Add("cilium uses netkit on kernel 6.8", "cilium netkit needs 6.8", []string{"cilium", "networking"}, 8)
if err != nil {
t.Fatalf("Add failed: %v", err)
}
if item.ID == "" {
t.Fatal("expected non-empty ID")
}
@@ -59,7 +62,10 @@ func TestStoreAddAndQuery(t *testing.T) {
func TestStoreRemoveAndPin(t *testing.T) {
s := NewStore(100000)
item := s.Add("test content", "", nil, 5)
item, err := s.Add("test content", "", nil, 5)
if err != nil {
t.Fatalf("Add failed: %v", err)
}
if !s.Pin(item.ID) {
t.Fatal("pin failed")
}
@@ -79,36 +85,47 @@ func TestStoreRemoveAndPin(t *testing.T) {
}
func TestCompaction(t *testing.T) {
s := NewStore(200) // small budget: ~200 tokens = ~800 chars
// Budget of 20 tokens. Disable auto-compact so items survive Add().
// Each item is ~12 tokens, so 3 items = ~36 tokens > budget.
s := NewStore(20)
s.Configure(0, boolPtr(false), 0)
// Store items that exceed budget
s.Add("alpha bravo charlie delta echo foxtrot golf hotel", "short alpha", []string{"a"}, 3)
s.Add("india juliet kilo lima mike november oscar papa", "", []string{"b"}, 5)
s.Add("quebec romeo sierra tango uniform victor whiskey", "", []string{"c"}, 8)
if _, err := s.Add("alpha bravo charlie delta echo foxtrot golf hotel", "short alpha", []string{"a"}, 3); err != nil {
t.Fatalf("Add: %v", err)
}
if _, err := s.Add("india juliet kilo lima mike november oscar papa", "", []string{"b"}, 5); err != nil {
t.Fatalf("Add: %v", err)
}
if _, err := s.Add("quebec romeo sierra tango uniform victor whiskey", "", []string{"c"}, 8); err != nil {
t.Fatalf("Add: %v", err)
}
_, used, _, _ := s.Status()
if used == 0 {
t.Fatal("expected non-zero usage")
}
result := s.Compact(0.5) // compact to 50%
result := s.Compact(0.5) // compact to 50% of 20 = 10 tokens
if result.TokensAfter > result.TokensBefore {
t.Error("compaction should not increase tokens")
}
if result.TokensFreed == 0 && result.TokensBefore > 100 {
if result.TokensFreed == 0 {
t.Error("expected some tokens freed")
}
// Item with summary should have been promoted
if result.Summarized == 0 {
t.Log("note: no summary promotions (may depend on budget math)")
t.Error("expected at least one summary promotion")
}
}
func TestSummaryPromotion(t *testing.T) {
s := NewStore(100) // very tight budget
// Budget of 20 tokens. Content is ~23 tokens, summary is ~7 tokens.
// Disable auto-compact. Compaction to 0.3 = target 6 tokens, so promotion must happen.
s := NewStore(20)
s.Configure(0, boolPtr(false), 0)
item := s.Add(
item, _ := s.Add(
"this is a very long content string that takes many tokens to represent in the context window",
"long content, many tokens",
nil, 5,
@@ -116,7 +133,7 @@ func TestSummaryPromotion(t *testing.T) {
result := s.Compact(0.3) // aggressive compaction
if result.Summarized == 0 {
t.Log("note: summary promotion did not trigger")
t.Error("expected summary promotion to trigger")
}
got, ok := s.Get(item.ID)
@@ -129,24 +146,40 @@ func TestSummaryPromotion(t *testing.T) {
}
func TestDeduplication(t *testing.T) {
s := NewStore(100)
// Budget of 20 tokens, two items with 5/6 word overlap (>70%).
// Disable auto-compact so both items survive Add().
s := NewStore(20)
s.Configure(0, boolPtr(false), 0)
s.Add("alpha bravo charlie delta echo foxtrot", "", []string{"a"}, 5)
s.Add("alpha bravo charlie delta echo golf", "", []string{"b"}, 5)
if _, err := s.Add("alpha bravo charlie delta echo foxtrot", "", []string{"a"}, 5); err != nil {
t.Fatalf("Add: %v", err)
}
if _, err := s.Add("alpha bravo charlie delta echo golf", "", []string{"b"}, 5); err != nil {
t.Fatalf("Add: %v", err)
}
_, _, countBefore, _ := s.Status()
s.Compact(0.3)
if countBefore != 2 {
t.Fatalf("expected 2 items before compact, got %d", countBefore)
}
result := s.Compact(0.3)
_, _, countAfter, _ := s.Status()
if countAfter >= countBefore {
t.Log("note: dedup did not reduce count (similarity may be below threshold)")
t.Errorf("dedup should reduce count: before=%d after=%d", countBefore, countAfter)
}
if result.Deduplicated == 0 {
t.Error("expected at least one deduplication")
}
}
func TestUpdateSummary(t *testing.T) {
s := NewStore(100000)
item := s.Add("full content here", "", nil, 5)
item, err := s.Add("full content here", "", nil, 5)
if err != nil {
t.Fatalf("Add failed: %v", err)
}
if item.Summary != "" {
t.Fatal("should have no summary initially")
}
@@ -182,13 +215,13 @@ func TestJaccardSimilarity(t *testing.T) {
}
func TestAutoCompact(t *testing.T) {
s := NewStore(50) // very small: 50 tokens = ~200 chars
s := NewStore(10) // very small: 10 tokens
s.Configure(0, boolPtr(true), 0.5)
// Add items that should trigger auto-compaction
s.Add("first item with some content padding", "first", nil, 3)
s.Add("second item with more content padding", "second", nil, 3)
s.Add("third item triggering compaction now", "third", nil, 3)
// Add items that should trigger auto-compaction (errors ignored; auto-compact may evict)
s.Add("first item with some content padding", "first", nil, 3) //nolint:gosec
s.Add("second item with more content padding", "second", nil, 3) //nolint:gosec
s.Add("third item triggering compaction now", "third", nil, 3) //nolint:gosec
_, used, _, usage := s.Status()
// Auto-compact should have kept usage reasonable
@@ -197,4 +230,279 @@ func TestAutoCompact(t *testing.T) {
}
}
func TestUnpin(t *testing.T) {
s := NewStore(100000)
item, err := s.Add("test content for unpin", "", nil, 5)
if err != nil {
t.Fatalf("Add failed: %v", err)
}
// Pin then unpin
if !s.Pin(item.ID) {
t.Fatal("pin failed")
}
got, _ := s.Get(item.ID)
if !got.Pinned {
t.Fatal("should be pinned")
}
if !s.Unpin(item.ID) {
t.Fatal("unpin failed")
}
got, _ = s.Get(item.ID)
if got.Pinned {
t.Fatal("should be unpinned")
}
// Unpin non-existent item
if s.Unpin("nonexistent") {
t.Error("unpin of non-existent item should return false")
}
}
func TestListItems(t *testing.T) {
s := NewStore(100000)
// Add 5 items
for i := 0; i < 5; i++ {
_, err := s.Add("content "+string(rune('a'+i)), "", nil, 5)
if err != nil {
t.Fatalf("Add %d failed: %v", i, err)
}
}
// List all
items, total := s.ListItems(0, 10)
if total != 5 {
t.Errorf("expected total 5, got %d", total)
}
if len(items) != 5 {
t.Errorf("expected 5 items, got %d", len(items))
}
// List with offset
items, total = s.ListItems(3, 10)
if total != 5 {
t.Errorf("expected total 5, got %d", total)
}
if len(items) != 2 {
t.Errorf("expected 2 items with offset 3, got %d", len(items))
}
// List with limit
items, total = s.ListItems(0, 2)
if total != 5 {
t.Errorf("expected total 5, got %d", total)
}
if len(items) != 2 {
t.Errorf("expected 2 items with limit 2, got %d", len(items))
}
// List beyond end
items, total = s.ListItems(10, 5)
if total != 5 {
t.Errorf("expected total 5, got %d", total)
}
if items != nil {
t.Errorf("expected nil items beyond end, got %d", len(items))
}
}
func TestBulkAdd(t *testing.T) {
s := NewStore(100000)
bulkItems := []BulkItem{
{Content: "first bulk item", Summary: "first", Tags: []string{"bulk"}, Importance: 5},
{Content: "second bulk item", Summary: "second", Tags: []string{"bulk"}, Importance: 7},
{Content: "third bulk item", Summary: "", Tags: nil, Importance: 3},
}
results, errs := s.BulkAdd(bulkItems)
if len(results) != 3 {
t.Fatalf("expected 3 results, got %d", len(results))
}
for i, err := range errs {
if err != nil {
t.Errorf("bulk add item %d failed: %v", i, err)
}
}
for i, item := range results {
if item == nil {
t.Errorf("bulk add item %d is nil", i)
} else if item.ID == "" {
t.Errorf("bulk add item %d has empty ID", i)
}
}
_, _, count, _ := s.Status()
if count != 3 {
t.Errorf("expected 3 items in store, got %d", count)
}
}
func TestExport(t *testing.T) {
s := NewStore(100000)
if _, err := s.Add("full content one", "summary one", []string{"a"}, 5); err != nil {
t.Fatalf("Add: %v", err)
}
if _, err := s.Add("full content two", "", []string{"b"}, 7); err != nil {
t.Fatalf("Add: %v", err)
}
// Export all
items := s.Export(false)
if len(items) != 2 {
t.Fatalf("expected 2 exported items, got %d", len(items))
}
for _, item := range items {
if strings.HasPrefix(item.Content, "summary") {
t.Error("full export should not replace content with summary")
}
}
// Export summaries only
items = s.Export(true)
if len(items) != 2 {
t.Fatalf("expected 2 exported items, got %d", len(items))
}
foundSummary := false
foundFull := false
for _, item := range items {
if item.Content == "summary one" {
foundSummary = true
}
if item.Content == "full content two" {
foundFull = true // no summary available, keep full content
}
}
if !foundSummary {
t.Error("summaries_only should replace content with summary where available")
}
if !foundFull {
t.Error("summaries_only should keep full content when no summary available")
}
}
func TestItemCountLimit(t *testing.T) {
// We can't add 10001 items in a test (too slow), but we can test the limit
// by lowering the effective count. Instead, test with a smaller approach:
// fill the store to capacity and verify the error.
s := NewStore(1000000)
// Override: we test by directly checking the error message
// Add one item, then manipulate to test boundary
item, err := s.Add("test", "", nil, 5)
if err != nil {
t.Fatalf("first add failed: %v", err)
}
if item == nil {
t.Fatal("expected non-nil item")
}
// Add content that's too large
bigContent := strings.Repeat("x", maxContentBytes+1)
_, err = s.Add(bigContent, "", nil, 5)
if err == nil {
t.Error("expected error for oversized content")
}
if err != nil && !strings.Contains(err.Error(), "content too large") {
t.Errorf("unexpected error: %v", err)
}
}
func TestContentSizeLimit(t *testing.T) {
s := NewStore(100000)
// Exactly at limit should succeed
content := strings.Repeat("x", maxContentBytes)
_, err := s.Add(content, "", nil, 5)
if err != nil {
t.Errorf("content at exact limit should succeed: %v", err)
}
// Over limit should fail
content = strings.Repeat("x", maxContentBytes+1)
_, err = s.Add(content, "", nil, 5)
if err == nil {
t.Error("content over limit should fail")
}
}
func TestQueryAccessCountFix(t *testing.T) {
s := NewStore(100000)
// Add 5 items
ids := make([]string, 5)
for i := 0; i < 5; i++ {
item, _ := s.Add("item "+string(rune('a'+i)), "", nil, 5)
ids[i] = item.ID
}
// Query with limit 2 - only the top 2 should get access bumps
s.Query("item", nil, 2)
// Check that we got exactly 2 items with AccessCount > 0
bumped := 0
for _, id := range ids {
got, ok := s.Get(id)
if !ok {
t.Fatalf("item %s not found", id)
}
// Get itself bumps access count by 1, so items that were
// bumped by Query will have AccessCount >= 2 after Get
if got.AccessCount >= 2 {
bumped++
}
}
// Only the 2 items returned by Query should have been bumped
// (plus the Get call bumps all by 1)
if bumped > 2 {
t.Errorf("expected at most 2 items bumped by query, got %d", bumped)
}
}
func TestGetReturnsValueCopy(t *testing.T) {
s := NewStore(100000)
item, _ := s.Add("original content", "", nil, 5)
got, ok := s.Get(item.ID)
if !ok {
t.Fatal("item not found")
}
// Mutating the returned copy should not affect the store.
// We use a helper to avoid the unusedwrite lint.
mutateItemContent(&got, "mutated")
got2, _ := s.Get(item.ID)
if got2.Content != "original content" {
t.Error("Get should return value copy; mutation should not affect store")
}
}
func mutateItemContent(item *Item, content string) { item.Content = content }
func TestQueryReturnsValueCopies(t *testing.T) {
s := NewStore(100000)
if _, err := s.Add("original query content", "", []string{"test"}, 5); err != nil {
t.Fatalf("Add: %v", err)
}
results := s.Query("original", nil, 10)
if len(results) != 1 {
t.Fatalf("expected 1 result, got %d", len(results))
}
// Mutating the returned copy should not affect the store
results[0].Content = "mutated"
results2 := s.Query("original", nil, 10)
if len(results2) != 1 {
t.Fatalf("expected 1 result, got %d", len(results2))
}
if results2[0].Content == "mutated" {
t.Error("Query should return value copies; mutation should not affect store")
}
}
func boolPtr(b bool) *bool { return &b }