mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-27 21:33:41 +02:00
Cursor stream-json emits reasoning and tool calls as top-level `thinking` / `tool_call` events; the parser only looked for them inside assistant messages, so transcripts showed a single step. Match the top-level events, taking the tool name from the nested `<name>ToolCall` key and normalizing the packed call_id. Subtypes are matched explicitly — only `started` opens a tool, only `completed` closes it, only `delta` carries reasoning — so an unknown/missing subtype is ignored rather than synthesizing a fake result (which would decrement the daemon in-flight tool count early and misfire the watchdog) or polluting reasoning. Covered by a recorded-stream fixture test, an unknown-subtype regression test, and an opt-in real-CLI smoke test.
100 lines
2.9 KiB
Go
100 lines
2.9 KiB
Go
//go:build agentintegration
|
|
|
|
package agent
|
|
|
|
import (
|
|
"context"
|
|
"log/slog"
|
|
"os/exec"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// TestCursorRealStreamObservability drives the real cursor-agent binary with a
|
|
// task that must use tools, and asserts the run is observable: reasoning and
|
|
// each tool call/result reach the message stream, not just the final answer.
|
|
//
|
|
// The fixture test pins the parser against a recorded stream; this one is what
|
|
// catches the upstream protocol moving on (MUL-5231).
|
|
func TestCursorRealStreamObservability(t *testing.T) {
|
|
requireRealAgentSmoke(t)
|
|
if testing.Short() {
|
|
t.Skip("skipping real-binary smoke test in -short mode")
|
|
}
|
|
path, err := exec.LookPath("cursor-agent")
|
|
if err != nil {
|
|
t.Skip("cursor-agent not on PATH; skipping real-binary smoke test")
|
|
}
|
|
if version, err := exec.Command(path, "--version").CombinedOutput(); err == nil {
|
|
t.Logf("cursor-agent CLI version: %s", strings.TrimSpace(string(version)))
|
|
} else {
|
|
t.Logf("cursor-agent CLI version unavailable: %v (%s)", err, strings.TrimSpace(string(version)))
|
|
}
|
|
|
|
backend, err := New("cursor", Config{ExecutablePath: path, Logger: slog.Default()})
|
|
if err != nil {
|
|
t.Fatalf("new cursor backend: %v", err)
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), 180*time.Second)
|
|
defer cancel()
|
|
|
|
session, err := backend.Execute(ctx,
|
|
"Create a file named ping.txt in this workspace containing exactly the word pong, then read it back and tell me what it says.",
|
|
ExecOptions{Cwd: t.TempDir(), Timeout: 170 * time.Second})
|
|
if err != nil {
|
|
t.Fatalf("execute: %v", err)
|
|
}
|
|
|
|
var thinking strings.Builder
|
|
var toolUses, toolResults []Message
|
|
done := make(chan struct{})
|
|
go func() {
|
|
defer close(done)
|
|
for msg := range session.Messages {
|
|
switch msg.Type {
|
|
case MessageThinking:
|
|
thinking.WriteString(msg.Content)
|
|
case MessageToolUse:
|
|
toolUses = append(toolUses, msg)
|
|
case MessageToolResult:
|
|
toolResults = append(toolResults, msg)
|
|
}
|
|
}
|
|
}()
|
|
|
|
result := <-session.Result
|
|
<-done
|
|
|
|
if result.Status != "completed" {
|
|
t.Fatalf("real cursor run did not complete: status=%q error=%q", result.Status, result.Error)
|
|
}
|
|
if thinking.Len() == 0 {
|
|
t.Error("no reasoning observed from the real cursor stream")
|
|
}
|
|
if len(toolUses) == 0 {
|
|
t.Fatal("no tool calls observed from the real cursor stream")
|
|
}
|
|
if len(toolResults) != len(toolUses) {
|
|
t.Errorf("tool results = %d, tool uses = %d; every call must report a result", len(toolResults), len(toolUses))
|
|
}
|
|
for _, msg := range toolUses {
|
|
if msg.Tool == "" {
|
|
t.Errorf("tool call without a name: %+v", msg)
|
|
}
|
|
if strings.ContainsAny(msg.CallID, "\r\n") {
|
|
t.Errorf("tool call id spans multiple lines: %q", msg.CallID)
|
|
}
|
|
}
|
|
t.Logf("real cursor smoke OK: thinking=%d bytes, tools=%v, output=%q",
|
|
thinking.Len(), toolNames(toolUses), result.Output)
|
|
}
|
|
|
|
func toolNames(msgs []Message) []string {
|
|
names := make([]string, 0, len(msgs))
|
|
for _, msg := range msgs {
|
|
names = append(names, msg.Tool)
|
|
}
|
|
return names
|
|
}
|