iop/apps/node/internal/adapters/cli/cli_workspace_test.go
toki 01dc2ef78b refactor: readability baseline 및 테스트 구조 개선
- agent-readable-repository-refactor 완료: 기존 테스트 파일 아카이브 이동
- 새 테스트 파일 추가 (edge, node, client, config, readability)
- readability_audit 스크립트 및 baseline 추가
- roadmap/SDD 문서 갱신
- agent-client/pi/extensions/openai-sampling-parameters 추가
2026-07-17 16:02:12 +09:00

418 lines
12 KiB
Go

package cli
import (
"context"
"iop/apps/node/internal/runtime"
"iop/packages/go/config"
"os"
"strings"
"testing"
"time"
)
func TestCLIOneshotExecutorCwd(t *testing.T) {
c := New(config.CLIConf{
Profiles: map[string]config.CLIProfileConf{
"oneshot-pwd": {
Command: "pwd",
},
},
}, nil)
tmpDir := t.TempDir()
spec := runtime.ExecutionSpec{
Target: "oneshot-pwd",
Workspace: tmpDir,
}
sink := &testSink{}
err := c.Execute(context.Background(), spec, sink)
if err != nil {
t.Fatalf("Execute failed: %v", err)
}
var output string
for _, ev := range sink.events {
if ev.Type == runtime.EventTypeDelta {
output += ev.Delta
}
}
resolved, err := os.Readlink(tmpDir)
if err != nil {
resolved = tmpDir
}
if !strings.Contains(output, tmpDir) && !strings.Contains(output, resolved) {
t.Errorf("expected output to contain workspace path %q, got: %q", tmpDir, output)
}
}
func TestCLIPersistentNonTerminalCwd(t *testing.T) {
c := New(config.CLIConf{
Profiles: map[string]config.CLIProfileConf{
"persistent-pwd": {
Command: "sh",
Persistent: true,
},
},
}, nil)
tmpDir := t.TempDir()
spec := runtime.ExecutionSpec{
Target: "persistent-pwd",
Workspace: tmpDir,
SessionID: "sess-cwd-nt",
SessionMode: runtime.SessionModeCreateIfMissing,
Input: map[string]any{"prompt": "pwd\n"},
}
sink := &testSink{}
err := c.Execute(context.Background(), spec, sink)
if err != nil {
t.Fatalf("Execute failed: %v", err)
}
defer func() {
_ = c.Stop(context.Background())
}()
var output string
success := false
resolved, err := os.Readlink(tmpDir)
if err != nil {
resolved = tmpDir
}
// Wait up to 2 seconds for output
for i := 0; i < 20; i++ {
output = ""
for _, ev := range sink.events {
if ev.Type == runtime.EventTypeDelta {
output += ev.Delta
}
}
if strings.Contains(output, tmpDir) || strings.Contains(output, resolved) {
success = true
break
}
time.Sleep(100 * time.Millisecond)
}
if !success {
t.Errorf("expected output to contain workspace path %q, got: %q", tmpDir, output)
}
}
func TestCLIPersistentTerminalCwd(t *testing.T) {
c := New(config.CLIConf{
Profiles: map[string]config.CLIProfileConf{
"persistent-term-pwd": {
Command: "sh",
Persistent: true,
Terminal: true,
},
},
}, nil)
tmpDir := t.TempDir()
spec := runtime.ExecutionSpec{
Target: "persistent-term-pwd",
Workspace: tmpDir,
SessionID: "sess-cwd-term",
SessionMode: runtime.SessionModeCreateIfMissing,
Input: map[string]any{"prompt": "pwd"},
}
sink := &testSink{}
err := c.Execute(context.Background(), spec, sink)
if err != nil {
t.Fatalf("Execute failed: %v", err)
}
defer func() {
_ = c.Stop(context.Background())
}()
var output string
success := false
resolved, err := os.Readlink(tmpDir)
if err != nil {
resolved = tmpDir
}
// Wait up to 2 seconds for output
for i := 0; i < 20; i++ {
output = ""
for _, ev := range sink.events {
if ev.Type == runtime.EventTypeDelta {
output += ev.Delta
}
}
if strings.Contains(output, tmpDir) || strings.Contains(output, resolved) {
success = true
break
}
time.Sleep(100 * time.Millisecond)
}
if !success {
t.Errorf("expected output to contain workspace path %q, got: %q", tmpDir, output)
}
}
func TestCLIWorkspacePreflightFailures(t *testing.T) {
// 1. prepareWorkspaceDir helper tests
t.Run("Helper", func(t *testing.T) {
// Blank workspace
dir, err := prepareWorkspaceDir("")
if err != nil || dir != "" {
t.Errorf("expected empty dir and nil error for blank workspace, got dir=%q, err=%v", dir, err)
}
dir, err = prepareWorkspaceDir(" ")
if err != nil || dir != "" {
t.Errorf("expected empty dir and nil error for whitespace workspace, got dir=%q, err=%v", dir, err)
}
// Not found directory
nonExistentPath := "/nonexistent/path/for/workspace/test"
dir, err = prepareWorkspaceDir(nonExistentPath)
if err == nil || !strings.Contains(err.Error(), "cli adapter: workspace not found") {
t.Errorf("expected 'workspace not found' error, got dir=%q, err=%v", dir, err)
}
// File is not a directory
tmpFile, err := os.CreateTemp("", "test-workspace-file-*")
if err != nil {
t.Fatalf("failed to create temp file: %v", err)
}
defer os.Remove(tmpFile.Name())
_ = tmpFile.Close()
dir, err = prepareWorkspaceDir(tmpFile.Name())
if err == nil || !strings.Contains(err.Error(), "cli adapter: workspace is not a directory") {
t.Errorf("expected 'workspace is not a directory' error, got dir=%q, err=%v", dir, err)
}
// Inaccessible directory
if os.Geteuid() != 0 {
// Create a directory and make it inaccessible
inaccessibleDir := t.TempDir() + "/inaccessible"
if err := os.Mkdir(inaccessibleDir, 0000); err != nil {
t.Fatalf("failed to create inaccessible dir: %v", err)
}
dir, err = prepareWorkspaceDir(inaccessibleDir)
if err == nil || !strings.Contains(err.Error(), "cli adapter: workspace inaccessible") {
t.Errorf("expected 'workspace inaccessible' error, got dir=%q, err=%v", dir, err)
}
// Restore permission for cleanup
_ = os.Chmod(inaccessibleDir, 0700)
} else {
t.Log("running as root; skipping inaccessible workspace helper test")
}
})
// 2. Execute path preflight error propagation
t.Run("ExecuteOneshot", func(t *testing.T) {
c := New(config.CLIConf{
Profiles: map[string]config.CLIProfileConf{
"oneshot-pwd": {
Command: "pwd",
},
},
}, nil)
spec := runtime.ExecutionSpec{
RunID: "oneshot-run-id",
Target: "oneshot-pwd",
Workspace: "/nonexistent/path/for/workspace/test",
}
sink := &testSink{}
err := c.Execute(context.Background(), spec, sink)
if err == nil {
t.Fatal("expected execute to fail for nonexistent workspace")
}
if !strings.Contains(err.Error(), "cli adapter: workspace not found") {
t.Errorf("expected 'workspace not found' error, got: %v", err)
}
// Verify sink EventTypeError event
hasErrorEvent := false
for _, ev := range sink.events {
if ev.Type == runtime.EventTypeError {
hasErrorEvent = true
if ev.RunID != spec.RunID {
t.Errorf("expected EventTypeError RunID to be %q, got %q", spec.RunID, ev.RunID)
}
if !strings.Contains(ev.Error, "cli adapter: workspace not found") {
t.Errorf("expected EventTypeError Error to contain 'workspace not found', got %q", ev.Error)
}
}
}
if !hasErrorEvent {
t.Errorf("expected sink to contain EventTypeError event, but it did not")
}
})
t.Run("ExecutePersistent", func(t *testing.T) {
c := New(config.CLIConf{
Profiles: map[string]config.CLIProfileConf{
"persistent-pwd": {
Command: "sh",
Persistent: true,
},
},
}, nil)
spec := runtime.ExecutionSpec{
RunID: "persistent-run-id",
Target: "persistent-pwd",
Workspace: "/nonexistent/path/for/workspace/test",
SessionID: "sess-preflight-fail",
SessionMode: runtime.SessionModeCreateIfMissing,
Input: map[string]any{"prompt": "pwd\n"},
}
sink := &testSink{}
err := c.Execute(context.Background(), spec, sink)
if err == nil {
t.Fatal("expected execute to fail for nonexistent workspace")
}
if !strings.Contains(err.Error(), "cli adapter: workspace not found") {
t.Errorf("expected 'workspace not found' error, got: %v", err)
}
// Verify sink EventTypeError event
hasErrorEvent := false
for _, ev := range sink.events {
if ev.Type == runtime.EventTypeError {
hasErrorEvent = true
if ev.RunID != spec.RunID {
t.Errorf("expected EventTypeError RunID to be %q, got %q", spec.RunID, ev.RunID)
}
if !strings.Contains(ev.Error, "cli adapter: workspace not found") {
t.Errorf("expected EventTypeError Error to contain 'workspace not found', got %q", ev.Error)
}
}
}
if !hasErrorEvent {
t.Errorf("expected sink to contain EventTypeError event, but it did not")
}
})
t.Run("ExecuteCodexAppServer", func(t *testing.T) {
c := New(config.CLIConf{
Profiles: map[string]config.CLIProfileConf{
"codex-app": {
Mode: modeCodexAppServer,
Command: "codex",
Persistent: true,
Args: []string{"--app-server"},
},
},
}, nil)
spec := runtime.ExecutionSpec{
RunID: "codex-run-id",
Target: "codex-app",
Workspace: "/nonexistent/path/for/workspace/test",
SessionID: "sess-codex-preflight-fail",
SessionMode: runtime.SessionModeCreateIfMissing,
Input: map[string]any{"prompt": "some prompt"},
}
sink := &testSink{}
err := c.Execute(context.Background(), spec, sink)
if err == nil {
t.Fatal("expected execute to fail for nonexistent workspace")
}
if !strings.Contains(err.Error(), "cli adapter: start codex app-server") ||
!strings.Contains(err.Error(), "cli adapter: workspace not found") {
t.Errorf("expected 'start codex app-server' and 'workspace not found' errors, got: %v", err)
}
// Verify sink EventTypeError event
hasErrorEvent := false
for _, ev := range sink.events {
if ev.Type == runtime.EventTypeError {
hasErrorEvent = true
if ev.RunID != spec.RunID {
t.Errorf("expected EventTypeError RunID to be %q, got %q", spec.RunID, ev.RunID)
}
if !strings.Contains(ev.Error, "cli adapter: start codex app-server") ||
!strings.Contains(ev.Error, "cli adapter: workspace not found") {
t.Errorf("expected EventTypeError Error to contain 'start codex app-server' and 'workspace not found', got %q", ev.Error)
}
}
}
if !hasErrorEvent {
t.Errorf("expected sink to contain EventTypeError event, but it did not")
}
})
t.Run("ExecuteOpencodeSSE", func(t *testing.T) {
c := New(config.CLIConf{
Profiles: map[string]config.CLIProfileConf{
"opencode-sse-target": {
Mode: modeOpencodeSSE,
Command: "opencode",
Persistent: true,
Args: []string{"serve"}, // triggers opencode-sse mode
},
},
}, nil)
spec := runtime.ExecutionSpec{
RunID: "opencode-run-id",
Target: "opencode-sse-target",
Workspace: "/nonexistent/path/for/workspace/test",
SessionID: "sess-opencode-preflight-fail",
SessionMode: runtime.SessionModeCreateIfMissing,
Input: map[string]any{"prompt": "some prompt"},
}
sink := &testSink{}
err := c.Execute(context.Background(), spec, sink)
if err == nil {
t.Fatal("expected execute to fail for nonexistent workspace")
}
if !strings.Contains(err.Error(), "cli adapter: start opencode server") ||
!strings.Contains(err.Error(), "cli adapter: workspace not found") {
t.Errorf("expected 'start opencode server' and 'workspace not found' errors, got: %v", err)
}
// Verify sink EventTypeError event
hasErrorEvent := false
for _, ev := range sink.events {
if ev.Type == runtime.EventTypeError {
hasErrorEvent = true
if ev.RunID != spec.RunID {
t.Errorf("expected EventTypeError RunID to be %q, got %q", spec.RunID, ev.RunID)
}
if !strings.Contains(ev.Error, "cli adapter: start opencode server") ||
!strings.Contains(ev.Error, "cli adapter: workspace not found") {
t.Errorf("expected EventTypeError Error to contain 'start opencode server' and 'workspace not found', got %q", ev.Error)
}
}
}
if !hasErrorEvent {
t.Errorf("expected sink to contain EventTypeError event, but it did not")
}
})
}
func TestCLIProcessExitFailureRemainsDistinct(t *testing.T) {
c := New(config.CLIConf{
Profiles: map[string]config.CLIProfileConf{
"fail-oneshot": {
Command: "sh",
Args: []string{"-c", "exit 7"},
},
},
}, nil)
tmpDir := t.TempDir()
spec := runtime.ExecutionSpec{
Target: "fail-oneshot",
Workspace: tmpDir,
}
sink := &testSink{}
err := c.Execute(context.Background(), spec, sink)
if err == nil {
t.Fatal("expected command execution to fail")
}
errStr := err.Error()
// Command failure should NOT look like a workspace error.
if strings.Contains(errStr, "workspace not found") ||
strings.Contains(errStr, "workspace is not a directory") ||
strings.Contains(errStr, "workspace inaccessible") {
t.Errorf("process exit failure should be distinct from workspace preflight error, got: %q", errStr)
}
if !strings.Contains(errStr, "exit status 7") && !strings.Contains(errStr, "exit code 7") {
t.Errorf("expected error to mention exit status/code 7, got: %q", errStr)
}
}