mirror of
https://github.com/chenhg5/cc-connect.git
synced 2026-10-25 17:36:43 +08:00
fix(core): suppress ellipsis-only events and handle context indicator in footer
- Skip ellipsis-only thinking/text events that add no content - When replyFooterEnabled, render context indicator inside footer instead of appending separately - Fix duplicate side channel text when context indicator is present - Add regression tests for duplicate suppression with context indicator Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
1 parent
099f7bdf26
commit
34af331a89
5 files changed
+283
-39
No files matched your search
@@ -139,3 +139,5 @@ Desktop.ini
|
||||
*.pfx
|
||||
secrets/
|
||||
credentials/
|
||||
|
||||
.codex
|
||||
@@ -134,7 +134,7 @@ test-release: pre-test
|
||||
test-release-local:
|
||||
go test ./tests/release_local/...
|
||||
go test ./config
|
||||
go test ./core -run 'TestEngineSendToSessionWithAttachments|TestCmdList_AllSessionsVisibleAfterRepeatedNew|TestCmdList_SessionVisibleDuringAgentProcessing|TestEngine_Alias|TestEngine_BannedWords|TestEngine_DisabledCommands'
|
||||
go test ./core -run 'TestEngineSendToSessionWithAttachments|TestProcessInteractiveEvents_SuppressesDuplicateSideChannelText|TestCmdList_AllSessionsVisibleAfterRepeatedNew|TestCmdList_SessionVisibleDuringAgentProcessing|TestEngine_Alias|TestEngine_BannedWords|TestEngine_DisabledCommands'
|
||||
go test ./platform/feishu -run 'TestUserIDFromEventFallsBackToUserID|TestResolveUserNameSkipsInvalidLookupID|TestNew_CanDisableInteractiveCards'
|
||||
|
||||
# Legacy: runs unit tests only
|
||||
|
||||
+86
-37
@@ -3278,23 +3278,23 @@ func (e *Engine) runUnsolicitedReader(ctx context.Context, cancel context.Cancel
|
||||
if autoApprove {
|
||||
result = PermissionResult{Behavior: "allow", UpdatedInput: event.ToolInputRaw}
|
||||
}
|
||||
reqID := event.RequestID
|
||||
respondCtx := ctx // capture current unsolicited reader context
|
||||
go func() {
|
||||
// Run in a goroutine to keep reader iterations fast, but honour
|
||||
// the reader's context so we don't call into a dead session after
|
||||
// stopUnsolicitedReader cancels the context.
|
||||
select {
|
||||
case <-respondCtx.Done():
|
||||
return
|
||||
default:
|
||||
}
|
||||
if err := agentSession.RespondPermission(reqID, result); err != nil {
|
||||
if respondCtx.Err() == nil {
|
||||
slog.Error("unsolicited: failed to respond permission", "error", err)
|
||||
reqID := event.RequestID
|
||||
respondCtx := ctx // capture current unsolicited reader context
|
||||
go func() {
|
||||
// Run in a goroutine to keep reader iterations fast, but honour
|
||||
// the reader's context so we don't call into a dead session after
|
||||
// stopUnsolicitedReader cancels the context.
|
||||
select {
|
||||
case <-respondCtx.Done():
|
||||
return
|
||||
default:
|
||||
}
|
||||
}
|
||||
}()
|
||||
if err := agentSession.RespondPermission(reqID, result); err != nil {
|
||||
if respondCtx.Err() == nil {
|
||||
slog.Error("unsolicited: failed to respond permission", "error", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
if !autoApprove {
|
||||
toolName := event.ToolName
|
||||
if toolName == "" {
|
||||
@@ -3485,6 +3485,9 @@ func (e *Engine) processInteractiveEvents(state *interactiveState, session *Sess
|
||||
|
||||
switch event.Type {
|
||||
case EventThinking:
|
||||
if isEllipsisOnly(event.Content) {
|
||||
break
|
||||
}
|
||||
if hasRichCard {
|
||||
// When thinking messages are suppressed, skip card creation.
|
||||
if !e.display.ThinkingMessages {
|
||||
@@ -3712,7 +3715,7 @@ func (e *Engine) processInteractiveEvents(state *interactiveState, session *Sess
|
||||
}
|
||||
|
||||
case EventText:
|
||||
if event.Content != "" {
|
||||
if event.Content != "" && !isEllipsisOnly(event.Content) {
|
||||
if len(textParts) == 0 {
|
||||
if hasRichCard {
|
||||
if cardMessageID == nil {
|
||||
@@ -3943,16 +3946,23 @@ func (e *Engine) processInteractiveEvents(state *interactiveState, session *Sess
|
||||
})
|
||||
}
|
||||
|
||||
contextText := ""
|
||||
if e.showContextIndicator && !isSilent {
|
||||
if sdkPlausible {
|
||||
cleanResponse += contextIndicator(event.InputTokens)
|
||||
contextText = contextIndicatorText(event.InputTokens)
|
||||
} else if selfPct > 0 {
|
||||
cleanResponse += fmt.Sprintf("\n[ctx: ~%d%%]", selfPct)
|
||||
contextText = fmt.Sprintf("[ctx: ~%d%%]", selfPct)
|
||||
}
|
||||
}
|
||||
if !isSilent {
|
||||
if footer := e.buildReplyFooter(replyAgent, state.agentSession, workspaceDir, replyFooterContextText(replyFooterSessionContextUsage(state.agentSession), e.i18n)); footer != "" {
|
||||
footerContext := replyFooterContextText(replyFooterSessionContextUsage(state.agentSession), e.i18n)
|
||||
if contextText != "" && e.replyFooterEnabled {
|
||||
footerContext = contextText
|
||||
}
|
||||
if footer := e.buildReplyFooter(replyAgent, state.agentSession, workspaceDir, footerContext); footer != "" {
|
||||
cleanResponse = appendReplyFooter(cleanResponse, footer)
|
||||
} else if contextText != "" {
|
||||
cleanResponse += "\n" + contextText
|
||||
}
|
||||
}
|
||||
fullResponse = cleanResponse
|
||||
@@ -4030,26 +4040,22 @@ func (e *Engine) processInteractiveEvents(state *interactiveState, session *Sess
|
||||
// only send the unsent remainder. When tool progress is hidden, tool events don't surface
|
||||
// side-channel messages and segmentStart stays 0, so keep normal finalize flow.
|
||||
sp.discard()
|
||||
if segmentStart < len(textParts) {
|
||||
unsent := strings.Join(textParts[segmentStart:], "")
|
||||
if unsent != "" {
|
||||
for _, chunk := range splitMessage(unsent, maxPlatformMessageLen) {
|
||||
if err := sendWorkspaceWithError(p, replyCtx, chunk); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
unsent := strings.Join(textParts[segmentStart:], "")
|
||||
if strings.TrimSpace(unsent) != "" {
|
||||
unsent = appendFinalMetadataToSegment(unsent, fullResponse)
|
||||
}
|
||||
} else if suppressDuplicate {
|
||||
sp.discard()
|
||||
if metaOnly := strings.TrimSpace(strings.TrimPrefix(fullResponse, baseResponse)); metaOnly != "" {
|
||||
for _, chunk := range splitMessage(metaOnly, maxPlatformMessageLen) {
|
||||
if unsent != "" {
|
||||
for _, chunk := range splitMessage(unsent, maxPlatformMessageLen) {
|
||||
if err := sendWorkspaceWithError(p, replyCtx, chunk); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if suppressDuplicate {
|
||||
sp.discard()
|
||||
slog.Debug("EventResult: suppressed duplicate side-channel text", "response_len", len(fullResponse))
|
||||
} else if sp.finish(fullResponse) {
|
||||
slog.Debug("EventResult: finalized stream preview in-place", "response_len", len(fullResponse))
|
||||
} else {
|
||||
slog.Debug("EventResult: sending via p.Send (preview inactive or failed)", "response_len", len(fullResponse), "chunks", len(splitMessage(fullResponse, maxPlatformMessageLen)))
|
||||
for _, chunk := range splitMessage(fullResponse, maxPlatformMessageLen) {
|
||||
@@ -5243,6 +5249,12 @@ func (e *Engine) buildReplyFooter(agent Agent, session AgentSession, workspaceDi
|
||||
|
||||
var parts []string
|
||||
hasStatus := false
|
||||
contextLeft = strings.TrimSpace(contextLeft)
|
||||
contextFirst := strings.HasPrefix(contextLeft, "[ctx:")
|
||||
if contextFirst {
|
||||
parts = append(parts, contextLeft)
|
||||
hasStatus = true
|
||||
}
|
||||
if model := replyFooterModel(session, agent); model != "" {
|
||||
parts = append(parts, model)
|
||||
hasStatus = true
|
||||
@@ -5251,8 +5263,10 @@ func (e *Engine) buildReplyFooter(agent Agent, session AgentSession, workspaceDi
|
||||
parts = append(parts, effort)
|
||||
hasStatus = true
|
||||
}
|
||||
if left := strings.TrimSpace(contextLeft); left != "" {
|
||||
parts = append(parts, left)
|
||||
if contextFirst {
|
||||
// Already added before model so "[ctx]" stays on the same footer line.
|
||||
} else if contextLeft != "" {
|
||||
parts = append(parts, contextLeft)
|
||||
hasStatus = true
|
||||
} else if usage := e.replyFooterUsageText(session, agent); usage != "" {
|
||||
parts = append(parts, usage)
|
||||
@@ -5481,6 +5495,28 @@ func appendReplyFooter(content, footer string) string {
|
||||
return content + "\n\n*" + footer + "*"
|
||||
}
|
||||
|
||||
func appendFinalMetadataToSegment(segment, fullResponse string) string {
|
||||
segment = strings.TrimRight(segment, "\n ")
|
||||
if segment == "" {
|
||||
return fullResponse
|
||||
}
|
||||
fullResponse = strings.TrimSpace(fullResponse)
|
||||
if fullResponse == "" || strings.TrimSpace(segment) == fullResponse {
|
||||
return segment
|
||||
}
|
||||
|
||||
metadata := ""
|
||||
if idx := strings.LastIndex(fullResponse, "\n\n*"); idx >= 0 && strings.HasSuffix(fullResponse, "*") {
|
||||
metadata = fullResponse[idx:]
|
||||
} else if match := ctxSelfReportRe.FindString(fullResponse); match != "" {
|
||||
metadata = "\n" + strings.TrimSpace(match)
|
||||
}
|
||||
if metadata == "" || strings.Contains(segment, strings.TrimSpace(metadata)) {
|
||||
return segment
|
||||
}
|
||||
return segment + metadata
|
||||
}
|
||||
|
||||
func (e *Engine) cmdShow(p Platform, msg *Message, args []string) {
|
||||
rawRef := strings.TrimSpace(strings.Join(args, " "))
|
||||
if rawRef == "" {
|
||||
@@ -8609,7 +8645,7 @@ func (e *Engine) SendToSessionWithAttachments(sessionKey, message string, images
|
||||
if err := p.Send(e.ctx, replyCtx, message); err != nil {
|
||||
return err
|
||||
}
|
||||
if state != nil && (len(images) > 0 || len(files) > 0) {
|
||||
if state != nil {
|
||||
state.mu.Lock()
|
||||
state.sideText = strings.TrimSpace(message)
|
||||
state.mu.Unlock()
|
||||
@@ -13328,6 +13364,14 @@ const modelContextWindow = 200_000 // generic fallback window for heuristic cont
|
||||
|
||||
// contextIndicator returns a suffix like "\n[ctx: ~42%]" based on SDK-reported input tokens.
|
||||
func contextIndicator(inputTokens int) string {
|
||||
text := contextIndicatorText(inputTokens)
|
||||
if text == "" {
|
||||
return ""
|
||||
}
|
||||
return "\n" + text
|
||||
}
|
||||
|
||||
func contextIndicatorText(inputTokens int) string {
|
||||
if inputTokens <= 0 {
|
||||
return ""
|
||||
}
|
||||
@@ -13335,7 +13379,7 @@ func contextIndicator(inputTokens int) string {
|
||||
if pct > 100 {
|
||||
pct = 100
|
||||
}
|
||||
return fmt.Sprintf("\n[ctx: ~%d%%]", pct)
|
||||
return fmt.Sprintf("[ctx: ~%d%%]", pct)
|
||||
}
|
||||
|
||||
// ctxSelfReportRe matches agent self-reported context lines like "[ctx: ~42%]".
|
||||
@@ -13378,6 +13422,11 @@ func couldBeSilentPrefix(text string) bool {
|
||||
return strings.HasPrefix("NO_REPLY", strings.ToUpper(t))
|
||||
}
|
||||
|
||||
func isEllipsisOnly(text string) bool {
|
||||
t := strings.TrimSpace(text)
|
||||
return t == "..." || t == "…"
|
||||
}
|
||||
|
||||
// parseSelfReportedCtx extracts the percentage from a self-reported "[ctx: ~XX%]" line.
|
||||
func parseSelfReportedCtx(s string) int {
|
||||
m := ctxSelfReportRe.FindString(s)
|
||||
|
||||
@@ -994,6 +994,36 @@ func TestProcessInteractiveEvents_SuppressesDuplicateSideChannelText(t *testing.
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessInteractiveEvents_SuppressesDuplicateSideChannelTextWithContextIndicator(t *testing.T) {
|
||||
p := &stubMediaPlatform{stubPlatformEngine: stubPlatformEngine{n: "test"}}
|
||||
e := NewEngine("test", &stubAgent{}, []Platform{p}, "", LangEnglish)
|
||||
sessionKey := "test:user1"
|
||||
session := e.sessions.GetOrCreateActive(sessionKey)
|
||||
agentSession := newControllableSession("s1")
|
||||
state := &interactiveState{
|
||||
agentSession: agentSession,
|
||||
platform: p,
|
||||
replyCtx: "ctx-1",
|
||||
}
|
||||
e.interactiveStates[sessionKey] = state
|
||||
|
||||
sideText := "已发送 AGENTS.md 文件给你。"
|
||||
if err := e.SendToSessionWithAttachments(sessionKey, sideText, nil, []FileAttachment{{
|
||||
MimeType: "text/markdown",
|
||||
Data: []byte("body"),
|
||||
FileName: "AGENTS.md",
|
||||
}}); err != nil {
|
||||
t.Fatalf("SendToSessionWithAttachments returned error: %v", err)
|
||||
}
|
||||
|
||||
agentSession.events <- Event{Type: EventResult, Content: sideText, InputTokens: 52000, Done: true}
|
||||
e.processInteractiveEvents(state, session, e.sessions, sessionKey, "m1", time.Now(), nil, nil, nil)
|
||||
|
||||
if got := p.getSent(); len(got) != 1 || got[0] != sideText {
|
||||
t.Fatalf("sent text = %#v, want only the side-channel message without duplicate ctx reply", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessInteractiveEvents_DoesNotSuppressDifferentFinalText(t *testing.T) {
|
||||
p := &stubMediaPlatform{stubPlatformEngine: stubPlatformEngine{n: "test"}}
|
||||
e := NewEngine("test", &stubAgent{}, []Platform{p}, "", LangEnglish)
|
||||
@@ -1075,6 +1105,109 @@ func TestProcessInteractiveEvents_AppendsReplyFooterWhenEnabled(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessInteractiveEvents_AppendsContextIndicatorInsideReplyFooter(t *testing.T) {
|
||||
homeDir := t.TempDir()
|
||||
t.Setenv("HOME", homeDir)
|
||||
|
||||
agent := &stubReplyFooterAgent{
|
||||
stubModelModeAgent: stubModelModeAgent{model: "glm-5.1"},
|
||||
workDir: filepath.Join(homeDir, "code", "TechStudio", "projects", "core", "agents", "ceo"),
|
||||
}
|
||||
p := &stubPlatformEngine{n: "telegram"}
|
||||
e := NewEngine("test", agent, []Platform{p}, "", LangEnglish)
|
||||
e.SetReplyFooterEnabled(true)
|
||||
|
||||
sessionKey := "telegram:user-footer-context"
|
||||
session := e.sessions.GetOrCreateActive(sessionKey)
|
||||
agentSession := newControllableSession("s-footer-context")
|
||||
state := &interactiveState{
|
||||
agentSession: agentSession,
|
||||
platform: p,
|
||||
replyCtx: "ctx-footer-context",
|
||||
agent: agent,
|
||||
}
|
||||
e.interactiveStates[sessionKey] = state
|
||||
|
||||
agentSession.events <- Event{Type: EventResult, Content: "answer", InputTokens: 28000, Done: true}
|
||||
e.processInteractiveEvents(state, session, e.sessions, sessionKey, "m-footer-context", time.Now(), nil, nil, state.replyCtx)
|
||||
|
||||
sent := p.getSent()
|
||||
if len(sent) != 1 {
|
||||
t.Fatalf("sent = %#v, want one final reply", sent)
|
||||
}
|
||||
want := "answer\n\n*[ctx: ~14%] · glm-5.1 · ~/code/TechStudio/projects/core/agents/ceo*"
|
||||
if sent[0] != want {
|
||||
t.Fatalf("final reply = %q, want %q", sent[0], want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessInteractiveEvents_ToolSegmentsKeepFinalFooter(t *testing.T) {
|
||||
homeDir := t.TempDir()
|
||||
t.Setenv("HOME", homeDir)
|
||||
|
||||
agent := &stubReplyFooterAgent{
|
||||
stubModelModeAgent: stubModelModeAgent{model: "glm-5.1"},
|
||||
workDir: filepath.Join(homeDir, "code", "TechStudio", "projects", "core", "agents", "ceo"),
|
||||
}
|
||||
p := &stubPlatformEngine{n: "telegram"}
|
||||
e := NewEngine("test", agent, []Platform{p}, "", LangEnglish)
|
||||
e.SetReplyFooterEnabled(true)
|
||||
e.SetDisplayConfig(DisplayCfg{ThinkingMessages: true, ThinkingMaxLen: 300, ToolMaxLen: 500, ToolMessages: true})
|
||||
|
||||
sessionKey := "telegram:user-tool-footer"
|
||||
session := e.sessions.GetOrCreateActive(sessionKey)
|
||||
agentSession := newControllableSession("s-tool-footer")
|
||||
state := &interactiveState{
|
||||
agentSession: agentSession,
|
||||
platform: p,
|
||||
replyCtx: "ctx-tool-footer",
|
||||
agent: agent,
|
||||
}
|
||||
e.interactiveStates[sessionKey] = state
|
||||
|
||||
agentSession.events <- Event{Type: EventText, Content: "先检查一下。"}
|
||||
agentSession.events <- Event{Type: EventToolUse, ToolName: "Bash", ToolInput: "pwd"}
|
||||
agentSession.events <- Event{Type: EventText, Content: "已处理完成。"}
|
||||
agentSession.events <- Event{Type: EventResult, Content: "已处理完成。", InputTokens: 28000, Done: true}
|
||||
e.processInteractiveEvents(state, session, e.sessions, sessionKey, "m-tool-footer", time.Now(), nil, nil, state.replyCtx)
|
||||
|
||||
sent := p.getSent()
|
||||
if len(sent) == 0 {
|
||||
t.Fatal("sent = nil, want final reply")
|
||||
}
|
||||
final := sent[len(sent)-1]
|
||||
want := "已处理完成。\n\n*[ctx: ~14%] · glm-5.1 · ~/code/TechStudio/projects/core/agents/ceo*"
|
||||
if final != want {
|
||||
t.Fatalf("final reply = %q, want %q\nall sent = %#v", final, want, sent)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessInteractiveEvents_DropsStandaloneEllipsisProgress(t *testing.T) {
|
||||
p := &stubPlatformEngine{n: "telegram"}
|
||||
e := NewEngine("test", &stubAgent{}, []Platform{p}, "", LangEnglish)
|
||||
e.SetDisplayConfig(DisplayCfg{ThinkingMessages: true, ThinkingMaxLen: 300, ToolMaxLen: 500, ToolMessages: true})
|
||||
|
||||
sessionKey := "telegram:user-ellipsis"
|
||||
session := e.sessions.GetOrCreateActive(sessionKey)
|
||||
agentSession := newControllableSession("s-ellipsis")
|
||||
state := &interactiveState{
|
||||
agentSession: agentSession,
|
||||
platform: p,
|
||||
replyCtx: "ctx-ellipsis",
|
||||
}
|
||||
e.interactiveStates[sessionKey] = state
|
||||
|
||||
agentSession.events <- Event{Type: EventThinking, Content: "..."}
|
||||
agentSession.events <- Event{Type: EventText, Content: "..."}
|
||||
agentSession.events <- Event{Type: EventResult, Content: "done", Done: true}
|
||||
e.processInteractiveEvents(state, session, e.sessions, sessionKey, "m-ellipsis", time.Now(), nil, nil, state.replyCtx)
|
||||
|
||||
sent := p.getSent()
|
||||
if len(sent) != 1 || sent[0] != "done" {
|
||||
t.Fatalf("sent = %#v, want only final answer without standalone ellipsis progress", sent)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessInteractiveEvents_DoesNotAppendReplyFooterWhenDisabled(t *testing.T) {
|
||||
homeDir := t.TempDir()
|
||||
t.Setenv("HOME", homeDir)
|
||||
|
||||
@@ -118,12 +118,16 @@ func (s *recordingSession) Close() error {
|
||||
}
|
||||
|
||||
func (s *recordingSession) releaseFirstResult(content string) {
|
||||
s.releaseFirstEvent(core.Event{Type: core.EventResult, Content: content, Done: true})
|
||||
}
|
||||
|
||||
func (s *recordingSession) releaseFirstEvent(event core.Event) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if !s.blocked {
|
||||
return
|
||||
}
|
||||
s.events <- core.Event{Type: core.EventResult, Content: content, Done: true}
|
||||
s.events <- event
|
||||
s.blocked = false
|
||||
}
|
||||
|
||||
@@ -327,6 +331,62 @@ func TestSendToSessionWithAttachmentsDeliversTextImagesAndFiles(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendToSessionWithAttachmentsDoesNotDuplicateEchoedFinalTextWithContextIndicator(t *testing.T) {
|
||||
engine, agent, platform := newMediaEngine(t)
|
||||
agent.session.blockFirstResult()
|
||||
|
||||
msg := mediaMessage("start long task")
|
||||
engine.ReceiveMessage(platform, msg)
|
||||
agent.session.waitRecords(t, 1)
|
||||
|
||||
sideText := "delivery ready"
|
||||
err := engine.SendToSessionWithAttachments(
|
||||
msg.SessionKey,
|
||||
sideText,
|
||||
nil,
|
||||
[]core.FileAttachment{{MimeType: "text/plain", FileName: "report.txt", Data: []byte("report")}},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("SendToSessionWithAttachments() error = %v", err)
|
||||
}
|
||||
|
||||
agent.session.releaseFirstEvent(core.Event{
|
||||
Type: core.EventResult,
|
||||
Content: sideText,
|
||||
InputTokens: 52000,
|
||||
Done: true,
|
||||
})
|
||||
|
||||
deadline := time.Now().Add(300 * time.Millisecond)
|
||||
var lastTexts []string
|
||||
for time.Now().Before(deadline) {
|
||||
texts, _, _, _ := platform.snapshot()
|
||||
lastTexts = texts
|
||||
count := 0
|
||||
for _, text := range texts {
|
||||
if strings.Contains(text, sideText) {
|
||||
count++
|
||||
}
|
||||
if strings.Contains(text, "[ctx:") {
|
||||
t.Fatalf("unexpected duplicate context indicator reply: %#v", texts)
|
||||
}
|
||||
}
|
||||
if count > 1 {
|
||||
t.Fatalf("texts = %#v, want no duplicate delivery message", texts)
|
||||
}
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
count := 0
|
||||
for _, text := range lastTexts {
|
||||
if strings.Contains(text, sideText) {
|
||||
count++
|
||||
}
|
||||
}
|
||||
if count != 1 {
|
||||
t.Fatalf("texts = %#v, want exactly one side-channel delivery message", lastTexts)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendToSessionWithAttachmentsRespectsDisabledAttachmentSend(t *testing.T) {
|
||||
engine, agent, platform := newMediaEngine(t)
|
||||
msg := mediaMessage("establish active session")
|
||||
|
||||
Reference in new issue
Block a user