mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-27 07:16:19 +09:00
6acdd191c7
commit bcdb026ae43e4f95d0b2c4f9bd440a2df9d6b514 Author: Ran <16112591+chen-ran@users.noreply.github.com> Date: Thu Feb 12 17:10:32 2026 +0800 chore: update DEVELOPMENT.md commit30281742efMerge:ca5c6a15b05f13Author: BBQ <bbq@BBQdeMacBook-Air.local> Date: Thu Feb 12 15:49:17 2026 +0800 merge(github/main): integrate fx dependency injection framework Merge upstream fx refactor and adapt all services to use go.uber.org/fx for dependency injection. Resolve conflicts in main.go, server.go, and service constructors while preserving our domain model changes. - Fix telegram adapter panic on shutdown (double close channel) - Fix feishu adapter processing messages after stop - Increase directory lookup timeout from 2s to 5s commitca5c6a1866Author: BBQ <bbq@BBQdeMacBook-Air.local> Date: Thu Feb 12 15:33:09 2026 +0800 refactor(core): restructure conversation, channel and message domains - Rename chat module to conversation with flow-based architecture - Move channelidentities into channel/identities subpackage - Add channel/route for routing logic - Add message service with event hub - Add MCP providers: container, directory, schedule - Refactor Feishu/Telegram adapters with directory and stream support - Add platform management page and channel badges in web UI - Update database schema for conversations, messages and channel routes - Add @memoh/shared package for cross-package type definitions commit75e2ef0467Merge:d99ba3801cb6c8Author: BBQ <bbq@BBQdeMacBook-Air.local> Date: Thu Feb 12 14:45:49 2026 +0800 merge(github): merge github/main, resolve index.ts URL conflict Keep our defensive absolute-URL check in createAuthFetcher. commitd99ba38b7dMerge:860e20f35ce7d1Author: BBQ <bbq@BBQdeMacBook-Air.local> Date: Thu Feb 12 05:20:18 2026 +0800 merge(github): merge github/main, keep our code and docs/spec commit860e20fe70Author: BBQ <bbq@BBQdeMacBook-Air.local> Date: Wed Feb 11 22:13:27 2026 +0800 docs(docs): add concepts and style guides for VitePress site - Add concepts: identity-and-binding, index (en/zh) - Add style: terminology (en/zh) - Update index and zh/index - Update .vitepress/config.ts commita75fdb8040Author: BBQ <bbq@BBQdeMacBook-Air.local> Date: Wed Feb 11 17:37:16 2026 +0800 refactor(mcp): standardize unified tool gateway on go-sdk Split business executors from federation sources and migrate unified tool/federation transports to the official go-sdk for stricter MCP compliance and safer session lifecycle handling. Add targeted regression tests for accept compatibility, initialization retries, pending cleanup, and include updated swagger artifacts. commit02b33c8e85Author: BBQ <bbq@BBQdeMacBook-Air.local> Date: Wed Feb 11 15:42:21 2026 +0800 refactor(core): finalize user-centric identity and policy cleanup Unify auth and chat identity semantics around user_id, enforce personal-bot owner-only authorization, and remove legacy compatibility branches in integration tests. commit06e8619a37Author: BBQ <bbq@BBQdeMacBook-Air.local> Date: Wed Feb 11 14:47:03 2026 +0800 refactor(core): migrate channel identity and binding across app Align channel identity and bind flow across backend and app-facing layers, including generated swagger artifacts and package lock updates while excluding docs content changes.
210 lines
5.6 KiB
Go
210 lines
5.6 KiB
Go
package telegram
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
|
|
|
"github.com/memohai/memoh/internal/channel"
|
|
)
|
|
|
|
func TestMarkdownToTelegramHTML(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
contains []string
|
|
absent []string
|
|
}{
|
|
{
|
|
name: "bold",
|
|
input: "this is **bold** text",
|
|
contains: []string{"<b>bold</b>"},
|
|
absent: []string{"**"},
|
|
},
|
|
{
|
|
name: "italic",
|
|
input: "this is *italic* text",
|
|
contains: []string{"<i>italic</i>"},
|
|
},
|
|
{
|
|
name: "bold and italic",
|
|
input: "**bold** and *italic*",
|
|
contains: []string{"<b>bold</b>", "<i>italic</i>"},
|
|
},
|
|
{
|
|
name: "strikethrough",
|
|
input: "this is ~~deleted~~ text",
|
|
contains: []string{"<s>deleted</s>"},
|
|
absent: []string{"~~"},
|
|
},
|
|
{
|
|
name: "inline code",
|
|
input: "use `fmt.Println` here",
|
|
contains: []string{"<code>fmt.Println</code>"},
|
|
absent: []string{"`fmt.Println`"},
|
|
},
|
|
{
|
|
name: "link",
|
|
input: "visit [Google](https://google.com)",
|
|
contains: []string{`<a href="https://google.com">Google</a>`},
|
|
},
|
|
{
|
|
name: "heading",
|
|
input: "# Title\nsome text",
|
|
contains: []string{"<b>Title</b>"},
|
|
absent: []string{"# Title"},
|
|
},
|
|
{
|
|
name: "unordered list",
|
|
input: "- first\n- second\n+ third",
|
|
contains: []string{"• first", "• second", "• third"},
|
|
},
|
|
{
|
|
name: "fenced code block",
|
|
input: "```go\nfmt.Println(\"hello\")\n```",
|
|
contains: []string{`<pre><code class="language-go">`, "fmt.Println", "</code></pre>"},
|
|
},
|
|
{
|
|
name: "fenced code block without language",
|
|
input: "```\nplain code\n```",
|
|
contains: []string{"<pre>plain code</pre>"},
|
|
},
|
|
{
|
|
name: "html entities escaped",
|
|
input: "a < b && c > d",
|
|
contains: []string{"<", "&&", ">"},
|
|
absent: []string{"< b", "> d"},
|
|
},
|
|
{
|
|
name: "code block preserves content",
|
|
input: "```\n**not bold** <tag>\n```",
|
|
contains: []string{"**not bold**", "<tag>"},
|
|
absent: []string{"<b>", "<tag>"},
|
|
},
|
|
{
|
|
name: "inline code preserves content",
|
|
input: "use `**not bold**` inline",
|
|
contains: []string{"<code>**not bold**</code>"},
|
|
absent: []string{"<b>"},
|
|
},
|
|
{
|
|
name: "blockquote",
|
|
input: "> quoted line\n> another line",
|
|
contains: []string{"<blockquote>"},
|
|
},
|
|
{
|
|
name: "empty input",
|
|
input: "",
|
|
contains: nil,
|
|
},
|
|
{
|
|
name: "plain text no conversion",
|
|
input: "just plain text here",
|
|
contains: []string{"just plain text here"},
|
|
},
|
|
{
|
|
name: "link with ampersand in url",
|
|
input: "[search](https://example.com?a=1&b=2)",
|
|
contains: []string{`<a href="https://example.com?a=1&b=2">search</a>`},
|
|
},
|
|
{
|
|
name: "bold inside link",
|
|
input: "**[click here](https://example.com)**",
|
|
contains: []string{"<b>", `<a href="https://example.com">click here</a>`, "</b>"},
|
|
},
|
|
{
|
|
name: "mixed formatting",
|
|
input: "# Summary\n\n**Hello** world, visit [docs](https://docs.io).\n\n- item one\n- item two\n\n```python\nprint(\"hi\")\n```",
|
|
contains: []string{"<b>Summary</b>", "<b>Hello</b>", `<a href="https://docs.io">docs</a>`, "• item one", `class="language-python"`},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := markdownToTelegramHTML(tt.input)
|
|
for _, want := range tt.contains {
|
|
if !strings.Contains(result, want) {
|
|
t.Errorf("expected result to contain %q, got:\n%s", want, result)
|
|
}
|
|
}
|
|
for _, absent := range tt.absent {
|
|
if strings.Contains(result, absent) {
|
|
t.Errorf("expected result NOT to contain %q, got:\n%s", absent, result)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFormatTelegramOutput(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
text string
|
|
format channel.MessageFormat
|
|
wantMode string
|
|
wantContains string
|
|
}{
|
|
{
|
|
name: "markdown format returns html mode",
|
|
text: "**bold**",
|
|
format: channel.MessageFormatMarkdown,
|
|
wantMode: tgbotapi.ModeHTML,
|
|
wantContains: "<b>bold</b>",
|
|
},
|
|
{
|
|
name: "plain format returns empty mode",
|
|
text: "hello",
|
|
format: channel.MessageFormatPlain,
|
|
wantMode: "",
|
|
},
|
|
{
|
|
name: "empty text returns empty mode",
|
|
text: "",
|
|
format: channel.MessageFormatMarkdown,
|
|
wantMode: "",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
text, mode := formatTelegramOutput(tt.text, tt.format)
|
|
if mode != tt.wantMode {
|
|
t.Errorf("expected mode %q, got %q", tt.wantMode, mode)
|
|
}
|
|
if tt.wantContains != "" && !strings.Contains(text, tt.wantContains) {
|
|
t.Errorf("expected text to contain %q, got %q", tt.wantContains, text)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSplitCodeBlocks(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
want int // expected number of segments
|
|
}{
|
|
{name: "no code blocks", input: "hello world", want: 1},
|
|
{name: "one code block", input: "before```code```after", want: 3},
|
|
{name: "two code blocks", input: "a```b```c```d```e", want: 5},
|
|
{name: "unclosed code block", input: "before```unclosed", want: 1},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
segments := splitCodeBlocks(tt.input)
|
|
if len(segments) != tt.want {
|
|
t.Errorf("expected %d segments, got %d: %v", tt.want, len(segments), segments)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestTelegramEscapeHTML(t *testing.T) {
|
|
input := `a < b & c > d`
|
|
result := telegramEscapeHTML(input)
|
|
if result != "a < b & c > d" {
|
|
t.Errorf("unexpected escape result: %s", result)
|
|
}
|
|
}
|