package effects import ( "context" "log/slog" "testing" "github.com/enmanuel/agents/pkg/decision" ) // fakeMatrixSender records calls for assertions. type fakeMatrixSender struct { calls []senderCall } type senderCall struct { method string roomID string threadRootID string inReplyTo string markdown string } func (f *fakeMatrixSender) SendText(ctx context.Context, roomID, text string) error { f.calls = append(f.calls, senderCall{method: "SendText", roomID: roomID, markdown: text}) return nil } func (f *fakeMatrixSender) SendMarkdown(ctx context.Context, roomID, markdown string) error { f.calls = append(f.calls, senderCall{method: "SendMarkdown", roomID: roomID, markdown: markdown}) return nil } func (f *fakeMatrixSender) SendMarkdownGetID(ctx context.Context, roomID, markdown string) (string, error) { f.calls = append(f.calls, senderCall{method: "SendMarkdownGetID", roomID: roomID, markdown: markdown}) return "$fake_event_id", nil } func (f *fakeMatrixSender) EditMessage(ctx context.Context, roomID, originalEventID, markdown string) error { f.calls = append(f.calls, senderCall{method: "EditMessage", roomID: roomID, inReplyTo: originalEventID, markdown: markdown}) return nil } func (f *fakeMatrixSender) SendReplyMarkdown(ctx context.Context, roomID, inReplyTo, markdown string) error { f.calls = append(f.calls, senderCall{method: "SendReplyMarkdown", roomID: roomID, inReplyTo: inReplyTo, markdown: markdown}) return nil } func (f *fakeMatrixSender) SendThreadMarkdown(ctx context.Context, roomID, threadRootID, inReplyTo, markdown string) error { f.calls = append(f.calls, senderCall{method: "SendThreadMarkdown", roomID: roomID, threadRootID: threadRootID, inReplyTo: inReplyTo, markdown: markdown}) return nil } func (f *fakeMatrixSender) SendTyping(ctx context.Context, roomID string, typing bool) error { return nil } func TestExecuteReply_PlainMarkdown(t *testing.T) { sender := &fakeMatrixSender{} runner := NewRunner(sender, nil, slog.Default()) actions := []decision.Action{{ Kind: decision.ActionKindReply, Reply: &decision.ReplyAction{Content: "hello"}, }} results := runner.Execute(context.Background(), "!room:test", actions) if len(results) != 1 || results[0].Err != nil { t.Fatalf("unexpected results: %+v", results) } if len(sender.calls) != 1 { t.Fatalf("expected 1 call, got %d", len(sender.calls)) } c := sender.calls[0] if c.method != "SendMarkdown" { t.Errorf("expected SendMarkdown, got %s", c.method) } if c.roomID != "!room:test" { t.Errorf("expected room !room:test, got %s", c.roomID) } } func TestExecuteReply_WithInReplyTo(t *testing.T) { sender := &fakeMatrixSender{} runner := NewRunner(sender, nil, slog.Default()) actions := []decision.Action{{ Kind: decision.ActionKindReply, Reply: &decision.ReplyAction{Content: "hello", InReplyTo: "$evt1"}, }} runner.Execute(context.Background(), "!room:test", actions) if len(sender.calls) != 1 { t.Fatalf("expected 1 call, got %d", len(sender.calls)) } c := sender.calls[0] if c.method != "SendReplyMarkdown" { t.Errorf("expected SendReplyMarkdown, got %s", c.method) } if c.inReplyTo != "$evt1" { t.Errorf("expected inReplyTo=$evt1, got %s", c.inReplyTo) } } func TestExecuteReply_WithThread(t *testing.T) { sender := &fakeMatrixSender{} runner := NewRunner(sender, nil, slog.Default()) actions := []decision.Action{{ Kind: decision.ActionKindReply, Reply: &decision.ReplyAction{ Content: "thread reply", ThreadID: "$root", InReplyTo: "$evt2", }, }} runner.Execute(context.Background(), "!room:test", actions) if len(sender.calls) != 1 { t.Fatalf("expected 1 call, got %d", len(sender.calls)) } c := sender.calls[0] if c.method != "SendThreadMarkdown" { t.Errorf("expected SendThreadMarkdown, got %s", c.method) } if c.threadRootID != "$root" { t.Errorf("expected threadRootID=$root, got %s", c.threadRootID) } if c.inReplyTo != "$evt2" { t.Errorf("expected inReplyTo=$evt2, got %s", c.inReplyTo) } if c.roomID != "!room:test" { t.Errorf("expected room !room:test, got %s", c.roomID) } } func TestExecuteReply_ThreadWithoutInReplyTo(t *testing.T) { sender := &fakeMatrixSender{} runner := NewRunner(sender, nil, slog.Default()) actions := []decision.Action{{ Kind: decision.ActionKindReply, Reply: &decision.ReplyAction{ Content: "thread reply no fallback", ThreadID: "$root", }, }} runner.Execute(context.Background(), "!room:test", actions) if len(sender.calls) != 1 { t.Fatalf("expected 1 call, got %d", len(sender.calls)) } c := sender.calls[0] if c.method != "SendThreadMarkdown" { t.Errorf("expected SendThreadMarkdown, got %s", c.method) } // inReplyTo should be empty; SendThreadMarkdown will default to threadRootID if c.inReplyTo != "" { t.Errorf("expected empty inReplyTo, got %s", c.inReplyTo) } } func TestExecuteReply_NilReply(t *testing.T) { sender := &fakeMatrixSender{} runner := NewRunner(sender, nil, slog.Default()) actions := []decision.Action{{ Kind: decision.ActionKindReply, Reply: nil, }} results := runner.Execute(context.Background(), "!room:test", actions) if len(results) != 1 { t.Fatalf("expected 1 result, got %d", len(results)) } if results[0].Err == nil { t.Error("expected error for nil reply") } }