feat(03-01): extend TrafficClass to 14 classes with hash-bucketed unknowns

- Replace ClassUnknown with ClassUnknown1-4 (unknown-1 through unknown-4)
- AllClasses() now returns 14 elements
- Add hashBucket() function routing unrecognized traffic deterministically
- Update synth/config.go: NumLayers=14, 4 dissonant unknown tone configs at 862-1047 Hz
- Add TestAllClassesCount and TestHashBucketDistribution tests
- Add TestClassFreqConfigsComplete and TestNumLayersMatchesAllClasses to config_test.go
- Update TestNewBankHas14Layers, TestMixerNoClip for 14 classes
- All classify and synth tests pass
This commit is contained in:
2026-03-26 13:11:24 +01:00
parent c8536dac94
commit ff3ec7eb78
6 changed files with 127 additions and 19 deletions
+58 -2
View File
@@ -1,6 +1,7 @@
package classify_test
import (
"strings"
"testing"
"github.com/gopacket/gopacket"
@@ -220,8 +221,8 @@ func TestClassify(t *testing.T) {
t.Run("TestClassifyUnknown", func(t *testing.T) {
pkt := buildUnknownPacket(t)
got := c.Classify(pkt)
if got.Class != classify.ClassUnknown {
t.Errorf("Unknown packet: got class %q, want %q", got.Class, classify.ClassUnknown)
if !strings.HasPrefix(string(got.Class), "unknown-") {
t.Errorf("Unknown packet: got class %q, want unknown-N bucket", got.Class)
}
})
@@ -241,3 +242,58 @@ func TestClassify(t *testing.T) {
}
})
}
func TestAllClassesCount(t *testing.T) {
classes := classify.AllClasses()
if len(classes) != 14 {
t.Errorf("AllClasses() returned %d classes, want 14", len(classes))
}
// No plain "unknown" should exist
for _, c := range classes {
if c == "unknown" {
t.Error("AllClasses() still contains plain \"unknown\" — should be removed per D-04")
}
}
// All 4 buckets must be present
buckets := map[classify.TrafficClass]bool{
classify.ClassUnknown1: false, classify.ClassUnknown2: false,
classify.ClassUnknown3: false, classify.ClassUnknown4: false,
}
for _, c := range classes {
if _, ok := buckets[c]; ok {
buckets[c] = true
}
}
for bucket, found := range buckets {
if !found {
t.Errorf("AllClasses() missing bucket %q", bucket)
}
}
}
func TestHashBucketDistribution(t *testing.T) {
// hashBucket is unexported, test through Classify with a minimal rule set
// that has no catch-all TCP/UDP rules, so unmatched ports reach hashBucket.
minimalRules := []classify.Rule{
{Protocol: "icmp", DstPort: 0, Class: classify.ClassICMP},
{Protocol: "tcp", DstPort: 443, Class: classify.ClassHTTPS},
}
c := classify.NewClassifier(minimalRules)
seen := make(map[classify.TrafficClass]bool)
// Try a range of unmatched TCP ports to hit all 4 buckets
for port := uint16(10000); port < 11000; port++ {
pkt := buildTCPPacket(t, port)
got := c.Classify(pkt)
if strings.HasPrefix(string(got.Class), "unknown-") {
seen[got.Class] = true
}
}
for _, bucket := range []classify.TrafficClass{
classify.ClassUnknown1, classify.ClassUnknown2,
classify.ClassUnknown3, classify.ClassUnknown4,
} {
if !seen[bucket] {
t.Errorf("hashBucket never produced %q across ports 10000-10999", bucket)
}
}
}