picoclaw/pkg/agent/loop_pdf_wait_test.go
dj-oyu d7a2e3cb1c feat: two-phase PDF follow-up message handling
Add two-phase message handling for PDF files sent without text (common
in Telegram reply-with-attachment flow):

Phase 1 (pre-OCR, 5s wait):
- Send hint message when bare PDF arrives
- Wait up to 5 seconds for follow-up message with OCR keywords
  (e.g. "figures"/"図版") to set OCR flags before processing starts

Phase 2 (during OCR):
- Run processMessage/OCR concurrently while draining the message queue
- Buffer same-chat messages as user instructions for post-OCR LLM turn
- Support cancel keywords ("cancel"/"中止"/"キャンセル") to abort OCR
- After OCR + initial LLM response, merge buffered messages into a
  follow-up LLM turn so user instructions are processed with context

Refactors llmWorker into llmWorkerNormal/llmWorkerPDF with shared
helpers (sendResponseIfNeeded, resetMessageTool).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 05:03:47 +09:00

87 lines
2.2 KiB
Go

package agent
import (
"testing"
"github.com/sipeed/picoclaw/pkg/bus"
)
func TestMessageHasBareFile(t *testing.T) {
tests := []struct {
name string
content string
want bool
}{
{"bare file only", "[file]", true},
{"file with newline", "\n[file]", true},
{"file with whitespace", " [file] ", true},
{"file with caption", "please analyze this\n[file]", false},
{"file with Japanese", "\u56f3\u7248\u4ed8\u304d\n[file]", false},
{"no file", "hello world", false},
{"image only", "[image: photo]", false},
{"file and image bare", "[image: photo]\n[file]", true},
{"empty", "", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
msg := bus.InboundMessage{Content: tt.content}
if got := messageHasBareFile(msg); got != tt.want {
t.Errorf("messageHasBareFile(%q) = %v, want %v", tt.content, got, tt.want)
}
})
}
}
func TestIsCancelKeyword(t *testing.T) {
tests := []struct {
content string
want bool
}{
{"cancel", true},
{"Cancel this", true},
{"ABORT", true},
{"\u4e2d\u6b62\u3057\u3066", true},
{"キャンセル", true},
{"やめ", true},
{"やめて", true},
{"please continue", false},
{"summarize this", false},
{"", false},
}
for _, tt := range tests {
t.Run(tt.content, func(t *testing.T) {
if got := isCancelKeyword(tt.content); got != tt.want {
t.Errorf("isCancelKeyword(%q) = %v, want %v", tt.content, got, tt.want)
}
})
}
}
func TestMergeBufferedMessages(t *testing.T) {
buffered := []bus.InboundMessage{
{ChatID: "chat1", Content: "summarize this"},
{ChatID: "chat2", Content: "other chat"},
{ChatID: "chat1", Content: "in Japanese"},
}
got := mergeBufferedMessages(buffered, "chat1")
want := "summarize this\nin Japanese"
if got != want {
t.Errorf("mergeBufferedMessages = %q, want %q", got, want)
}
}
func TestExtractNonChatMessages(t *testing.T) {
buffered := []bus.InboundMessage{
{ChatID: "chat1", Content: "same"},
{ChatID: "chat2", Content: "other"},
{ChatID: "chat1", Content: "same2"},
}
got := extractNonChatMessages(buffered, "chat1")
if len(got) != 1 || got[0].Content != "other" {
t.Errorf("extractNonChatMessages: got %d messages, want 1", len(got))
}
}