- opencode SSE 스트림 파서 어댑터 추가 (opencode_sse.go) - 블랙박스/내부 테스트 파일 추가 - node/edge 애플리케이션의 CLI 어댑터 개선 - 설정 파일 및 README 업데이트 - agent-task opencode_sse_stream 추가
202 lines
5.7 KiB
Go
202 lines
5.7 KiB
Go
package cli
|
|
|
|
import (
|
|
"encoding/json"
|
|
"testing"
|
|
)
|
|
|
|
func TestParseOpencodeRunArgs(t *testing.T) {
|
|
args := []string{
|
|
"--attach", "http://127.0.0.1:9876",
|
|
"--model", "ollama-dgx/qwen3.6:35b-a3b-bf16",
|
|
"--agent", "build",
|
|
"--variant", "fast",
|
|
"--title", "demo",
|
|
"--dir", "/work",
|
|
"--dangerously-skip-permissions",
|
|
}
|
|
opts := parseOpencodeRunArgs(args)
|
|
if opts.AttachURL != "http://127.0.0.1:9876" {
|
|
t.Errorf("attach: got %q", opts.AttachURL)
|
|
}
|
|
if opts.Model != "ollama-dgx/qwen3.6:35b-a3b-bf16" {
|
|
t.Errorf("model: got %q", opts.Model)
|
|
}
|
|
if opts.Agent != "build" {
|
|
t.Errorf("agent: got %q", opts.Agent)
|
|
}
|
|
if opts.Variant != "fast" {
|
|
t.Errorf("variant: got %q", opts.Variant)
|
|
}
|
|
if opts.Title != "demo" {
|
|
t.Errorf("title: got %q", opts.Title)
|
|
}
|
|
if opts.Dir != "/work" {
|
|
t.Errorf("dir: got %q", opts.Dir)
|
|
}
|
|
if !opts.DangerouslySkipPermissions {
|
|
t.Error("dangerously-skip-permissions should be true")
|
|
}
|
|
}
|
|
|
|
func TestParseOpencodeRunArgs_NoAttach(t *testing.T) {
|
|
args := []string{"--title", "untitled", "--model", "m"}
|
|
opts := parseOpencodeRunArgs(args)
|
|
if opts.AttachURL != "" {
|
|
t.Errorf("expected empty attach, got %q", opts.AttachURL)
|
|
}
|
|
if opts.Title != "untitled" || opts.Model != "m" {
|
|
t.Errorf("unexpected: %+v", opts)
|
|
}
|
|
}
|
|
|
|
func TestOpencodeModelPayload(t *testing.T) {
|
|
cases := []struct {
|
|
in string
|
|
wantProv string
|
|
wantMod string
|
|
wantErr bool
|
|
}{
|
|
{"ollama-dgx/qwen3.6:35b-a3b-bf16", "ollama-dgx", "qwen3.6:35b-a3b-bf16", false},
|
|
{"anthropic/claude-sonnet-4-6", "anthropic", "claude-sonnet-4-6", false},
|
|
{"openrouter/google/gemini-2.5-pro", "openrouter", "google/gemini-2.5-pro", false},
|
|
{"missing-slash", "", "", true},
|
|
{"/missing-provider", "", "", true},
|
|
{"missing-model/", "", "", true},
|
|
}
|
|
for _, tc := range cases {
|
|
got, err := opencodeModelPayload(tc.in)
|
|
if tc.wantErr {
|
|
if err == nil {
|
|
t.Errorf("opencodeModelPayload(%q): expected error, got nil", tc.in)
|
|
}
|
|
continue
|
|
}
|
|
if err != nil {
|
|
t.Errorf("opencodeModelPayload(%q): unexpected error %v", tc.in, err)
|
|
continue
|
|
}
|
|
if got["providerID"] != tc.wantProv {
|
|
t.Errorf("opencodeModelPayload(%q) providerID = %v, want %q", tc.in, got["providerID"], tc.wantProv)
|
|
}
|
|
if got["modelID"] != tc.wantMod {
|
|
t.Errorf("opencodeModelPayload(%q) modelID = %v, want %q", tc.in, got["modelID"], tc.wantMod)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestOpencodeStatusIdle(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
props map[string]any
|
|
want bool
|
|
}{
|
|
{"string idle", map[string]any{"status": "idle"}, true},
|
|
{"object idle", map[string]any{"status": map[string]any{"type": "idle"}}, true},
|
|
{"object busy", map[string]any{"status": map[string]any{"type": "busy"}}, false},
|
|
{"string busy", map[string]any{"status": "busy"}, false},
|
|
{"missing", map[string]any{}, false},
|
|
{"non-map number", map[string]any{"status": 1.0}, false},
|
|
{"nil props", nil, false},
|
|
}
|
|
for _, tc := range cases {
|
|
if got := opencodeStatusIdle(tc.props); got != tc.want {
|
|
t.Errorf("opencodeStatusIdle(%s) = %v, want %v", tc.name, got, tc.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestOpencodeEventEnvelope(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
in string
|
|
want string
|
|
ok bool
|
|
}{
|
|
{
|
|
name: "plain event",
|
|
in: `{"id":"evt_1","type":"session.idle","properties":{"sessionID":"ses_1"}}`,
|
|
want: "session.idle",
|
|
ok: true,
|
|
},
|
|
{
|
|
name: "global payload event",
|
|
in: `{"directory":"/work","payload":{"id":"evt_1","type":"message.part.delta","properties":{"sessionID":"ses_1"}}}`,
|
|
want: "message.part.delta",
|
|
ok: true,
|
|
},
|
|
{
|
|
name: "sync event ignored",
|
|
in: `{"directory":"/work","payload":{"type":"sync","syncEvent":{"type":"message.part.delta.1"}}}`,
|
|
ok: false,
|
|
},
|
|
}
|
|
for _, tc := range cases {
|
|
got, ok := opencodeEventEnvelope([]byte(tc.in))
|
|
if ok != tc.ok {
|
|
t.Fatalf("%s: ok=%v, want %v", tc.name, ok, tc.ok)
|
|
}
|
|
if ok && got.Type != tc.want {
|
|
t.Fatalf("%s: type=%q, want %q", tc.name, got.Type, tc.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestOpencodeMessagePartDeltaText(t *testing.T) {
|
|
partTypes := map[string]string{
|
|
"prt_text": "text",
|
|
"prt_reasoning": "reasoning",
|
|
}
|
|
cases := []struct {
|
|
name string
|
|
props map[string]any
|
|
want string
|
|
}{
|
|
{"text part", map[string]any{"partID": "prt_text", "field": "text", "delta": "OK"}, "OK"},
|
|
{"reasoning part ignored", map[string]any{"partID": "prt_reasoning", "field": "text", "delta": "thinking"}, ""},
|
|
{"unknown part ignored", map[string]any{"partID": "prt_missing", "field": "text", "delta": "lost"}, ""},
|
|
{"non text field ignored", map[string]any{"partID": "prt_text", "field": "metadata", "delta": "lost"}, ""},
|
|
}
|
|
for _, tc := range cases {
|
|
if got := opencodeMessagePartDeltaText(tc.props, partTypes); got != tc.want {
|
|
t.Fatalf("%s: got %q, want %q", tc.name, got, tc.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestOpencodeStepTokensFromPart(t *testing.T) {
|
|
var props map[string]any
|
|
if err := json.Unmarshal([]byte(`{"part":{"type":"step-finish","tokens":{"input":7,"output":1}}}`), &props); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
in, out, ok := opencodeStepTokens(props)
|
|
if !ok || in != 7 || out != 1 {
|
|
t.Fatalf("tokens: got %d/%d ok=%v, want 7/1 true", in, out, ok)
|
|
}
|
|
}
|
|
|
|
func TestDecodeSSEDataLine(t *testing.T) {
|
|
cases := []struct {
|
|
in string
|
|
want string
|
|
shouldGet bool
|
|
}{
|
|
{"data: {\"a\":1}", `{"a":1}`, true},
|
|
{"data:{\"a\":1}", `{"a":1}`, true},
|
|
{"event: ping", "", false},
|
|
{": comment", "", false},
|
|
{"", "", false},
|
|
{"data:", "", false},
|
|
{"data: ", "", false},
|
|
}
|
|
for _, tc := range cases {
|
|
got, ok := decodeSSEDataLine([]byte(tc.in))
|
|
if ok != tc.shouldGet {
|
|
t.Errorf("decodeSSEDataLine(%q) ok=%v, want %v", tc.in, ok, tc.shouldGet)
|
|
continue
|
|
}
|
|
if ok && string(got) != tc.want {
|
|
t.Errorf("decodeSSEDataLine(%q) = %q, want %q", tc.in, string(got), tc.want)
|
|
}
|
|
}
|
|
}
|