Files
compaction-mcp/content_test.go
T
lukaszraczylo dded4ec04c 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
2026-03-07 18:31:00 +00:00

321 lines
8.3 KiB
Go

package main
import (
"strings"
"testing"
)
func TestDetectContentType(t *testing.T) {
tests := []struct {
name string
content string
want ContentType
}{
{
name: "error with Error: prefix",
content: "Error: connection refused to postgres on port 5432",
want: ContentError,
},
{
name: "error with panic",
content: "panic: runtime error: index out of range [3] with length 2",
want: ContentError,
},
{
name: "error with FAIL",
content: "FAIL compaction-mcp [build failed]",
want: ContentError,
},
{
name: "error with Traceback",
content: "Traceback (most recent call last):\n File \"main.py\", line 1",
want: ContentError,
},
{
name: "error with goroutine",
content: "goroutine 1 [running]:\nmain.main()\n\t/app/main.go:12",
want: ContentError,
},
{
name: "code with backtick fence",
content: "Here is the fix:\n```go\nfunc main() {}\n```",
want: ContentCode,
},
{
name: "code with func keyword",
content: "func NewStore(budget int) *Store {\n\treturn &Store{}\n}",
want: ContentCode,
},
{
name: "code with import keyword",
content: "import (\n\t\"fmt\"\n\t\"os\"\n)",
want: ContentCode,
},
{
name: "code with high bracket density",
content: "{{{}}}(())[[]]{()}{{}}",
want: ContentCode,
},
{
name: "decision with decided",
content: "We decided to use SQLite for local storage instead of BoltDB",
want: ContentDecision,
},
{
name: "decision with going with",
content: "After discussion, going with the monorepo approach for simplicity",
want: ContentDecision,
},
{
name: "decision with approach:",
content: "approach: sidecar proxy with Envoy for service mesh",
want: ContentDecision,
},
{
name: "tool output with dollar prompt",
content: "$ go test -v ./...\nPASS\nok \tcompaction-mcp\t0.003s",
want: ContentToolOutput,
},
{
name: "tool output with angle bracket prompt",
content: "> ls -la /etc/nginx/\ntotal 48\ndrwxr-xr-x 2 root root 4096 Jan 1 00:00 conf.d",
want: ContentToolOutput,
},
{
name: "tool output with table chars",
content: "Name │ Status │ Age\n───────────├────────├─────\nnginx-pod │ Running│ 2d",
want: ContentToolOutput,
},
{
name: "prose default",
content: "The cilium project provides networking for Kubernetes clusters using eBPF technology.",
want: ContentProse,
},
{
name: "prose simple sentence",
content: "Let's meet tomorrow to discuss the architecture.",
want: ContentProse,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := DetectContentType(tt.content)
if got != tt.want {
t.Errorf("DetectContentType() = %s, want %s",
ContentTypeName(got), ContentTypeName(tt.want))
}
})
}
}
func TestDetectPriority(t *testing.T) {
tests := []struct {
name string
content string
want ContentType
}{
{
name: "error beats code",
content: "Error: compilation failed\nfunc main() {\n\tpanic(\"bad\")\n}",
want: ContentError,
},
{
name: "error beats decision",
content: "We decided to fix the panic: runtime error in production",
want: ContentError,
},
{
name: "code beats decision",
content: "We decided to use this:\nfunc Handle() {}",
want: ContentCode,
},
{
name: "code beats tool output",
content: "$ cat main.go\npackage main\nimport \"fmt\"",
want: ContentToolOutput, // starts with "$ " so tool output wins first in priority? No -- Error > Code > Decision > ToolOutput
},
}
// The last case: "$ " prefix makes it tool output, but "import " makes it code.
// Priority is Error > Code > Decision > ToolOutput, so Code should win.
// But "$ " prefix is checked in isToolOutput, and isCode is checked first.
// Actually: DetectContentType checks error first, then code, then decision, then tool output.
// "import " is in the content so isCode returns true => ContentCode.
// Fix the expected value:
tests[3].want = ContentCode
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := DetectContentType(tt.content)
if got != tt.want {
t.Errorf("DetectContentType() = %s, want %s",
ContentTypeName(got), ContentTypeName(tt.want))
}
})
}
}
func TestAutoTags(t *testing.T) {
tests := []struct {
name string
content string
want []string // subset that must appear
}{
{
name: "go file paths produce go tag",
content: "Modified /Users/dev/project/main.go and store.go to fix the issue",
want: []string{"go"},
},
{
name: "typescript file produces typescript tag",
content: "Check the component in src/App.tsx for the bug",
want: []string{"react"},
},
{
name: "python file produces python tag",
content: "Updated models.py with new schema",
want: []string{"python"},
},
{
name: "URL produces reference tag",
content: "See https://kubernetes.io/docs/concepts/ for details",
want: []string{"reference", "kubernetes"},
},
{
name: "kubernetes keyword",
content: "Deploy to Kubernetes cluster using helm chart",
want: []string{"kubernetes"},
},
{
name: "error content gets error tag",
content: "Error: connection refused to redis server",
want: []string{"error", "redis"},
},
{
name: "docker keyword",
content: "Build the Docker image with multi-stage builds",
want: []string{"docker"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := AutoTags(tt.content)
gotSet := make(map[string]struct{}, len(got))
for _, tag := range got {
gotSet[tag] = struct{}{}
}
for _, w := range tt.want {
if _, ok := gotSet[w]; !ok {
t.Errorf("AutoTags() missing expected tag %q, got %v", w, got)
}
}
})
}
}
func TestAutoTagsMax(t *testing.T) {
// Content with many possible tags to verify the cap at 5.
content := "Error: kubernetes docker cilium postgres nginx redis https://example.com main.go script.py app.tsx"
tags := AutoTags(content)
if len(tags) > maxTags {
t.Errorf("AutoTags() returned %d tags, want at most %d: %v", len(tags), maxTags, tags)
}
// Should have some tags
if len(tags) == 0 {
t.Error("AutoTags() returned no tags for content-rich input")
}
}
func TestScoreMultiplier(t *testing.T) {
tests := []struct {
ct ContentType
want float64
}{
{ContentError, 1.5},
{ContentDecision, 1.3},
{ContentCode, 1.2},
{ContentProse, 1.0},
{ContentToolOutput, 0.7},
}
for _, tt := range tests {
t.Run(ContentTypeName(tt.ct), func(t *testing.T) {
got := ScoreMultiplier(tt.ct)
if got != tt.want {
t.Errorf("ScoreMultiplier(%s) = %f, want %f",
ContentTypeName(tt.ct), got, tt.want)
}
})
}
}
func TestDecayHalfLife(t *testing.T) {
tests := []struct {
ct ContentType
want float64
}{
{ContentError, 30},
{ContentDecision, 360},
{ContentCode, 360},
{ContentProse, 120},
{ContentToolOutput, 15},
}
for _, tt := range tests {
t.Run(ContentTypeName(tt.ct), func(t *testing.T) {
got := DecayHalfLifeMinutes(tt.ct)
if got != tt.want {
t.Errorf("DecayHalfLifeMinutes(%s) = %f, want %f",
ContentTypeName(tt.ct), got, tt.want)
}
})
}
}
func TestContentTypeName(t *testing.T) {
tests := []struct {
want string
ct ContentType
}{
{"prose", ContentProse},
{"code", ContentCode},
{"error", ContentError},
{"tool-output", ContentToolOutput},
{"decision", ContentDecision},
}
for _, tt := range tests {
t.Run(tt.want, func(t *testing.T) {
got := ContentTypeName(tt.ct)
if got != tt.want {
t.Errorf("ContentTypeName() = %q, want %q", got, tt.want)
}
})
}
}
func TestEmptyContent(t *testing.T) {
ct := DetectContentType("")
if ct != ContentProse {
t.Errorf("DetectContentType(\"\") = %s, want prose", ContentTypeName(ct))
}
tags := AutoTags("")
if len(tags) != 0 {
t.Errorf("AutoTags(\"\") = %v, want empty", tags)
}
// Also test whitespace-only
ct = DetectContentType(" ")
if ct != ContentProse {
t.Errorf("DetectContentType(whitespace) = %s, want prose", ContentTypeName(ct))
}
tags = AutoTags(strings.Repeat(" ", 100))
if len(tags) != 0 {
t.Errorf("AutoTags(whitespace) = %v, want empty", tags)
}
}