Files
Memoh/internal/settings/service_test.go
T
Acbox 473d559042 feat(channel): structured tool-call IM display with edit-in-place
Introduce a new `show_tool_calls_in_im` bot setting plus a full overhaul of
how tool calls are surfaced in IM channels:

- Add per-bot setting + migration (0072) and expose through settings API /
  handlers / frontend SDK.
- Introduce a `toolCallDroppingStream` wrapper that filters tool_call_* events
  when the setting is off, keeping the rest of the stream intact.
- Add a shared `ToolCallPresentation` model (Header / Body blocks / Footer)
  with plain and Markdown renderers, and a per-tool formatter registry that
  produces rich output (e.g. `web_search` link lists, `list` directory
  previews, `exec` stdout/stderr tails) instead of raw JSON dumps.
- High-capability adapters (Telegram, Feishu, Matrix, Slack, Discord) now
  flush pre-text and then send ONE tool-call message per call, editing it
  in-place from `running` to `completed` / `failed`; mapping from callID to
  platform message ID is tracked per stream, with a fallback to a new
  message if the edit fails. Low-capability adapters (WeCom, QQ, DingTalk)
  keep posting a single final message, but now benefit from the same rich
  per-tool formatting.
- Suppress the early duplicate `EventToolCallStart` (from
  `sdk.ToolInputStartPart`) so that the SDK's final `StreamToolCallPart`
  remains the single source of truth for tool call start, preventing
  duplicated "running" bubbles in IM.
- Stop auto-populating `InputSummary` / `ResultSummary` after a per-tool
  formatter runs, which previously leaked the raw JSON result as a
  fallback footer underneath the formatted body.

Add regression tests for the formatters, the Markdown renderer, the
edit-in-place flow on Telegram/Matrix, and the JSON-leak guard on `list`.
2026-04-23 20:49:44 +08:00

70 lines
1.8 KiB
Go

package settings
import (
"testing"
"github.com/memohai/memoh/internal/db/sqlc"
)
func TestNormalizeBotSettingsReadRow_ShowToolCallsInIMDefault(t *testing.T) {
t.Parallel()
row := sqlc.GetSettingsByBotIDRow{
Language: "en",
ReasoningEnabled: false,
ReasoningEffort: "medium",
HeartbeatEnabled: false,
HeartbeatInterval: 60,
CompactionEnabled: false,
CompactionThreshold: 0,
CompactionRatio: 80,
ShowToolCallsInIm: false,
}
got := normalizeBotSettingsReadRow(row)
if got.ShowToolCallsInIM {
t.Fatalf("expected default ShowToolCallsInIM=false, got true")
}
}
func TestNormalizeBotSettingsReadRow_ShowToolCallsInIMPropagates(t *testing.T) {
t.Parallel()
row := sqlc.GetSettingsByBotIDRow{
Language: "en",
ReasoningEffort: "medium",
HeartbeatInterval: 60,
CompactionRatio: 80,
ShowToolCallsInIm: true,
}
got := normalizeBotSettingsReadRow(row)
if !got.ShowToolCallsInIM {
t.Fatalf("expected ShowToolCallsInIM=true to propagate from row")
}
}
func TestUpsertRequestShowToolCallsInIM_PointerSemantics(t *testing.T) {
t.Parallel()
// When the field is nil, the UpsertRequest should not touch the current
// setting. When non-nil, the dereferenced value should win. We exercise
// the small gate block without hitting the database.
current := Settings{ShowToolCallsInIM: true}
var req UpsertRequest
if req.ShowToolCallsInIM != nil {
current.ShowToolCallsInIM = *req.ShowToolCallsInIM
}
if !current.ShowToolCallsInIM {
t.Fatalf("nil pointer must leave current value unchanged")
}
off := false
req.ShowToolCallsInIM = &off
if req.ShowToolCallsInIM != nil {
current.ShowToolCallsInIM = *req.ShowToolCallsInIM
}
if current.ShowToolCallsInIM {
t.Fatalf("explicit false pointer must clear the flag")
}
}