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
32 changes: 21 additions & 11 deletions app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,12 @@ import (
)

type App struct {
ctx context.Context
engine *core.Engine
settings *core.Settings
profile *core.UserProfile
paths AppPaths
ctx context.Context
engine *core.Engine
settings *core.Settings
profile *core.UserProfile
paths AppPaths
persistPreferredRoot bool
}

type AppPaths struct {
Expand Down Expand Up @@ -76,16 +77,25 @@ type QuoteKeywordRegenerationResult struct {
}

func NewApp(root string) (*App, error) {
return NewAppWithOptions(root, AppOptions{PersistPreferredRoot: true})
}

type AppOptions struct {
PersistPreferredRoot bool
}

func NewAppWithOptions(root string, opts AppOptions) (*App, error) {
runtimeState, err := OpenRuntime(root)
if err != nil {
return nil, err
}

return &App{
engine: runtimeState.Engine,
settings: runtimeState.Settings,
profile: runtimeState.Profile,
paths: runtimeState.Paths,
engine: runtimeState.Engine,
settings: runtimeState.Settings,
profile: runtimeState.Profile,
paths: runtimeState.Paths,
persistPreferredRoot: opts.PersistPreferredRoot,
}, nil
}

Expand Down Expand Up @@ -256,12 +266,12 @@ func (a *App) ApplyRuntimeProvider(provider core.ProviderConfig) error {
}

func (a *App) SaveSettings(settings core.Settings) (*core.Settings, error) {
nextRuntime, err := SwitchRuntime(&RuntimeState{
nextRuntime, err := SwitchRuntimeWithOptions(&RuntimeState{
Engine: a.engine,
Settings: a.settings,
Profile: a.profile,
Paths: a.paths,
}, &settings)
}, &settings, RuntimeSwitchOptions{PersistPreferredRoot: a.persistPreferredRoot})
if err != nil {
if nextRuntime != nil {
a.engine = nextRuntime.Engine
Expand Down
92 changes: 63 additions & 29 deletions app/app_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ import (
func TestDesktopBackendQuoteShareRoundTrip(t *testing.T) {
t.Parallel()

alice, err := NewApp(filepath.Join(t.TempDir(), "alice"))
alice, err := NewAppWithOptions(filepath.Join(t.TempDir(), "alice"), AppOptions{})
if err != nil {
t.Fatalf("NewApp(alice) error = %v", err)
t.Fatalf("NewAppWithOptions(alice) error = %v", err)
}
t.Cleanup(func() { alice.Shutdown(context.Background()) })

Expand All @@ -35,9 +35,9 @@ func TestDesktopBackendQuoteShareRoundTrip(t *testing.T) {
t.Fatalf("Stat(exportPath) error = %v", err)
}

bob, err := NewApp(filepath.Join(t.TempDir(), "bob"))
bob, err := NewAppWithOptions(filepath.Join(t.TempDir(), "bob"), AppOptions{})
if err != nil {
t.Fatalf("NewApp(bob) error = %v", err)
t.Fatalf("NewAppWithOptions(bob) error = %v", err)
}
t.Cleanup(func() { bob.Shutdown(context.Background()) })

Expand Down Expand Up @@ -70,9 +70,9 @@ func TestDesktopBackendQuoteShareRoundTrip(t *testing.T) {
func TestDesktopBackendBootstrapState(t *testing.T) {
t.Parallel()

app, err := NewApp(filepath.Join(t.TempDir(), "desktop"))
app, err := NewAppWithOptions(filepath.Join(t.TempDir(), "desktop"), AppOptions{})
if err != nil {
t.Fatalf("NewApp() error = %v", err)
t.Fatalf("NewAppWithOptions() error = %v", err)
}
t.Cleanup(func() { app.Shutdown(context.Background()) })

Expand Down Expand Up @@ -105,9 +105,9 @@ func TestDesktopBackendBootstrapState(t *testing.T) {
func TestDesktopBackendCountQuotes(t *testing.T) {
t.Parallel()

app, err := NewApp(filepath.Join(t.TempDir(), "desktop-count"))
app, err := NewAppWithOptions(filepath.Join(t.TempDir(), "desktop-count"), AppOptions{})
if err != nil {
t.Fatalf("NewApp() error = %v", err)
t.Fatalf("NewAppWithOptions() error = %v", err)
}
t.Cleanup(func() { app.Shutdown(context.Background()) })

Expand Down Expand Up @@ -135,9 +135,9 @@ func TestDesktopBackendCountQuotes(t *testing.T) {
func TestDesktopBackendRecallHistoryLifecycle(t *testing.T) {
t.Parallel()

app, err := NewApp(filepath.Join(t.TempDir(), "desktop-history"))
app, err := NewAppWithOptions(filepath.Join(t.TempDir(), "desktop-history"), AppOptions{})
if err != nil {
t.Fatalf("NewApp() error = %v", err)
t.Fatalf("NewAppWithOptions() error = %v", err)
}
t.Cleanup(func() { app.Shutdown(context.Background()) })

Expand Down Expand Up @@ -216,9 +216,9 @@ func TestDesktopBackendRecallHistoryLifecycle(t *testing.T) {
func TestDesktopBackendSaveRecallAsQuote(t *testing.T) {
t.Parallel()

app, err := NewApp(filepath.Join(t.TempDir(), "desktop-recall-quote"))
app, err := NewAppWithOptions(filepath.Join(t.TempDir(), "desktop-recall-quote"), AppOptions{})
if err != nil {
t.Fatalf("NewApp() error = %v", err)
t.Fatalf("NewAppWithOptions() error = %v", err)
}
t.Cleanup(func() { app.Shutdown(context.Background()) })

Expand Down Expand Up @@ -248,9 +248,9 @@ func TestDesktopBackendSaveRecallAsQuote(t *testing.T) {
func TestDesktopBackendImportQuotesPayload(t *testing.T) {
t.Parallel()

app, err := NewApp(filepath.Join(t.TempDir(), "desktop-import-payload"))
app, err := NewAppWithOptions(filepath.Join(t.TempDir(), "desktop-import-payload"), AppOptions{})
if err != nil {
t.Fatalf("NewApp() error = %v", err)
t.Fatalf("NewAppWithOptions() error = %v", err)
}
t.Cleanup(func() { app.Shutdown(context.Background()) })

Expand All @@ -268,9 +268,9 @@ func TestDesktopBackendImportQuotesPayload(t *testing.T) {
t.Fatalf("PreviewQuoteExport() error = %v", err)
}

target, err := NewApp(filepath.Join(t.TempDir(), "desktop-import-target"))
target, err := NewAppWithOptions(filepath.Join(t.TempDir(), "desktop-import-target"), AppOptions{})
if err != nil {
t.Fatalf("NewApp(target) error = %v", err)
t.Fatalf("NewAppWithOptions(target) error = %v", err)
}
t.Cleanup(func() { target.Shutdown(context.Background()) })

Expand All @@ -292,9 +292,9 @@ func TestNewAppUsesDefaultStorageWhenRootIsEmpty(t *testing.T) {
t.Setenv("XDG_CONFIG_HOME", filepath.Join(t.TempDir(), "xdg-config"))
t.Setenv("XDG_STATE_HOME", filepath.Join(t.TempDir(), "xdg-state"))

app, err := NewApp("")
app, err := NewAppWithOptions("", AppOptions{})
if err != nil {
t.Fatalf("NewApp(\"\") error = %v", err)
t.Fatalf("NewAppWithOptions(\"\") error = %v", err)
}
t.Cleanup(func() { app.Shutdown(context.Background()) })

Expand All @@ -313,7 +313,8 @@ func TestNewAppUsesDefaultStorageWhenRootIsEmpty(t *testing.T) {
}

func TestDesktopBackendSaveSettingsSwitchesStorageRoot(t *testing.T) {
t.Setenv("XDG_CONFIG_HOME", filepath.Join(t.TempDir(), "xdg-config"))
xdgConfig := filepath.Join(t.TempDir(), "xdg-config")
t.Setenv("XDG_CONFIG_HOME", xdgConfig)

sourceRoot := filepath.Join(t.TempDir(), "desktop-source")
targetRoot := filepath.Join(t.TempDir(), "desktop-target")
Expand Down Expand Up @@ -371,15 +372,18 @@ func TestDesktopBackendSaveSettingsSwitchesStorageRoot(t *testing.T) {
if preferredRoot != absTarget {
t.Fatalf("preferred root = %q, want %q", preferredRoot, absTarget)
}
if _, err := os.Stat(filepath.Join(xdgConfig, "irecall", config.PreferredRootFileName)); err != nil {
t.Fatalf("preferred root marker missing from isolated config dir: %v", err)
}
}

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

root := filepath.Join(t.TempDir(), "runtime-provider")
app, err := NewApp(root)
app, err := NewAppWithOptions(root, AppOptions{})
if err != nil {
t.Fatalf("NewApp() error = %v", err)
t.Fatalf("NewAppWithOptions() error = %v", err)
}
t.Cleanup(func() { app.Shutdown(context.Background()) })

Expand Down Expand Up @@ -410,9 +414,9 @@ func TestApplyRuntimeProviderInitializesMissingSettings(t *testing.T) {
func TestDesktopBackendUpdateAndDeleteQuotes(t *testing.T) {
t.Parallel()

app, err := NewApp(filepath.Join(t.TempDir(), "desktop-update-delete"))
app, err := NewAppWithOptions(filepath.Join(t.TempDir(), "desktop-update-delete"), AppOptions{})
if err != nil {
t.Fatalf("NewApp() error = %v", err)
t.Fatalf("NewAppWithOptions() error = %v", err)
}
t.Cleanup(func() { app.Shutdown(context.Background()) })

Expand Down Expand Up @@ -452,9 +456,9 @@ func TestDesktopBackendUpdateAndDeleteQuotes(t *testing.T) {
func TestDesktopBackendPasswordAndAPITokenHelpers(t *testing.T) {
t.Parallel()

app, err := NewApp(filepath.Join(t.TempDir(), "desktop-auth-helpers"))
app, err := NewAppWithOptions(filepath.Join(t.TempDir(), "desktop-auth-helpers"), AppOptions{})
if err != nil {
t.Fatalf("NewApp() error = %v", err)
t.Fatalf("NewAppWithOptions() error = %v", err)
}
t.Cleanup(func() { app.Shutdown(context.Background()) })

Expand Down Expand Up @@ -512,9 +516,9 @@ func TestDesktopBackendPasswordAndAPITokenHelpers(t *testing.T) {
func TestDesktopBackendGetUserProfileReflectsSavedProfile(t *testing.T) {
t.Parallel()

app, err := NewApp(filepath.Join(t.TempDir(), "desktop-profile"))
app, err := NewAppWithOptions(filepath.Join(t.TempDir(), "desktop-profile"), AppOptions{})
if err != nil {
t.Fatalf("NewApp() error = %v", err)
t.Fatalf("NewAppWithOptions() error = %v", err)
}
t.Cleanup(func() { app.Shutdown(context.Background()) })

Expand All @@ -539,9 +543,9 @@ func TestDesktopBackendGetUserProfileReflectsSavedProfile(t *testing.T) {
func TestDesktopBackendRunRecallWithMockLLM(t *testing.T) {
t.Parallel()

app, err := NewApp(filepath.Join(t.TempDir(), "desktop-run-recall"))
app, err := NewAppWithOptions(filepath.Join(t.TempDir(), "desktop-run-recall"), AppOptions{})
if err != nil {
t.Fatalf("NewApp() error = %v", err)
t.Fatalf("NewAppWithOptions() error = %v", err)
}
t.Cleanup(func() { app.Shutdown(context.Background()) })

Expand Down Expand Up @@ -575,3 +579,33 @@ func TestDesktopBackendRunRecallWithMockLLM(t *testing.T) {
t.Fatalf("response = %q, want joined mock quote content", result.Response)
}
}

func TestDesktopBackendCanSkipPreferredRootPersistence(t *testing.T) {
xdgConfig := filepath.Join(t.TempDir(), "xdg-config")
t.Setenv("XDG_CONFIG_HOME", xdgConfig)

sourceRoot := filepath.Join(t.TempDir(), "source")
targetRoot := filepath.Join(t.TempDir(), "target")
app, err := NewAppWithOptions(sourceRoot, AppOptions{})
if err != nil {
t.Fatalf("NewAppWithOptions() error = %v", err)
}
t.Cleanup(func() { app.Shutdown(context.Background()) })

settings := *app.GetSettings()
settings.RootDir = targetRoot
if _, err := app.SaveSettings(settings); err != nil {
t.Fatalf("SaveSettings() error = %v", err)
}

preferredRoot, err := config.LoadPreferredRootPath()
if err != nil {
t.Fatalf("LoadPreferredRootPath() error = %v", err)
}
if preferredRoot != "" {
t.Fatalf("preferred root = %q, want empty when app persistence disabled", preferredRoot)
}
if _, err := os.Stat(filepath.Join(xdgConfig, "irecall", config.PreferredRootFileName)); !os.IsNotExist(err) {
t.Fatalf("preferred root marker exists or stat failed: %v", err)
}
}
22 changes: 17 additions & 5 deletions app/runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,14 @@ func OpenRuntime(root string) (*RuntimeState, error) {
}

func SwitchRuntime(current *RuntimeState, nextSettings *core.Settings) (*RuntimeState, error) {
return SwitchRuntimeWithOptions(current, nextSettings, RuntimeSwitchOptions{PersistPreferredRoot: true})
}

type RuntimeSwitchOptions struct {
PersistPreferredRoot bool
}

func SwitchRuntimeWithOptions(current *RuntimeState, nextSettings *core.Settings, opts RuntimeSwitchOptions) (*RuntimeState, error) {
if current == nil || current.Engine == nil {
return nil, fmt.Errorf("runtime is not initialized")
}
Expand All @@ -83,8 +91,10 @@ func SwitchRuntime(current *RuntimeState, nextSettings *core.Settings) (*Runtime
if err := current.Engine.SaveSettings(context.Background(), nextSettings); err != nil {
return nil, err
}
if err := config.SavePreferredRootPath(nextRoot); err != nil {
return nil, fmt.Errorf("persist preferred root: %w", err)
if opts.PersistPreferredRoot {
if err := config.SavePreferredRootPath(nextRoot); err != nil {
return nil, fmt.Errorf("persist preferred root: %w", err)
}
}
current.Settings = nextSettings
current.Paths.RootDir = nextRoot
Expand Down Expand Up @@ -129,9 +139,11 @@ func SwitchRuntime(current *RuntimeState, nextSettings *core.Settings) (*Runtime
}
nextRuntime.Settings = nextSettings

if err := config.SavePreferredRootPath(nextRoot); err != nil {
_ = nextRuntime.Engine.Close()
return restoreOnError(fmt.Errorf("persist preferred root: %w", err))
if opts.PersistPreferredRoot {
if err := config.SavePreferredRootPath(nextRoot); err != nil {
_ = nextRuntime.Engine.Close()
return restoreOnError(fmt.Errorf("persist preferred root: %w", err))
}
}

return nextRuntime, nil
Expand Down
28 changes: 26 additions & 2 deletions app/runtime_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,30 @@ func TestSwitchRuntimeUsesExistingTargetWithoutCopyingSourceData(t *testing.T) {
}
}

func TestSwitchRuntimeCanSkipPreferredRootPersistence(t *testing.T) {
t.Setenv("XDG_CONFIG_HOME", filepath.Join(t.TempDir(), "xdg-config"))

current := openRuntimeForTest(t, filepath.Join(t.TempDir(), "current"))
defer func() { _ = current.Engine.Close() }()

next := *current.Settings
next.RootDir = filepath.Join(t.TempDir(), "target")

switched, err := SwitchRuntimeWithOptions(current, &next, RuntimeSwitchOptions{PersistPreferredRoot: false})
if err != nil {
t.Fatalf("SwitchRuntimeWithOptions() error = %v", err)
}
defer func() { _ = switched.Engine.Close() }()

preferredRoot, err := config.LoadPreferredRootPath()
if err != nil {
t.Fatalf("LoadPreferredRootPath() error = %v", err)
}
if preferredRoot != "" {
t.Fatalf("preferred root = %q, want empty when persistence disabled", preferredRoot)
}
}

func TestSwitchRuntimeRestoresOriginalRuntimeOnCopyFailure(t *testing.T) {
sourceRoot := filepath.Join(t.TempDir(), "source")
seedQuoteForRuntimeTest(t, sourceRoot, "restore me")
Expand Down Expand Up @@ -291,9 +315,9 @@ func openRuntimeForTest(t *testing.T, root string) *RuntimeState {
func seedQuoteForRuntimeTest(t *testing.T, root, content string) {
t.Helper()

app, err := NewApp(root)
app, err := NewAppWithOptions(root, AppOptions{})
if err != nil {
t.Fatalf("NewApp(%q) error = %v", root, err)
t.Fatalf("NewAppWithOptions(%q) error = %v", root, err)
}
if _, err := app.AddQuote(content); err != nil {
_ = app.engine.Close()
Expand Down
Loading
Loading