mirror of
https://github.com/usestrix/strix.git
synced 2026-08-16 09:26:39 +02:00
285 lines
7.9 KiB
Go
285 lines
7.9 KiB
Go
package app
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"os"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/usestrix/strix/tui/internal/protocol"
|
|
)
|
|
|
|
func writeEnvelopeFrame(writer io.Writer, envelope protocol.Envelope) error {
|
|
raw, err := json.Marshal(envelope)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var header [4]byte
|
|
binary.BigEndian.PutUint32(header[:], uint32(len(raw)))
|
|
return writeAll(writer, append(header[:], raw...))
|
|
}
|
|
|
|
func readEnvelopeFrame(reader io.Reader) (protocol.Envelope, error) {
|
|
var header [4]byte
|
|
if _, err := io.ReadFull(reader, header[:]); err != nil {
|
|
return protocol.Envelope{}, err
|
|
}
|
|
raw := make([]byte, binary.BigEndian.Uint32(header[:]))
|
|
if _, err := io.ReadFull(reader, raw); err != nil {
|
|
return protocol.Envelope{}, err
|
|
}
|
|
var envelope protocol.Envelope
|
|
return envelope, json.Unmarshal(raw, &envelope)
|
|
}
|
|
|
|
func TestHandshakeValidatesHelloAndSendsReady(t *testing.T) {
|
|
server, connection := net.Pipe()
|
|
client := newClient(connection)
|
|
serverErr := make(chan error, 1)
|
|
go func() {
|
|
defer server.Close()
|
|
payload, _ := json.Marshal(protocol.Hello{Capabilities: protocol.Capabilities})
|
|
if err := writeEnvelopeFrame(server, protocol.Envelope{Version: protocol.Version, Type: "hello", Payload: payload}); err != nil {
|
|
serverErr <- err
|
|
return
|
|
}
|
|
var header [4]byte
|
|
if _, err := io.ReadFull(server, header[:]); err != nil {
|
|
serverErr <- err
|
|
return
|
|
}
|
|
raw := make([]byte, binary.BigEndian.Uint32(header[:]))
|
|
if _, err := io.ReadFull(server, raw); err != nil {
|
|
serverErr <- err
|
|
return
|
|
}
|
|
var ready protocol.Envelope
|
|
if err := json.Unmarshal(raw, &ready); err != nil {
|
|
serverErr <- err
|
|
return
|
|
}
|
|
var readyPayload protocol.Hello
|
|
if err := json.Unmarshal(ready.Payload, &readyPayload); err != nil {
|
|
serverErr <- err
|
|
return
|
|
}
|
|
if ready.Type != "ready" || ready.Version != protocol.Version || !reflect.DeepEqual(readyPayload.Capabilities, protocol.Capabilities) {
|
|
serverErr <- fmt.Errorf("unexpected ready: %#v %#v", ready, readyPayload)
|
|
return
|
|
}
|
|
serverErr <- nil
|
|
}()
|
|
|
|
if err := client.Handshake(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := <-serverErr; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestHandshakeRejectsMismatchBeforeReady(t *testing.T) {
|
|
server, connection := net.Pipe()
|
|
client := newClient(connection)
|
|
go func() {
|
|
defer server.Close()
|
|
payload, _ := json.Marshal(protocol.Hello{Capabilities: []string{"state-revisions"}})
|
|
_ = writeEnvelopeFrame(server, protocol.Envelope{Version: 2, Type: "hello", Payload: payload})
|
|
}()
|
|
|
|
err := client.Handshake()
|
|
if err == nil || !strings.Contains(err.Error(), "protocol mismatch") {
|
|
t.Fatalf("handshake error = %v, want protocol mismatch", err)
|
|
}
|
|
}
|
|
|
|
func TestReadRejectsOversizedCollectionLengthBeforePayload(t *testing.T) {
|
|
server, connection := net.Pipe()
|
|
client := newClient(connection)
|
|
written := make(chan error, 1)
|
|
go func() {
|
|
var header [4]byte
|
|
binary.BigEndian.PutUint32(header[:], maxCollectionBytes+1)
|
|
_, err := server.Write(header[:])
|
|
written <- err
|
|
}()
|
|
|
|
_, err := client.Read()
|
|
if err == nil || !strings.Contains(err.Error(), "invalid TUI IPC message size") {
|
|
t.Fatalf("read error = %v", err)
|
|
}
|
|
if err := <-written; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
server.Close()
|
|
}
|
|
|
|
func TestClientPreventsDuplicateCommandsAndRequiresExactCorrelation(t *testing.T) {
|
|
connection := &recordingConn{}
|
|
client := newClient(connection)
|
|
requestID, err := client.Send("setup.select_model", map[string]string{"model": "openai/gpt-5"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := client.Send("setup.select_model", map[string]string{"model": "openai/gpt-5.1"}); !errors.Is(err, ErrCommandPending) {
|
|
t.Fatalf("duplicate error = %v, want ErrCommandPending", err)
|
|
}
|
|
if client.Resolve("unknown", "setup.select_model") || client.Resolve(requestID, "models.list") {
|
|
t.Fatal("unknown or mismatched result resolved pending request")
|
|
}
|
|
if !client.Resolve(requestID, "setup.select_model") {
|
|
t.Fatal("exact result did not resolve pending request")
|
|
}
|
|
if _, err := client.Send("setup.select_model", map[string]string{"model": "openai/gpt-5.1"}); err != nil {
|
|
t.Fatalf("command remained blocked after success: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestClientRejectsOversizedCommandBeforeWrite(t *testing.T) {
|
|
connection := &recordingConn{}
|
|
client := newClient(connection)
|
|
_, err := client.Send("setup.set_instruction", map[string]string{"instruction": strings.Repeat("x", maxCommandBytes)})
|
|
if err == nil || !strings.Contains(err.Error(), "exceeds") {
|
|
t.Fatalf("oversized send error = %v", err)
|
|
}
|
|
if connection.Len() != 0 || len(client.pending) != 0 {
|
|
t.Fatal("oversized command was written or left pending")
|
|
}
|
|
}
|
|
|
|
func TestClientReadsCollectionFrameLargerThanOneMegabyte(t *testing.T) {
|
|
server, connection := net.Pipe()
|
|
client := &Client{conn: connection}
|
|
payload, err := json.Marshal(map[string]string{"content": string(bytes.Repeat([]byte("x"), 2<<20))})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
raw, err := json.Marshal(protocol.Envelope{
|
|
Version: protocol.Version,
|
|
Type: "collection_bootstrap",
|
|
Payload: payload,
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
writeErr := make(chan error, 1)
|
|
go func() {
|
|
defer server.Close()
|
|
var header [4]byte
|
|
binary.BigEndian.PutUint32(header[:], uint32(len(raw)))
|
|
if _, err := server.Write(header[:]); err != nil {
|
|
writeErr <- err
|
|
return
|
|
}
|
|
_, err := server.Write(raw)
|
|
writeErr <- err
|
|
}()
|
|
|
|
message, err := client.Read()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if message.Type != "collection_bootstrap" {
|
|
t.Fatalf("message type = %q, want collection_bootstrap", message.Type)
|
|
}
|
|
if err := <-writeErr; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestConnectFromEnvironmentAuthenticatesTCPTransport(t *testing.T) {
|
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer listener.Close()
|
|
|
|
t.Setenv("STRIX_TUI_ADDR", listener.Addr().String())
|
|
t.Setenv("STRIX_TUI_TOKEN", "one-use-token")
|
|
t.Setenv("STRIX_TUI_FD", "")
|
|
|
|
serverErr := make(chan error, 1)
|
|
go func() {
|
|
connection, acceptErr := listener.Accept()
|
|
if acceptErr != nil {
|
|
serverErr <- acceptErr
|
|
return
|
|
}
|
|
defer connection.Close()
|
|
token := make([]byte, len("one-use-token"))
|
|
if _, readErr := io.ReadFull(connection, token); readErr != nil {
|
|
serverErr <- readErr
|
|
return
|
|
}
|
|
if string(token) != "one-use-token" {
|
|
serverErr <- os.ErrPermission
|
|
return
|
|
}
|
|
raw, marshalErr := json.Marshal(protocol.Envelope{
|
|
Version: protocol.Version,
|
|
Type: "hello",
|
|
Payload: json.RawMessage(`{}`),
|
|
})
|
|
if marshalErr != nil {
|
|
serverErr <- marshalErr
|
|
return
|
|
}
|
|
var header [4]byte
|
|
binary.BigEndian.PutUint32(header[:], uint32(len(raw)))
|
|
if writeErr := writeAll(connection, append(header[:], raw...)); writeErr != nil {
|
|
serverErr <- writeErr
|
|
return
|
|
}
|
|
serverErr <- nil
|
|
}()
|
|
|
|
client, err := ConnectFromEnvironment()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer client.Close()
|
|
message, err := client.Read()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if message.Type != "hello" {
|
|
t.Fatalf("message type = %q, want hello", message.Type)
|
|
}
|
|
if err := <-serverErr; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if os.Getenv("STRIX_TUI_ADDR") != "" || os.Getenv("STRIX_TUI_TOKEN") != "" {
|
|
t.Fatal("TCP transport credentials were not removed from the environment")
|
|
}
|
|
}
|
|
|
|
func TestConnectFromEnvironmentRequiresCompleteTransport(t *testing.T) {
|
|
t.Setenv("STRIX_TUI_FD", "")
|
|
t.Setenv("STRIX_TUI_ADDR", "127.0.0.1:1")
|
|
t.Setenv("STRIX_TUI_TOKEN", "")
|
|
|
|
_, err := ConnectFromEnvironment()
|
|
if err == nil || !strings.Contains(err.Error(), "STRIX_TUI_ADDR and STRIX_TUI_TOKEN") {
|
|
t.Fatalf("error = %v, want missing transport error", err)
|
|
}
|
|
}
|
|
|
|
func TestConnectFromEnvironmentPrefersInheritedDescriptor(t *testing.T) {
|
|
t.Setenv("STRIX_TUI_FD", "not-a-number")
|
|
t.Setenv("STRIX_TUI_ADDR", "127.0.0.1:1")
|
|
t.Setenv("STRIX_TUI_TOKEN", "token")
|
|
|
|
_, err := ConnectFromEnvironment()
|
|
if err == nil || !strings.Contains(err.Error(), "invalid STRIX_TUI_FD") {
|
|
t.Fatalf("error = %v, want inherited descriptor parse error", err)
|
|
}
|
|
}
|