mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-06 14:00:09 +02:00
* fix: handle square brackets in agent names for mention parsing (#1991) The mention regex used [^\]]* to match labels, which broke when agent names contained square brackets (e.g. David[TF]). The ] inside the name caused the regex to stop matching prematurely, silently dropping the mention. Changes: - Backend (mention.go): Switch to .+? (non-greedy) anchored on ](mention:// to correctly match labels with brackets - Frontend (mention-extension.ts): Same regex fix in tokenizer, plus escape [ and ] in renderMarkdown to prevent creating ambiguous markdown syntax - Add comprehensive tests for ParseMentions covering bracket names Fixes #1991 * fix: add optional chaining for match group access Fixes TS2532: Object is possibly 'undefined' on match[1] when calling .replace() in the mention tokenizer. * fix: tighten mention tokenizer to reject ordinary Markdown links - Replace .+? with (?:\\.|[^\]])+ in start() and tokenize() regexes so the label cannot cross a ]( Markdown link boundary - Escaped brackets (\[ \]) from renderMarkdown() are still accepted - Add frontend tokenizer/serializer round-trip tests: - Plain mention - Escaped brackets (David[TF]) round-trip - Normal Markdown link + mention on same line (regression) - Multiple links before mention - Nested brackets (Bot[v2][beta]) - Issue mentions without @ prefix Addresses review feedback on #1992. * fix: add type assertions for tiptap MarkdownTokenizer interface in tests The tiptap MarkdownTokenizer type allows start to be string | function and tokenize to accept 3 arguments. Our extension always provides single-arg functions, so cast them for TypeScript satisfaction. Fixes CI typecheck failure in @multica/views package. * fix: cast renderMarkdown to single-arg shape and reset file modes to 0644
102 lines
3.3 KiB
Go
102 lines
3.3 KiB
Go
package util
|
|
|
|
import (
|
|
"testing"
|
|
)
|
|
|
|
func TestParseMentions(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
content string
|
|
want []Mention
|
|
}{
|
|
{
|
|
name: "simple agent mention",
|
|
content: "[@Agent](mention://agent/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa) please fix",
|
|
want: []Mention{{Type: "agent", ID: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"}},
|
|
},
|
|
{
|
|
name: "agent name with square brackets",
|
|
content: "[@David[TF]](mention://agent/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa) please fix",
|
|
want: []Mention{{Type: "agent", ID: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"}},
|
|
},
|
|
{
|
|
name: "agent name with nested brackets",
|
|
content: "[@Bot[v2][beta]](mention://agent/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa) help",
|
|
want: []Mention{{Type: "agent", ID: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"}},
|
|
},
|
|
{
|
|
name: "multiple mentions with brackets",
|
|
content: "[@A[1]](mention://agent/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa) and [@B[2]](mention://agent/bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb)",
|
|
want: []Mention{
|
|
{Type: "agent", ID: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"},
|
|
{Type: "agent", ID: "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"},
|
|
},
|
|
},
|
|
{
|
|
name: "issue mention without @",
|
|
content: "[MUL-123](mention://issue/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa) is related",
|
|
want: []Mention{{Type: "issue", ID: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"}},
|
|
},
|
|
{
|
|
name: "member mention",
|
|
content: "[@Bob](mention://member/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa) look",
|
|
want: []Mention{{Type: "member", ID: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"}},
|
|
},
|
|
{
|
|
name: "all mention",
|
|
content: "[@All](mention://all/all) heads up",
|
|
want: []Mention{{Type: "all", ID: "all"}},
|
|
},
|
|
{
|
|
name: "deduplicate same mention",
|
|
content: "[@A](mention://agent/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa) and again [@A](mention://agent/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa)",
|
|
want: []Mention{{Type: "agent", ID: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"}},
|
|
},
|
|
{
|
|
name: "no mentions",
|
|
content: "just a plain comment",
|
|
want: nil,
|
|
},
|
|
{
|
|
name: "escaped brackets in label",
|
|
content: `[@David\[TF\]](mention://agent/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa) hi`,
|
|
want: []Mention{{Type: "agent", ID: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"}},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := ParseMentions(tt.content)
|
|
if len(got) != len(tt.want) {
|
|
t.Fatalf("ParseMentions() returned %d mentions, want %d\ngot: %+v\nwant: %+v", len(got), len(tt.want), got, tt.want)
|
|
}
|
|
for i := range got {
|
|
if got[i].Type != tt.want[i].Type || got[i].ID != tt.want[i].ID {
|
|
t.Errorf("mention[%d] = %+v, want %+v", i, got[i], tt.want[i])
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestHasMentionAll(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
mentions []Mention
|
|
want bool
|
|
}{
|
|
{"empty", nil, false},
|
|
{"no all", []Mention{{Type: "agent", ID: "x"}}, false},
|
|
{"has all", []Mention{{Type: "all", ID: "all"}}, true},
|
|
{"mixed", []Mention{{Type: "agent", ID: "x"}, {Type: "all", ID: "all"}}, true},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if got := HasMentionAll(tt.mentions); got != tt.want {
|
|
t.Errorf("HasMentionAll() = %v, want %v", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|