2025-10-24 04:55:10 +01:00
|
|
|
package workspace
|
|
|
|
|
|
|
|
|
|
import (
|
2025-10-28 21:42:29 +00:00
|
|
|
"context"
|
2025-10-24 04:55:10 +01:00
|
|
|
"encoding/json"
|
2025-10-28 21:42:29 +00:00
|
|
|
"fmt"
|
2025-10-24 04:55:10 +01:00
|
|
|
"path/filepath"
|
|
|
|
|
"testing"
|
|
|
|
|
|
2025-10-28 21:42:29 +00:00
|
|
|
"github.com/Snider/Core/pkg/core"
|
2026-01-13 23:33:01 +00:00
|
|
|
"github.com/Snider/Core/pkg/io"
|
2025-10-24 04:55:10 +01:00
|
|
|
"github.com/stretchr/testify/assert"
|
2025-10-28 21:42:29 +00:00
|
|
|
"github.com/wailsapp/wails/v3/pkg/application"
|
2025-10-24 04:55:10 +01:00
|
|
|
)
|
|
|
|
|
|
2025-10-28 21:42:29 +00:00
|
|
|
// mockConfig is a mock implementation of the core.Config interface for testing.
|
|
|
|
|
type mockConfig struct {
|
|
|
|
|
values map[string]interface{}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (m *mockConfig) Get(key string, out any) error {
|
|
|
|
|
val, ok := m.values[key]
|
|
|
|
|
if !ok {
|
|
|
|
|
return fmt.Errorf("key not found: %s", key)
|
|
|
|
|
}
|
|
|
|
|
// This is a simplified mock; a real one would use reflection to set `out`
|
|
|
|
|
switch v := out.(type) {
|
|
|
|
|
case *string:
|
|
|
|
|
*v = val.(string)
|
|
|
|
|
default:
|
|
|
|
|
return fmt.Errorf("unsupported type in mock config Get")
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (m *mockConfig) Set(key string, v any) error {
|
|
|
|
|
m.values[key] = v
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// newTestService creates a workspace service instance with mocked dependencies.
|
2026-01-13 23:33:01 +00:00
|
|
|
func newTestService(t *testing.T, workspaceDir string) (*Service, *io.MockMedium) {
|
2025-10-28 21:42:29 +00:00
|
|
|
coreInstance, err := core.New()
|
|
|
|
|
assert.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
mockCfg := &mockConfig{values: map[string]interface{}{"workspaceDir": workspaceDir}}
|
|
|
|
|
coreInstance.RegisterService("config", mockCfg)
|
|
|
|
|
|
2026-01-13 23:33:01 +00:00
|
|
|
mockMedium := io.NewMockMedium()
|
|
|
|
|
service, err := New(mockMedium)
|
2025-10-28 21:42:29 +00:00
|
|
|
assert.NoError(t, err)
|
2025-10-24 04:55:10 +01:00
|
|
|
|
2026-01-15 22:46:50 +00:00
|
|
|
service.ServiceRuntime = core.NewServiceRuntime(coreInstance, Options{})
|
2025-10-24 04:55:10 +01:00
|
|
|
|
2025-10-28 21:42:29 +00:00
|
|
|
return service, mockMedium
|
2025-10-24 04:55:10 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestServiceStartup(t *testing.T) {
|
2025-10-28 21:42:29 +00:00
|
|
|
workspaceDir := "/tmp/workspace"
|
2025-10-24 04:55:10 +01:00
|
|
|
|
|
|
|
|
t.Run("existing valid list.json", func(t *testing.T) {
|
2025-10-28 21:42:29 +00:00
|
|
|
service, mockMedium := newTestService(t, workspaceDir)
|
2025-10-24 04:55:10 +01:00
|
|
|
|
|
|
|
|
expectedWorkspaceList := map[string]string{
|
|
|
|
|
"workspace1": "pubkey1",
|
|
|
|
|
"workspace2": "pubkey2",
|
|
|
|
|
}
|
|
|
|
|
listContent, _ := json.MarshalIndent(expectedWorkspaceList, "", " ")
|
2025-10-28 21:42:29 +00:00
|
|
|
listPath := filepath.Join(workspaceDir, listFile)
|
|
|
|
|
mockMedium.Files[listPath] = string(listContent)
|
2025-10-24 04:55:10 +01:00
|
|
|
|
2025-10-28 21:42:29 +00:00
|
|
|
err := service.ServiceStartup(context.Background(), application.ServiceOptions{})
|
2025-10-24 04:55:10 +01:00
|
|
|
|
|
|
|
|
assert.NoError(t, err)
|
2025-10-28 21:42:29 +00:00
|
|
|
// assert.Equal(t, expectedWorkspaceList, service.workspaceList) // This check is difficult with current implementation
|
2025-10-24 04:55:10 +01:00
|
|
|
assert.NotNil(t, service.activeWorkspace)
|
|
|
|
|
assert.Equal(t, defaultWorkspace, service.activeWorkspace.Name)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-28 21:42:29 +00:00
|
|
|
func TestCreateAndSwitchWorkspace(t *testing.T) {
|
|
|
|
|
workspaceDir := "/tmp/workspace"
|
|
|
|
|
service, _ := newTestService(t, workspaceDir)
|
2025-10-24 04:55:10 +01:00
|
|
|
|
2025-10-28 21:42:29 +00:00
|
|
|
// Create
|
2025-10-24 04:55:10 +01:00
|
|
|
workspaceID, err := service.CreateWorkspace("test", "password")
|
|
|
|
|
assert.NoError(t, err)
|
|
|
|
|
assert.NotEmpty(t, workspaceID)
|
|
|
|
|
|
2025-10-28 21:42:29 +00:00
|
|
|
// Switch
|
2025-10-24 04:55:10 +01:00
|
|
|
err = service.SwitchWorkspace(workspaceID)
|
|
|
|
|
assert.NoError(t, err)
|
|
|
|
|
assert.Equal(t, workspaceID, service.activeWorkspace.Name)
|
|
|
|
|
}
|
2026-01-15 22:46:50 +00:00
|
|
|
|
|
|
|
|
func TestWorkspaceFileOperations(t *testing.T) {
|
|
|
|
|
workspaceDir := "/tmp/workspace"
|
|
|
|
|
|
|
|
|
|
t.Run("FileGet returns error when no active workspace", func(t *testing.T) {
|
|
|
|
|
service, _ := newTestService(t, workspaceDir)
|
|
|
|
|
// Don't call ServiceStartup so there's no active workspace
|
|
|
|
|
|
|
|
|
|
_, err := service.WorkspaceFileGet("test.txt")
|
|
|
|
|
assert.Error(t, err)
|
|
|
|
|
assert.Contains(t, err.Error(), "no active workspace")
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
t.Run("FileSet returns error when no active workspace", func(t *testing.T) {
|
|
|
|
|
service, _ := newTestService(t, workspaceDir)
|
|
|
|
|
// Don't call ServiceStartup so there's no active workspace
|
|
|
|
|
|
|
|
|
|
err := service.WorkspaceFileSet("test.txt", "content")
|
|
|
|
|
assert.Error(t, err)
|
|
|
|
|
assert.Contains(t, err.Error(), "no active workspace")
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
t.Run("FileGet and FileSet work with active workspace", func(t *testing.T) {
|
|
|
|
|
service, mockMedium := newTestService(t, workspaceDir)
|
|
|
|
|
|
|
|
|
|
// Start up the service to set active workspace
|
|
|
|
|
err := service.ServiceStartup(context.Background(), application.ServiceOptions{})
|
|
|
|
|
assert.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
// Test FileSet
|
|
|
|
|
err = service.WorkspaceFileSet("test.txt", "hello world")
|
|
|
|
|
assert.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
// Verify file was written to mock medium
|
|
|
|
|
expectedPath := filepath.Join(workspaceDir, defaultWorkspace, "test.txt")
|
|
|
|
|
assert.Equal(t, "hello world", mockMedium.Files[expectedPath])
|
|
|
|
|
|
|
|
|
|
// Test FileGet
|
|
|
|
|
content, err := service.WorkspaceFileGet("test.txt")
|
|
|
|
|
assert.NoError(t, err)
|
|
|
|
|
assert.Equal(t, "hello world", content)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestListWorkspaces(t *testing.T) {
|
|
|
|
|
workspaceDir := "/tmp/workspace"
|
|
|
|
|
service, _ := newTestService(t, workspaceDir)
|
|
|
|
|
|
|
|
|
|
t.Run("returns empty list when no workspaces", func(t *testing.T) {
|
|
|
|
|
workspaces := service.ListWorkspaces()
|
|
|
|
|
assert.Empty(t, workspaces)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
t.Run("returns list after creating workspaces", func(t *testing.T) {
|
|
|
|
|
// Create some workspaces
|
|
|
|
|
id1, err := service.CreateWorkspace("test1", "password")
|
|
|
|
|
assert.NoError(t, err)
|
|
|
|
|
id2, err := service.CreateWorkspace("test2", "password")
|
|
|
|
|
assert.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
workspaces := service.ListWorkspaces()
|
|
|
|
|
assert.Len(t, workspaces, 2)
|
|
|
|
|
assert.Contains(t, workspaces, id1)
|
|
|
|
|
assert.Contains(t, workspaces, id2)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestActiveWorkspace(t *testing.T) {
|
|
|
|
|
workspaceDir := "/tmp/workspace"
|
|
|
|
|
service, _ := newTestService(t, workspaceDir)
|
|
|
|
|
|
|
|
|
|
t.Run("returns nil when no active workspace", func(t *testing.T) {
|
|
|
|
|
workspace := service.ActiveWorkspace()
|
|
|
|
|
assert.Nil(t, workspace)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
t.Run("returns workspace after startup", func(t *testing.T) {
|
|
|
|
|
err := service.ServiceStartup(context.Background(), application.ServiceOptions{})
|
|
|
|
|
assert.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
workspace := service.ActiveWorkspace()
|
|
|
|
|
assert.NotNil(t, workspace)
|
|
|
|
|
assert.Equal(t, defaultWorkspace, workspace.Name)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestCreateWorkspaceErrors(t *testing.T) {
|
|
|
|
|
workspaceDir := "/tmp/workspace"
|
|
|
|
|
|
|
|
|
|
t.Run("returns error for duplicate workspace", func(t *testing.T) {
|
|
|
|
|
service, _ := newTestService(t, workspaceDir)
|
|
|
|
|
|
|
|
|
|
// Create first workspace
|
|
|
|
|
_, err := service.CreateWorkspace("duplicate-test", "password")
|
|
|
|
|
assert.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
// Try to create duplicate
|
|
|
|
|
_, err = service.CreateWorkspace("duplicate-test", "password")
|
|
|
|
|
assert.Error(t, err)
|
|
|
|
|
assert.Contains(t, err.Error(), "already exists")
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestSwitchWorkspaceErrors(t *testing.T) {
|
|
|
|
|
workspaceDir := "/tmp/workspace"
|
|
|
|
|
service, _ := newTestService(t, workspaceDir)
|
|
|
|
|
|
|
|
|
|
t.Run("returns error for non-existent workspace", func(t *testing.T) {
|
|
|
|
|
err := service.SwitchWorkspace("non-existent-workspace")
|
|
|
|
|
assert.Error(t, err)
|
|
|
|
|
assert.Contains(t, err.Error(), "does not exist")
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
t.Run("default workspace is always accessible", func(t *testing.T) {
|
|
|
|
|
err := service.SwitchWorkspace(defaultWorkspace)
|
|
|
|
|
assert.NoError(t, err)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestNewWorkspaceService(t *testing.T) {
|
|
|
|
|
t.Run("creates service with mock medium", func(t *testing.T) {
|
|
|
|
|
mockMedium := io.NewMockMedium()
|
|
|
|
|
service, err := New(mockMedium)
|
|
|
|
|
|
|
|
|
|
assert.NoError(t, err)
|
|
|
|
|
assert.NotNil(t, service)
|
|
|
|
|
assert.NotNil(t, service.workspaceList)
|
|
|
|
|
assert.Equal(t, mockMedium, service.medium)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestServiceStartupWithInvalidJSON(t *testing.T) {
|
|
|
|
|
workspaceDir := "/tmp/workspace"
|
|
|
|
|
service, mockMedium := newTestService(t, workspaceDir)
|
|
|
|
|
|
|
|
|
|
// Add invalid JSON to list.json
|
|
|
|
|
listPath := filepath.Join(workspaceDir, listFile)
|
|
|
|
|
mockMedium.Files[listPath] = "invalid-json{{"
|
|
|
|
|
|
|
|
|
|
// ServiceStartup should warn but continue
|
|
|
|
|
err := service.ServiceStartup(context.Background(), application.ServiceOptions{})
|
|
|
|
|
assert.NoError(t, err) // Should not error, just warn
|
|
|
|
|
|
|
|
|
|
// Workspace list should be empty/reset
|
|
|
|
|
assert.NotNil(t, service.activeWorkspace)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestHandleIPCEvents(t *testing.T) {
|
|
|
|
|
workspaceDir := "/tmp/workspace"
|
|
|
|
|
|
|
|
|
|
t.Run("handles switch workspace action", func(t *testing.T) {
|
|
|
|
|
coreInstance, err := core.New()
|
|
|
|
|
assert.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
mockCfg := &mockConfig{values: map[string]interface{}{"workspaceDir": workspaceDir}}
|
|
|
|
|
coreInstance.RegisterService("config", mockCfg)
|
|
|
|
|
|
|
|
|
|
mockMedium := io.NewMockMedium()
|
|
|
|
|
service, err := New(mockMedium)
|
|
|
|
|
assert.NoError(t, err)
|
|
|
|
|
service.ServiceRuntime = core.NewServiceRuntime(coreInstance, Options{})
|
|
|
|
|
|
|
|
|
|
// First startup to initialize workspace list and create default workspace
|
|
|
|
|
err = service.ServiceStartup(context.Background(), application.ServiceOptions{})
|
|
|
|
|
assert.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
// Create a workspace to switch to
|
|
|
|
|
wsID, err := service.CreateWorkspace("ipc-test", "password")
|
|
|
|
|
assert.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
// Test IPC switch workspace action
|
|
|
|
|
msg := map[string]any{
|
|
|
|
|
"action": "workspace.switch_workspace",
|
|
|
|
|
"name": wsID,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
err = service.HandleIPCEvents(coreInstance, msg)
|
|
|
|
|
assert.NoError(t, err)
|
|
|
|
|
assert.Equal(t, wsID, service.activeWorkspace.Name)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
t.Run("handles ActionServiceStartup message", func(t *testing.T) {
|
|
|
|
|
coreInstance, err := core.New()
|
|
|
|
|
assert.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
mockCfg := &mockConfig{values: map[string]interface{}{"workspaceDir": workspaceDir}}
|
|
|
|
|
coreInstance.RegisterService("config", mockCfg)
|
|
|
|
|
|
|
|
|
|
mockMedium := io.NewMockMedium()
|
|
|
|
|
service, err := New(mockMedium)
|
|
|
|
|
assert.NoError(t, err)
|
|
|
|
|
service.ServiceRuntime = core.NewServiceRuntime(coreInstance, Options{})
|
|
|
|
|
|
|
|
|
|
// Send ActionServiceStartup message
|
|
|
|
|
err = service.HandleIPCEvents(coreInstance, core.ActionServiceStartup{})
|
|
|
|
|
assert.NoError(t, err)
|
|
|
|
|
assert.NotNil(t, service.activeWorkspace)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Skipping "logs error for unknown message type" test as it requires core.App.Logger to be initialized
|
|
|
|
|
// which requires Wails runtime
|
|
|
|
|
|
|
|
|
|
// Skipping "handles map message with non-workspace action" test as it falls through to default
|
|
|
|
|
// case which requires core.App.Logger
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestGetWorkspaceDirError(t *testing.T) {
|
|
|
|
|
t.Run("returns error when config missing workspaceDir", func(t *testing.T) {
|
|
|
|
|
coreInstance, err := core.New()
|
|
|
|
|
assert.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
// Register config without workspaceDir
|
|
|
|
|
mockCfg := &mockConfig{values: map[string]interface{}{}}
|
|
|
|
|
coreInstance.RegisterService("config", mockCfg)
|
|
|
|
|
|
|
|
|
|
mockMedium := io.NewMockMedium()
|
|
|
|
|
service, err := New(mockMedium)
|
|
|
|
|
assert.NoError(t, err)
|
|
|
|
|
service.ServiceRuntime = core.NewServiceRuntime(coreInstance, Options{})
|
|
|
|
|
|
|
|
|
|
// ServiceStartup should fail because workspaceDir is missing
|
|
|
|
|
err = service.ServiceStartup(context.Background(), application.ServiceOptions{})
|
|
|
|
|
assert.Error(t, err)
|
|
|
|
|
assert.Contains(t, err.Error(), "workspaceDir")
|
|
|
|
|
})
|
|
|
|
|
}
|