diff --git a/pkg/channels/base.go b/pkg/channels/base.go index 1aae47faa..6bf4a0d56 100644 --- a/pkg/channels/base.go +++ b/pkg/channels/base.go @@ -18,8 +18,8 @@ import ( ) var ( - fastIDCounter uint64 - fastIDPrefix string + uniqueIDCounter uint64 + uniqueIDPrefix string ) func init() { @@ -29,14 +29,15 @@ func init() { // fallback to time-based prefix binary.BigEndian.PutUint64(b[:], uint64(time.Now().UnixNano())) } - fastIDPrefix = hex.EncodeToString(b[:]) + uniqueIDPrefix = hex.EncodeToString(b[:]) } -// fastID generates a unique ID using a random prefix and an atomic counter. -// Much cheaper than uuid.New() which calls crypto/rand.Read on every invocation. -func fastID() string { - n := atomic.AddUint64(&fastIDCounter, 1) - return fastIDPrefix + strconv.FormatUint(n, 16) +// uniqueID generates a process-unique ID using a random prefix and an atomic counter. +// This ID is intended for internal correlation (e.g. media scope keys) and is NOT +// cryptographically secure — it must not be used in contexts where unpredictability matters. +func uniqueID() string { + n := atomic.AddUint64(&uniqueIDCounter, 1) + return uniqueIDPrefix + strconv.FormatUint(n, 16) } type Channel interface { @@ -289,7 +290,7 @@ func (c *BaseChannel) GetPlaceholderRecorder() PlaceholderRecorder { func BuildMediaScope(channel, chatID, messageID string) string { id := messageID if id == "" { - id = fastID() + id = uniqueID() } return channel + ":" + chatID + ":" + id } diff --git a/pkg/channels/split_test.go b/pkg/channels/split_test.go index d6356bdb9..a922f9558 100644 --- a/pkg/channels/split_test.go +++ b/pkg/channels/split_test.go @@ -147,6 +147,191 @@ func TestSplitMessage(t *testing.T) { } } +// --- Helper function tests for index-based rune operations --- + +func TestFindLastNewlineInRange(t *testing.T) { + runes := []rune("aaa\nbbb\nccc") + // Indices: 0123 4567 89 10 + + tests := []struct { + name string + start, end int + searchWindow int + want int + }{ + {"finds last newline in full range", 0, 11, 200, 7}, + {"finds newline within search window", 0, 11, 4, 7}, + {"narrow window misses newline outside window", 4, 11, 3, 3}, // returns start-1 (not found) + {"no newline in range", 0, 3, 200, -1}, // start-1 = -1 + {"range limited to first segment", 0, 4, 200, 3}, + {"search window of 1 at newline", 0, 8, 1, 7}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got := findLastNewlineInRange(runes, tc.start, tc.end, tc.searchWindow) + if got != tc.want { + t.Errorf("findLastNewlineInRange(runes, %d, %d, %d) = %d, want %d", + tc.start, tc.end, tc.searchWindow, got, tc.want) + } + }) + } +} + +func TestFindLastSpaceInRange(t *testing.T) { + runes := []rune("abc def\tghi") + // Indices: 0123 4567 89 10 + + tests := []struct { + name string + start, end int + searchWindow int + want int + }{ + {"finds tab as last space/tab", 0, 11, 200, 7}, + {"finds space when tab out of window", 0, 7, 200, 3}, + {"no space in range", 0, 3, 200, -1}, + {"narrow window finds tab", 5, 11, 4, 7}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got := findLastSpaceInRange(runes, tc.start, tc.end, tc.searchWindow) + if got != tc.want { + t.Errorf("findLastSpaceInRange(runes, %d, %d, %d) = %d, want %d", + tc.start, tc.end, tc.searchWindow, got, tc.want) + } + }) + } +} + +func TestFindNewlineFrom(t *testing.T) { + runes := []rune("hello\nworld\n") + + tests := []struct { + name string + from int + want int + }{ + {"from start", 0, 5}, + {"from after first newline", 6, 11}, + {"from past all newlines", 12, -1}, + {"from newline itself", 5, 5}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got := findNewlineFrom(runes, tc.from) + if got != tc.want { + t.Errorf("findNewlineFrom(runes, %d) = %d, want %d", tc.from, got, tc.want) + } + }) + } +} + +func TestFindLastUnclosedCodeBlockInRange(t *testing.T) { + tests := []struct { + name string + content string + start, end int + want int + }{ + { + name: "no code blocks", + content: "hello world", + start: 0, end: 11, + want: -1, + }, + { + name: "complete code block", + content: "```go\ncode\n```", + start: 0, end: 14, + want: -1, + }, + { + name: "unclosed code block", + content: "text\n```go\ncode here", + start: 0, end: 20, + want: 5, + }, + { + name: "closed then unclosed", + content: "```a\n```\n```b\ncode", + start: 0, end: 17, + want: 9, + }, + { + name: "search within subrange", + content: "```a\n```\n```b\ncode", + start: 9, end: 17, + want: 9, + }, + { + name: "subrange with no code blocks", + content: "```a\n```\nhello", + start: 9, end: 14, + want: -1, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + runes := []rune(tc.content) + got := findLastUnclosedCodeBlockInRange(runes, tc.start, tc.end) + if got != tc.want { + t.Errorf("findLastUnclosedCodeBlockInRange(%q, %d, %d) = %d, want %d", + tc.content, tc.start, tc.end, got, tc.want) + } + }) + } +} + +func TestFindNextClosingCodeBlockInRange(t *testing.T) { + tests := []struct { + name string + content string + startIdx int + end int + want int + }{ + { + name: "finds closing fence", + content: "code\n```\nmore", + startIdx: 0, end: 13, + want: 8, // position after ``` + }, + { + name: "no closing fence", + content: "just code here", + startIdx: 0, end: 14, + want: -1, + }, + { + name: "fence at start of search", + content: "```end", + startIdx: 0, end: 6, + want: 3, + }, + { + name: "fence outside range", + content: "code\n```", + startIdx: 0, end: 4, + want: -1, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + runes := []rune(tc.content) + got := findNextClosingCodeBlockInRange(runes, tc.startIdx, tc.end) + if got != tc.want { + t.Errorf("findNextClosingCodeBlockInRange(%q, %d, %d) = %d, want %d", + tc.content, tc.startIdx, tc.end, got, tc.want) + } + }) + } +} + func TestSplitMessage_CodeBlockIntegrity(t *testing.T) { // Focused test for the core requirement: splitting inside a code block preserves syntax highlighting