package run import ( "context" "errors" "strings" "testing" "gitea.stevedudenhoeffer.com/steve/majordomo/llm" "gitea.stevedudenhoeffer.com/steve/executus/tool" ) // stagerFunc is a test InputFileStager: it records each staged file and returns // a deterministic file_id ("file_"), or an error if err is set. type stagerFunc struct { staged []stagedRec err error } type stagedRec struct { runID, agentID, name, mime string size int } func (s *stagerFunc) StageInputFile(_ context.Context, runID, agentID, name, mime string, content []byte) (string, error) { if s.err != nil { return "", s.err } s.staged = append(s.staged, stagedRec{runID, agentID, name, mime, len(content)}) return "file_" + name, nil } func newStagerExecutor(s InputFileStager) *Executor { return New(Config{ Registry: tool.NewRegistry(), Models: func(ctx context.Context, _ string) (context.Context, llm.Model, error) { return ctx, nil, nil }, Ports: Ports{InputFiles: s}, }) } // TestStageInputFiles: files are staged via the port and an [ATTACHED FILES] // descriptor (with each file_id) is appended to the prompt. func TestStageInputFiles(t *testing.T) { st := &stagerFunc{} ex := newStagerExecutor(st) out := ex.stageInputFiles(context.Background(), "run-1", "agent-1", []tool.InputFile{{Name: "clip.mp3", MimeType: "audio/mpeg", Data: []byte("abcd")}}, "transcribe this") if len(st.staged) != 1 || st.staged[0].name != "clip.mp3" { t.Fatalf("staged = %+v, want one clip.mp3", st.staged) } if st.staged[0].runID != "run-1" || st.staged[0].agentID != "agent-1" { t.Errorf("stager got runID/agentID = %q/%q, want run-1/agent-1", st.staged[0].runID, st.staged[0].agentID) } for _, want := range []string{"transcribe this", "[ATTACHED FILES]", "clip.mp3", "file_clip.mp3", "audio/mpeg"} { if !strings.Contains(out, want) { t.Errorf("output missing %q:\n%s", want, out) } } } // TestStageInputFilesNoStager: a nil port leaves the prompt untouched and never // drops the run. func TestStageInputFilesNoStager(t *testing.T) { ex := newStagerExecutor(nil) // Ports.InputFiles == nil out := ex.stageInputFiles(context.Background(), "r", "a", []tool.InputFile{{Name: "x.bin", Data: []byte("z")}}, "prompt") if out != "prompt" { t.Errorf("nil stager changed the prompt: %q", out) } } // TestStageInputFilesNoFiles: no attachments leaves the prompt untouched. func TestStageInputFilesNoFiles(t *testing.T) { ex := newStagerExecutor(&stagerFunc{}) out := ex.stageInputFiles(context.Background(), "r", "a", nil, "prompt") if out != "prompt" { t.Errorf("no files changed the prompt: %q", out) } } // TestStageInputFilesDedup: colliding base names are disambiguated so they don't // clobber each other at /workspace/. func TestStageInputFilesDedup(t *testing.T) { st := &stagerFunc{} ex := newStagerExecutor(st) out := ex.stageInputFiles(context.Background(), "r", "a", []tool.InputFile{ {Name: "a.wav", MimeType: "audio/wav", Data: []byte("1")}, {Name: "a.wav", MimeType: "audio/wav", Data: []byte("2")}, }, "go") if len(st.staged) != 2 { t.Fatalf("staged %d files, want 2", len(st.staged)) } if st.staged[0].name != "a.wav" || st.staged[1].name != "a-2.wav" { t.Errorf("dedup names = %q, %q; want a.wav, a-2.wav", st.staged[0].name, st.staged[1].name) } if !strings.Contains(out, "a-2.wav") { t.Errorf("output missing disambiguated name:\n%s", out) } } // TestStageInputFilesSkipsBad: empty + oversized files are skipped; a save error // drops only that file. With nothing staged, the prompt is unchanged. func TestStageInputFilesSkipsBad(t *testing.T) { // Empty data → skipped; with no good files the prompt is returned as-is. ex := newStagerExecutor(&stagerFunc{}) if out := ex.stageInputFiles(context.Background(), "r", "a", []tool.InputFile{{Name: "empty.bin", Data: nil}}, "p"); out != "p" { t.Errorf("empty file should be skipped, got %q", out) } // A stager error → that file is dropped; nothing staged → prompt unchanged. exErr := newStagerExecutor(&stagerFunc{err: errors.New("disk full")}) if out := exErr.stageInputFiles(context.Background(), "r", "a", []tool.InputFile{{Name: "x.bin", Data: []byte("z")}}, "p"); out != "p" { t.Errorf("save error should drop the file and leave the prompt, got %q", out) } }