refactor: reorganize cli adapters test structure
- Remove redundant test files (cancel_reason_test, cli_test, codex_exec_args_test, emitters_internal_test, factory_test, registry_test) - Consolidate adapter tests into adapters_blackbox_test.go - Add cli_internal_test.go for internal CLI tests - Update lifecycle_blackbox_test.go
This commit is contained in:
parent
0fb7c67b18
commit
af0da6b822
8 changed files with 638 additions and 889 deletions
|
|
@ -12,12 +12,80 @@ import (
|
|||
iop "iop/proto/gen/iop"
|
||||
)
|
||||
|
||||
// lifecycleAdapter records Start/Stop calls.
|
||||
// --- BuildFromPayload tests ---
|
||||
|
||||
func TestBuildFromPayload_MockAlwaysPresent(t *testing.T) {
|
||||
reg, err := adapters.BuildFromPayload(&iop.NodeConfigPayload{}, zap.NewNop())
|
||||
if err != nil {
|
||||
t.Fatalf("build from payload: %v", err)
|
||||
}
|
||||
if _, ok := reg.Get("mock"); !ok {
|
||||
t.Fatal("expected mock adapter to be registered")
|
||||
}
|
||||
if got := len(reg.All()); got != 1 {
|
||||
t.Fatalf("expected 1 adapter, got %d", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildFromPayload_OllamaEnabled(t *testing.T) {
|
||||
reg, err := adapters.BuildFromPayload(&iop.NodeConfigPayload{
|
||||
Adapters: []*iop.AdapterConfig{
|
||||
{
|
||||
Type: "ollama",
|
||||
Enabled: true,
|
||||
Config: &iop.AdapterConfig_Ollama{Ollama: &iop.OllamaAdapterConfig{BaseUrl: "http://localhost:11434"}},
|
||||
},
|
||||
},
|
||||
}, zap.NewNop())
|
||||
if err != nil {
|
||||
t.Fatalf("build from payload: %v", err)
|
||||
}
|
||||
if _, ok := reg.Get("mock"); !ok {
|
||||
t.Fatal("expected mock adapter to be registered")
|
||||
}
|
||||
if _, ok := reg.Get("ollama"); !ok {
|
||||
t.Fatal("expected ollama adapter to be registered")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildFromPayload_MultipleAdapters(t *testing.T) {
|
||||
reg, err := adapters.BuildFromPayload(&iop.NodeConfigPayload{
|
||||
Adapters: []*iop.AdapterConfig{
|
||||
{Type: "ollama", Enabled: true, Config: &iop.AdapterConfig_Ollama{Ollama: &iop.OllamaAdapterConfig{BaseUrl: "x"}}},
|
||||
{Type: "vllm", Enabled: true, Config: &iop.AdapterConfig_Vllm{Vllm: &iop.VllmAdapterConfig{Endpoint: "y"}}},
|
||||
{Type: "cli", Enabled: true, Config: &iop.AdapterConfig_Cli{Cli: &iop.CLIAdapterConfig{
|
||||
Profiles: map[string]*iop.CLIProfileConfig{
|
||||
"codex": {Command: "codex", Persistent: true, ResponseIdleTimeoutMs: 1500, StartupIdleTimeoutMs: 300},
|
||||
},
|
||||
}}},
|
||||
},
|
||||
}, zap.NewNop())
|
||||
if err != nil {
|
||||
t.Fatalf("build from payload: %v", err)
|
||||
}
|
||||
for _, name := range []string{"mock", "ollama", "vllm", "cli"} {
|
||||
if _, ok := reg.Get(name); !ok {
|
||||
t.Fatalf("expected %s adapter to be registered", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildFromPayload_UnknownType(t *testing.T) {
|
||||
_, err := adapters.BuildFromPayload(&iop.NodeConfigPayload{
|
||||
Adapters: []*iop.AdapterConfig{
|
||||
{Type: "unknown", Enabled: true},
|
||||
},
|
||||
}, zap.NewNop())
|
||||
if err == nil {
|
||||
t.Fatal("expected unknown adapter type error")
|
||||
}
|
||||
}
|
||||
|
||||
// --- Registry lifecycle tests ---
|
||||
|
||||
type lifecycleAdapter struct {
|
||||
name string
|
||||
started []string
|
||||
stopped []string
|
||||
log *[]string
|
||||
name string
|
||||
log *[]string
|
||||
}
|
||||
|
||||
func (a *lifecycleAdapter) Name() string { return a.name }
|
||||
|
|
@ -36,6 +104,49 @@ func (a *lifecycleAdapter) Stop(_ context.Context) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
type failingLifecycleAdapter struct {
|
||||
name string
|
||||
log *[]string
|
||||
}
|
||||
|
||||
func (a *failingLifecycleAdapter) Name() string { return a.name }
|
||||
func (a *failingLifecycleAdapter) Capabilities(_ context.Context) (noderuntime.Capabilities, error) {
|
||||
return noderuntime.Capabilities{AdapterName: a.name}, nil
|
||||
}
|
||||
func (a *failingLifecycleAdapter) Execute(_ context.Context, _ noderuntime.ExecutionSpec, _ noderuntime.EventSink) error {
|
||||
return nil
|
||||
}
|
||||
func (a *failingLifecycleAdapter) Start(_ context.Context) error {
|
||||
*a.log = append(*a.log, "start:"+a.name)
|
||||
return fmt.Errorf("start failed: %s", a.name)
|
||||
}
|
||||
func (a *failingLifecycleAdapter) Stop(_ context.Context) error {
|
||||
*a.log = append(*a.log, "stop:"+a.name)
|
||||
return nil
|
||||
}
|
||||
|
||||
type failingStopAdapter struct {
|
||||
name string
|
||||
log *[]string
|
||||
stopErr error
|
||||
}
|
||||
|
||||
func (a *failingStopAdapter) Name() string { return a.name }
|
||||
func (a *failingStopAdapter) Capabilities(_ context.Context) (noderuntime.Capabilities, error) {
|
||||
return noderuntime.Capabilities{AdapterName: a.name}, nil
|
||||
}
|
||||
func (a *failingStopAdapter) Execute(_ context.Context, _ noderuntime.ExecutionSpec, _ noderuntime.EventSink) error {
|
||||
return nil
|
||||
}
|
||||
func (a *failingStopAdapter) Start(_ context.Context) error {
|
||||
*a.log = append(*a.log, "start:"+a.name)
|
||||
return nil
|
||||
}
|
||||
func (a *failingStopAdapter) Stop(_ context.Context) error {
|
||||
*a.log = append(*a.log, "stop:"+a.name)
|
||||
return a.stopErr
|
||||
}
|
||||
|
||||
func TestRegistryLifecycle_StartStopOrder(t *testing.T) {
|
||||
log := []string{}
|
||||
reg := adapters.NewRegistry()
|
||||
|
|
@ -61,28 +172,6 @@ func TestRegistryLifecycle_StartStopOrder(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// failingLifecycleAdapter starts successfully only once then always errors.
|
||||
type failingLifecycleAdapter struct {
|
||||
name string
|
||||
log *[]string
|
||||
}
|
||||
|
||||
func (a *failingLifecycleAdapter) Name() string { return a.name }
|
||||
func (a *failingLifecycleAdapter) Capabilities(_ context.Context) (noderuntime.Capabilities, error) {
|
||||
return noderuntime.Capabilities{AdapterName: a.name}, nil
|
||||
}
|
||||
func (a *failingLifecycleAdapter) Execute(_ context.Context, _ noderuntime.ExecutionSpec, _ noderuntime.EventSink) error {
|
||||
return nil
|
||||
}
|
||||
func (a *failingLifecycleAdapter) Start(_ context.Context) error {
|
||||
*a.log = append(*a.log, "start:"+a.name)
|
||||
return fmt.Errorf("start failed: %s", a.name)
|
||||
}
|
||||
func (a *failingLifecycleAdapter) Stop(_ context.Context) error {
|
||||
*a.log = append(*a.log, "stop:"+a.name)
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestRegistryLifecycle_StartFailureStopsStartedAdapters(t *testing.T) {
|
||||
log := []string{}
|
||||
reg := adapters.NewRegistry()
|
||||
|
|
@ -105,33 +194,9 @@ func TestRegistryLifecycle_StartFailureStopsStartedAdapters(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// failingStopAdapter stops successfully once then always errors.
|
||||
type failingStopAdapter struct {
|
||||
name string
|
||||
log *[]string
|
||||
stopErr error
|
||||
}
|
||||
|
||||
func (a *failingStopAdapter) Name() string { return a.name }
|
||||
func (a *failingStopAdapter) Capabilities(_ context.Context) (noderuntime.Capabilities, error) {
|
||||
return noderuntime.Capabilities{AdapterName: a.name}, nil
|
||||
}
|
||||
func (a *failingStopAdapter) Execute(_ context.Context, _ noderuntime.ExecutionSpec, _ noderuntime.EventSink) error {
|
||||
return nil
|
||||
}
|
||||
func (a *failingStopAdapter) Start(_ context.Context) error {
|
||||
*a.log = append(*a.log, "start:"+a.name)
|
||||
return nil
|
||||
}
|
||||
func (a *failingStopAdapter) Stop(_ context.Context) error {
|
||||
*a.log = append(*a.log, "stop:"+a.name)
|
||||
return a.stopErr
|
||||
}
|
||||
|
||||
func TestRegistryLifecycle_StopContinuesOnFailingAdapter(t *testing.T) {
|
||||
log := []string{}
|
||||
reg := adapters.NewRegistry()
|
||||
// first stops successfully, second fails
|
||||
reg.Register(&lifecycleAdapter{name: "first", log: &log})
|
||||
reg.Register(&failingStopAdapter{name: "second", log: &log, stopErr: fmt.Errorf("stop failed: second")})
|
||||
|
||||
|
|
@ -139,14 +204,10 @@ func TestRegistryLifecycle_StopContinuesOnFailingAdapter(t *testing.T) {
|
|||
if err := reg.Start(ctx); err != nil {
|
||||
t.Fatalf("Start: %v", err)
|
||||
}
|
||||
|
||||
err := reg.Stop(ctx)
|
||||
// Should return error because second adapter fails
|
||||
if err == nil {
|
||||
if err := reg.Stop(ctx); err == nil {
|
||||
t.Fatal("expected non-nil error from Stop")
|
||||
}
|
||||
|
||||
// Both adapters should have been stopped regardless of error
|
||||
want := []string{"start:first", "start:second", "stop:second", "stop:first"}
|
||||
if len(log) != len(want) {
|
||||
t.Fatalf("expected log %v, got %v", want, log)
|
||||
|
|
@ -164,7 +225,6 @@ func TestRegistryLifecycle_NonLifecycleAdapterSkipped(t *testing.T) {
|
|||
t.Fatalf("build: %v", err)
|
||||
}
|
||||
ctx := context.Background()
|
||||
// mock adapter has no lifecycle — should not panic or error
|
||||
if err := reg.Start(ctx); err != nil {
|
||||
t.Fatalf("Start: %v", err)
|
||||
}
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCancelEventForContext_DeadlineMapsToTimeout(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 0)
|
||||
defer cancel()
|
||||
<-ctx.Done()
|
||||
got := cancelEventForContext(ctx.Err())
|
||||
if got != "timeout" {
|
||||
t.Fatalf("expected 'timeout', got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCancelEventForContext_CanceledMapsToUserCancel(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
<-ctx.Done()
|
||||
got := cancelEventForContext(ctx.Err())
|
||||
if got != "user-cancel" {
|
||||
t.Fatalf("expected 'user-cancel', got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCancelEventForContext_NilError(t *testing.T) {
|
||||
got := cancelEventForContext(nil)
|
||||
if got != "context-done" {
|
||||
t.Fatalf("expected 'context-done', got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCancelEventForContext_SiblingError(t *testing.T) {
|
||||
type customErr struct{ error }
|
||||
got := cancelEventForContext(customErr{io.EOF})
|
||||
if got != "context-done" {
|
||||
t.Fatalf("expected 'context-done', got %q", got)
|
||||
}
|
||||
}
|
||||
424
apps/node/internal/adapters/cli/cli_internal_test.go
Normal file
424
apps/node/internal/adapters/cli/cli_internal_test.go
Normal file
|
|
@ -0,0 +1,424 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"iop/apps/node/internal/runtime"
|
||||
"iop/packages/config"
|
||||
)
|
||||
|
||||
// --- cancel reason tests ---
|
||||
|
||||
func TestCancelEventForContext_DeadlineMapsToTimeout(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 0)
|
||||
defer cancel()
|
||||
<-ctx.Done()
|
||||
got := cancelEventForContext(ctx.Err())
|
||||
if got != "timeout" {
|
||||
t.Fatalf("expected 'timeout', got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCancelEventForContext_CanceledMapsToUserCancel(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
<-ctx.Done()
|
||||
got := cancelEventForContext(ctx.Err())
|
||||
if got != "user-cancel" {
|
||||
t.Fatalf("expected 'user-cancel', got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCancelEventForContext_NilError(t *testing.T) {
|
||||
got := cancelEventForContext(nil)
|
||||
if got != "context-done" {
|
||||
t.Fatalf("expected 'context-done', got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCancelEventForContext_SiblingError(t *testing.T) {
|
||||
type customErr struct{ error }
|
||||
got := cancelEventForContext(customErr{io.EOF})
|
||||
if got != "context-done" {
|
||||
t.Fatalf("expected 'context-done', got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
// --- codex exec args tests ---
|
||||
|
||||
func TestCodexExecArgs_NoSessionUsesProfileArgs(t *testing.T) {
|
||||
profile := config.CLIProfileConf{
|
||||
Command: "codex",
|
||||
Args: []string{"exec", "--json"},
|
||||
}
|
||||
|
||||
args := codexExecArgs(profile, "", "hello prompt")
|
||||
expected := []string{"exec", "--json", "hello prompt"}
|
||||
if len(args) != len(expected) {
|
||||
t.Fatalf("expected %d args, got %d: %v", len(expected), len(args), args)
|
||||
}
|
||||
for i, v := range expected {
|
||||
if args[i] != v {
|
||||
t.Errorf("args[%d]: expected %q, got %q", i, v, args[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCodexExecArgs_WithSessionUsesResumeArgs(t *testing.T) {
|
||||
profile := config.CLIProfileConf{
|
||||
Args: []string{"exec", "--json"},
|
||||
ResumeArgs: []string{"exec", "resume", "--color", "never"},
|
||||
}
|
||||
|
||||
args := codexExecArgs(profile, "session-abc", "hello prompt")
|
||||
expected := []string{"exec", "resume", "--color", "never", "session-abc", "hello prompt"}
|
||||
if len(args) != len(expected) {
|
||||
t.Fatalf("expected %d args, got %d: %v", len(expected), len(args), args)
|
||||
}
|
||||
for i, v := range expected {
|
||||
if args[i] != v {
|
||||
t.Errorf("args[%d]: expected %q, got %q", i, v, args[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- driveJSONLines tests ---
|
||||
|
||||
type testSink struct {
|
||||
events []runtime.RuntimeEvent
|
||||
}
|
||||
|
||||
func (s *testSink) Emit(_ context.Context, e runtime.RuntimeEvent) error {
|
||||
s.events = append(s.events, e)
|
||||
return nil
|
||||
}
|
||||
|
||||
type mockLineEmitter struct {
|
||||
name string
|
||||
emitFn func(line string) ([]runtime.RuntimeEvent, error)
|
||||
}
|
||||
|
||||
func (m *mockLineEmitter) Name() string { return m.name }
|
||||
func (m *mockLineEmitter) Emit(line string) ([]runtime.RuntimeEvent, error) {
|
||||
if m.emitFn != nil {
|
||||
return m.emitFn(line)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func TestDriveJSONLines_DispatchesEmitterEvents(t *testing.T) {
|
||||
input := `{"type":"message","role":"assistant","content":"hello"}
|
||||
not-json
|
||||
{"type":"message","role":"assistant","content":"world"}`
|
||||
outBuf := &strings.Builder{}
|
||||
sink := &testSink{}
|
||||
outReader := strings.NewReader(input + "\n")
|
||||
|
||||
mockEmitter := &mockLineEmitter{
|
||||
name: "mock",
|
||||
emitFn: func(line string) ([]runtime.RuntimeEvent, error) {
|
||||
return []runtime.RuntimeEvent{
|
||||
{Type: runtime.EventTypeDelta, Delta: "a:" + line},
|
||||
{Type: runtime.EventTypeDelta, Delta: "b:" + line},
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
|
||||
outputTokens, err := driveJSONLines(context.Background(), outReader, sink, "run-1", outBuf, mockEmitter, 4*1024*1024)
|
||||
if err != nil {
|
||||
t.Fatalf("driveJSONLines: %v", err)
|
||||
}
|
||||
if got := len(sink.events); got != 4 {
|
||||
t.Fatalf("expected 4 events, got %d", got)
|
||||
}
|
||||
for i, ev := range sink.events {
|
||||
if ev.RunID != "run-1" {
|
||||
t.Errorf("event %d: RunID = %q, want %q", i, ev.RunID, "run-1")
|
||||
}
|
||||
if ev.Timestamp.IsZero() {
|
||||
t.Errorf("event %d: Timestamp is zero", i)
|
||||
}
|
||||
if ev.Type == runtime.EventTypeDelta {
|
||||
outputTokens += len(strings.Fields(ev.Delta))
|
||||
}
|
||||
}
|
||||
raw := outBuf.String()
|
||||
if !strings.Contains(raw, `{"type":"message"`) {
|
||||
t.Fatalf("outBuf missing JSON line: %q", raw)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDriveJSONLines_StopsOnEmitterError(t *testing.T) {
|
||||
input := `{"type":"text"}
|
||||
{"type":"error"}`
|
||||
outBuf := &strings.Builder{}
|
||||
sink := &testSink{}
|
||||
outReader := strings.NewReader(input + "\n")
|
||||
|
||||
callCount := 0
|
||||
mockEmitter := &mockLineEmitter{
|
||||
name: "mock",
|
||||
emitFn: func(line string) ([]runtime.RuntimeEvent, error) {
|
||||
callCount++
|
||||
if callCount == 2 {
|
||||
return nil, errors.New("emitter failure")
|
||||
}
|
||||
return []runtime.RuntimeEvent{{Type: runtime.EventTypeDelta, Delta: "ok"}}, nil
|
||||
},
|
||||
}
|
||||
|
||||
_, err := driveJSONLines(context.Background(), outReader, sink, "run-2", outBuf, mockEmitter, 4*1024*1024)
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
if err.Error() != "emitter failure" {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if callCount != 2 {
|
||||
t.Fatalf("expected emitter called 2 times, got %d", callCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDriveJSONLines_AccumulatesRawOutput(t *testing.T) {
|
||||
lines := `{"type":"text","part":{"type":"text","text":"a"}}
|
||||
{"type":"text","part":{"type":"text","text":"b"}}`
|
||||
outBuf := &strings.Builder{}
|
||||
sink := &testSink{}
|
||||
outReader := strings.NewReader(lines + "\n")
|
||||
|
||||
outputTokens, err := driveJSONLines(context.Background(), outReader, sink, "run-int", outBuf, opencodeJSONEmitter{}, 4*1024*1024)
|
||||
if err != nil {
|
||||
t.Fatalf("driveJSONLines: %v", err)
|
||||
}
|
||||
raw := outBuf.String()
|
||||
if !strings.Contains(raw, `{"type":"text"`) {
|
||||
t.Fatalf("outBuf missing expected line: %q", raw)
|
||||
}
|
||||
if newlineCount := strings.Count(raw, "\n"); newlineCount != 2 {
|
||||
t.Fatalf("expected 2 newlines in outBuf, got %d", newlineCount)
|
||||
}
|
||||
if outputTokens != 2 {
|
||||
t.Fatalf("expected 2 outputTokens, got %d", outputTokens)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDriveJSONLines_ScannerBufferMax(t *testing.T) {
|
||||
longLine := strings.Repeat("x", 100) + `{"type":"text"}`
|
||||
outBuf := &strings.Builder{}
|
||||
sink := &testSink{}
|
||||
outReader := strings.NewReader(longLine + "\n")
|
||||
|
||||
outputTokens, err := driveJSONLines(context.Background(), outReader, sink, "run-buf", outBuf, &mockLineEmitter{name: "buf"}, 50)
|
||||
if err != nil {
|
||||
t.Fatalf("driveJSONLines: %v", err)
|
||||
}
|
||||
if outputTokens != 0 {
|
||||
t.Fatalf("expected 0 outputTokens for line too long for buffer, got %d", outputTokens)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDriveJSONLines_SkipsEmptyAndNonJSONLines(t *testing.T) {
|
||||
input := "\n\nnot json at all\n \n{\"type\":\"text\"}\n"
|
||||
outBuf := &strings.Builder{}
|
||||
sink := &testSink{}
|
||||
outReader := strings.NewReader(input)
|
||||
|
||||
callCount := 0
|
||||
mockEmitter := &mockLineEmitter{
|
||||
name: "mock",
|
||||
emitFn: func(line string) ([]runtime.RuntimeEvent, error) {
|
||||
callCount++
|
||||
return []runtime.RuntimeEvent{{Type: runtime.EventTypeDelta, Delta: line}}, nil
|
||||
},
|
||||
}
|
||||
|
||||
_, err := driveJSONLines(context.Background(), outReader, sink, "run-skip", outBuf, mockEmitter, 4*1024*1024)
|
||||
if err != nil {
|
||||
t.Fatalf("driveJSONLines: %v", err)
|
||||
}
|
||||
if callCount != 1 {
|
||||
t.Fatalf("expected emitter called 1 time, got %d", callCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDriveJSONLines_OutputTokensCountedForDeltaOnly(t *testing.T) {
|
||||
outBuf := &strings.Builder{}
|
||||
sink := &testSink{}
|
||||
outReader := strings.NewReader(`{"type":"error"}
|
||||
{"type":"delta"}` + "\n")
|
||||
|
||||
mockEmitter := &mockLineEmitter{
|
||||
name: "mock",
|
||||
emitFn: func(line string) ([]runtime.RuntimeEvent, error) {
|
||||
if strings.Contains(line, "error") {
|
||||
return []runtime.RuntimeEvent{{Type: runtime.EventTypeError, Error: "bad"}}, nil
|
||||
}
|
||||
return []runtime.RuntimeEvent{{Type: runtime.EventTypeDelta, Delta: "one two three"}}, nil
|
||||
},
|
||||
}
|
||||
|
||||
outputTokens, err := driveJSONLines(context.Background(), outReader, sink, "run-tokens", outBuf, mockEmitter, 4*1024*1024)
|
||||
if err != nil {
|
||||
t.Fatalf("driveJSONLines: %v", err)
|
||||
}
|
||||
if outputTokens != 3 {
|
||||
t.Fatalf("expected 3 outputTokens, got %d", outputTokens)
|
||||
}
|
||||
}
|
||||
|
||||
// --- emitter unit tests (edge cases not covered by blackbox tests) ---
|
||||
|
||||
func TestStreamJSONEmitter_SkipsNonAssistantRoles(t *testing.T) {
|
||||
e := streamJSONEmitter{}
|
||||
events, _ := e.Emit(`{"type":"message","role":"user","content":"hi"}`)
|
||||
if len(events) != 0 {
|
||||
t.Fatalf("expected 0 events for user role, got %d", len(events))
|
||||
}
|
||||
}
|
||||
|
||||
func TestStreamJSONEmitter_ErrorEvent(t *testing.T) {
|
||||
e := streamJSONEmitter{}
|
||||
events, _ := e.Emit(`{"type":"error","error":"something broke"}`)
|
||||
if len(events) != 1 || events[0].Type != runtime.EventTypeError || events[0].Error != "something broke" {
|
||||
t.Fatalf("unexpected events: %+v", events)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStreamJSONEmitter_EmptyContentSkipped(t *testing.T) {
|
||||
e := streamJSONEmitter{}
|
||||
events, _ := e.Emit(`{"type":"message","role":"assistant","content":""}`)
|
||||
if len(events) != 0 {
|
||||
t.Fatalf("expected 0 events for empty content, got %d", len(events))
|
||||
}
|
||||
}
|
||||
|
||||
func TestClaudeJSONEmitter_ErrorResult(t *testing.T) {
|
||||
e := claudeJSONEmitter{}
|
||||
events, _ := e.Emit(`{"type":"result","is_error":true,"result":"API timeout"}`)
|
||||
if len(events) != 1 || events[0].Type != runtime.EventTypeError || events[0].Error != "API timeout" {
|
||||
t.Fatalf("unexpected events: %+v", events)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClaudeJSONEmitter_NonTextDeltaSkipped(t *testing.T) {
|
||||
e := claudeJSONEmitter{}
|
||||
events, _ := e.Emit(`{"type":"stream_event","event":{"type":"content_block_delta","delta":{"type":"image_delta","data":"base64"}}}`)
|
||||
if len(events) != 0 {
|
||||
t.Fatalf("expected 0 events for non-text delta, got %d", len(events))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCodexJSONEmitter_ItemDeltaBecomesDelta(t *testing.T) {
|
||||
e := codexJSONEmitter{}
|
||||
events, _ := e.Emit(`{"type":"item.delta","item":{"type":"agent_message","delta":"Doing."}}`)
|
||||
if len(events) != 1 || events[0].Delta != "Doing." {
|
||||
t.Fatalf("unexpected events: %+v", events)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCodexJSONEmitter_OutputTextDeltaBecomesDelta(t *testing.T) {
|
||||
e := codexJSONEmitter{}
|
||||
events, _ := e.Emit(`{"type":"response.output_text.delta","delta":"chunk"}`)
|
||||
if len(events) != 1 || events[0].Delta != "chunk" {
|
||||
t.Fatalf("unexpected events: %+v", events)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCodexJSONEmitter_ContentOutputTextFallsBackToDelta(t *testing.T) {
|
||||
e := codexJSONEmitter{}
|
||||
events, _ := e.Emit(`{"type":"item.updated","item":{"type":"agent_message","message":{"content":[{"type":"output_text","text":"partial text"}]}}}`)
|
||||
if len(events) != 1 || events[0].Delta != "partial text" {
|
||||
t.Fatalf("unexpected events: %+v", events)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCodexJSONEmitter_TurnFailedBecomesError(t *testing.T) {
|
||||
e := codexJSONEmitter{}
|
||||
events, _ := e.Emit(`{"type":"turn.failed","error":{"message":"quota exceeded"}}`)
|
||||
if len(events) != 1 || events[0].Type != runtime.EventTypeError || events[0].Error != "quota exceeded" {
|
||||
t.Fatalf("unexpected events: %+v", events)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCodexJSONEmitter_StandardErrorEvent(t *testing.T) {
|
||||
e := codexJSONEmitter{}
|
||||
events, _ := e.Emit(`{"type":"error","message":"network timeout"}`)
|
||||
if len(events) != 1 || events[0].Error != "network timeout" {
|
||||
t.Fatalf("unexpected events: %+v", events)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCodexJSONEmitter_NonAgentMessageSkipped(t *testing.T) {
|
||||
e := codexJSONEmitter{}
|
||||
events, _ := e.Emit(`{"type":"item.completed","item":{"type":"tool_call","text":"ls -la"}}`)
|
||||
if len(events) != 0 {
|
||||
t.Fatalf("expected 0 events for non-agent_message item, got %d", len(events))
|
||||
}
|
||||
}
|
||||
|
||||
func TestClineJSONEmitter_CompletionError(t *testing.T) {
|
||||
e := clineJSONEmitter{}
|
||||
events, _ := e.Emit(`{"type":"completion","status":"error","error":"task failed"}`)
|
||||
if len(events) != 1 || events[0].Error != "task failed" {
|
||||
t.Fatalf("unexpected events: %+v", events)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClineJSONEmitter_SayErrorWithFallback(t *testing.T) {
|
||||
e := clineJSONEmitter{}
|
||||
events, _ := e.Emit(`{"type":"say","say":"error","message":"fallback msg"}`)
|
||||
if len(events) != 1 || events[0].Error != "fallback msg" {
|
||||
t.Fatalf("unexpected events: %+v", events)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClineJSONEmitter_CompletionErrorWithoutErrorField(t *testing.T) {
|
||||
e := clineJSONEmitter{}
|
||||
events, _ := e.Emit(`{"type":"completion","status":"error"}`)
|
||||
if len(events) != 1 || events[0].Error != "cline task failed" {
|
||||
t.Fatalf("unexpected events: %+v", events)
|
||||
}
|
||||
}
|
||||
|
||||
// --- emitter registry tests ---
|
||||
|
||||
func TestEmitters_HaveDistinctNames(t *testing.T) {
|
||||
testCases := []struct {
|
||||
emitter lineEmitter
|
||||
want string
|
||||
}{
|
||||
{streamJSONEmitter{}, "stream-json"},
|
||||
{claudeJSONEmitter{}, "claude-json"},
|
||||
{codexJSONEmitter{}, "codex-json"},
|
||||
{opencodeJSONEmitter{}, "opencode-json"},
|
||||
{clineJSONEmitter{}, "cline-json"},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
if got := tc.emitter.Name(); got != tc.want {
|
||||
t.Errorf("%T.Name() = %q, want %q", tc.emitter, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestJsonEmitters_RegistryMatchesImpls(t *testing.T) {
|
||||
expectedKeys := []string{"stream-json", "claude-json", "codex-json", "opencode-json", "cline-json"}
|
||||
for _, key := range expectedKeys {
|
||||
reg, ok := jsonEmitters[key]
|
||||
if !ok {
|
||||
t.Fatalf("jsonEmitters[%q] not found in registry", key)
|
||||
}
|
||||
if reg.emitter.Name() != key {
|
||||
t.Errorf("jsonEmitters[%q].emitter.Name() = %q, want %q", key, reg.emitter.Name(), key)
|
||||
}
|
||||
if reg.scanBufMax < 1024 {
|
||||
t.Errorf("jsonEmitters[%q].scanBufMax = %d, expected at least 1024", key, reg.scanBufMax)
|
||||
}
|
||||
}
|
||||
if len(jsonEmitters) != len(expectedKeys) {
|
||||
t.Errorf("expected %d registered emitters, got %d", len(expectedKeys), len(jsonEmitters))
|
||||
}
|
||||
}
|
||||
|
|
@ -1,117 +0,0 @@
|
|||
package cli_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"iop/apps/node/internal/adapters/cli"
|
||||
"iop/apps/node/internal/adapters/cli/status"
|
||||
"iop/apps/node/internal/runtime"
|
||||
"iop/packages/config"
|
||||
)
|
||||
|
||||
func TestCLIHandleCommandUsageStatusUsesSelectedAgent(t *testing.T) {
|
||||
c := cli.New(config.CLIConf{
|
||||
Enabled: true,
|
||||
Profiles: map[string]config.CLIProfileConf{
|
||||
"codex": {
|
||||
Command: "/tmp/fake-codex",
|
||||
},
|
||||
"cline-m1": {
|
||||
Command: "cline-m1",
|
||||
},
|
||||
},
|
||||
}, zap.NewNop())
|
||||
|
||||
// Inject fake checker
|
||||
c.StatusChecker = func(ctx context.Context, agent string, profile config.CLIProfileConf) (*status.UsageStatus, error) {
|
||||
if agent == "codex" {
|
||||
if profile.Command != "/tmp/fake-codex" {
|
||||
t.Errorf("expected command /tmp/fake-codex, got %q", profile.Command)
|
||||
}
|
||||
return &status.UsageStatus{RawOutput: "fake-codex-status"}, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Test missing agent
|
||||
_, err := c.HandleCommand(context.Background(), runtime.CommandRequest{
|
||||
Type: runtime.CommandTypeUsageStatus,
|
||||
Model: "unknown",
|
||||
})
|
||||
if err == nil || !strings.Contains(err.Error(), "unknown agent") {
|
||||
t.Errorf("expected unknown agent error, got %v", err)
|
||||
}
|
||||
|
||||
// Test codex
|
||||
resp, err := c.HandleCommand(context.Background(), runtime.CommandRequest{
|
||||
RequestID: "req-123",
|
||||
Type: runtime.CommandTypeUsageStatus,
|
||||
Model: "codex",
|
||||
SessionID: "sess-456",
|
||||
Adapter: "cli",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("HandleCommand failed: %v", err)
|
||||
}
|
||||
if resp.UsageStatus == nil || resp.UsageStatus.RawOutput != "fake-codex-status" {
|
||||
t.Errorf("unexpected usage status: %+v", resp.UsageStatus)
|
||||
}
|
||||
if resp.RequestID != "req-123" {
|
||||
t.Errorf("expected RequestID req-123, got %s", resp.RequestID)
|
||||
}
|
||||
if resp.SessionID != "sess-456" {
|
||||
t.Errorf("expected SessionID sess-456, got %s", resp.SessionID)
|
||||
}
|
||||
|
||||
// Test unsupported command type
|
||||
_, err = c.HandleCommand(context.Background(), runtime.CommandRequest{
|
||||
Type: runtime.CommandType("invalid"),
|
||||
Model: "codex",
|
||||
})
|
||||
if err == nil || !strings.Contains(err.Error(), "unsupported command") {
|
||||
t.Errorf("expected unsupported command error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCapabilities_OnlyConfiguredProfiles(t *testing.T) {
|
||||
c := cli.New(config.CLIConf{
|
||||
Enabled: true,
|
||||
Profiles: map[string]config.CLIProfileConf{
|
||||
"foo": {Command: "foo"},
|
||||
"bar": {Command: "bar"},
|
||||
},
|
||||
}, zap.NewNop())
|
||||
|
||||
caps, err := c.Capabilities(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
want := []string{"bar", "foo"}
|
||||
if len(caps.Models) != len(want) {
|
||||
t.Fatalf("expected models %v, got %v", want, caps.Models)
|
||||
}
|
||||
for i, m := range want {
|
||||
if caps.Models[i] != m {
|
||||
t.Errorf("Models[%d] = %q, want %q", i, caps.Models[i], m)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCapabilities_EmptyProfilesReturnsEmptyModels(t *testing.T) {
|
||||
c := cli.New(config.CLIConf{
|
||||
Enabled: true,
|
||||
Profiles: map[string]config.CLIProfileConf{},
|
||||
}, zap.NewNop())
|
||||
|
||||
caps, err := c.Capabilities(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(caps.Models) != 0 {
|
||||
t.Errorf("expected empty models, got %v", caps.Models)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"iop/packages/config"
|
||||
)
|
||||
|
||||
func TestCodexExecArgs_NoSessionUsesProfileArgs(t *testing.T) {
|
||||
profile := config.CLIProfileConf{
|
||||
Command: "codex",
|
||||
Args: []string{"exec", "--json"},
|
||||
}
|
||||
|
||||
args := codexExecArgs(profile, "", "hello prompt")
|
||||
expected := []string{"exec", "--json", "hello prompt"}
|
||||
if len(args) != len(expected) {
|
||||
t.Fatalf("expected %d args, got %d: %v", len(expected), len(args), args)
|
||||
}
|
||||
for i, v := range expected {
|
||||
if args[i] != v {
|
||||
t.Errorf("args[%d]: expected %q, got %q", i, v, args[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCodexExecArgs_WithSessionUsesResumeArgs(t *testing.T) {
|
||||
profile := config.CLIProfileConf{
|
||||
Args: []string{"exec", "--json"},
|
||||
ResumeArgs: []string{"exec", "resume", "--color", "never"},
|
||||
}
|
||||
|
||||
args := codexExecArgs(profile, "session-abc", "hello prompt")
|
||||
expected := []string{"exec", "resume", "--color", "never", "session-abc", "hello prompt"}
|
||||
if len(args) != len(expected) {
|
||||
t.Fatalf("expected %d args, got %d: %v", len(expected), len(args), args)
|
||||
}
|
||||
for i, v := range expected {
|
||||
if args[i] != v {
|
||||
t.Errorf("args[%d]: expected %q, got %q", i, v, args[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,551 +0,0 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"iop/apps/node/internal/runtime"
|
||||
)
|
||||
|
||||
// --- driveJSONLines tests ---
|
||||
|
||||
type testSink struct {
|
||||
events []runtime.RuntimeEvent
|
||||
}
|
||||
|
||||
func (s *testSink) Emit(_ context.Context, e runtime.RuntimeEvent) error {
|
||||
s.events = append(s.events, e)
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestDriveJSONLines_DispatchesEmitterEvents(t *testing.T) {
|
||||
input := `{"type":"message","role":"assistant","content":"hello"}
|
||||
not-json
|
||||
{"type":"message","role":"assistant","content":"world"}`
|
||||
outBuf := &strings.Builder{}
|
||||
sink := &testSink{}
|
||||
outReader := strings.NewReader(input + "\n")
|
||||
|
||||
// mockEmitter returns two events per line.
|
||||
mockEmitter := &mockLineEmitter{
|
||||
name: "mock",
|
||||
emitFn: func(line string) ([]runtime.RuntimeEvent, error) {
|
||||
return []runtime.RuntimeEvent{
|
||||
{Type: runtime.EventTypeDelta, Delta: "a:" + line},
|
||||
{Type: runtime.EventTypeDelta, Delta: "b:" + line},
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
|
||||
outputTokens, err := driveJSONLines(context.Background(), outReader, sink, "run-1", outBuf, mockEmitter, 4*1024*1024)
|
||||
if err != nil {
|
||||
t.Fatalf("driveJSONLines: %v", err)
|
||||
}
|
||||
|
||||
if got := len(sink.events); got != 4 {
|
||||
t.Fatalf("expected 4 events, got %d", got)
|
||||
}
|
||||
|
||||
// Verify RunID and Timestamp are set.
|
||||
for i, ev := range sink.events {
|
||||
if ev.RunID != "run-1" {
|
||||
t.Errorf("event %d: RunID = %q, want %q", i, ev.RunID, "run-1")
|
||||
}
|
||||
if ev.Timestamp.IsZero() {
|
||||
t.Errorf("event %d: Timestamp is zero", i)
|
||||
}
|
||||
if ev.Type == runtime.EventTypeDelta {
|
||||
outputTokens += len(strings.Fields(ev.Delta))
|
||||
}
|
||||
}
|
||||
|
||||
// outBuf should contain all raw lines.
|
||||
raw := outBuf.String()
|
||||
if !strings.Contains(raw, `{"type":"message"`) {
|
||||
t.Fatalf("outBuf missing JSON line: %q", raw)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDriveJSONLines_StopsOnEmitterError(t *testing.T) {
|
||||
input := `{"type":"text"}
|
||||
{"type":"error"}`
|
||||
outBuf := &strings.Builder{}
|
||||
sink := &testSink{}
|
||||
outReader := strings.NewReader(input + "\n")
|
||||
|
||||
callCount := 0
|
||||
mockEmitter := &mockLineEmitter{
|
||||
name: "mock",
|
||||
emitFn: func(line string) ([]runtime.RuntimeEvent, error) {
|
||||
callCount++
|
||||
if callCount == 2 {
|
||||
return nil, errors.New("emitter failure")
|
||||
}
|
||||
return []runtime.RuntimeEvent{{Type: runtime.EventTypeDelta, Delta: "ok"}}, nil
|
||||
},
|
||||
}
|
||||
|
||||
_, err := driveJSONLines(context.Background(), outReader, sink, "run-2", outBuf, mockEmitter, 4*1024*1024)
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
if err.Error() != "emitter failure" {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if callCount != 2 {
|
||||
t.Fatalf("expected emitter called 2 times, got %d", callCount)
|
||||
}
|
||||
}
|
||||
|
||||
// mockLineEmitter implements lineEmitter for testing.
|
||||
type mockLineEmitter struct {
|
||||
name string
|
||||
emitFn func(line string) ([]runtime.RuntimeEvent, error)
|
||||
}
|
||||
|
||||
func (m *mockLineEmitter) Name() string { return m.name }
|
||||
func (m *mockLineEmitter) Emit(line string) ([]runtime.RuntimeEvent, error) {
|
||||
if m.emitFn != nil {
|
||||
return m.emitFn(line)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// --- stream-json emitter tests ---
|
||||
|
||||
func TestStreamJSONEmitter_AssistantMessageBecomesDelta(t *testing.T) {
|
||||
e := streamJSONEmitter{}
|
||||
line := `{"type":"message","role":"assistant","content":"Hello world"}`
|
||||
events, err := e.Emit(line)
|
||||
if err != nil {
|
||||
t.Fatalf("Emit: %v", err)
|
||||
}
|
||||
if len(events) != 1 {
|
||||
t.Fatalf("expected 1 event, got %d", len(events))
|
||||
}
|
||||
if events[0].Type != runtime.EventTypeDelta {
|
||||
t.Fatalf("event type = %q, want %q", events[0].Type, runtime.EventTypeDelta)
|
||||
}
|
||||
if events[0].Delta != "Hello world" {
|
||||
t.Fatalf("delta = %q, want %q", events[0].Delta, "Hello world")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStreamJSONEmitter_SkipsNonAssistantRoles(t *testing.T) {
|
||||
e := streamJSONEmitter{}
|
||||
line := `{"type":"message","role":"user","content":"hi"}`
|
||||
events, _ := e.Emit(line)
|
||||
if len(events) != 0 {
|
||||
t.Fatalf("expected 0 events for user role, got %d", len(events))
|
||||
}
|
||||
}
|
||||
|
||||
func TestStreamJSONEmitter_ErrorEvent(t *testing.T) {
|
||||
e := streamJSONEmitter{}
|
||||
line := `{"type":"error","error":"something broke"}`
|
||||
events, _ := e.Emit(line)
|
||||
if len(events) != 1 {
|
||||
t.Fatalf("expected 1 event, got %d", len(events))
|
||||
}
|
||||
if events[0].Type != runtime.EventTypeError {
|
||||
t.Fatalf("event type = %q, want %q", events[0].Type, runtime.EventTypeError)
|
||||
}
|
||||
if events[0].Error != "something broke" {
|
||||
t.Fatalf("error = %q, want %q", events[0].Error, "something broke")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStreamJSONEmitter_EmptyContentSkipped(t *testing.T) {
|
||||
e := streamJSONEmitter{}
|
||||
line := `{"type":"message","role":"assistant","content":""}`
|
||||
events, _ := e.Emit(line)
|
||||
if len(events) != 0 {
|
||||
t.Fatalf("expected 0 events for empty content, got %d", len(events))
|
||||
}
|
||||
}
|
||||
|
||||
// --- claude-json emitter tests ---
|
||||
|
||||
func TestClaudeJSONEmitter_TextDelta(t *testing.T) {
|
||||
e := claudeJSONEmitter{}
|
||||
line := `{"type":"stream_event","event":{"type":"content_block_delta","delta":{"type":"text_delta","text":"partial"}}}`
|
||||
events, _ := e.Emit(line)
|
||||
if len(events) != 1 {
|
||||
t.Fatalf("expected 1 event, got %d", len(events))
|
||||
}
|
||||
if events[0].Delta != "partial" {
|
||||
t.Fatalf("delta = %q, want %q", events[0].Delta, "partial")
|
||||
}
|
||||
}
|
||||
|
||||
func TestClaudeJSONEmitter_ErrorResult(t *testing.T) {
|
||||
e := claudeJSONEmitter{}
|
||||
line := `{"type":"result","is_error":true,"result":"API timeout"}`
|
||||
events, _ := e.Emit(line)
|
||||
if len(events) != 1 {
|
||||
t.Fatalf("expected 1 event, got %d", len(events))
|
||||
}
|
||||
if events[0].Type != runtime.EventTypeError {
|
||||
t.Fatalf("event type = %q, want %q", events[0].Type, runtime.EventTypeError)
|
||||
}
|
||||
if events[0].Error != "API timeout" {
|
||||
t.Fatalf("error = %q, want %q", events[0].Error, "API timeout")
|
||||
}
|
||||
}
|
||||
|
||||
func TestClaudeJSONEmitter_NonTextDeltaSkipped(t *testing.T) {
|
||||
e := claudeJSONEmitter{}
|
||||
line := `{"type":"stream_event","event":{"type":"content_block_delta","delta":{"type":"image_delta","data":"base64"}}}`
|
||||
events, _ := e.Emit(line)
|
||||
if len(events) != 0 {
|
||||
t.Fatalf("expected 0 events for non-text delta, got %d", len(events))
|
||||
}
|
||||
}
|
||||
|
||||
// --- codex-json emitter tests ---
|
||||
|
||||
func TestCodexJSONEmitter_AgentMessageBecomesDelta(t *testing.T) {
|
||||
e := codexJSONEmitter{}
|
||||
line := `{"type":"item.completed","item":{"type":"agent_message","text":"Done."}}`
|
||||
events, _ := e.Emit(line)
|
||||
if len(events) != 1 {
|
||||
t.Fatalf("expected 1 event, got %d", len(events))
|
||||
}
|
||||
if events[0].Delta != "Done." {
|
||||
t.Fatalf("delta = %q, want %q", events[0].Delta, "Done.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCodexJSONEmitter_ItemDeltaBecomesDelta(t *testing.T) {
|
||||
e := codexJSONEmitter{}
|
||||
line := `{"type":"item.delta","item":{"type":"agent_message","delta":"Doing."}}`
|
||||
events, _ := e.Emit(line)
|
||||
if len(events) != 1 {
|
||||
t.Fatalf("expected 1 event, got %d", len(events))
|
||||
}
|
||||
if events[0].Delta != "Doing." {
|
||||
t.Fatalf("delta = %q, want %q", events[0].Delta, "Doing.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCodexJSONEmitter_OutputTextDeltaBecomesDelta(t *testing.T) {
|
||||
e := codexJSONEmitter{}
|
||||
line := `{"type":"response.output_text.delta","delta":"chunk"}`
|
||||
events, _ := e.Emit(line)
|
||||
if len(events) != 1 {
|
||||
t.Fatalf("expected 1 event, got %d", len(events))
|
||||
}
|
||||
if events[0].Delta != "chunk" {
|
||||
t.Fatalf("delta = %q, want %q", events[0].Delta, "chunk")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCodexJSONEmitter_ContentOutputTextFallsBackToDelta(t *testing.T) {
|
||||
e := codexJSONEmitter{}
|
||||
line := `{"type":"item.updated","item":{"type":"agent_message","message":{"content":[{"type":"output_text","text":"partial text"}]}}}`
|
||||
events, _ := e.Emit(line)
|
||||
if len(events) != 1 {
|
||||
t.Fatalf("expected 1 event, got %d", len(events))
|
||||
}
|
||||
if events[0].Delta != "partial text" {
|
||||
t.Fatalf("delta = %q, want %q", events[0].Delta, "partial text")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCodexJSONEmitter_TurnFailedBecomesError(t *testing.T) {
|
||||
e := codexJSONEmitter{}
|
||||
line := `{"type":"turn.failed","error":{"message":"quota exceeded"}}`
|
||||
events, _ := e.Emit(line)
|
||||
if len(events) != 1 {
|
||||
t.Fatalf("expected 1 event, got %d", len(events))
|
||||
}
|
||||
if events[0].Type != runtime.EventTypeError {
|
||||
t.Fatalf("event type = %q, want %q", events[0].Type, runtime.EventTypeError)
|
||||
}
|
||||
if events[0].Error != "quota exceeded" {
|
||||
t.Fatalf("error = %q, want %q", events[0].Error, "quota exceeded")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCodexJSONEmitter_StandardErrorEvent(t *testing.T) {
|
||||
e := codexJSONEmitter{}
|
||||
line := `{"type":"error","message":"network timeout"}`
|
||||
events, _ := e.Emit(line)
|
||||
if len(events) != 1 {
|
||||
t.Fatalf("expected 1 event, got %d", len(events))
|
||||
}
|
||||
if events[0].Error != "network timeout" {
|
||||
t.Fatalf("error = %q, want %q", events[0].Error, "network timeout")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCodexJSONEmitter_NonAgentMessageSkipped(t *testing.T) {
|
||||
e := codexJSONEmitter{}
|
||||
line := `{"type":"item.completed","item":{"type":"tool_call","text":"ls -la"}}`
|
||||
events, _ := e.Emit(line)
|
||||
if len(events) != 0 {
|
||||
t.Fatalf("expected 0 events for non-agent_message item, got %d", len(events))
|
||||
}
|
||||
}
|
||||
|
||||
// --- opencode-json emitter tests ---
|
||||
|
||||
func TestOpencodeJSONEmitter_TextPart(t *testing.T) {
|
||||
e := opencodeJSONEmitter{}
|
||||
line := `{"type":"text","part":{"type":"text","text":"response text"}}`
|
||||
events, _ := e.Emit(line)
|
||||
if len(events) != 1 {
|
||||
t.Fatalf("expected 1 event, got %d", len(events))
|
||||
}
|
||||
if events[0].Delta != "response text" {
|
||||
t.Fatalf("delta = %q, want %q", events[0].Delta, "response text")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpencodeJSONEmitter_NestedErrorMessage(t *testing.T) {
|
||||
e := opencodeJSONEmitter{}
|
||||
line := `{"type":"error","error":{"name":"UnknownError","data":{"message":"Model not found"}}}`
|
||||
events, _ := e.Emit(line)
|
||||
if len(events) != 1 {
|
||||
t.Fatalf("expected 1 event, got %d", len(events))
|
||||
}
|
||||
if events[0].Error != "Model not found" {
|
||||
t.Fatalf("error = %q, want %q", events[0].Error, "Model not found")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpencodeJSONEmitter_FallbackToErrorName(t *testing.T) {
|
||||
e := opencodeJSONEmitter{}
|
||||
line := `{"type":"error","error":{"name":"ProviderUnavailable"}}`
|
||||
events, _ := e.Emit(line)
|
||||
if len(events) != 1 {
|
||||
t.Fatalf("expected 1 event, got %d", len(events))
|
||||
}
|
||||
if events[0].Error != "ProviderUnavailable" {
|
||||
t.Fatalf("error = %q, want %q", events[0].Error, "ProviderUnavailable")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpencodeJSONEmitter_EmptyTextPartSkipped(t *testing.T) {
|
||||
e := opencodeJSONEmitter{}
|
||||
line := `{"type":"text","part":{"type":"text","text":""}}`
|
||||
events, _ := e.Emit(line)
|
||||
if len(events) != 0 {
|
||||
t.Fatalf("expected 0 events for empty text, got %d", len(events))
|
||||
}
|
||||
}
|
||||
|
||||
// --- cline-json emitter tests ---
|
||||
|
||||
func TestClineJSONEmitter_TextEvent(t *testing.T) {
|
||||
e := clineJSONEmitter{}
|
||||
line := `{"type":"say","say":"text","text":"Hello from Cline"}`
|
||||
events, _ := e.Emit(line)
|
||||
if len(events) != 1 {
|
||||
t.Fatalf("expected 1 event, got %d", len(events))
|
||||
}
|
||||
if events[0].Delta != "Hello from Cline" {
|
||||
t.Fatalf("delta = %q, want %q", events[0].Delta, "Hello from Cline")
|
||||
}
|
||||
}
|
||||
|
||||
func TestClineJSONEmitter_AskApiReqFailedBecomesError(t *testing.T) {
|
||||
e := clineJSONEmitter{}
|
||||
line := `{"type":"ask","ask":"api_req_failed","text":"model unavailable"}`
|
||||
events, _ := e.Emit(line)
|
||||
if len(events) != 1 {
|
||||
t.Fatalf("expected 1 event, got %d", len(events))
|
||||
}
|
||||
if events[0].Type != runtime.EventTypeError {
|
||||
t.Fatalf("event type = %q, want %q", events[0].Type, runtime.EventTypeError)
|
||||
}
|
||||
if events[0].Error != "model unavailable" {
|
||||
t.Fatalf("error = %q, want %q", events[0].Error, "model unavailable")
|
||||
}
|
||||
}
|
||||
|
||||
func TestClineJSONEmitter_CompletionError(t *testing.T) {
|
||||
e := clineJSONEmitter{}
|
||||
line := `{"type":"completion","status":"error","error":"task failed"}`
|
||||
events, _ := e.Emit(line)
|
||||
if len(events) != 1 {
|
||||
t.Fatalf("expected 1 event, got %d", len(events))
|
||||
}
|
||||
if events[0].Error != "task failed" {
|
||||
t.Fatalf("error = %q, want %q", events[0].Error, "task failed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestClineJSONEmitter_SayErrorWithFallback(t *testing.T) {
|
||||
e := clineJSONEmitter{}
|
||||
line := `{"type":"say","say":"error","message":"fallback msg"}`
|
||||
events, _ := e.Emit(line)
|
||||
if len(events) != 1 {
|
||||
t.Fatalf("expected 1 event, got %d", len(events))
|
||||
}
|
||||
if events[0].Error != "fallback msg" {
|
||||
t.Fatalf("error = %q, want %q", events[0].Error, "fallback msg")
|
||||
}
|
||||
}
|
||||
|
||||
func TestClineJSONEmitter_CompletionErrorWithoutErrorField(t *testing.T) {
|
||||
e := clineJSONEmitter{}
|
||||
line := `{"type":"completion","status":"error"}`
|
||||
events, _ := e.Emit(line)
|
||||
if len(events) != 1 {
|
||||
t.Fatalf("expected 1 event, got %d", len(events))
|
||||
}
|
||||
if events[0].Error != "cline task failed" {
|
||||
t.Fatalf("error = %q, want %q", events[0].Error, "cline task failed")
|
||||
}
|
||||
}
|
||||
|
||||
// --- Emitter Name tests ---
|
||||
|
||||
func TestEmitters_HaveDistinctNames(t *testing.T) {
|
||||
testCases := []struct {
|
||||
emitter lineEmitter
|
||||
want string
|
||||
}{
|
||||
{streamJSONEmitter{}, "stream-json"},
|
||||
{claudeJSONEmitter{}, "claude-json"},
|
||||
{codexJSONEmitter{}, "codex-json"},
|
||||
{opencodeJSONEmitter{}, "opencode-json"},
|
||||
{clineJSONEmitter{}, "cline-json"},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
if got := tc.emitter.Name(); got != tc.want {
|
||||
t.Errorf("%T.Name() = %q, want %q", tc.emitter, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- jsonEmitters registry consistency test ---
|
||||
|
||||
func TestJsonEmitters_RegistryMatchesImpls(t *testing.T) {
|
||||
expectedKeys := []string{"stream-json", "claude-json", "codex-json", "opencode-json", "cline-json"}
|
||||
for _, key := range expectedKeys {
|
||||
reg, ok := jsonEmitters[key]
|
||||
if !ok {
|
||||
t.Fatalf("jsonEmitters[%q] not found in registry", key)
|
||||
}
|
||||
if reg.emitter.Name() != key {
|
||||
t.Errorf("jsonEmitters[%q].emitter.Name() = %q, want %q", key, reg.emitter.Name(), key)
|
||||
}
|
||||
if reg.scanBufMax < 1024 {
|
||||
t.Errorf("jsonEmitters[%q].scanBufMax = %d, expected at least 1024", key, reg.scanBufMax)
|
||||
}
|
||||
}
|
||||
// Ensure no extra keys in registry.
|
||||
if len(jsonEmitters) != len(expectedKeys) {
|
||||
t.Errorf("expected %d registered emitters, got %d", len(expectedKeys), len(jsonEmitters))
|
||||
}
|
||||
}
|
||||
|
||||
// --- driveJSONLines integration: raw output accumulation ---
|
||||
|
||||
func TestDriveJSONLines_AccumulatesRawOutput(t *testing.T) {
|
||||
lines := `{"type":"text","part":{"type":"text","text":"a"}}
|
||||
{"type":"text","part":{"type":"text","text":"b"}}`
|
||||
outBuf := &strings.Builder{}
|
||||
sink := &testSink{}
|
||||
outReader := strings.NewReader(lines + "\n")
|
||||
|
||||
outputTokens, err := driveJSONLines(context.Background(), outReader, sink, "run-int", outBuf, opencodeJSONEmitter{}, 4*1024*1024)
|
||||
if err != nil {
|
||||
t.Fatalf("driveJSONLines: %v", err)
|
||||
}
|
||||
|
||||
// outBuf should contain the raw lines with newlines.
|
||||
raw := outBuf.String()
|
||||
if !strings.Contains(raw, `{"type":"text"`) {
|
||||
t.Fatalf("outBuf missing expected line: %q", raw)
|
||||
}
|
||||
// Count newlines in outBuf — should match input lines.
|
||||
newlineCount := strings.Count(raw, "\n")
|
||||
if newlineCount != 2 {
|
||||
t.Fatalf("expected 2 newlines in outBuf, got %d", newlineCount)
|
||||
}
|
||||
// 2 delta events emitted, each with 1 word in delta.
|
||||
if outputTokens != 2 {
|
||||
t.Fatalf("expected 2 outputTokens, got %d", outputTokens)
|
||||
}
|
||||
}
|
||||
|
||||
// --- driveJSONLines: scanner buffer size respected ---
|
||||
|
||||
func TestDriveJSONLines_ScannerBufferMax(t *testing.T) {
|
||||
// Create a line larger than a small scanBufMax.
|
||||
longLine := strings.Repeat("x", 100) + `{"type":"text"}`
|
||||
outBuf := &strings.Builder{}
|
||||
sink := &testSink{}
|
||||
outReader := strings.NewReader(longLine + "\n")
|
||||
|
||||
// With a 50-byte buffer max, the line will be cut off at 50 bytes
|
||||
// and won't start with '{', so it will be skipped.
|
||||
outputTokens, err := driveJSONLines(context.Background(), outReader, sink, "run-buf", outBuf, &mockLineEmitter{name: "buf"}, 50)
|
||||
if err != nil {
|
||||
t.Fatalf("driveJSONLines: %v", err)
|
||||
}
|
||||
// The line starts with 'x' not '{', so emitter is never called,
|
||||
// and no events emitted, outputTokens == 0.
|
||||
if outputTokens != 0 {
|
||||
t.Fatalf("expected 0 outputTokens for line too long for buffer, got %d", outputTokens)
|
||||
}
|
||||
}
|
||||
|
||||
// --- driveJSONLines: empty and non-JSON lines are skipped ---
|
||||
|
||||
func TestDriveJSONLines_SkipsEmptyAndNonJSONLines(t *testing.T) {
|
||||
input := "\n\nnot json at all\n \n{\"type\":\"text\"}\n"
|
||||
outBuf := &strings.Builder{}
|
||||
sink := &testSink{}
|
||||
outReader := strings.NewReader(input)
|
||||
|
||||
callCount := 0
|
||||
mockEmitter := &mockLineEmitter{
|
||||
name: "mock",
|
||||
emitFn: func(line string) ([]runtime.RuntimeEvent, error) {
|
||||
callCount++
|
||||
return []runtime.RuntimeEvent{{Type: runtime.EventTypeDelta, Delta: line}}, nil
|
||||
},
|
||||
}
|
||||
|
||||
_, err := driveJSONLines(context.Background(), outReader, sink, "run-skip", outBuf, mockEmitter, 4*1024*1024)
|
||||
if err != nil {
|
||||
t.Fatalf("driveJSONLines: %v", err)
|
||||
}
|
||||
if callCount != 1 {
|
||||
t.Fatalf("expected emitter called 1 time, got %d", callCount)
|
||||
}
|
||||
}
|
||||
|
||||
// --- driveJSONLines: outputTokens counted only for delta events ---
|
||||
|
||||
func TestDriveJSONLines_OutputTokensCountedForDeltaOnly(t *testing.T) {
|
||||
outBuf := &strings.Builder{}
|
||||
sink := &testSink{}
|
||||
outReader := strings.NewReader(`{"type":"error"}
|
||||
{"type":"delta"}` + "\n")
|
||||
|
||||
mockEmitter := &mockLineEmitter{
|
||||
name: "mock",
|
||||
emitFn: func(line string) ([]runtime.RuntimeEvent, error) {
|
||||
if strings.Contains(line, "error") {
|
||||
return []runtime.RuntimeEvent{{Type: runtime.EventTypeError, Error: "bad"}}, nil
|
||||
}
|
||||
return []runtime.RuntimeEvent{{Type: runtime.EventTypeDelta, Delta: "one two three"}}, nil
|
||||
},
|
||||
}
|
||||
|
||||
outputTokens, err := driveJSONLines(context.Background(), outReader, sink, "run-tokens", outBuf, mockEmitter, 4*1024*1024)
|
||||
if err != nil {
|
||||
t.Fatalf("driveJSONLines: %v", err)
|
||||
}
|
||||
// "one two three" has 3 fields; error event contributes 0.
|
||||
if outputTokens != 3 {
|
||||
t.Fatalf("expected 3 outputTokens, got %d", outputTokens)
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
|
|
@ -11,6 +12,7 @@ import (
|
|||
|
||||
clipkg "iop/apps/node/internal/adapters/cli"
|
||||
"iop/apps/node/internal/adapters/cli/internal/testutil"
|
||||
"iop/apps/node/internal/adapters/cli/status"
|
||||
noderuntime "iop/apps/node/internal/runtime"
|
||||
"iop/packages/config"
|
||||
)
|
||||
|
|
@ -382,3 +384,98 @@ func TestCLIStopStopsAllLogicalSessions(t *testing.T) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCLIHandleCommandUsageStatusUsesSelectedAgent(t *testing.T) {
|
||||
c := clipkg.New(config.CLIConf{
|
||||
Enabled: true,
|
||||
Profiles: map[string]config.CLIProfileConf{
|
||||
"codex": {Command: "/tmp/fake-codex"},
|
||||
"cline-m1": {Command: "cline-m1"},
|
||||
},
|
||||
}, zap.NewNop())
|
||||
|
||||
c.StatusChecker = func(ctx context.Context, agent string, profile config.CLIProfileConf) (*status.UsageStatus, error) {
|
||||
if agent == "codex" {
|
||||
if profile.Command != "/tmp/fake-codex" {
|
||||
t.Errorf("expected command /tmp/fake-codex, got %q", profile.Command)
|
||||
}
|
||||
return &status.UsageStatus{RawOutput: "fake-codex-status"}, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
_, err := c.HandleCommand(context.Background(), noderuntime.CommandRequest{
|
||||
Type: noderuntime.CommandTypeUsageStatus,
|
||||
Model: "unknown",
|
||||
})
|
||||
if err == nil || !strings.Contains(err.Error(), "unknown agent") {
|
||||
t.Errorf("expected unknown agent error, got %v", err)
|
||||
}
|
||||
|
||||
resp, err := c.HandleCommand(context.Background(), noderuntime.CommandRequest{
|
||||
RequestID: "req-123",
|
||||
Type: noderuntime.CommandTypeUsageStatus,
|
||||
Model: "codex",
|
||||
SessionID: "sess-456",
|
||||
Adapter: "cli",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("HandleCommand failed: %v", err)
|
||||
}
|
||||
if resp.UsageStatus == nil || resp.UsageStatus.RawOutput != "fake-codex-status" {
|
||||
t.Errorf("unexpected usage status: %+v", resp.UsageStatus)
|
||||
}
|
||||
if resp.RequestID != "req-123" {
|
||||
t.Errorf("expected RequestID req-123, got %s", resp.RequestID)
|
||||
}
|
||||
if resp.SessionID != "sess-456" {
|
||||
t.Errorf("expected SessionID sess-456, got %s", resp.SessionID)
|
||||
}
|
||||
|
||||
_, err = c.HandleCommand(context.Background(), noderuntime.CommandRequest{
|
||||
Type: noderuntime.CommandType("invalid"),
|
||||
Model: "codex",
|
||||
})
|
||||
if err == nil || !strings.Contains(err.Error(), "unsupported command") {
|
||||
t.Errorf("expected unsupported command error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCapabilities_OnlyConfiguredProfiles(t *testing.T) {
|
||||
c := clipkg.New(config.CLIConf{
|
||||
Enabled: true,
|
||||
Profiles: map[string]config.CLIProfileConf{
|
||||
"foo": {Command: "foo"},
|
||||
"bar": {Command: "bar"},
|
||||
},
|
||||
}, zap.NewNop())
|
||||
|
||||
caps, err := c.Capabilities(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
want := []string{"bar", "foo"}
|
||||
if len(caps.Models) != len(want) {
|
||||
t.Fatalf("expected models %v, got %v", want, caps.Models)
|
||||
}
|
||||
for i, m := range want {
|
||||
if caps.Models[i] != m {
|
||||
t.Errorf("Models[%d] = %q, want %q", i, caps.Models[i], m)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCapabilities_EmptyProfilesReturnsEmptyModels(t *testing.T) {
|
||||
c := clipkg.New(config.CLIConf{
|
||||
Enabled: true,
|
||||
Profiles: map[string]config.CLIProfileConf{},
|
||||
}, zap.NewNop())
|
||||
|
||||
caps, err := c.Capabilities(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(caps.Models) != 0 {
|
||||
t.Errorf("expected empty models, got %v", caps.Models)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,79 +0,0 @@
|
|||
package adapters_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"iop/apps/node/internal/adapters"
|
||||
iop "iop/proto/gen/iop"
|
||||
)
|
||||
|
||||
func TestBuildFromPayload_MockAlwaysPresent(t *testing.T) {
|
||||
reg, err := adapters.BuildFromPayload(&iop.NodeConfigPayload{}, zap.NewNop())
|
||||
if err != nil {
|
||||
t.Fatalf("build from payload: %v", err)
|
||||
}
|
||||
if _, ok := reg.Get("mock"); !ok {
|
||||
t.Fatal("expected mock adapter to be registered")
|
||||
}
|
||||
if got := len(reg.All()); got != 1 {
|
||||
t.Fatalf("expected 1 adapter, got %d", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildFromPayload_OllamaEnabled(t *testing.T) {
|
||||
reg, err := adapters.BuildFromPayload(&iop.NodeConfigPayload{
|
||||
Adapters: []*iop.AdapterConfig{
|
||||
{
|
||||
Type: "ollama",
|
||||
Enabled: true,
|
||||
Config: &iop.AdapterConfig_Ollama{
|
||||
Ollama: &iop.OllamaAdapterConfig{BaseUrl: "http://localhost:11434"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}, zap.NewNop())
|
||||
if err != nil {
|
||||
t.Fatalf("build from payload: %v", err)
|
||||
}
|
||||
if _, ok := reg.Get("mock"); !ok {
|
||||
t.Fatal("expected mock adapter to be registered")
|
||||
}
|
||||
if _, ok := reg.Get("ollama"); !ok {
|
||||
t.Fatal("expected ollama adapter to be registered")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildFromPayload_MultipleAdapters(t *testing.T) {
|
||||
reg, err := adapters.BuildFromPayload(&iop.NodeConfigPayload{
|
||||
Adapters: []*iop.AdapterConfig{
|
||||
{Type: "ollama", Enabled: true, Config: &iop.AdapterConfig_Ollama{Ollama: &iop.OllamaAdapterConfig{BaseUrl: "x"}}},
|
||||
{Type: "vllm", Enabled: true, Config: &iop.AdapterConfig_Vllm{Vllm: &iop.VllmAdapterConfig{Endpoint: "y"}}},
|
||||
{Type: "cli", Enabled: true, Config: &iop.AdapterConfig_Cli{Cli: &iop.CLIAdapterConfig{
|
||||
Profiles: map[string]*iop.CLIProfileConfig{
|
||||
"codex": {Command: "codex", Persistent: true, ResponseIdleTimeoutMs: 1500, StartupIdleTimeoutMs: 300},
|
||||
},
|
||||
}}},
|
||||
},
|
||||
}, zap.NewNop())
|
||||
if err != nil {
|
||||
t.Fatalf("build from payload: %v", err)
|
||||
}
|
||||
for _, name := range []string{"mock", "ollama", "vllm", "cli"} {
|
||||
if _, ok := reg.Get(name); !ok {
|
||||
t.Fatalf("expected %s adapter to be registered", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildFromPayload_UnknownType(t *testing.T) {
|
||||
_, err := adapters.BuildFromPayload(&iop.NodeConfigPayload{
|
||||
Adapters: []*iop.AdapterConfig{
|
||||
{Type: "unknown", Enabled: true},
|
||||
},
|
||||
}, zap.NewNop())
|
||||
if err == nil {
|
||||
t.Fatal("expected unknown adapter type error")
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue