Files
meshcore-analyzer/cmd/server/discovered_channels_test.go
T
Kpa-clawbot 83881e6b71 fix(#688): auto-discover hashtag channels from message text (#1071)
## Summary

Auto-discovers previously-unknown hashtag channels by scanning decoded
channel message text for `#name` mentions and surfacing them via
`GetChannels`.

Workflow (per the issue):
1. New channel message arrives on a known channel
2. Decoded text is scanned for `#hashtag` mentions
3. Any mention that doesn't match an existing channel is surfaced as a
discovered channel (`discovered: true`, `messageCount: 0`)
4. Future traffic on that channel will populate the entry once it has
its own packets

## Changes

- `cmd/server/discovered_channels.go` — new file.
`extractHashtagsFromText` parses `#name` mentions from free text,
deduped, order-preserving. Trailing punctuation is excluded by the
character class.
- `cmd/server/store.go` — `GetChannels` now scans CHAN packet text for
hashtags after building the primary channel map, and appends any unseen
hashtag mentions as discovered entries.
- `cmd/server/discovered_channels_test.go` — new tests covering parser
edge cases (single, multi, dedup, punctuation, none, bare `#`) and
end-to-end discovery via `GetChannels`.

## TDD

- Red: `34f1817` — stub returns `nil`, both new tests fail on assertion
(verified).
- Green: `d27b3ed` — real implementation, full `cmd/server` test suite
passes (21.7s).

## Notes

- Discovered channels carry `messageCount: 0` and `lastActivity` set to
the most recent mention's `firstSeen`, so they sort naturally alongside
real channels.
- Names are matched against existing entries by both `#name` and bare
`name` so a channel that already has decoded traffic isn't
double-listed.
- The existing `channelsCache` (15s) covers the new code path; no
separate invalidation needed since the source data (`byPayloadType[5]`)
drives both maps.

Fixes #688

---------

Co-authored-by: corescope-bot <bot@corescope.local>
2026-05-05 01:16:57 -07:00

86 lines
2.2 KiB
Go

package main
import (
"reflect"
"testing"
)
// TestExtractHashtagsFromText covers the parsing helper used to discover new
// hashtag channels from decoded message text (issue #688).
func TestExtractHashtagsFromText(t *testing.T) {
cases := []struct {
name string
in string
want []string
}{
{
name: "single mention from issue body",
in: "Hey, I created new channel called #mesh, please join",
want: []string{"#mesh"},
},
{
name: "multiple mentions preserve order",
in: "join #mesh and #wardriving today",
want: []string{"#mesh", "#wardriving"},
},
{
name: "dedup repeated mentions",
in: "#x then #x again",
want: []string{"#x"},
},
{
name: "ignores trailing punctuation",
in: "check #fun!",
want: []string{"#fun"},
},
{
name: "no hashtag returns nil",
in: "nothing to see here",
want: nil,
},
{
name: "bare # is not a channel",
in: "issue #",
want: nil,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got := extractHashtagsFromText(tc.in)
if !reflect.DeepEqual(got, tc.want) {
t.Fatalf("extractHashtagsFromText(%q): got %v, want %v", tc.in, got, tc.want)
}
})
}
}
// TestGetChannels_DiscoversHashtagsFromMessages verifies that when a decoded
// CHAN message body mentions a previously-unknown hashtag channel, that
// channel is auto-registered in the GetChannels output (#688).
func TestGetChannels_DiscoversHashtagsFromMessages(t *testing.T) {
// One known channel (#general) where someone announces a new channel #mesh.
pkt := makeGrpTx(198, "general", "Alice: Hey, I created new channel called #mesh, please join", "Alice")
ps := newChannelTestStore([]*StoreTx{pkt})
channels := ps.GetChannels("")
var sawGeneral, sawMesh bool
for _, ch := range channels {
switch ch["name"] {
case "general":
sawGeneral = true
case "#mesh":
sawMesh = true
if d, _ := ch["discovered"].(bool); !d {
t.Errorf("expected discovered=true on #mesh, got %v", ch["discovered"])
}
}
}
if !sawGeneral {
t.Error("expected the source channel 'general' in GetChannels output")
}
if !sawMesh {
t.Errorf("expected discovered hashtag channel '#mesh' in GetChannels output; got %d channels: %+v", len(channels), channels)
}
}