Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions internal/app/bootstrap_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1418,6 +1418,29 @@ func TestResolveBootstrapRuntimeMode(t *testing.T) {
}
}

func TestBuildRuntimeRejectsInvalidRuntimeMode(t *testing.T) {
t.Parallel()

_, err := BuildRuntime(context.Background(), BootstrapOptions{RuntimeMode: "invalid"})
if err == nil {
t.Fatalf("expected invalid runtime mode error")
}
}

func TestDefaultNewRemoteRuntimeAdapterReturnsInitError(t *testing.T) {
home := t.TempDir()
t.Setenv("HOME", home)
t.Setenv("USERPROFILE", home)

_, err := defaultNewRemoteRuntimeAdapter(services.RemoteRuntimeAdapterOptions{
ListenAddress: "ipc://127.0.0.1",
TokenFile: home + "/missing-token.json",
})
if err == nil {
t.Fatalf("expected defaultNewRemoteRuntimeAdapter to fail when token is missing")
}
}

func TestBuildRuntimeGatewayModeUsesRemoteAdapter(t *testing.T) {
disableBuiltinProviderAPIKeys(t)

Expand Down
90 changes: 90 additions & 0 deletions internal/cli/gateway_runtime_bridge_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package cli
import (
"context"
"errors"
"os"
"testing"
"time"

Expand Down Expand Up @@ -100,6 +101,50 @@ func (s *runtimeStub) ListSessionSkills(context.Context, string) ([]agentruntime
return nil, nil
}

type runtimeWithoutCreator struct {
base *runtimeStub
}

func (r *runtimeWithoutCreator) Submit(ctx context.Context, input agentruntime.PrepareInput) error {
return r.base.Submit(ctx, input)
}
func (r *runtimeWithoutCreator) PrepareUserInput(ctx context.Context, input agentruntime.PrepareInput) (agentruntime.UserInput, error) {
return r.base.PrepareUserInput(ctx, input)
}
func (r *runtimeWithoutCreator) Run(ctx context.Context, input agentruntime.UserInput) error {
return r.base.Run(ctx, input)
}
func (r *runtimeWithoutCreator) Compact(ctx context.Context, input agentruntime.CompactInput) (agentruntime.CompactResult, error) {
return r.base.Compact(ctx, input)
}
func (r *runtimeWithoutCreator) ExecuteSystemTool(ctx context.Context, input agentruntime.SystemToolInput) (tools.ToolResult, error) {
return r.base.ExecuteSystemTool(ctx, input)
}
func (r *runtimeWithoutCreator) ResolvePermission(ctx context.Context, input agentruntime.PermissionResolutionInput) error {
return r.base.ResolvePermission(ctx, input)
}
func (r *runtimeWithoutCreator) CancelActiveRun() bool {
return r.base.CancelActiveRun()
}
func (r *runtimeWithoutCreator) Events() <-chan agentruntime.RuntimeEvent {
return r.base.Events()
}
func (r *runtimeWithoutCreator) ListSessions(ctx context.Context) ([]agentsession.Summary, error) {
return r.base.ListSessions(ctx)
}
func (r *runtimeWithoutCreator) LoadSession(ctx context.Context, id string) (agentsession.Session, error) {
return r.base.LoadSession(ctx, id)
}
func (r *runtimeWithoutCreator) ActivateSessionSkill(ctx context.Context, sessionID string, skillID string) error {
return r.base.ActivateSessionSkill(ctx, sessionID, skillID)
}
func (r *runtimeWithoutCreator) DeactivateSessionSkill(ctx context.Context, sessionID string, skillID string) error {
return r.base.DeactivateSessionSkill(ctx, sessionID, skillID)
}
func (r *runtimeWithoutCreator) ListSessionSkills(ctx context.Context, sessionID string) ([]agentruntime.SessionSkillState, error) {
return r.base.ListSessionSkills(ctx, sessionID)
}

func TestNewGatewayRuntimePortBridgeRuntimeUnavailable(t *testing.T) {
bridge, err := newGatewayRuntimePortBridge(context.Background(), nil)
if err == nil {
Expand Down Expand Up @@ -302,6 +347,51 @@ func TestGatewayRuntimePortBridgeRuntimeMethods(t *testing.T) {
}
}

func TestGatewayRuntimePortBridgeLoadSessionNotFoundBranches(t *testing.T) {
t.Parallel()

base := &runtimeStub{
loadErr: errors.New("session not found"),
}
bridgeWithoutCreator, err := newGatewayRuntimePortBridge(context.Background(), &runtimeWithoutCreator{base: base})
if err != nil {
t.Fatalf("new bridge without creator: %v", err)
}
t.Cleanup(func() { _ = bridgeWithoutCreator.Close() })

if _, err := bridgeWithoutCreator.LoadSession(context.Background(), gateway.LoadSessionInput{
SubjectID: testBridgeSubjectID,
SessionID: "s-1",
}); !errors.Is(err, gateway.ErrRuntimeResourceNotFound) {
t.Fatalf("expected ErrRuntimeResourceNotFound, got %v", err)
}

stub := &runtimeStub{
loadErr: errors.New("file does not exist"),
createErr: errors.New("create failed"),
}
bridgeWithCreator, err := newGatewayRuntimePortBridge(context.Background(), stub)
if err != nil {
t.Fatalf("new bridge with creator: %v", err)
}
t.Cleanup(func() { _ = bridgeWithCreator.Close() })

if _, err := bridgeWithCreator.LoadSession(context.Background(), gateway.LoadSessionInput{
SubjectID: testBridgeSubjectID,
SessionID: "s-2",
}); err == nil || err.Error() != "create failed" {
t.Fatalf("expected create failed error, got %v", err)
}
}

func TestIsRuntimeNotFoundErrorIncludesOSErrNotExist(t *testing.T) {
t.Parallel()

if !isRuntimeNotFoundError(os.ErrNotExist) {
t.Fatalf("os.ErrNotExist should be treated as runtime not found")
}
}

func TestGatewayRuntimePortBridgeRuntimeMethodErrors(t *testing.T) {
stub := &runtimeStub{
submitErr: errors.New("submit failed"),
Expand Down
111 changes: 111 additions & 0 deletions internal/runtime/create_session_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,114 @@ func TestServiceCreateSessionReturnsOriginalErrorWhenMissingErrorIsNotSentinel(t
t.Fatalf("CreateSession() should not create on non-sentinel error, saves=%d", store.memoryStore.saves)
}
}

type createSessionDuplicateStore struct {
*createSessionUpsertStore
createErr error
loadHits int
loaded agentsession.Session
loadErr error
}

func (s *createSessionDuplicateStore) LoadSession(ctx context.Context, id string) (agentsession.Session, error) {
if err := ctx.Err(); err != nil {
return agentsession.Session{}, err
}
s.loadHits++
if s.loadHits == 1 {
return agentsession.Session{}, s.missingErr
}
if s.loadErr != nil {
return agentsession.Session{}, s.loadErr
}
return s.loaded, nil
}

func (s *createSessionDuplicateStore) CreateSession(ctx context.Context, input agentsession.CreateSessionInput) (agentsession.Session, error) {
if err := ctx.Err(); err != nil {
return agentsession.Session{}, err
}
if s.createErr != nil {
return agentsession.Session{}, s.createErr
}
return s.memoryStore.CreateSession(ctx, input)
}

func TestServiceCreateSessionBranches(t *testing.T) {
t.Parallel()

store := &createSessionUpsertStore{
memoryStore: newMemoryStore(),
missingErr: fmt.Errorf("load session row: %w", agentsession.ErrSessionNotFound),
}
service := &Service{
configManager: newRuntimeConfigManager(t),
sessionStore: store,
}

ctx, cancel := context.WithCancel(context.Background())
cancel()
if _, err := service.CreateSession(ctx, "session-canceled"); err == nil {
t.Fatalf("CreateSession() should reject canceled context")
}
if _, err := service.CreateSession(context.Background(), " "); err == nil {
t.Fatalf("CreateSession() should reject empty session id")
}
}

func TestServiceCreateSessionReturnsWorkdirResolutionError(t *testing.T) {
t.Parallel()

service := &Service{
sessionStore: newMemoryStore(),
// 不注入 configManager 会使默认 workdir 为空,触发 resolveWorkdirForSession 错误路径。
}
if _, err := service.CreateSession(context.Background(), "session-workdir"); err == nil {
t.Fatalf("CreateSession() should fail when default workdir cannot be resolved")
}
}

func TestServiceCreateSessionDuplicateCreateFallsBackToLoad(t *testing.T) {
t.Parallel()

store := &createSessionDuplicateStore{
createSessionUpsertStore: &createSessionUpsertStore{
memoryStore: newMemoryStore(),
missingErr: fmt.Errorf("load session row: %w", agentsession.ErrSessionNotFound),
},
createErr: fmt.Errorf("unique constraint failed"),
loaded: agentsession.Session{ID: "session-dup", Title: "loaded"},
}
service := &Service{
configManager: newRuntimeConfigManager(t),
sessionStore: store,
}

loaded, err := service.CreateSession(context.Background(), "session-dup")
if err != nil {
t.Fatalf("CreateSession() duplicate fallback error = %v", err)
}
if loaded.ID != "session-dup" || loaded.Title != "loaded" {
t.Fatalf("CreateSession() loaded session = %#v", loaded)
}
}

func TestCreateSessionErrorPredicates(t *testing.T) {
t.Parallel()

if isRuntimeSessionNotFoundError(nil) {
t.Fatalf("isRuntimeSessionNotFoundError(nil) should be false")
}
if !isRuntimeSessionNotFoundError(fmt.Errorf("wrapped: %w", agentsession.ErrSessionNotFound)) {
t.Fatalf("wrapped ErrSessionNotFound should be detected")
}

if isRuntimeSessionAlreadyExistsError(nil) {
t.Fatalf("isRuntimeSessionAlreadyExistsError(nil) should be false")
}
for _, text := range []string{"already exists", "UNIQUE CONSTRAINT", "duplicate key"} {
if !isRuntimeSessionAlreadyExistsError(fmt.Errorf("%s", text)) {
t.Fatalf("expected %q to be treated as already exists", text)
}
}
}
70 changes: 70 additions & 0 deletions internal/session/sqlite_store_additional_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,76 @@ func TestNormalizeCreateSessionInputDefaultsGeneratedID(t *testing.T) {
}
}

func TestSQLiteStoreCreateSessionPropagatesEnsureStorageDirsError(t *testing.T) {
t.Parallel()

store := &SQLiteStore{
projectDir: filepath.Join(t.TempDir(), "project"),
assetsDir: filepath.Join(t.TempDir(), "assets"),
dbPath: filepath.Join("/dev/null", "db.sqlite"),
}
_, err := store.CreateSession(context.Background(), CreateSessionInput{ID: "s1", Title: "title"})
if err == nil {
t.Fatalf("expected CreateSession() to fail when db dir cannot be created")
}
}

func TestSQLiteStoreEnsureStorageDirsErrorBranches(t *testing.T) {
t.Parallel()

dbDirErrStore := &SQLiteStore{
projectDir: filepath.Join(t.TempDir(), "project"),
assetsDir: filepath.Join(t.TempDir(), "assets"),
dbPath: filepath.Join("/dev/null", "db.sqlite"),
}
if err := dbDirErrStore.ensureStorageDirs(); err == nil || !strings.Contains(err.Error(), "create db dir") {
t.Fatalf("expected create db dir error, got %v", err)
}

projectDirErrStore := &SQLiteStore{
projectDir: filepath.Join("/dev/null", "project"),
assetsDir: filepath.Join(t.TempDir(), "assets"),
dbPath: filepath.Join(t.TempDir(), "db.sqlite"),
}
if err := projectDirErrStore.ensureStorageDirs(); err == nil || !strings.Contains(err.Error(), "create project dir") {
t.Fatalf("expected create project dir error, got %v", err)
}

assetsDirErrStore := &SQLiteStore{
projectDir: filepath.Join(t.TempDir(), "project"),
assetsDir: filepath.Join("/dev/null", "assets"),
dbPath: filepath.Join(t.TempDir(), "db.sqlite"),
}
if err := assetsDirErrStore.ensureStorageDirs(); err == nil || !strings.Contains(err.Error(), "create assets dir") {
t.Fatalf("expected create assets dir error, got %v", err)
}
}

func TestSQLiteStoreInitializePropagatesStorageDirError(t *testing.T) {
t.Parallel()

store := &SQLiteStore{
projectDir: filepath.Join(t.TempDir(), "project"),
assetsDir: filepath.Join(t.TempDir(), "assets"),
dbPath: filepath.Join("/dev/null", "db.sqlite"),
}
if err := store.initialize(context.Background()); err == nil {
t.Fatalf("expected initialize() to fail when storage dirs are invalid")
}
}

func TestWrapSessionNotFoundWithNilCause(t *testing.T) {
t.Parallel()

err := wrapSessionNotFound(nil)
if !errors.Is(err, ErrSessionNotFound) {
t.Fatalf("expected ErrSessionNotFound, got %v", err)
}
if !errors.Is(err, os.ErrNotExist) {
t.Fatalf("expected os.ErrNotExist, got %v", err)
}
}

func TestResolveUpdatedAtReturnsProvidedValue(t *testing.T) {
t.Parallel()

Expand Down
Loading
Loading