You've already forked agentic-coding-workflow
feat(acp): JSON-RPC 2.0 codec (number/string id, LF-delimited, concurrent-safe encoder)
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
package acp_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/yan1h/agent-coding-workflow/internal/acp"
|
||||
)
|
||||
|
||||
func TestJSONRPC_RoundtripRequestNumberID(t *testing.T) {
|
||||
t.Parallel()
|
||||
req, err := acp.NewRequestInt(42, "session/prompt", map[string]any{"text": "hi"})
|
||||
require.NoError(t, err)
|
||||
|
||||
var buf bytes.Buffer
|
||||
require.NoError(t, acp.NewEncoder(&buf).Encode(req))
|
||||
|
||||
dec := acp.NewDecoder(&buf, 0)
|
||||
msg, err := dec.DecodeMessage()
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.True(t, msg.IsRequest())
|
||||
assert.Equal(t, "session/prompt", msg.Method)
|
||||
id, ok := msg.IDInt64()
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, int64(42), id)
|
||||
}
|
||||
|
||||
func TestJSONRPC_RoundtripStringID(t *testing.T) {
|
||||
t.Parallel()
|
||||
idJSON, _ := json.Marshal("client-abc")
|
||||
req := &acp.Message{JSONRPC: "2.0", ID: idJSON, Method: "session/prompt"}
|
||||
|
||||
var buf bytes.Buffer
|
||||
require.NoError(t, acp.NewEncoder(&buf).Encode(req))
|
||||
|
||||
dec := acp.NewDecoder(&buf, 0)
|
||||
msg, _ := dec.DecodeMessage()
|
||||
s, ok := msg.IDString()
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "client-abc", s)
|
||||
|
||||
_, ok2 := msg.IDInt64()
|
||||
assert.False(t, ok2)
|
||||
}
|
||||
|
||||
func TestJSONRPC_Notification_NoID(t *testing.T) {
|
||||
t.Parallel()
|
||||
notif, _ := acp.NewNotification("session/update", map[string]string{"x": "y"})
|
||||
|
||||
var buf bytes.Buffer
|
||||
require.NoError(t, acp.NewEncoder(&buf).Encode(notif))
|
||||
|
||||
msg, _ := acp.NewDecoder(&buf, 0).DecodeMessage()
|
||||
assert.True(t, msg.IsNotification())
|
||||
assert.False(t, msg.IsRequest())
|
||||
assert.False(t, msg.IsResponse())
|
||||
}
|
||||
|
||||
func TestJSONRPC_Response(t *testing.T) {
|
||||
t.Parallel()
|
||||
idJSON, _ := json.Marshal(int64(7))
|
||||
resp, _ := acp.NewResponseOK(idJSON, map[string]string{"sessionId": "abc"})
|
||||
|
||||
var buf bytes.Buffer
|
||||
require.NoError(t, acp.NewEncoder(&buf).Encode(resp))
|
||||
|
||||
msg, _ := acp.NewDecoder(&buf, 0).DecodeMessage()
|
||||
assert.True(t, msg.IsResponse())
|
||||
|
||||
var result struct {
|
||||
SessionID string `json:"sessionId"`
|
||||
}
|
||||
require.NoError(t, json.Unmarshal(msg.Result, &result))
|
||||
assert.Equal(t, "abc", result.SessionID)
|
||||
}
|
||||
|
||||
func TestJSONRPC_ErrorResponse(t *testing.T) {
|
||||
t.Parallel()
|
||||
idJSON, _ := json.Marshal("client-1")
|
||||
resp := acp.NewResponseErr(idJSON, acp.JSONRPCMethodNotFound, "no such method")
|
||||
|
||||
var buf bytes.Buffer
|
||||
require.NoError(t, acp.NewEncoder(&buf).Encode(resp))
|
||||
|
||||
msg, _ := acp.NewDecoder(&buf, 0).DecodeMessage()
|
||||
require.NotNil(t, msg.Error)
|
||||
assert.Equal(t, acp.JSONRPCMethodNotFound, msg.Error.Code)
|
||||
assert.Equal(t, "no such method", msg.Error.Message)
|
||||
}
|
||||
|
||||
func TestJSONRPC_DecoderEOF(t *testing.T) {
|
||||
t.Parallel()
|
||||
dec := acp.NewDecoder(bytes.NewReader(nil), 0)
|
||||
_, err := dec.DecodeMessage()
|
||||
assert.ErrorIs(t, err, io.EOF)
|
||||
}
|
||||
|
||||
func TestJSONRPC_DecoderMaxSize(t *testing.T) {
|
||||
t.Parallel()
|
||||
huge := make([]byte, 200)
|
||||
for i := range huge {
|
||||
huge[i] = 'a'
|
||||
}
|
||||
body := []byte(`{"jsonrpc":"2.0","method":"x","params":"`)
|
||||
body = append(body, huge...)
|
||||
body = append(body, []byte(`"}`+"\n")...)
|
||||
|
||||
dec := acp.NewDecoder(bytes.NewReader(body), 100)
|
||||
_, err := dec.DecodeMessage()
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "exceeds max size")
|
||||
}
|
||||
|
||||
func TestJSONRPC_DecoderBadJSON(t *testing.T) {
|
||||
t.Parallel()
|
||||
dec := acp.NewDecoder(bytes.NewReader([]byte("not-json\n")), 0)
|
||||
_, err := dec.DecodeMessage()
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestJSONRPC_EncoderConcurrentSafe(t *testing.T) {
|
||||
t.Parallel()
|
||||
var buf bytes.Buffer
|
||||
enc := acp.NewEncoder(&buf)
|
||||
|
||||
const N = 100
|
||||
done := make(chan struct{}, N)
|
||||
for i := 0; i < N; i++ {
|
||||
go func(id int64) {
|
||||
req, _ := acp.NewRequestInt(id, "m", nil)
|
||||
_ = enc.Encode(req)
|
||||
done <- struct{}{}
|
||||
}(int64(i))
|
||||
}
|
||||
for i := 0; i < N; i++ {
|
||||
<-done
|
||||
}
|
||||
|
||||
dec := acp.NewDecoder(&buf, 0)
|
||||
count := 0
|
||||
for {
|
||||
_, err := dec.DecodeMessage()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
require.NoError(t, err)
|
||||
count++
|
||||
}
|
||||
assert.Equal(t, N, count)
|
||||
}
|
||||
Reference in New Issue
Block a user