- Introduced `shellQuoteForPlatform` to build shell-safe command strings tailored for Windows and POSIX environments. - Updated `buildSandboxEnvPrompt` to display the correct environment variable syntax based on the operating system. - Refactored the command copying logic in the runner to use platform-aware commands for copying custom OpenCode tools. - Removed outdated test scenarios related to the vision connector, streamlining the test suite.
162 lines
4.9 KiB
Go
162 lines
4.9 KiB
Go
package opencode_test
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"github.com/yaoapp/yao/agent/caller"
|
|
agentcontext "github.com/yaoapp/yao/agent/context"
|
|
sandboxtestutils "github.com/yaoapp/yao/agent/sandbox/v2/testutils"
|
|
oauthtypes "github.com/yaoapp/yao/openapi/oauth/types"
|
|
)
|
|
|
|
const defaultTimeout = 3 * time.Minute
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Scenario 1: Oneshot — new container per request, no session persistence
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestOpenCode_Oneshot(t *testing.T) {
|
|
sandboxtestutils.Prepare(t)
|
|
defer sandboxtestutils.Clean(t)
|
|
require.NotNil(t, caller.AgentGetterFunc)
|
|
|
|
const assistantID = "tests.sandbox-v2.opencode-oneshot-cli"
|
|
agent, err := caller.AgentGetterFunc(assistantID)
|
|
require.NoError(t, err)
|
|
|
|
chatID := fmt.Sprintf("e2e-oneshot-%d", time.Now().UnixMilli())
|
|
ctx := agentcontext.New(
|
|
context.Background(),
|
|
&oauthtypes.AuthorizedInfo{TeamID: "test-team-e2e", UserID: "test-user-e2e"},
|
|
chatID,
|
|
)
|
|
|
|
resp := streamAndWait(t, agent, ctx, "Reply exactly with: hello opencode sandbox", defaultTimeout)
|
|
|
|
require.NotNil(t, resp.Completion)
|
|
assert.Equal(t, "assistant", resp.Completion.Role)
|
|
content := contentString(t, resp)
|
|
t.Logf("Oneshot response: %s", content)
|
|
assert.Contains(t, strings.ToLower(content), "hello opencode sandbox")
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Scenario 2 & 3: Session — first turn (new conversation) + continuation
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestOpenCode_Session(t *testing.T) {
|
|
sandboxtestutils.Prepare(t)
|
|
defer sandboxtestutils.Clean(t)
|
|
require.NotNil(t, caller.AgentGetterFunc)
|
|
|
|
const assistantID = "tests.sandbox-v2.opencode-session-cli"
|
|
agent, err := caller.AgentGetterFunc(assistantID)
|
|
require.NoError(t, err)
|
|
|
|
chatID := fmt.Sprintf("e2e-session-%d", time.Now().UnixMilli())
|
|
|
|
// ── Turn 1: first message — creates a new session ──────────────────
|
|
t.Run("turn1_new_session", func(t *testing.T) {
|
|
ctx := agentcontext.New(
|
|
context.Background(),
|
|
&oauthtypes.AuthorizedInfo{TeamID: "test-team-e2e", UserID: "test-user-e2e"},
|
|
chatID,
|
|
)
|
|
|
|
resp := streamAndWait(t, agent, ctx,
|
|
"Remember this secret code: PINEAPPLE-42. Reply with: understood",
|
|
defaultTimeout,
|
|
)
|
|
|
|
require.NotNil(t, resp.Completion)
|
|
assert.Equal(t, "assistant", resp.Completion.Role)
|
|
content := contentString(t, resp)
|
|
t.Logf("Turn 1 response: %s", content)
|
|
assert.Contains(t, strings.ToLower(content), "understood")
|
|
})
|
|
|
|
// ── Turn 2: continuation — reuses the session ──────────────────────
|
|
t.Run("turn2_continue_session", func(t *testing.T) {
|
|
ctx := agentcontext.New(
|
|
context.Background(),
|
|
&oauthtypes.AuthorizedInfo{TeamID: "test-team-e2e", UserID: "test-user-e2e"},
|
|
chatID,
|
|
)
|
|
|
|
resp := streamAndWait(t, agent, ctx,
|
|
"What was the secret code I told you? Reply with just the code.",
|
|
defaultTimeout,
|
|
)
|
|
|
|
require.NotNil(t, resp.Completion)
|
|
assert.Equal(t, "assistant", resp.Completion.Role)
|
|
content := contentString(t, resp)
|
|
t.Logf("Turn 2 response: %s", content)
|
|
assert.Contains(t, strings.ToLower(content), "pineapple-42",
|
|
"continuation should recall secret from turn 1")
|
|
})
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func streamAndWait(
|
|
t *testing.T,
|
|
agent caller.AgentCaller,
|
|
ctx *agentcontext.Context,
|
|
prompt string,
|
|
timeout time.Duration,
|
|
) *agentcontext.Response {
|
|
t.Helper()
|
|
|
|
messages := []agentcontext.Message{{Role: "user", Content: prompt}}
|
|
|
|
done := make(chan struct{})
|
|
var resp *agentcontext.Response
|
|
var streamErr error
|
|
|
|
go func() {
|
|
defer close(done)
|
|
resp, streamErr = agent.Stream(ctx, messages)
|
|
}()
|
|
|
|
select {
|
|
case <-done:
|
|
case <-time.After(timeout):
|
|
t.Fatalf("timeout after %v", timeout)
|
|
}
|
|
|
|
if streamErr != nil {
|
|
t.Logf("Stream error: %v", streamErr)
|
|
}
|
|
require.NoError(t, streamErr, "Stream should not return error")
|
|
require.NotNil(t, resp, "response should not be nil")
|
|
|
|
if resp.Completion != nil {
|
|
t.Logf("Completion: role=%s content=%v", resp.Completion.Role, resp.Completion.Content)
|
|
}
|
|
|
|
require.NotNil(t, ctx.Buffer, "ctx.Buffer should not be nil")
|
|
msgs := ctx.Buffer.GetMessages()
|
|
t.Logf("buffer message count: %d", len(msgs))
|
|
for _, m := range msgs {
|
|
t.Logf(" seq=%d role=%s type=%s streaming=%v",
|
|
m.Sequence, m.Role, m.Type, m.IsStreaming)
|
|
}
|
|
|
|
return resp
|
|
}
|
|
|
|
func contentString(t *testing.T, resp *agentcontext.Response) string {
|
|
t.Helper()
|
|
s, ok := resp.Completion.Content.(string)
|
|
require.True(t, ok, "Content should be string, got %T", resp.Completion.Content)
|
|
return s
|
|
}
|