671 lines
19 KiB
Go
671 lines
19 KiB
Go
package cli_test
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"go.uber.org/zap"
|
|
|
|
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/go/config"
|
|
)
|
|
|
|
func TestCLIStartPersistentReturnsErrorWhenProcessExitsDuringStartup(t *testing.T) {
|
|
testutil.RequirePTYSupport(t)
|
|
|
|
cfg := config.CLIConf{
|
|
Enabled: true,
|
|
Profiles: map[string]config.CLIProfileConf{
|
|
"exits-immediately": {
|
|
Command: "sh",
|
|
Args: []string{"-c", "exit 2"},
|
|
Persistent: true,
|
|
Terminal: true,
|
|
StartupIdleTimeoutMS: 50,
|
|
},
|
|
},
|
|
}
|
|
c := clipkg.New(cfg, zap.NewNop())
|
|
|
|
if err := c.Start(context.Background()); err == nil {
|
|
t.Fatal("expected error when persistent process exits during startup")
|
|
}
|
|
}
|
|
|
|
func TestCLIStartCleansUpStartedProfilesWhenLaterProfileFails(t *testing.T) {
|
|
testutil.RequireUnixShell(t)
|
|
|
|
cfg := config.CLIConf{
|
|
Enabled: true,
|
|
Profiles: map[string]config.CLIProfileConf{
|
|
"ok-profile": {
|
|
Command: "sh",
|
|
Args: []string{"-c", `while IFS= read -r line; do printf "reply:%s\n" "$line"; done`},
|
|
Persistent: true,
|
|
Terminal: false,
|
|
StartupIdleTimeoutMS: 50,
|
|
ResponseIdleTimeoutMS: 200,
|
|
},
|
|
"fail-profile": {
|
|
Command: "sh",
|
|
Args: []string{"-c", "exit 2"},
|
|
Persistent: true,
|
|
Terminal: true,
|
|
StartupIdleTimeoutMS: 50,
|
|
},
|
|
},
|
|
}
|
|
c := clipkg.New(cfg, zap.NewNop())
|
|
ctx := context.Background()
|
|
|
|
if err := c.Start(ctx); err == nil {
|
|
t.Fatal("expected Start to fail when one persistent profile exits immediately")
|
|
}
|
|
|
|
_ = c.Stop(ctx)
|
|
_ = c.Stop(ctx)
|
|
|
|
// After rollback, the pre-started default session for ok-profile is gone.
|
|
// Verify with require-existing so lazy creation is not allowed.
|
|
for _, model := range []string{"ok-profile", "fail-profile"} {
|
|
sink := &testutil.FakeSink{}
|
|
if err := c.Execute(ctx, noderuntime.ExecutionSpec{
|
|
RunID: "post-cleanup",
|
|
Target: model,
|
|
SessionMode: noderuntime.SessionModeRequireExisting,
|
|
Input: map[string]any{"prompt": "test"},
|
|
}, sink); err == nil {
|
|
t.Fatalf("expected Execute for %q to fail after cleanup (require-existing), got nil", model)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCLIStartDeterministicOrder(t *testing.T) {
|
|
testutil.RequireUnixShell(t)
|
|
|
|
cfg := config.CLIConf{
|
|
Enabled: true,
|
|
Profiles: map[string]config.CLIProfileConf{
|
|
"Z-alive": {
|
|
Command: "sh",
|
|
Args: []string{"-c", `while IFS= read -r line; do printf "reply:%s\n" "$line"; done`},
|
|
Persistent: true,
|
|
Terminal: false,
|
|
StartupIdleTimeoutMS: 50,
|
|
ResponseIdleTimeoutMS: 200,
|
|
},
|
|
"Z-fail": {
|
|
Command: "sh",
|
|
Args: []string{"-c", "exit 2"},
|
|
Persistent: true,
|
|
Terminal: true,
|
|
StartupIdleTimeoutMS: 50,
|
|
},
|
|
},
|
|
}
|
|
c := clipkg.New(cfg, zap.NewNop())
|
|
ctx := context.Background()
|
|
|
|
if err := c.Start(ctx); err == nil {
|
|
t.Fatal("expected Start to fail when one persistent profile exits immediately")
|
|
}
|
|
|
|
_ = c.Stop(ctx)
|
|
_ = c.Stop(ctx)
|
|
|
|
// After rollback, pre-started default sessions must be gone.
|
|
for _, model := range []string{"Z-alive", "Z-fail"} {
|
|
sink := &testutil.FakeSink{}
|
|
if err := c.Execute(ctx, noderuntime.ExecutionSpec{
|
|
RunID: "post-cleanup",
|
|
Target: model,
|
|
SessionMode: noderuntime.SessionModeRequireExisting,
|
|
Input: map[string]any{"prompt": "test"},
|
|
}, sink); err == nil {
|
|
t.Fatalf("expected Execute for %q to fail after cleanup (require-existing), got nil", model)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCLIStartSkipsPersistentLazyProfileAndExecuteCreatesSession(t *testing.T) {
|
|
testutil.RequireUnixShell(t)
|
|
|
|
cfg := config.CLIConf{
|
|
Enabled: true,
|
|
Profiles: map[string]config.CLIProfileConf{
|
|
"lazy-echo": {
|
|
Command: "sh",
|
|
Args: []string{"-c", `while IFS= read -r line; do printf "reply:%s\n" "$line"; done`},
|
|
Persistent: true,
|
|
Terminal: false,
|
|
ResponseIdleTimeoutMS: 100,
|
|
StartupIdleTimeoutMS: 50,
|
|
Mode: "persistent-lazy",
|
|
},
|
|
},
|
|
}
|
|
c := clipkg.New(cfg, zap.NewNop())
|
|
ctx := context.Background()
|
|
|
|
if err := c.Start(ctx); err != nil {
|
|
t.Fatalf("start should not pre-start persistent-lazy profile: %v", err)
|
|
}
|
|
defer func() { _ = c.Stop(ctx) }()
|
|
|
|
requireExistingSink := &testutil.FakeSink{}
|
|
if err := c.Execute(ctx, noderuntime.ExecutionSpec{
|
|
RunID: "run-require-existing",
|
|
Target: "lazy-echo",
|
|
SessionMode: noderuntime.SessionModeRequireExisting,
|
|
Input: map[string]any{"prompt": "hello"},
|
|
}, requireExistingSink); err == nil {
|
|
t.Fatal("expected require-existing execute to fail before lazy session is created")
|
|
}
|
|
|
|
sink := &testutil.FakeSink{}
|
|
if err := c.Execute(ctx, noderuntime.ExecutionSpec{
|
|
RunID: "run-create",
|
|
Target: "lazy-echo",
|
|
Input: map[string]any{"prompt": "hello"},
|
|
}, sink); err != nil {
|
|
t.Fatalf("execute should create lazy persistent session: %v", err)
|
|
}
|
|
if combined := testutil.CollectDeltas(sink.Events()); !strings.Contains(combined, "reply:hello") {
|
|
t.Fatalf("expected lazy session reply in deltas, got %q", combined)
|
|
}
|
|
}
|
|
|
|
func TestCLIConcurrentExecuteAndStop(t *testing.T) {
|
|
testutil.RequirePTYSupport(t)
|
|
|
|
cfg := config.CLIConf{
|
|
Enabled: true,
|
|
Profiles: map[string]config.CLIProfileConf{
|
|
"slow-echo": {
|
|
Command: "sh",
|
|
Args: []string{"-c", `stty -echo; while IFS= read -r line; do sleep 0.05; printf "reply:%s\n" "$line"; done`},
|
|
Persistent: true,
|
|
Terminal: true,
|
|
ResponseIdleTimeoutMS: 50,
|
|
StartupIdleTimeoutMS: 50,
|
|
},
|
|
},
|
|
}
|
|
c := clipkg.New(cfg, zap.NewNop())
|
|
|
|
ctx := context.Background()
|
|
if err := c.Start(ctx); err != nil {
|
|
t.Fatalf("start: %v", err)
|
|
}
|
|
|
|
errCh := make(chan error, 10)
|
|
done := make(chan struct{})
|
|
|
|
go func() {
|
|
for i := 0; i < 3; i++ {
|
|
sink := &testutil.FakeSink{}
|
|
execCtx, cancel := context.WithTimeout(ctx, 500*time.Millisecond)
|
|
err := c.Execute(execCtx, noderuntime.ExecutionSpec{
|
|
RunID: fmt.Sprintf("run-conc-%d", i),
|
|
Target: "slow-echo",
|
|
Input: map[string]any{"prompt": fmt.Sprintf("conc-%d", i)},
|
|
}, sink)
|
|
cancel()
|
|
if err != nil {
|
|
errCh <- fmt.Errorf("concurrent execute %d: %w", i, err)
|
|
}
|
|
}
|
|
done <- struct{}{}
|
|
}()
|
|
|
|
go func() {
|
|
time.Sleep(200 * time.Millisecond)
|
|
_ = c.Stop(ctx)
|
|
done <- struct{}{}
|
|
}()
|
|
|
|
go func() {
|
|
time.Sleep(300 * time.Millisecond)
|
|
for i := 0; i < 2; i++ {
|
|
sink := &testutil.FakeSink{}
|
|
execCtx, cancel := context.WithTimeout(ctx, 200*time.Millisecond)
|
|
err := c.Execute(execCtx, noderuntime.ExecutionSpec{
|
|
RunID: fmt.Sprintf("run-post-stop-%d", i),
|
|
Target: "slow-echo",
|
|
Input: map[string]any{"prompt": fmt.Sprintf("post-stop-%d", i)},
|
|
}, sink)
|
|
cancel()
|
|
if err != nil {
|
|
errCh <- fmt.Errorf("post-stop execute %d: %w", i, err)
|
|
}
|
|
}
|
|
done <- struct{}{}
|
|
}()
|
|
|
|
timeout := time.After(10 * time.Second)
|
|
for completed := 0; completed < 3; completed++ {
|
|
select {
|
|
case <-done:
|
|
case <-timeout:
|
|
t.Fatal("TestCLIConcurrentExecuteAndStop timed out; possible deadlock")
|
|
}
|
|
}
|
|
close(errCh)
|
|
for range errCh {
|
|
}
|
|
}
|
|
|
|
func TestCLIStartPartialRollbackWithMarkers(t *testing.T) {
|
|
testutil.RequireUnixShell(t)
|
|
|
|
markerDir := t.TempDir()
|
|
startMarker := "START_MARKER_A"
|
|
startMarkerPath := filepath.Join(markerDir, "marker_a")
|
|
profileACmd := fmt.Sprintf(`mkdir -p %s; echo %s > %s/marker_a; while IFS= read -r line; do printf "reply:%%s\n" "$line"; done`,
|
|
markerDir, startMarker, markerDir)
|
|
|
|
cfg := config.CLIConf{
|
|
Enabled: true,
|
|
Profiles: map[string]config.CLIProfileConf{
|
|
"profile-a": {
|
|
Command: "sh",
|
|
Args: []string{"-c", profileACmd},
|
|
Persistent: true,
|
|
Terminal: true,
|
|
StartupIdleTimeoutMS: 50,
|
|
ResponseIdleTimeoutMS: 200,
|
|
},
|
|
"profile-b": {
|
|
Command: "sh",
|
|
Args: []string{"-c", "exit 2"},
|
|
Persistent: true,
|
|
Terminal: true,
|
|
StartupIdleTimeoutMS: 50,
|
|
},
|
|
},
|
|
}
|
|
c := clipkg.New(cfg, zap.NewNop())
|
|
ctx := context.Background()
|
|
|
|
if err := c.Start(ctx); err == nil {
|
|
t.Fatal("expected Start to fail when profile-b exits immediately")
|
|
}
|
|
|
|
_ = c.Stop(ctx)
|
|
time.Sleep(300 * time.Millisecond)
|
|
|
|
testutil.RequireEventually(t, 2*time.Second, 50*time.Millisecond, func() bool {
|
|
return testutil.ReadMarker(startMarkerPath) == startMarker
|
|
})
|
|
|
|
// After rollback, the pre-started default session for profile-a is gone.
|
|
sink := &testutil.FakeSink{}
|
|
if err := c.Execute(ctx, noderuntime.ExecutionSpec{
|
|
RunID: "post-rollback",
|
|
Target: "profile-a",
|
|
SessionMode: noderuntime.SessionModeRequireExisting,
|
|
Input: map[string]any{"prompt": "test"},
|
|
}, sink); err == nil {
|
|
t.Fatal("expected Execute for profile-a to fail with no-session after rollback (require-existing)")
|
|
}
|
|
}
|
|
|
|
func TestCLITerminateSessionStopsOnlyTargetSession(t *testing.T) {
|
|
testutil.RequireUnixShell(t)
|
|
|
|
cfg := config.CLIConf{
|
|
Enabled: true,
|
|
Profiles: map[string]config.CLIProfileConf{
|
|
"echo-term": {
|
|
Command: "sh",
|
|
Args: []string{"-c", `while IFS= read -r line; do printf "reply:%s\n" "$line"; done`},
|
|
Persistent: true,
|
|
Terminal: false,
|
|
ResponseIdleTimeoutMS: 200,
|
|
},
|
|
},
|
|
}
|
|
c := clipkg.New(cfg, zap.NewNop())
|
|
ctx := context.Background()
|
|
|
|
// Create session-a and session-b via lazy creation.
|
|
for _, sid := range []string{"session-a", "session-b"} {
|
|
sink := &testutil.FakeSink{}
|
|
err := c.Execute(ctx, noderuntime.ExecutionSpec{
|
|
RunID: "run-" + sid,
|
|
Target: "echo-term",
|
|
SessionID: sid,
|
|
Input: map[string]any{"prompt": "ping"},
|
|
}, sink)
|
|
if err != nil {
|
|
t.Fatalf("create %s: %v", sid, err)
|
|
}
|
|
}
|
|
|
|
// Terminate session-a only.
|
|
if err := c.TerminateSession(ctx, "echo-term", "session-a"); err != nil {
|
|
t.Fatalf("TerminateSession: %v", err)
|
|
}
|
|
|
|
// session-b must still respond.
|
|
sink := &testutil.FakeSink{}
|
|
err := c.Execute(ctx, noderuntime.ExecutionSpec{
|
|
RunID: "run-b-after",
|
|
Target: "echo-term",
|
|
SessionID: "session-b",
|
|
Input: map[string]any{"prompt": "pong"},
|
|
}, sink)
|
|
if err != nil {
|
|
t.Fatalf("session-b should still be alive: %v", err)
|
|
}
|
|
|
|
// session-a must be gone (require-existing fails).
|
|
sinkA := &testutil.FakeSink{}
|
|
err = c.Execute(ctx, noderuntime.ExecutionSpec{
|
|
RunID: "run-a-after",
|
|
Target: "echo-term",
|
|
SessionID: "session-a",
|
|
SessionMode: noderuntime.SessionModeRequireExisting,
|
|
Input: map[string]any{"prompt": "should-fail"},
|
|
}, sinkA)
|
|
if err == nil {
|
|
t.Fatal("expected error for terminated session-a with require-existing mode")
|
|
}
|
|
|
|
_ = c.Stop(ctx)
|
|
}
|
|
|
|
func TestCLIStopStopsAllLogicalSessions(t *testing.T) {
|
|
testutil.RequireUnixShell(t)
|
|
|
|
cfg := config.CLIConf{
|
|
Enabled: true,
|
|
Profiles: map[string]config.CLIProfileConf{
|
|
"echo-stop": {
|
|
Command: "sh",
|
|
Args: []string{"-c", `while IFS= read -r line; do printf "reply:%s\n" "$line"; done`},
|
|
Persistent: true,
|
|
Terminal: false,
|
|
ResponseIdleTimeoutMS: 200,
|
|
},
|
|
},
|
|
}
|
|
c := clipkg.New(cfg, zap.NewNop())
|
|
ctx := context.Background()
|
|
|
|
// Create three logical sessions.
|
|
for _, sid := range []string{"s1", "s2", "s3"} {
|
|
sink := &testutil.FakeSink{}
|
|
if err := c.Execute(ctx, noderuntime.ExecutionSpec{
|
|
RunID: "run-" + sid,
|
|
Target: "echo-stop",
|
|
SessionID: sid,
|
|
Input: map[string]any{"prompt": "ping"},
|
|
}, sink); err != nil {
|
|
t.Fatalf("create %s: %v", sid, err)
|
|
}
|
|
}
|
|
|
|
if err := c.Stop(ctx); err != nil {
|
|
t.Fatalf("Stop: %v", err)
|
|
}
|
|
|
|
// After Stop, all sessions must be gone (require-existing must fail).
|
|
for _, sid := range []string{"s1", "s2", "s3"} {
|
|
sink := &testutil.FakeSink{}
|
|
err := c.Execute(ctx, noderuntime.ExecutionSpec{
|
|
RunID: "run-after-" + sid,
|
|
Target: "echo-stop",
|
|
SessionID: sid,
|
|
SessionMode: noderuntime.SessionModeRequireExisting,
|
|
Input: map[string]any{"prompt": "should-fail"},
|
|
}, sink)
|
|
if err == nil {
|
|
t.Fatalf("session %q should be stopped after Stop()", sid)
|
|
}
|
|
}
|
|
}
|
|
|
|
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, target string, profile config.CLIProfileConf) (*status.UsageStatus, error) {
|
|
if target == "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,
|
|
Target: "unknown",
|
|
})
|
|
if err == nil || !strings.Contains(err.Error(), "unknown target") {
|
|
t.Errorf("expected unknown target error, got %v", err)
|
|
}
|
|
|
|
resp, err := c.HandleCommand(context.Background(), noderuntime.CommandRequest{
|
|
RequestID: "req-123",
|
|
Type: noderuntime.CommandTypeUsageStatus,
|
|
Target: "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)
|
|
}
|
|
|
|
if meta := resp.UsageStatus.Metadata; meta == nil || meta["parse_status"] != "raw_only" {
|
|
t.Errorf("expected parse_status=raw_only metadata, got %+v", meta)
|
|
}
|
|
|
|
_, err = c.HandleCommand(context.Background(), noderuntime.CommandRequest{
|
|
Type: noderuntime.CommandType("invalid"),
|
|
Target: "codex",
|
|
})
|
|
if err == nil || !strings.Contains(err.Error(), "unsupported command") {
|
|
t.Errorf("expected unsupported command error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestCLIHandleCommandUsageStatusMetadataOnly(t *testing.T) {
|
|
c := clipkg.New(config.CLIConf{
|
|
Enabled: true,
|
|
Profiles: map[string]config.CLIProfileConf{
|
|
"codex": {Command: "/tmp/fake-codex"},
|
|
},
|
|
}, zap.NewNop())
|
|
|
|
c.StatusChecker = func(_ context.Context, _ string, _ config.CLIProfileConf) (*status.UsageStatus, error) {
|
|
return &status.UsageStatus{
|
|
Metadata: map[string]string{"custom_key": "custom_value"},
|
|
}, nil
|
|
}
|
|
|
|
resp, err := c.HandleCommand(context.Background(), noderuntime.CommandRequest{
|
|
Type: noderuntime.CommandTypeUsageStatus,
|
|
Target: "codex",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("HandleCommand: %v", err)
|
|
}
|
|
if resp.UsageStatus == nil {
|
|
t.Fatal("expected UsageStatus, got nil")
|
|
}
|
|
if got, ok := resp.UsageStatus.Metadata["parse_status"]; ok {
|
|
t.Errorf("metadata-only status must not have parse_status annotation, got %q", got)
|
|
}
|
|
if got := resp.UsageStatus.Metadata["custom_key"]; got != "custom_value" {
|
|
t.Errorf("custom_key: got %q want %q", got, "custom_value")
|
|
}
|
|
}
|
|
|
|
func TestCLIHandleCommandUsageStatusEmptyFallback(t *testing.T) {
|
|
c := clipkg.New(config.CLIConf{
|
|
Enabled: true,
|
|
Profiles: map[string]config.CLIProfileConf{
|
|
"codex": {Command: "/tmp/fake-codex"},
|
|
},
|
|
}, zap.NewNop())
|
|
|
|
c.StatusChecker = func(_ context.Context, _ string, _ config.CLIProfileConf) (*status.UsageStatus, error) {
|
|
return &status.UsageStatus{}, nil
|
|
}
|
|
|
|
resp, err := c.HandleCommand(context.Background(), noderuntime.CommandRequest{
|
|
Type: noderuntime.CommandTypeUsageStatus,
|
|
Target: "codex",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("HandleCommand: %v", err)
|
|
}
|
|
if resp.UsageStatus == nil {
|
|
t.Fatal("expected UsageStatus, got nil")
|
|
}
|
|
if meta := resp.UsageStatus.Metadata; meta == nil || meta["parse_status"] != "empty" {
|
|
t.Errorf("expected parse_status=empty for truly empty status, got %+v", meta)
|
|
}
|
|
}
|
|
|
|
func TestCLIHandleCommandUsageStatusNilCheckerResult(t *testing.T) {
|
|
c := clipkg.New(config.CLIConf{
|
|
Enabled: true,
|
|
Profiles: map[string]config.CLIProfileConf{
|
|
"codex": {Command: "/tmp/fake-codex"},
|
|
},
|
|
}, zap.NewNop())
|
|
|
|
c.StatusChecker = func(_ context.Context, _ string, _ config.CLIProfileConf) (*status.UsageStatus, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
_, err := c.HandleCommand(context.Background(), noderuntime.CommandRequest{
|
|
Type: noderuntime.CommandTypeUsageStatus,
|
|
Target: "codex",
|
|
})
|
|
if err == nil || !strings.Contains(err.Error(), "nil result") {
|
|
t.Fatalf("expected nil result error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestCLIHandleCommandUsageStatusRawOnlyMetadata(t *testing.T) {
|
|
c := clipkg.New(config.CLIConf{
|
|
Enabled: true,
|
|
Profiles: map[string]config.CLIProfileConf{
|
|
"codex": {Command: "/tmp/fake-codex"},
|
|
},
|
|
}, zap.NewNop())
|
|
|
|
c.StatusChecker = func(_ context.Context, _ string, _ config.CLIProfileConf) (*status.UsageStatus, error) {
|
|
return &status.UsageStatus{RawOutput: "some raw usage output"}, nil
|
|
}
|
|
|
|
resp, err := c.HandleCommand(context.Background(), noderuntime.CommandRequest{
|
|
RequestID: "req-raw",
|
|
Type: noderuntime.CommandTypeUsageStatus,
|
|
Target: "codex",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("HandleCommand: %v", err)
|
|
}
|
|
if resp.UsageStatus == nil {
|
|
t.Fatal("expected UsageStatus, got nil")
|
|
}
|
|
if resp.UsageStatus.RawOutput != "some raw usage output" {
|
|
t.Errorf("RawOutput: got %q want %q", resp.UsageStatus.RawOutput, "some raw usage output")
|
|
}
|
|
if meta := resp.UsageStatus.Metadata; meta == nil || meta["parse_status"] != "raw_only" {
|
|
t.Errorf("expected parse_status=raw_only, got %+v", meta)
|
|
}
|
|
}
|
|
|
|
func TestCLIHandleCommandSessionListReturnsEmptyResult(t *testing.T) {
|
|
c := clipkg.New(config.CLIConf{
|
|
Enabled: true,
|
|
Profiles: map[string]config.CLIProfileConf{
|
|
"codex": {Command: "/tmp/fake-codex"},
|
|
},
|
|
}, zap.NewNop())
|
|
|
|
resp, err := c.HandleCommand(context.Background(), noderuntime.CommandRequest{
|
|
RequestID: "req-list",
|
|
Type: noderuntime.CommandTypeSessionList,
|
|
Adapter: "cli",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("HandleCommand: %v", err)
|
|
}
|
|
if resp.Result == nil {
|
|
t.Fatalf("expected result map, got nil")
|
|
}
|
|
if got := resp.Result["count"]; got != "0" {
|
|
t.Fatalf("count: got %q want %q", got, "0")
|
|
}
|
|
if got, ok := resp.Result["sessions"]; !ok || got != "" {
|
|
t.Fatalf("sessions: got %q present=%v want empty string", got, ok)
|
|
}
|
|
}
|
|
|
|
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.Targets) != len(want) {
|
|
t.Fatalf("expected models %v, got %v", want, caps.Targets)
|
|
}
|
|
for i, m := range want {
|
|
if caps.Targets[i] != m {
|
|
t.Errorf("Models[%d] = %q, want %q", i, caps.Targets[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.Targets) != 0 {
|
|
t.Errorf("expected empty models, got %v", caps.Targets)
|
|
}
|
|
}
|