|
|
|
|
@ -4558,6 +4558,27 @@ func (p *unexpectedTextAttachmentProvider) GetDefaultModel() string {
|
|
|
|
|
return "unexpected-text-attachment-model"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type unexpectedVisionProvider struct {
|
|
|
|
|
calls int
|
|
|
|
|
models []string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (p *unexpectedVisionProvider) Chat(
|
|
|
|
|
ctx context.Context,
|
|
|
|
|
messages []providers.Message,
|
|
|
|
|
tools []providers.ToolDefinition,
|
|
|
|
|
model string,
|
|
|
|
|
opts map[string]any,
|
|
|
|
|
) (*providers.LLMResponse, error) {
|
|
|
|
|
p.calls++
|
|
|
|
|
p.models = append(p.models, model)
|
|
|
|
|
return nil, fmt.Errorf("vision provider should not be called for this turn")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (p *unexpectedVisionProvider) GetDefaultModel() string {
|
|
|
|
|
return "unexpected-vision-model"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type loadImageThenTextFollowUpProvider struct {
|
|
|
|
|
path string
|
|
|
|
|
calls int
|
|
|
|
|
@ -4776,6 +4797,164 @@ func TestAgentLoop_UserAttachmentRoutesToImageModelAfterMediaResolution(t *testi
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestAgentLoop_TextFollowUpAfterUserAttachmentStaysOnTextModel(t *testing.T) {
|
|
|
|
|
workspace := t.TempDir()
|
|
|
|
|
pngPath := filepath.Join(workspace, "sample.png")
|
|
|
|
|
pngBytes := []byte{
|
|
|
|
|
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A,
|
|
|
|
|
0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52,
|
|
|
|
|
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x02,
|
|
|
|
|
0x00, 0x00, 0x00, 0x90, 0x77, 0x53, 0xDE,
|
|
|
|
|
}
|
|
|
|
|
if err := os.WriteFile(pngPath, pngBytes, 0o644); err != nil {
|
|
|
|
|
t.Fatalf("WriteFile() error = %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cfg := &config.Config{
|
|
|
|
|
Agents: config.AgentsConfig{
|
|
|
|
|
Defaults: config.AgentDefaults{
|
|
|
|
|
Workspace: workspace,
|
|
|
|
|
ModelName: "text-model",
|
|
|
|
|
ImageModel: "vision-model",
|
|
|
|
|
MaxTokens: 4096,
|
|
|
|
|
MaxToolIterations: 3,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
ModelList: []*config.ModelConfig{
|
|
|
|
|
{ModelName: "text-model", Model: "openai/text-model"},
|
|
|
|
|
{ModelName: "vision-model", Model: "openai/vision-model"},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
msgBus := bus.NewMessageBus()
|
|
|
|
|
textProvider := &unexpectedTextAttachmentProvider{}
|
|
|
|
|
al := NewAgentLoop(cfg, msgBus, textProvider)
|
|
|
|
|
|
|
|
|
|
store := media.NewFileMediaStore()
|
|
|
|
|
al.SetMediaStore(store)
|
|
|
|
|
ref, err := store.Store(pngPath, media.MediaMeta{ContentType: "image/png"}, "test:user-attachment-followup")
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("Store() error = %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
agent := al.registry.GetDefaultAgent()
|
|
|
|
|
if agent == nil {
|
|
|
|
|
t.Fatal("expected default agent")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
visionProvider := &resolvedImagePathVisionProvider{expectedPath: pngPath}
|
|
|
|
|
agent.CandidateProviders[providers.ModelKey("openai", "vision-model")] = visionProvider
|
|
|
|
|
|
|
|
|
|
sessionKey := "agent:main:telegram:direct:user1"
|
|
|
|
|
timeoutCtx, cancel := context.WithTimeout(context.Background(), responseTimeout)
|
|
|
|
|
defer cancel()
|
|
|
|
|
|
|
|
|
|
resp1, err := al.processMessage(timeoutCtx, testInboundMessage(bus.InboundMessage{
|
|
|
|
|
Context: bus.InboundContext{
|
|
|
|
|
Channel: "telegram",
|
|
|
|
|
ChatID: "chat1",
|
|
|
|
|
ChatType: "direct",
|
|
|
|
|
SenderID: "user1",
|
|
|
|
|
MessageID: "m1",
|
|
|
|
|
},
|
|
|
|
|
Content: "describe this image",
|
|
|
|
|
Media: []string{ref},
|
|
|
|
|
SessionKey: sessionKey,
|
|
|
|
|
}))
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("first processMessage() error = %v", err)
|
|
|
|
|
}
|
|
|
|
|
if resp1 != "vision direct answer" {
|
|
|
|
|
t.Fatalf("first response = %q, want %q", resp1, "vision direct answer")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
timeoutCtx2, cancel2 := context.WithTimeout(context.Background(), responseTimeout)
|
|
|
|
|
defer cancel2()
|
|
|
|
|
|
|
|
|
|
resp2, err := al.processMessage(timeoutCtx2, testInboundMessage(bus.InboundMessage{
|
|
|
|
|
Context: bus.InboundContext{
|
|
|
|
|
Channel: "telegram",
|
|
|
|
|
ChatID: "chat1",
|
|
|
|
|
ChatType: "direct",
|
|
|
|
|
SenderID: "user1",
|
|
|
|
|
MessageID: "m2",
|
|
|
|
|
},
|
|
|
|
|
Content: "now summarize it in one sentence",
|
|
|
|
|
SessionKey: sessionKey,
|
|
|
|
|
}))
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("second processMessage() error = %v", err)
|
|
|
|
|
}
|
|
|
|
|
if resp2 != "text model response" {
|
|
|
|
|
t.Fatalf("second response = %q, want %q", resp2, "text model response")
|
|
|
|
|
}
|
|
|
|
|
if textProvider.calls != 1 {
|
|
|
|
|
t.Fatalf("textProvider calls = %d, want 1", textProvider.calls)
|
|
|
|
|
}
|
|
|
|
|
if visionProvider.calls != 1 {
|
|
|
|
|
t.Fatalf("visionProvider calls = %d, want 1", visionProvider.calls)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestAgentLoop_GenericImagePlaceholderDoesNotRouteToImageModel(t *testing.T) {
|
|
|
|
|
workspace := t.TempDir()
|
|
|
|
|
|
|
|
|
|
cfg := &config.Config{
|
|
|
|
|
Agents: config.AgentsConfig{
|
|
|
|
|
Defaults: config.AgentDefaults{
|
|
|
|
|
Workspace: workspace,
|
|
|
|
|
ModelName: "text-model",
|
|
|
|
|
ImageModel: "vision-model",
|
|
|
|
|
MaxTokens: 4096,
|
|
|
|
|
MaxToolIterations: 3,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
ModelList: []*config.ModelConfig{
|
|
|
|
|
{ModelName: "text-model", Model: "openai/text-model"},
|
|
|
|
|
{ModelName: "vision-model", Model: "openai/vision-model"},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
msgBus := bus.NewMessageBus()
|
|
|
|
|
textProvider := &unexpectedTextAttachmentProvider{}
|
|
|
|
|
al := NewAgentLoop(cfg, msgBus, textProvider)
|
|
|
|
|
|
|
|
|
|
agent := al.registry.GetDefaultAgent()
|
|
|
|
|
if agent == nil {
|
|
|
|
|
t.Fatal("expected default agent")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
visionProvider := &unexpectedVisionProvider{}
|
|
|
|
|
agent.CandidateProviders[providers.ModelKey("openai", "vision-model")] = visionProvider
|
|
|
|
|
|
|
|
|
|
timeoutCtx, cancel := context.WithTimeout(context.Background(), responseTimeout)
|
|
|
|
|
defer cancel()
|
|
|
|
|
|
|
|
|
|
resp, err := al.processMessage(timeoutCtx, testInboundMessage(bus.InboundMessage{
|
|
|
|
|
Context: bus.InboundContext{
|
|
|
|
|
Channel: "telegram",
|
|
|
|
|
ChatID: "chat1",
|
|
|
|
|
ChatType: "direct",
|
|
|
|
|
SenderID: "user1",
|
|
|
|
|
MessageID: "m1",
|
|
|
|
|
},
|
|
|
|
|
Content: "this is only a placeholder [image: photo], answer in text",
|
|
|
|
|
SessionKey: "agent:main:telegram:direct:user1",
|
|
|
|
|
}))
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("processMessage() error = %v", err)
|
|
|
|
|
}
|
|
|
|
|
if resp != "text model response" {
|
|
|
|
|
t.Fatalf("response = %q, want %q", resp, "text model response")
|
|
|
|
|
}
|
|
|
|
|
if textProvider.calls != 1 {
|
|
|
|
|
t.Fatalf("textProvider calls = %d, want 1", textProvider.calls)
|
|
|
|
|
}
|
|
|
|
|
if visionProvider.calls != 0 {
|
|
|
|
|
t.Fatalf("visionProvider calls = %d, want 0", visionProvider.calls)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestAgentLoop_TextFollowUpAfterLoadImageStaysOnTextModel(t *testing.T) {
|
|
|
|
|
workspace := t.TempDir()
|
|
|
|
|
pngPath := filepath.Join(workspace, "sample.png")
|
|
|
|
|
@ -6264,7 +6443,7 @@ func TestResolveMediaRefs_ImageInjectsPathTag(t *testing.T) {
|
|
|
|
|
messages := []providers.Message{
|
|
|
|
|
{Role: "user", Content: "describe this", Media: []string{ref}},
|
|
|
|
|
}
|
|
|
|
|
result := resolveMediaRefs(messages, store, config.DefaultMaxMediaSize)
|
|
|
|
|
result := resolveMediaRefs(messages, store, config.DefaultMaxMediaSize, 0)
|
|
|
|
|
|
|
|
|
|
if len(result[0].Media) != 0 {
|
|
|
|
|
t.Fatalf("expected 0 media (images use path tags), got %d", len(result[0].Media))
|
|
|
|
|
@ -6297,7 +6476,7 @@ func TestResolveMediaRefs_ToolRoleImageAppendedAsUserMessage(t *testing.T) {
|
|
|
|
|
messages := []providers.Message{
|
|
|
|
|
toolResultPromptMessage("Image loaded", "call_tool_result_image", []string{ref}),
|
|
|
|
|
}
|
|
|
|
|
result := resolveMediaRefs(messages, store, config.DefaultMaxMediaSize)
|
|
|
|
|
result := resolveMediaRefs(messages, store, config.DefaultMaxMediaSize, 0)
|
|
|
|
|
|
|
|
|
|
// Tool message should have path tag but no base64
|
|
|
|
|
if len(result[0].Media) != 0 {
|
|
|
|
|
@ -6342,12 +6521,13 @@ func TestResolveMediaRefs_HistoricalToolRoleImageDoesNotAppendAsUserMessage(t *t
|
|
|
|
|
ref, _ := store.Store(pngPath, media.MediaMeta{}, "test")
|
|
|
|
|
|
|
|
|
|
messages := []providers.Message{
|
|
|
|
|
{Role: "tool", Content: "Image loaded", Media: []string{ref}},
|
|
|
|
|
toolResultPromptMessage("Image loaded", "call_historical_tool_result_image", []string{ref}),
|
|
|
|
|
{Role: "user", Content: "now summarize it in one sentence"},
|
|
|
|
|
}
|
|
|
|
|
result := resolveMediaRefs(messages, store, config.DefaultMaxMediaSize)
|
|
|
|
|
result := resolveMediaRefs(messages, store, config.DefaultMaxMediaSize, 1)
|
|
|
|
|
|
|
|
|
|
if len(result) != 1 {
|
|
|
|
|
t.Fatalf("expected only historical tool message to remain, got %d messages", len(result))
|
|
|
|
|
if len(result) != 2 {
|
|
|
|
|
t.Fatalf("expected historical tool message plus current follow-up, got %d messages", len(result))
|
|
|
|
|
}
|
|
|
|
|
if len(result[0].Media) != 0 {
|
|
|
|
|
t.Fatalf("expected 0 media in historical tool message, got %d", len(result[0].Media))
|
|
|
|
|
@ -6356,6 +6536,65 @@ func TestResolveMediaRefs_HistoricalToolRoleImageDoesNotAppendAsUserMessage(t *t
|
|
|
|
|
if !strings.Contains(result[0].Content, "[image:"+localPath+"]") {
|
|
|
|
|
t.Fatalf("expected image path tag in historical tool content, got %q", result[0].Content)
|
|
|
|
|
}
|
|
|
|
|
if result[1].Role != "user" || result[1].Content != "now summarize it in one sentence" {
|
|
|
|
|
t.Fatalf("expected current follow-up user message to remain untouched, got %#v", result[1])
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestResolveMediaRefs_HistoricalAndCurrentToolImagesOnlyRehydrateCurrentTurn(t *testing.T) {
|
|
|
|
|
store := media.NewFileMediaStore()
|
|
|
|
|
dir := t.TempDir()
|
|
|
|
|
|
|
|
|
|
historicalPath := filepath.Join(dir, "historical.png")
|
|
|
|
|
currentPath := filepath.Join(dir, "current.png")
|
|
|
|
|
pngHeader := []byte{
|
|
|
|
|
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A,
|
|
|
|
|
0x00, 0x00, 0x00, 0x0D,
|
|
|
|
|
0x49, 0x48, 0x44, 0x52,
|
|
|
|
|
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x02,
|
|
|
|
|
0x00, 0x00, 0x00,
|
|
|
|
|
0x90, 0x77, 0x53, 0xDE,
|
|
|
|
|
}
|
|
|
|
|
if err := os.WriteFile(historicalPath, pngHeader, 0o644); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if err := os.WriteFile(currentPath, pngHeader, 0o644); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
historicalRef, _ := store.Store(historicalPath, media.MediaMeta{}, "test")
|
|
|
|
|
currentRef, _ := store.Store(currentPath, media.MediaMeta{}, "test")
|
|
|
|
|
|
|
|
|
|
messages := []providers.Message{
|
|
|
|
|
toolResultPromptMessage("Historical image loaded", "call_hist_image", []string{historicalRef}),
|
|
|
|
|
{Role: "assistant", Content: "Now I will inspect a new image."},
|
|
|
|
|
toolResultPromptMessage("Current image loaded", "call_current_image", []string{currentRef}),
|
|
|
|
|
}
|
|
|
|
|
result := resolveMediaRefs(messages, store, config.DefaultMaxMediaSize, 1)
|
|
|
|
|
|
|
|
|
|
if len(result) != 4 {
|
|
|
|
|
t.Fatalf("expected 4 messages (historical tool + assistant + current tool + synthetic user), got %d", len(result))
|
|
|
|
|
}
|
|
|
|
|
if result[0].Role != "tool" || len(result[0].Media) != 0 {
|
|
|
|
|
t.Fatalf("historical tool message = %#v, want path-tagged tool without media", result[0])
|
|
|
|
|
}
|
|
|
|
|
if !strings.Contains(result[0].Content, "[image:"+historicalPath+"]") {
|
|
|
|
|
t.Fatalf("expected historical tool content to contain %q, got %q", "[image:"+historicalPath+"]", result[0].Content)
|
|
|
|
|
}
|
|
|
|
|
if result[1].Role != "assistant" || result[1].Content != "Now I will inspect a new image." {
|
|
|
|
|
t.Fatalf("assistant boundary message = %#v, want untouched assistant", result[1])
|
|
|
|
|
}
|
|
|
|
|
if result[2].Role != "tool" || len(result[2].Media) != 0 {
|
|
|
|
|
t.Fatalf("current tool message = %#v, want path-tagged tool without media", result[2])
|
|
|
|
|
}
|
|
|
|
|
if !strings.Contains(result[2].Content, "[image:"+currentPath+"]") {
|
|
|
|
|
t.Fatalf("expected current tool content to contain %q, got %q", "[image:"+currentPath+"]", result[2].Content)
|
|
|
|
|
}
|
|
|
|
|
if result[3].Role != "user" || len(result[3].Media) != 1 {
|
|
|
|
|
t.Fatalf("synthetic follow-up = %#v, want one-media user message", result[3])
|
|
|
|
|
}
|
|
|
|
|
if !strings.HasPrefix(result[3].Media[0], "data:image/png;base64,") {
|
|
|
|
|
t.Fatalf("expected base64 image in synthetic follow-up, got %q", result[3].Media[0][:40])
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestResolveMediaRefs_MultiToolCallPreservesOrdering(t *testing.T) {
|
|
|
|
|
@ -6379,7 +6618,7 @@ func TestResolveMediaRefs_MultiToolCallPreservesOrdering(t *testing.T) {
|
|
|
|
|
toolResultPromptMessage("Image loaded [image: photo]", "call_load_image_multi_tool", []string{imgRef}),
|
|
|
|
|
toolResultPromptMessage("file contents here", "call_read_file_multi_tool", nil),
|
|
|
|
|
}
|
|
|
|
|
result := resolveMediaRefs(messages, store, config.DefaultMaxMediaSize)
|
|
|
|
|
result := resolveMediaRefs(messages, store, config.DefaultMaxMediaSize, 0)
|
|
|
|
|
|
|
|
|
|
// assistant, tool#1, tool#2 must remain contiguous — no user in between
|
|
|
|
|
if result[0].Role != "assistant" {
|
|
|
|
|
@ -6404,6 +6643,52 @@ func TestResolveMediaRefs_MultiToolCallPreservesOrdering(t *testing.T) {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestResolveMediaRefs_MultipleCurrentToolImagesShareSingleSyntheticFollowUp(t *testing.T) {
|
|
|
|
|
store := media.NewFileMediaStore()
|
|
|
|
|
dir := t.TempDir()
|
|
|
|
|
|
|
|
|
|
firstPath := filepath.Join(dir, "first.png")
|
|
|
|
|
secondPath := filepath.Join(dir, "second.png")
|
|
|
|
|
pngHeader := []byte{
|
|
|
|
|
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A,
|
|
|
|
|
0x00, 0x00, 0x00, 0x0D,
|
|
|
|
|
0x49, 0x48, 0x44, 0x52,
|
|
|
|
|
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x02,
|
|
|
|
|
0x00, 0x00, 0x00,
|
|
|
|
|
0x90, 0x77, 0x53, 0xDE,
|
|
|
|
|
}
|
|
|
|
|
if err := os.WriteFile(firstPath, pngHeader, 0o644); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if err := os.WriteFile(secondPath, pngHeader, 0o644); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
firstRef, _ := store.Store(firstPath, media.MediaMeta{}, "test")
|
|
|
|
|
secondRef, _ := store.Store(secondPath, media.MediaMeta{}, "test")
|
|
|
|
|
|
|
|
|
|
messages := []providers.Message{
|
|
|
|
|
{Role: "assistant", Content: "I loaded two images for comparison."},
|
|
|
|
|
toolResultPromptMessage("First image loaded", "call_first_image", []string{firstRef}),
|
|
|
|
|
toolResultPromptMessage("Second image loaded", "call_second_image", []string{secondRef}),
|
|
|
|
|
}
|
|
|
|
|
result := resolveMediaRefs(messages, store, config.DefaultMaxMediaSize, 0)
|
|
|
|
|
|
|
|
|
|
if len(result) != 4 {
|
|
|
|
|
t.Fatalf("expected assistant + 2 tool results + 1 synthetic follow-up, got %d messages", len(result))
|
|
|
|
|
}
|
|
|
|
|
if result[3].Role != "user" {
|
|
|
|
|
t.Fatalf("synthetic follow-up role = %q, want user", result[3].Role)
|
|
|
|
|
}
|
|
|
|
|
if len(result[3].Media) != 2 {
|
|
|
|
|
t.Fatalf("synthetic follow-up media count = %d, want 2", len(result[3].Media))
|
|
|
|
|
}
|
|
|
|
|
for i, ref := range result[3].Media {
|
|
|
|
|
if !strings.HasPrefix(ref, "data:image/png;base64,") {
|
|
|
|
|
t.Fatalf("synthetic follow-up media[%d] missing data URL prefix: %q", i, ref[:40])
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestResolveMediaRefs_OversizedImageSkipsBase64KeepsPathTag(t *testing.T) {
|
|
|
|
|
store := media.NewFileMediaStore()
|
|
|
|
|
dir := t.TempDir()
|
|
|
|
|
@ -6421,7 +6706,7 @@ func TestResolveMediaRefs_OversizedImageSkipsBase64KeepsPathTag(t *testing.T) {
|
|
|
|
|
{Role: "user", Content: "hi", Media: []string{ref}},
|
|
|
|
|
}
|
|
|
|
|
// Use a tiny limit (1KB) so the file is oversized
|
|
|
|
|
result := resolveMediaRefs(messages, store, 1024)
|
|
|
|
|
result := resolveMediaRefs(messages, store, 1024, 0)
|
|
|
|
|
|
|
|
|
|
if len(result[0].Media) != 0 {
|
|
|
|
|
t.Fatalf("expected 0 media (oversized), got %d", len(result[0].Media))
|
|
|
|
|
@ -6446,7 +6731,7 @@ func TestResolveMediaRefs_UnknownTypeInjectsPath(t *testing.T) {
|
|
|
|
|
messages := []providers.Message{
|
|
|
|
|
{Role: "user", Content: "hi", Media: []string{ref}},
|
|
|
|
|
}
|
|
|
|
|
result := resolveMediaRefs(messages, store, config.DefaultMaxMediaSize)
|
|
|
|
|
result := resolveMediaRefs(messages, store, config.DefaultMaxMediaSize, 0)
|
|
|
|
|
|
|
|
|
|
if len(result[0].Media) != 0 {
|
|
|
|
|
t.Fatalf("expected 0 media entries, got %d", len(result[0].Media))
|
|
|
|
|
@ -6461,7 +6746,7 @@ func TestResolveMediaRefs_PassesThroughNonMediaRefs(t *testing.T) {
|
|
|
|
|
messages := []providers.Message{
|
|
|
|
|
{Role: "user", Content: "hi", Media: []string{"https://example.com/img.png"}},
|
|
|
|
|
}
|
|
|
|
|
result := resolveMediaRefs(messages, nil, config.DefaultMaxMediaSize)
|
|
|
|
|
result := resolveMediaRefs(messages, nil, config.DefaultMaxMediaSize, 0)
|
|
|
|
|
|
|
|
|
|
if len(result[0].Media) != 1 || result[0].Media[0] != "https://example.com/img.png" {
|
|
|
|
|
t.Fatalf("expected passthrough of non-media:// URL, got %v", result[0].Media)
|
|
|
|
|
@ -6486,7 +6771,7 @@ func TestResolveMediaRefs_DoesNotMutateOriginal(t *testing.T) {
|
|
|
|
|
}
|
|
|
|
|
originalRef := original[0].Media[0]
|
|
|
|
|
|
|
|
|
|
resolveMediaRefs(original, store, config.DefaultMaxMediaSize)
|
|
|
|
|
resolveMediaRefs(original, store, config.DefaultMaxMediaSize, 0)
|
|
|
|
|
|
|
|
|
|
if original[0].Media[0] != originalRef {
|
|
|
|
|
t.Fatal("resolveMediaRefs mutated original message slice")
|
|
|
|
|
@ -6506,7 +6791,7 @@ func TestResolveMediaRefs_UsesMetaContentType(t *testing.T) {
|
|
|
|
|
messages := []providers.Message{
|
|
|
|
|
{Role: "user", Content: "hi", Media: []string{ref}},
|
|
|
|
|
}
|
|
|
|
|
result := resolveMediaRefs(messages, store, config.DefaultMaxMediaSize)
|
|
|
|
|
result := resolveMediaRefs(messages, store, config.DefaultMaxMediaSize, 0)
|
|
|
|
|
|
|
|
|
|
if len(result[0].Media) != 0 {
|
|
|
|
|
t.Fatalf("expected 0 media (images use path tags), got %d", len(result[0].Media))
|
|
|
|
|
@ -6530,7 +6815,7 @@ func TestResolveMediaRefs_PDFInjectsFilePath(t *testing.T) {
|
|
|
|
|
messages := []providers.Message{
|
|
|
|
|
{Role: "user", Content: "report.pdf [file]", Media: []string{ref}},
|
|
|
|
|
}
|
|
|
|
|
result := resolveMediaRefs(messages, store, config.DefaultMaxMediaSize)
|
|
|
|
|
result := resolveMediaRefs(messages, store, config.DefaultMaxMediaSize, 0)
|
|
|
|
|
|
|
|
|
|
if len(result[0].Media) != 0 {
|
|
|
|
|
t.Fatalf("expected 0 media (non-image), got %d", len(result[0].Media))
|
|
|
|
|
@ -6552,7 +6837,7 @@ func TestResolveMediaRefs_AudioInjectsAudioPath(t *testing.T) {
|
|
|
|
|
messages := []providers.Message{
|
|
|
|
|
{Role: "user", Content: "voice.ogg [audio]", Media: []string{ref}},
|
|
|
|
|
}
|
|
|
|
|
result := resolveMediaRefs(messages, store, config.DefaultMaxMediaSize)
|
|
|
|
|
result := resolveMediaRefs(messages, store, config.DefaultMaxMediaSize, 0)
|
|
|
|
|
|
|
|
|
|
if len(result[0].Media) != 0 {
|
|
|
|
|
t.Fatalf("expected 0 media, got %d", len(result[0].Media))
|
|
|
|
|
@ -6574,7 +6859,7 @@ func TestResolveMediaRefs_VideoInjectsVideoPath(t *testing.T) {
|
|
|
|
|
messages := []providers.Message{
|
|
|
|
|
{Role: "user", Content: "clip.mp4 [video]", Media: []string{ref}},
|
|
|
|
|
}
|
|
|
|
|
result := resolveMediaRefs(messages, store, config.DefaultMaxMediaSize)
|
|
|
|
|
result := resolveMediaRefs(messages, store, config.DefaultMaxMediaSize, 0)
|
|
|
|
|
|
|
|
|
|
if len(result[0].Media) != 0 {
|
|
|
|
|
t.Fatalf("expected 0 media, got %d", len(result[0].Media))
|
|
|
|
|
@ -6596,7 +6881,7 @@ func TestResolveMediaRefs_NoGenericTagAppendsPath(t *testing.T) {
|
|
|
|
|
messages := []providers.Message{
|
|
|
|
|
{Role: "user", Content: "here is my data", Media: []string{ref}},
|
|
|
|
|
}
|
|
|
|
|
result := resolveMediaRefs(messages, store, config.DefaultMaxMediaSize)
|
|
|
|
|
result := resolveMediaRefs(messages, store, config.DefaultMaxMediaSize, 0)
|
|
|
|
|
|
|
|
|
|
expected := "here is my data [file:" + csvPath + "]"
|
|
|
|
|
if result[0].Content != expected {
|
|
|
|
|
@ -6688,7 +6973,7 @@ func TestResolveMediaRefs_JSONContentPrependsPathTag(t *testing.T) {
|
|
|
|
|
messages := []providers.Message{
|
|
|
|
|
{Role: "user", Content: jsonContent, Media: []string{ref}},
|
|
|
|
|
}
|
|
|
|
|
result := resolveMediaRefs(messages, store, config.DefaultMaxMediaSize)
|
|
|
|
|
result := resolveMediaRefs(messages, store, config.DefaultMaxMediaSize, 0)
|
|
|
|
|
|
|
|
|
|
want := "[image:" + pngPath + "]\n" + jsonContent
|
|
|
|
|
if result[0].Content != want {
|
|
|
|
|
@ -6708,7 +6993,7 @@ func TestResolveMediaRefs_EmptyContentGetsPathTag(t *testing.T) {
|
|
|
|
|
messages := []providers.Message{
|
|
|
|
|
{Role: "user", Content: "", Media: []string{ref}},
|
|
|
|
|
}
|
|
|
|
|
result := resolveMediaRefs(messages, store, config.DefaultMaxMediaSize)
|
|
|
|
|
result := resolveMediaRefs(messages, store, config.DefaultMaxMediaSize, 0)
|
|
|
|
|
|
|
|
|
|
expected := "[file:" + docPath + "]"
|
|
|
|
|
if result[0].Content != expected {
|
|
|
|
|
@ -6737,7 +7022,7 @@ func TestResolveMediaRefs_MixedImageAndFile(t *testing.T) {
|
|
|
|
|
messages := []providers.Message{
|
|
|
|
|
{Role: "user", Content: "check these [file]", Media: []string{imgRef, fileRef}},
|
|
|
|
|
}
|
|
|
|
|
result := resolveMediaRefs(messages, store, config.DefaultMaxMediaSize)
|
|
|
|
|
result := resolveMediaRefs(messages, store, config.DefaultMaxMediaSize, 0)
|
|
|
|
|
|
|
|
|
|
if len(result[0].Media) != 0 {
|
|
|
|
|
t.Fatalf("expected 0 media (all types use path tags), got %d", len(result[0].Media))
|
|
|
|
|
|