gomcp/internal/domain/entropy/gate_test.go

161 lines
5.1 KiB
Go

package entropy
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestShannonEntropy_Empty(t *testing.T) {
assert.Equal(t, 0.0, ShannonEntropy(""))
}
func TestShannonEntropy_SingleChar(t *testing.T) {
// All same character → 0 entropy.
assert.InDelta(t, 0.0, ShannonEntropy("aaaaaaa"), 0.001)
}
func TestShannonEntropy_TwoEqualChars(t *testing.T) {
// Two chars with equal frequency → 1 bit.
assert.InDelta(t, 1.0, ShannonEntropy("abababab"), 0.001)
}
func TestShannonEntropy_EnglishText(t *testing.T) {
// Natural English text: ~3.5-4.5 bits/char.
text := "The quick brown fox jumps over the lazy dog and then runs away into the forest"
e := ShannonEntropy(text)
assert.Greater(t, e, 3.0)
assert.Less(t, e, 5.0)
}
func TestShannonEntropy_RandomLike(t *testing.T) {
// High-entropy random-like text.
text := "x7#kQ9!mZ2$pW4&nR6*jL8@cF0^tB3"
e := ShannonEntropy(text)
assert.Greater(t, e, 4.0, "random-like text should have high entropy")
}
func TestShannonEntropy_Russian(t *testing.T) {
// Russian text also follows entropy principles.
text := "Быстрая коричневая лиса прыгает через ленивую собаку и убегает в лес"
e := ShannonEntropy(text)
assert.Greater(t, e, 3.0)
assert.Less(t, e, 5.5)
}
func TestGate_AllowsNormalText(t *testing.T) {
g := NewGate(nil)
result := g.Check("The quick brown fox jumps over the lazy dog")
assert.True(t, result.IsAllowed)
assert.False(t, result.IsBlocked)
assert.Greater(t, result.Entropy, 0.0)
}
func TestGate_BlocksHighEntropy(t *testing.T) {
g := NewGate(&GateConfig{MaxEntropy: 3.0}) // Very strict threshold
// Random-looking text with high entropy.
result := g.Check("x7#kQ9!mZ2$pW4&nR6*jL8@cF0^tB3yH5%vD1")
assert.True(t, result.IsBlocked)
assert.Contains(t, result.BlockReason, "too chaotic")
}
func TestGate_BlocksEntropyGrowth(t *testing.T) {
g := NewGate(&GateConfig{MaxEntropyGrowth: 0.1}) // Very strict
// First check: structured text.
r1 := g.Check("hello hello hello hello hello hello hello hello")
assert.True(t, r1.IsAllowed)
// Second check: much more chaotic text → entropy growth.
r2 := g.Check("x7#kQ9!mZ2$pW4&nR6*jL8@cF0^tB3yH5%vD1eG7")
assert.True(t, r2.IsBlocked)
assert.Contains(t, r2.BlockReason, "divergent recursion")
}
func TestGate_DetectsStagnation(t *testing.T) {
g := NewGate(&GateConfig{
MaxIterationsWithoutDecline: 2,
MaxEntropy: 10, // Don't block on absolute
MaxEntropyGrowth: 10, // Don't block on growth
MinTextLength: 5,
})
text := "The quick brown fox jumps over the lazy dog repeatedly"
r1 := g.Check(text) // history=[], no comparison, flat=0
assert.True(t, r1.IsAllowed, "1st check: no history yet")
r2 := g.Check(text) // flat becomes 1, 1 < 2
assert.True(t, r2.IsAllowed, "2nd check: flat=1 < threshold=2")
r3 := g.Check(text) // flat becomes 2, 2 >= 2 → BLOCK
assert.True(t, r3.IsBlocked, "3rd check: flat=2, should block. entropy=%.4f", r3.Entropy)
assert.Contains(t, r3.BlockReason, "recursive collapse")
}
func TestGate_ShortTextAllowed(t *testing.T) {
g := NewGate(nil)
result := g.Check("hi")
assert.True(t, result.IsAllowed)
assert.Equal(t, 0.0, result.Entropy) // Too short to measure
}
func TestGate_Reset(t *testing.T) {
g := NewGate(nil)
g.Check("some text for the entropy gate")
require.Len(t, g.History(), 1)
g.Reset()
assert.Empty(t, g.History())
}
func TestGate_History(t *testing.T) {
g := NewGate(nil)
g.Check("first text for entropy measurement test")
g.Check("second text for entropy measurement test two")
h := g.History()
assert.Len(t, h, 2)
// History should be immutable copy.
h[0] = 999
assert.NotEqual(t, 999.0, g.History()[0])
}
func TestAnalyzeText(t *testing.T) {
result := AnalyzeText("hello world")
assert.Greater(t, result["entropy"].(float64), 0.0)
assert.Greater(t, result["char_count"].(int), 0)
assert.Greater(t, result["unique_chars"].(int), 0)
assert.Greater(t, result["redundancy"].(float64), 0.0)
assert.Less(t, result["redundancy"].(float64), 1.0)
}
func TestGate_ProgressiveCompression_Passes(t *testing.T) {
// Simulate a healthy recursive loop where entropy decreases.
g := NewGate(nil)
texts := []string{
"Please help me write a function that processes user authentication tokens securely",
"write function processes authentication tokens securely",
"write authentication function securely",
}
for _, text := range texts {
r := g.Check(text)
assert.True(t, r.IsAllowed, "healthy compression should pass: %s", text)
}
}
func TestGate_AdversarialInjection_Blocked(t *testing.T) {
g := NewGate(&GateConfig{MaxEntropy: 4.0}) // Strict threshold
// Adversarial text with many unique special characters.
adversarial := strings.Repeat("!@#$%^&*()_+{}|:<>?", 5)
r := g.Check(adversarial)
assert.True(t, r.IsBlocked, "adversarial injection should be blocked, entropy=%.3f", r.Entropy)
}
func TestCountUniqueRunes(t *testing.T) {
assert.Equal(t, 3, countUniqueRunes("aabbcc"))
assert.Equal(t, 1, countUniqueRunes("aaaa"))
assert.Equal(t, 0, countUniqueRunes(""))
}