--- phase: 10-classification-layer plan: 02 type: execute wave: 2 depends_on: - 10-01 files_modified: - classify/classifier_test.go autonomous: true requirements: - PROTO-01 - PROTO-02 - PROTO-03 - PROTO-04 - PROTO-05 - PROTO-06 - PROTO-07 - PROTO-08 - PROTO-09 must_haves: truths: - "Every new protocol port is tested and classifies to the correct TrafficClass" - "Plain/TLS port variants of the same protocol classify to the same class" - "SIP is tested on both TCP and UDP transports" - "QUIC (UDP 443) classifies as ClassQUIC, not ClassHTTPS" - "All 10 existing protocol tests still pass unchanged (PROTO-09)" - "TestAllClassesCount expects 32" artifacts: - path: "classify/classifier_test.go" provides: "26 new subtests covering all new port/protocol combinations" contains: "ClassIMAP" key_links: - from: "classify/classifier_test.go" to: "classify/types.go" via: "test assertions reference new TrafficClass constants" pattern: "classify\\.Class(IMAP|RDP|MySQL|QUIC|SIP)" - from: "classify/classifier_test.go" to: "classify/rules.go" via: "NewClassifier(DefaultRules) uses updated rules" pattern: "classify\\.DefaultRules" --- Add comprehensive test coverage for all 21 new protocol classifications and update TestAllClassesCount. Purpose: Verify every new port-matching rule in DefaultRules produces the correct TrafficClass. This is the verification gate for Phase 10 — `go test ./classify/...` must pass. Output: Updated classify/classifier_test.go with 26 new subtests and updated count assertion. @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/10-classification-layer/10-01-SUMMARY.md @classify/classifier_test.go @classify/types.go @classify/rules.go From classify/classifier_test.go: ```go func buildTCPPacket(t *testing.T, dstPort uint16) gopacket.Packet func buildUDPPacket(t *testing.T, dstPort uint16) gopacket.Packet func buildICMPPacket(t *testing.T) gopacket.Packet // Existing TestClassify subtests follow pattern: t.Run("TestClassifyHTTPS", func(t *testing.T) { pkt := buildTCPPacket(t, 443) got := c.Classify(pkt) if got.Class != classify.ClassHTTPS { t.Errorf("HTTPS packet: got class %q, want %q", got.Class, classify.ClassHTTPS) } }) ``` From classify/types.go (after Plan 01): ```go // New constants available: ClassIMAP, ClassPOP3, ClassSMTPSub, ClassFTP, ClassSMB, // ClassTFTP, ClassRDP, ClassTelnet, ClassVNC, ClassMySQL, ClassPostgreSQL, ClassRedis, // ClassMongoDB, ClassMDNS, ClassSSDP, ClassSNMP, ClassSIP, ClassQUIC, // ClassLDAP, ClassKerberos, ClassSyslog ``` Task 1: Add 26 new classification subtests to TestClassify classify/classifier_test.go - classify/classifier_test.go (existing test structure and helpers) - classify/types.go (verify new constants from Plan 01) - classify/rules.go (verify new rules from Plan 01) Add the following subtests inside the existing `TestClassify` function, after the existing subtests and before the `TestRulesAreOrderDependent` subtest. Use the same pattern as existing subtests: `buildTCPPacket` or `buildUDPPacket` with the port, then assert `got.Class` matches the expected constant. Complete list of 26 new subtests to add: ```go // --- Mail (PROTO-01) --- t.Run("TestClassifyIMAP_port143", func(t *testing.T) { pkt := buildTCPPacket(t, 143) got := c.Classify(pkt) if got.Class != classify.ClassIMAP { t.Errorf("IMAP port 143: got class %q, want %q", got.Class, classify.ClassIMAP) } }) t.Run("TestClassifyIMAP_port993", func(t *testing.T) { pkt := buildTCPPacket(t, 993) got := c.Classify(pkt) if got.Class != classify.ClassIMAP { t.Errorf("IMAPS port 993: got class %q, want %q", got.Class, classify.ClassIMAP) } }) t.Run("TestClassifyPOP3_port110", func(t *testing.T) { pkt := buildTCPPacket(t, 110) got := c.Classify(pkt) if got.Class != classify.ClassPOP3 { t.Errorf("POP3 port 110: got class %q, want %q", got.Class, classify.ClassPOP3) } }) t.Run("TestClassifyPOP3_port995", func(t *testing.T) { pkt := buildTCPPacket(t, 995) got := c.Classify(pkt) if got.Class != classify.ClassPOP3 { t.Errorf("POP3S port 995: got class %q, want %q", got.Class, classify.ClassPOP3) } }) t.Run("TestClassifySMTPSub_port587", func(t *testing.T) { pkt := buildTCPPacket(t, 587) got := c.Classify(pkt) if got.Class != classify.ClassSMTPSub { t.Errorf("SMTP-sub port 587: got class %q, want %q", got.Class, classify.ClassSMTPSub) } }) // --- File Transfer (PROTO-02) --- t.Run("TestClassifyFTP_port20", func(t *testing.T) { pkt := buildTCPPacket(t, 20) got := c.Classify(pkt) if got.Class != classify.ClassFTP { t.Errorf("FTP data port 20: got class %q, want %q", got.Class, classify.ClassFTP) } }) t.Run("TestClassifyFTP_port21", func(t *testing.T) { pkt := buildTCPPacket(t, 21) got := c.Classify(pkt) if got.Class != classify.ClassFTP { t.Errorf("FTP control port 21: got class %q, want %q", got.Class, classify.ClassFTP) } }) t.Run("TestClassifySMB_port445", func(t *testing.T) { pkt := buildTCPPacket(t, 445) got := c.Classify(pkt) if got.Class != classify.ClassSMB { t.Errorf("SMB port 445: got class %q, want %q", got.Class, classify.ClassSMB) } }) t.Run("TestClassifyTFTP_port69", func(t *testing.T) { pkt := buildUDPPacket(t, 69) got := c.Classify(pkt) if got.Class != classify.ClassTFTP { t.Errorf("TFTP port 69: got class %q, want %q", got.Class, classify.ClassTFTP) } }) // --- Remote Access (PROTO-03) --- t.Run("TestClassifyRDP_port3389", func(t *testing.T) { pkt := buildTCPPacket(t, 3389) got := c.Classify(pkt) if got.Class != classify.ClassRDP { t.Errorf("RDP port 3389: got class %q, want %q", got.Class, classify.ClassRDP) } }) t.Run("TestClassifyTelnet_port23", func(t *testing.T) { pkt := buildTCPPacket(t, 23) got := c.Classify(pkt) if got.Class != classify.ClassTelnet { t.Errorf("Telnet port 23: got class %q, want %q", got.Class, classify.ClassTelnet) } }) t.Run("TestClassifyVNC_port5900", func(t *testing.T) { pkt := buildTCPPacket(t, 5900) got := c.Classify(pkt) if got.Class != classify.ClassVNC { t.Errorf("VNC port 5900: got class %q, want %q", got.Class, classify.ClassVNC) } }) // --- Database (PROTO-04) --- t.Run("TestClassifyMySQL_port3306", func(t *testing.T) { pkt := buildTCPPacket(t, 3306) got := c.Classify(pkt) if got.Class != classify.ClassMySQL { t.Errorf("MySQL port 3306: got class %q, want %q", got.Class, classify.ClassMySQL) } }) t.Run("TestClassifyPostgreSQL_port5432", func(t *testing.T) { pkt := buildTCPPacket(t, 5432) got := c.Classify(pkt) if got.Class != classify.ClassPostgreSQL { t.Errorf("PostgreSQL port 5432: got class %q, want %q", got.Class, classify.ClassPostgreSQL) } }) t.Run("TestClassifyRedis_port6379", func(t *testing.T) { pkt := buildTCPPacket(t, 6379) got := c.Classify(pkt) if got.Class != classify.ClassRedis { t.Errorf("Redis port 6379: got class %q, want %q", got.Class, classify.ClassRedis) } }) t.Run("TestClassifyMongoDB_port27017", func(t *testing.T) { pkt := buildTCPPacket(t, 27017) got := c.Classify(pkt) if got.Class != classify.ClassMongoDB { t.Errorf("MongoDB port 27017: got class %q, want %q", got.Class, classify.ClassMongoDB) } }) // --- Discovery (PROTO-05) --- t.Run("TestClassifyMDNS_port5353", func(t *testing.T) { pkt := buildUDPPacket(t, 5353) got := c.Classify(pkt) if got.Class != classify.ClassMDNS { t.Errorf("mDNS port 5353: got class %q, want %q", got.Class, classify.ClassMDNS) } }) t.Run("TestClassifySDP_port1900", func(t *testing.T) { pkt := buildUDPPacket(t, 1900) got := c.Classify(pkt) if got.Class != classify.ClassSSDP { t.Errorf("SSDP port 1900: got class %q, want %q", got.Class, classify.ClassSSDP) } }) t.Run("TestClassifySNMP_port161", func(t *testing.T) { pkt := buildUDPPacket(t, 161) got := c.Classify(pkt) if got.Class != classify.ClassSNMP { t.Errorf("SNMP port 161: got class %q, want %q", got.Class, classify.ClassSNMP) } }) t.Run("TestClassifySNMP_port162", func(t *testing.T) { pkt := buildUDPPacket(t, 162) got := c.Classify(pkt) if got.Class != classify.ClassSNMP { t.Errorf("SNMP-trap port 162: got class %q, want %q", got.Class, classify.ClassSNMP) } }) // --- VoIP (PROTO-06) --- SIP runs on both TCP and UDP t.Run("TestClassifySIP_TCP5060", func(t *testing.T) { pkt := buildTCPPacket(t, 5060) got := c.Classify(pkt) if got.Class != classify.ClassSIP { t.Errorf("SIP TCP 5060: got class %q, want %q", got.Class, classify.ClassSIP) } }) t.Run("TestClassifySIP_UDP5060", func(t *testing.T) { pkt := buildUDPPacket(t, 5060) got := c.Classify(pkt) if got.Class != classify.ClassSIP { t.Errorf("SIP UDP 5060: got class %q, want %q", got.Class, classify.ClassSIP) } }) // --- Web extension (PROTO-07) --- QUIC is UDP 443, must NOT match HTTPS (TCP 443) t.Run("TestClassifyQUIC_UDP443", func(t *testing.T) { pkt := buildUDPPacket(t, 443) got := c.Classify(pkt) if got.Class != classify.ClassQUIC { t.Errorf("QUIC UDP 443: got class %q, want %q", got.Class, classify.ClassQUIC) } }) // --- Infrastructure extension (PROTO-08) --- t.Run("TestClassifyLDAP_port389", func(t *testing.T) { pkt := buildTCPPacket(t, 389) got := c.Classify(pkt) if got.Class != classify.ClassLDAP { t.Errorf("LDAP port 389: got class %q, want %q", got.Class, classify.ClassLDAP) } }) t.Run("TestClassifyLDAP_port636", func(t *testing.T) { pkt := buildTCPPacket(t, 636) got := c.Classify(pkt) if got.Class != classify.ClassLDAP { t.Errorf("LDAPS port 636: got class %q, want %q", got.Class, classify.ClassLDAP) } }) t.Run("TestClassifyKerberos_TCP88", func(t *testing.T) { pkt := buildTCPPacket(t, 88) got := c.Classify(pkt) if got.Class != classify.ClassKerberos { t.Errorf("Kerberos TCP 88: got class %q, want %q", got.Class, classify.ClassKerberos) } }) t.Run("TestClassifyKerberos_UDP88", func(t *testing.T) { pkt := buildUDPPacket(t, 88) got := c.Classify(pkt) if got.Class != classify.ClassKerberos { t.Errorf("Kerberos UDP 88: got class %q, want %q", got.Class, classify.ClassKerberos) } }) t.Run("TestClassifySyslog_port514", func(t *testing.T) { pkt := buildUDPPacket(t, 514) got := c.Classify(pkt) if got.Class != classify.ClassSyslog { t.Errorf("Syslog port 514: got class %q, want %q", got.Class, classify.ClassSyslog) } }) ``` That is 28 subtests (not 26 — the PROTO-06 SIP tests cover 2 extra transport variants). Add them inside `TestClassify`, after the existing `TestClassifyUnknown` subtest and before `TestRulesAreOrderDependent`. cd /home/dev/workspace/yoloyolo && go test ./classify/... -run TestClassify -v 2>&1 | tail -40 - classifier_test.go contains `TestClassifyIMAP_port143` - classifier_test.go contains `TestClassifyIMAP_port993` - classifier_test.go contains `TestClassifyPOP3_port110` - classifier_test.go contains `TestClassifyPOP3_port995` - classifier_test.go contains `TestClassifySMTPSub_port587` - classifier_test.go contains `TestClassifyFTP_port20` - classifier_test.go contains `TestClassifyFTP_port21` - classifier_test.go contains `TestClassifySMB_port445` - classifier_test.go contains `TestClassifyTFTP_port69` - classifier_test.go contains `TestClassifyRDP_port3389` - classifier_test.go contains `TestClassifyTelnet_port23` - classifier_test.go contains `TestClassifyVNC_port5900` - classifier_test.go contains `TestClassifyMySQL_port3306` - classifier_test.go contains `TestClassifyPostgreSQL_port5432` - classifier_test.go contains `TestClassifyRedis_port6379` - classifier_test.go contains `TestClassifyMongoDB_port27017` - classifier_test.go contains `TestClassifyMDNS_port5353` - classifier_test.go contains `TestClassifySNMP_port161` - classifier_test.go contains `TestClassifySNMP_port162` - classifier_test.go contains `TestClassifySIP_TCP5060` - classifier_test.go contains `TestClassifySIP_UDP5060` - classifier_test.go contains `TestClassifyQUIC_UDP443` - classifier_test.go contains `TestClassifyLDAP_port389` - classifier_test.go contains `TestClassifyLDAP_port636` - classifier_test.go contains `TestClassifyKerberos_TCP88` - classifier_test.go contains `TestClassifyKerberos_UDP88` - classifier_test.go contains `TestClassifySyslog_port514` - `go test ./classify/... -run TestClassify` passes (exit 0) 28 new subtests added covering all new port/protocol/transport combinations, all pass Task 2: Update TestAllClassesCount from 14 to 32 classify/classifier_test.go - classify/classifier_test.go (current TestAllClassesCount with hardcoded 14) In `TestAllClassesCount`, change the hardcoded assertion from `!= 14` to `!= 32`. The line: ```go if len(classes) != 14 { t.Errorf("AllClasses() returned %d classes, want 14", len(classes)) } ``` Becomes: ```go if len(classes) != 32 { t.Errorf("AllClasses() returned %d classes, want 32", len(classes)) } ``` The count is 32 = 14 existing + 18 new (LDAP, Kerberos, Syslog excluded from AllClasses per D-01/D-02). This will become 35 in Phase 11 when those three are added. cd /home/dev/workspace/yoloyolo && go test ./classify/... -run TestAllClassesCount -v - classifier_test.go contains `!= 32` (not `!= 14`) - classifier_test.go contains `want 32` (not `want 14`) - `go test ./classify/... -run TestAllClassesCount` passes (exit 0) TestAllClassesCount asserts 32 classes, test passes - `go test ./classify/... -v` — all tests pass (existing + 28 new subtests + updated count) - `go test ./classify/... -count=1` — no cached results, clean pass - Existing tests (ICMP, DNS, HTTPS, HTTP, SSH, SMTP, NTP, DHCP, OtherTCP, OtherUDP, Unknown) still pass (PROTO-09) - Note: `go test ./synth/...` is EXPECTED TO FAIL after Phase 10 because AllClasses() now has 32 entries but ClassFreqConfigs only has 14 entries. This is intentional — Phase 11 will add the missing 18 ClassFreqConfigs entries. The phase gate is `go test ./classify/...` only. - `go test ./classify/...` passes with 0 failures - 28 new subtests verify every new port/protocol combination - TestAllClassesCount expects 32 - All existing 10 protocol tests pass unchanged (PROTO-09 regression check) After completion, create `.planning/phases/10-classification-layer/10-02-SUMMARY.md`