package workspace import ( "context" "errors" "reflect" "runtime" "sync" "testing" "time" preferencesbiz "github.com/tutti-os/tutti/services/tuttid/biz/preferences" ) func TestSQLiteStoreGetDesktopPreferencesDefaultsWhenUnset(t *testing.T) { t.Parallel() store := openTestSQLiteStore(t) preferences, err := store.GetDesktopPreferences(context.Background()) if err != nil { t.Fatalf("GetDesktopPreferences() error = %v", err) } if preferences.Initialized { t.Fatal("GetDesktopPreferences() initialized = true, want false") } if preferences.Locale != "en" { t.Fatalf("GetDesktopPreferences() locale = %q, want en", preferences.Locale) } if preferences.DockPlacement != "bottom" { t.Fatalf("GetDesktopPreferences() dockPlacement = %q, want bottom", preferences.DockPlacement) } if preferences.DockIconStyle != "default" { t.Fatalf("GetDesktopPreferences() dockIconStyle = %q, want default", preferences.DockIconStyle) } if preferences.DeletedAgentConversationRetentionDays != 30 { t.Fatalf("GetDesktopPreferences() retention days = %d, want 30", preferences.DeletedAgentConversationRetentionDays) } if preferences.DefaultAgentProvider != "tutti-agent" { t.Fatalf("GetDesktopPreferences() defaultAgentProvider = %q, want tutti-agent", preferences.DefaultAgentProvider) } if preferences.AgentConversationDetailMode != "coding" { t.Fatalf("GetDesktopPreferences() agentConversationDetailMode = %q, want coding", preferences.AgentConversationDetailMode) } if preferences.AgentDockLayout != "unified" { t.Fatalf("GetDesktopPreferences() agentDockLayout = %q, want unified", preferences.AgentDockLayout) } if preferences.ThemeSource != "dark" { t.Fatalf("GetDesktopPreferences() themeSource = %q, want dark", preferences.ThemeSource) } if preferences.SleepPreventionMode != "never" { t.Fatalf("GetDesktopPreferences() sleepPreventionMode = %q, want never", preferences.SleepPreventionMode) } if preferences.BrowserUseConnectionMode != "isolated" { t.Fatalf("GetDesktopPreferences() browserUseConnectionMode = %q, want isolated", preferences.BrowserUseConnectionMode) } if preferences.AppCatalogChannel != "production" { t.Fatalf("GetDesktopPreferences() appCatalogChannel = %q, want production", preferences.AppCatalogChannel) } if preferences.FileDefaultOpenersByExtension["html"] != "appBrowser" { t.Fatalf("GetDesktopPreferences() html opener = %q, want appBrowser", preferences.FileDefaultOpenersByExtension["html"]) } if len(preferences.AgentGUIConversationRailCollapsedByProvider) != 0 { t.Fatalf("GetDesktopPreferences() rail collapsed preferences = %#v, want empty", preferences.AgentGUIConversationRailCollapsedByProvider) } if len(preferences.AgentSessionLaunchModesByWorkspace) != 0 { t.Fatalf("GetDesktopPreferences() agent session launch modes = %#v, want empty", preferences.AgentSessionLaunchModesByWorkspace) } if preferences.UpdatePolicy != "prompt" { t.Fatalf("GetDesktopPreferences() updatePolicy = %q, want prompt", preferences.UpdatePolicy) } if preferences.UpdateChannel != "stable" { t.Fatalf("GetDesktopPreferences() updateChannel = %q, want stable", preferences.UpdateChannel) } } func TestSQLiteStorePutDesktopPreferencesPersistsValue(t *testing.T) { t.Parallel() store := openTestSQLiteStore(t) ctx := context.Background() saved, err := store.PutDesktopPreferences(ctx, preferencesbiz.DesktopPreferences{ AgentCLIUpdateCheckEnabled: false, AgentComposerDefaultsByProvider: map[string]preferencesbiz.AgentComposerDefaults{ "codex": { Model: "gpt-5", PermissionModeID: "full-access", ReasoningEffort: "high", }, }, AgentGUIConversationRailCollapsedByProvider: map[string]bool{ "codex": true, "claude-code": false, }, AgentSessionLaunchModesByWorkspace: map[string]map[string]string{ "workspace-1": { "project-1": "worktree", "project-2": "local", }, }, AgentConversationDetailMode: "general", AgentDockLayout: "unified", DefaultAgentProvider: "claude-code", BrowserUseConnectionMode: "autoConnect", AppCatalogChannel: "staging", DockIconStyle: "default", DockPlacement: "left", DeletedAgentConversationRetentionDays: 15, FileDefaultOpenersByExtension: map[string]string{ "html": "fileViewer", "pdf": "defaultBrowser", }, Initialized: true, Locale: "zh-CN", MinimizeAnimation: "scale", SleepPreventionMode: "whileAgentRunning", ThemeSource: "dark", UpdateChannel: "rc", UpdatePolicy: "auto", }) if err != nil { t.Fatalf("PutDesktopPreferences() error = %v", err) } if !saved.Initialized { t.Fatal("PutDesktopPreferences() initialized = false, want true") } if saved.AgentCLIUpdateCheckEnabled { t.Fatal("PutDesktopPreferences() agent CLI update check = true, want false") } reloaded, err := store.GetDesktopPreferences(ctx) if err != nil { t.Fatalf("GetDesktopPreferences() error = %v", err) } if !reloaded.Initialized { t.Fatal("GetDesktopPreferences() initialized = false, want true") } if reloaded.AgentCLIUpdateCheckEnabled { t.Fatal("GetDesktopPreferences() agent CLI update check = true, want false") } if reloaded.Locale != "zh-CN" { t.Fatalf("GetDesktopPreferences() locale = %q, want zh-CN", reloaded.Locale) } if reloaded.DockPlacement != "left" { t.Fatalf("GetDesktopPreferences() dockPlacement = %q, want left", reloaded.DockPlacement) } if reloaded.DeletedAgentConversationRetentionDays != 15 { t.Fatalf("GetDesktopPreferences() retention days = %d, want 15", reloaded.DeletedAgentConversationRetentionDays) } if reloaded.DefaultAgentProvider != "claude-code" { t.Fatalf("GetDesktopPreferences() defaultAgentProvider = %q, want claude-code", reloaded.DefaultAgentProvider) } if reloaded.AgentConversationDetailMode != "general" { t.Fatalf("GetDesktopPreferences() agentConversationDetailMode = %q, want general", reloaded.AgentConversationDetailMode) } if reloaded.AgentDockLayout != "unified" { t.Fatalf("GetDesktopPreferences() agentDockLayout = %q, want unified", reloaded.AgentDockLayout) } if reloaded.ThemeSource != "dark" { t.Fatalf("GetDesktopPreferences() themeSource = %q, want dark", reloaded.ThemeSource) } if reloaded.SleepPreventionMode != "whileAgentRunning" { t.Fatalf("GetDesktopPreferences() sleepPreventionMode = %q, want whileAgentRunning", reloaded.SleepPreventionMode) } if reloaded.BrowserUseConnectionMode != "autoConnect" { t.Fatalf("GetDesktopPreferences() browserUseConnectionMode = %q, want autoConnect", reloaded.BrowserUseConnectionMode) } if reloaded.AppCatalogChannel != "staging" { t.Fatalf("GetDesktopPreferences() appCatalogChannel = %q, want staging", reloaded.AppCatalogChannel) } if reloaded.FileDefaultOpenersByExtension["html"] != "fileViewer" || reloaded.FileDefaultOpenersByExtension["pdf"] != "defaultBrowser" { t.Fatalf("GetDesktopPreferences() file default openers = %#v, want html/pdf", reloaded.FileDefaultOpenersByExtension) } if !reloaded.AgentGUIConversationRailCollapsedByProvider["codex"] { t.Fatalf("GetDesktopPreferences() codex rail collapsed = false, want true") } if collapsed, ok := reloaded.AgentGUIConversationRailCollapsedByProvider["claude-code"]; !ok || collapsed { t.Fatalf("GetDesktopPreferences() claude rail collapsed = %v/%v, want present false", collapsed, ok) } workspaceLaunchModes := reloaded.AgentSessionLaunchModesByWorkspace["workspace-1"] if workspaceLaunchModes["project-1"] != "worktree" || workspaceLaunchModes["project-2"] != "local" { t.Fatalf("GetDesktopPreferences() agent session launch modes = %#v, want project-1/worktree and project-2/local", reloaded.AgentSessionLaunchModesByWorkspace) } if reloaded.UpdatePolicy != "auto" { t.Fatalf("GetDesktopPreferences() updatePolicy = %q, want auto", reloaded.UpdatePolicy) } if reloaded.UpdateChannel != "rc" { t.Fatalf("GetDesktopPreferences() updateChannel = %q, want rc", reloaded.UpdateChannel) } codexDefaults := reloaded.AgentComposerDefaultsByProvider["codex"] if codexDefaults.Model != "gpt-5" || codexDefaults.PermissionModeID != "full-access" || codexDefaults.ReasoningEffort != "high" { t.Fatalf("GetDesktopPreferences() codex composer defaults = %#v, want gpt-5/full-access/high", codexDefaults) } } func TestSQLiteStoreInitializeDesktopPreferencesCreatesMissingRow(t *testing.T) { t.Parallel() store := openTestSQLiteStore(t) candidate := preferencesbiz.DefaultDesktopPreferences() candidate.Locale = "zh-CN" stored, created, err := store.InitializeDesktopPreferences(context.Background(), candidate) if err != nil { t.Fatalf("InitializeDesktopPreferences() error = %v", err) } if !created { t.Fatal("InitializeDesktopPreferences() created = false, want true") } if !stored.Initialized || stored.Locale != "zh-CN" { t.Fatalf("stored preferences = %#v", stored) } if !stored.FeatureFlags[preferencesbiz.DesktopStandaloneAgentModeFeatureFlag] { t.Fatalf("stored feature flags = %#v, want standalone Agent mode enabled", stored.FeatureFlags) } } func TestSQLiteStoreInitializeDesktopPreferencesPreservesExistingLegacyRow(t *testing.T) { t.Parallel() store := openTestSQLiteStore(t) ctx := context.Background() existing := preferencesbiz.DefaultDesktopPreferences() existing.FeatureFlags = map[string]bool{} existing.Locale = "en" existing.ThemeSource = "light" if _, err := store.PutDesktopPreferences(ctx, existing); err != nil { t.Fatalf("PutDesktopPreferences() error = %v", err) } candidate := preferencesbiz.DefaultDesktopPreferences() candidate.Locale = "zh-CN" candidate.ThemeSource = "dark" stored, created, err := store.InitializeDesktopPreferences(ctx, candidate) if err != nil { t.Fatalf("InitializeDesktopPreferences() error = %v", err) } if created { t.Fatal("InitializeDesktopPreferences() created = true, want false") } if stored.Locale != "en" || stored.ThemeSource != "light" { t.Fatalf("stored locale/theme = %q/%q, want en/light", stored.Locale, stored.ThemeSource) } if len(stored.FeatureFlags) != 0 { t.Fatalf("stored feature flags = %#v, want legacy empty flags", stored.FeatureFlags) } } func TestSQLiteStoreInitializeDesktopPreferencesCommitsExactlyOneCompleteCandidateUnderConcurrentWriters(t *testing.T) { t.Parallel() store := openTestSQLiteStore(t) ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() writerGate, err := store.writeDB.Conn(ctx) if err != nil { t.Fatalf("hold SQLite writer connection: %v", err) } gateReleased := false releaseGate := func() { if gateReleased { return } gateReleased = true if err := writerGate.Close(); err != nil { t.Errorf("release SQLite writer connection: %v", err) } } defer releaseGate() candidateA := preferencesbiz.DefaultDesktopPreferences() candidateA.Initialized = true candidateA.Locale = "zh-CN" candidateA.ThemeSource = "dark" candidateA.FeatureFlags = map[string]bool{ preferencesbiz.DesktopStandaloneAgentModeFeatureFlag: true, "test.candidate-a": true, } candidateB := preferencesbiz.DefaultDesktopPreferences() candidateB.Initialized = true candidateB.DockPlacement = "left" candidateB.Locale = "en" candidateB.ThemeSource = "light" candidateB.FeatureFlags = map[string]bool{ preferencesbiz.DesktopStandaloneAgentModeFeatureFlag: false, "test.candidate-b": true, } candidates := []preferencesbiz.DesktopPreferences{candidateA, candidateB} type initializationResult struct { created bool err error preferences preferencesbiz.DesktopPreferences } results := make(chan initializationResult, len(candidates)) var writers sync.WaitGroup baselineWaitCount := store.writeDB.Stats().WaitCount for _, candidate := range candidates { candidate := candidate writers.Add(1) go func() { defer writers.Done() stored, created, err := store.InitializeDesktopPreferences(ctx, candidate) results <- initializationResult{ created: created, err: err, preferences: stored, } }() } wantWaitCount := baselineWaitCount + int64(len(candidates)) for store.writeDB.Stats().WaitCount < wantWaitCount && ctx.Err() == nil { runtime.Gosched() } queuedWaitCount := store.writeDB.Stats().WaitCount releaseGate() writers.Wait() close(results) if queuedWaitCount < wantWaitCount { t.Fatalf("SQLite writer wait count = %d, want at least %d before gate release: %v", queuedWaitCount, wantWaitCount, ctx.Err()) } createdCount := 0 var returned []preferencesbiz.DesktopPreferences for result := range results { if result.err != nil { t.Fatalf("InitializeDesktopPreferences() error = %v", result.err) } if result.created { createdCount++ } returned = append(returned, result.preferences) } if createdCount != 1 { t.Fatalf("created results = %d, want exactly 1", createdCount) } if len(returned) != len(candidates) || !reflect.DeepEqual(returned[0], returned[1]) { t.Fatalf("returned preferences = %#v, want both writers to observe one canonical row", returned) } final, err := store.GetDesktopPreferences(context.Background()) if err != nil { t.Fatalf("GetDesktopPreferences() error = %v", err) } if !reflect.DeepEqual(returned[0], final) { t.Fatalf("returned preferences = %#v, final = %#v", returned[0], final) } if !reflect.DeepEqual(final, candidateA) && !reflect.DeepEqual(final, candidateB) { t.Fatalf("final preferences = %#v, want one complete candidate without merging", final) } } func TestSQLiteStoreDesktopPreferencesAgentConversationDetailModeMigrationAndNormalize(t *testing.T) { t.Parallel() store := openTestSQLiteStore(t) ctx := context.Background() hasAgentConversationDetailMode, err := store.hasColumn(ctx, "desktop_preferences", "agent_conversation_detail_mode") if err != nil { t.Fatalf("hasColumn() error = %v", err) } if !hasAgentConversationDetailMode { t.Fatal("desktop_preferences.agent_conversation_detail_mode column missing after migration") } _, err = store.writeDB.ExecContext(ctx, ` INSERT INTO desktop_preferences ( id, default_agent_provider, agent_conversation_detail_mode, agent_dock_layout, dock_icon_style, dock_placement, locale, theme_source, sleep_prevention_mode, update_channel, update_policy, agent_composer_defaults_by_provider_json, agent_gui_conversation_rail_collapsed_by_provider_json, file_default_openers_by_extension_json, app_catalog_channel, browser_use_connection_mode, minimize_animation, show_app_developer_sources, workbench_window_snapping_enabled, workbench_window_snapping_shortcut_preset, updated_at_unix_ms ) VALUES ( 'desktop', 'codex', 'daily', 'sideBySide', 'default', 'bottom', 'en', 'dark', 'never', 'rc', 'prompt', '{}', '{}', '{}', 'production', 'isolated', 'scale', 0, 0, 'commandArrows', 1 )`) if err != nil { t.Fatalf("insert desktop preferences with invalid conversation detail mode: %v", err) } preferences, err := store.GetDesktopPreferences(ctx) if err != nil { t.Fatalf("GetDesktopPreferences() error = %v", err) } if preferences.AgentConversationDetailMode != "coding" { t.Fatalf("GetDesktopPreferences() agentConversationDetailMode = %q, want coding", preferences.AgentConversationDetailMode) } if preferences.AgentDockLayout != "unified" { t.Fatalf("GetDesktopPreferences() agentDockLayout = %q, want unified", preferences.AgentDockLayout) } } func TestSQLiteStorePutDesktopPreferencesPersistsAgentComposerDefaultsByAgentTarget(t *testing.T) { t.Parallel() store := openTestSQLiteStore(t) input := preferencesbiz.DefaultDesktopPreferences() input.AgentComposerDefaultsByProvider = map[string]preferencesbiz.AgentComposerDefaults{ "codex": {Model: "gpt-5"}, } input.AgentComposerDefaultsByAgentTarget = map[string]preferencesbiz.AgentComposerDefaults{ "local:codex": { Model: "gpt-5-codex", PermissionModeID: "full-access", ReasoningEffort: "high", Speed: "fast", }, } if _, err := store.PutDesktopPreferences(context.Background(), input); err != nil { t.Fatalf("PutDesktopPreferences() error = %v", err) } preferences, err := store.GetDesktopPreferences(context.Background()) if err != nil { t.Fatalf("GetDesktopPreferences() error = %v", err) } codexDefaults := preferences.AgentComposerDefaultsByAgentTarget["local:codex"] if codexDefaults.Model != "gpt-5-codex" || codexDefaults.PermissionModeID != "full-access" || codexDefaults.ReasoningEffort != "high" || codexDefaults.Speed != "fast" { t.Fatalf("agent target defaults = %#v, want persisted round-trip", codexDefaults) } if preferences.AgentComposerDefaultsByProvider["codex"].Model != "gpt-5" { t.Fatalf("legacy provider defaults = %#v, want preserved", preferences.AgentComposerDefaultsByProvider) } } func TestSQLiteStoreMigrationBackfillsAgentComposerDefaultsByAgentTarget(t *testing.T) { t.Parallel() store := openTestSQLiteStore(t) // Simulate a pre-migration database: legacy provider-keyed defaults // exist, the agent-target column is empty, and the data migration marker // is absent. legacy := preferencesbiz.DefaultDesktopPreferences() legacy.AgentComposerDefaultsByProvider = map[string]preferencesbiz.AgentComposerDefaults{ "codex": {Model: "gpt-5", PermissionModeID: "full-access"}, "gemini": {Model: "legacy-gemini-pro"}, } if _, err := store.PutDesktopPreferences(context.Background(), legacy); err != nil { t.Fatalf("PutDesktopPreferences() error = %v", err) } if _, err := store.writeDB.ExecContext(context.Background(), ` DELETE FROM tuttid_schema_migrations WHERE id = ? `, schemaMigrationDesktopPreferencesAgentComposerDefaultsByAgentTargetV1); err != nil { t.Fatalf("reset migration marker: %v", err) } if err := store.applyDesktopPreferencesAgentComposerDefaultsByAgentTargetV1(context.Background()); err != nil { t.Fatalf("applyDesktopPreferencesAgentComposerDefaultsByAgentTargetV1() error = %v", err) } preferences, err := store.GetDesktopPreferences(context.Background()) if err != nil { t.Fatalf("GetDesktopPreferences() error = %v", err) } codexDefaults := preferences.AgentComposerDefaultsByAgentTarget["local:codex"] if codexDefaults.Model != "gpt-5" || codexDefaults.PermissionModeID != "full-access" { t.Fatalf("backfilled codex defaults = %#v, want legacy values", codexDefaults) } if _, ok := preferences.AgentComposerDefaultsByAgentTarget["local:gemini"]; ok { t.Fatalf("local:gemini defaults were backfilled: %#v", preferences.AgentComposerDefaultsByAgentTarget) } // Re-running the backfill must not clobber newer agent-target data. model := "gpt-5-codex" if _, err := store.PatchAgentComposerDefaultsForTarget(context.Background(), "local:codex", preferencesbiz.AgentComposerDefaultsPatch{ preferencesbiz.AgentComposerDefaultsFieldModel: &model, preferencesbiz.AgentComposerDefaultsFieldPermissionModeID: nil, }); err != nil { t.Fatalf("PatchAgentComposerDefaultsForTarget() error = %v", err) } if err := store.backfillAgentComposerDefaultsByAgentTarget(context.Background()); err != nil { t.Fatalf("backfillAgentComposerDefaultsByAgentTarget() error = %v", err) } preferences, err = store.GetDesktopPreferences(context.Background()) if err != nil { t.Fatalf("GetDesktopPreferences() error = %v", err) } if preferences.AgentComposerDefaultsByAgentTarget["local:codex"].Model != "gpt-5-codex" { t.Fatalf("agent target defaults = %#v, want newer data preserved", preferences.AgentComposerDefaultsByAgentTarget) } } func TestSQLiteStorePatchAgentComposerDefaultsForTargetMergesLatestFieldsAndPreservesPreferences(t *testing.T) { t.Parallel() store := openTestSQLiteStore(t) ctx := context.Background() stale := preferencesbiz.DefaultDesktopPreferences() stale.Locale = "zh-CN" stale.ThemeSource = "light" if _, err := store.PutDesktopPreferences(ctx, stale); err != nil { t.Fatalf("PutDesktopPreferences() error = %v", err) } permission := "full-access" if _, err := store.PatchAgentComposerDefaultsForTarget(ctx, "local:opencode", preferencesbiz.AgentComposerDefaultsPatch{ preferencesbiz.AgentComposerDefaultsFieldPermissionModeID: &permission, }); err != nil { t.Fatalf("patch permission: %v", err) } model := "openai/gpt-5" reasoning := "high" speed := "fast" if _, err := store.PatchAgentComposerDefaultsForTarget(ctx, "local:opencode", preferencesbiz.AgentComposerDefaultsPatch{ preferencesbiz.AgentComposerDefaultsFieldModel: &model, preferencesbiz.AgentComposerDefaultsFieldReasoningEffort: &reasoning, preferencesbiz.AgentComposerDefaultsFieldSpeed: &speed, }); err != nil { t.Fatalf("patch remaining fields: %v", err) } otherModel := "claude-sonnet-4" if _, err := store.PatchAgentComposerDefaultsForTarget(ctx, "local:claude-code", preferencesbiz.AgentComposerDefaultsPatch{ preferencesbiz.AgentComposerDefaultsFieldModel: &otherModel, }); err != nil { t.Fatalf("patch other target: %v", err) } // A full preference update based on an older snapshot must not overwrite // any target defaults committed after that snapshot was read. stale.Locale = "en" if _, err := store.PutDesktopPreferences(ctx, stale); err != nil { t.Fatalf("put stale full preferences: %v", err) } got, err := store.GetDesktopPreferences(ctx) if err != nil { t.Fatalf("GetDesktopPreferences() error = %v", err) } opencode := got.AgentComposerDefaultsByAgentTarget["local:opencode"] if opencode.Model != model || opencode.PermissionModeID != permission || opencode.ReasoningEffort != reasoning || opencode.Speed != speed { t.Fatalf("opencode defaults = %#v", opencode) } if got.AgentComposerDefaultsByAgentTarget["local:claude-code"].Model != otherModel { t.Fatalf("target defaults = %#v", got.AgentComposerDefaultsByAgentTarget) } if got.Locale != "en" || got.ThemeSource != "light" { t.Fatalf("unrelated preferences locale=%q theme=%q", got.Locale, got.ThemeSource) } // Repeating a SET is naturally idempotent. if _, err := store.PatchAgentComposerDefaultsForTarget(ctx, "local:opencode", preferencesbiz.AgentComposerDefaultsPatch{ preferencesbiz.AgentComposerDefaultsFieldPermissionModeID: &permission, }); err != nil { t.Fatalf("repeat permission patch: %v", err) } } func TestSQLiteStorePatchAgentComposerDefaultsForTargetRejectsMissingPreferencesRow(t *testing.T) { t.Parallel() store := openTestSQLiteStore(t) model := "gpt-5" _, err := store.PatchAgentComposerDefaultsForTarget(context.Background(), "local:codex", preferencesbiz.AgentComposerDefaultsPatch{ preferencesbiz.AgentComposerDefaultsFieldModel: &model, }) if !errors.Is(err, ErrDesktopPreferencesNotInitialized) { t.Fatalf("PatchAgentComposerDefaultsForTarget() error = %v, want %v", err, ErrDesktopPreferencesNotInitialized) } var rows int if err := store.readDB.QueryRowContext(context.Background(), `SELECT COUNT(*) FROM desktop_preferences`).Scan(&rows); err != nil { t.Fatalf("count desktop preferences rows: %v", err) } if rows != 0 { t.Fatalf("desktop preferences rows = %d, want 0", rows) } } func TestSQLiteStorePatchAgentComposerDefaultsForTargetPersistsCodexSaverMode(t *testing.T) { t.Parallel() store := openTestSQLiteStore(t) if _, err := store.PutDesktopPreferences(context.Background(), preferencesbiz.DefaultDesktopPreferences()); err != nil { t.Fatalf("PutDesktopPreferences() error = %v", err) } if _, err := store.PatchAgentComposerDefaultsForTarget(context.Background(), "local:codex", preferencesbiz.AgentComposerDefaultsPatch{ preferencesbiz.AgentComposerDefaultsFieldCodexSaverMode: true, }); err != nil { t.Fatalf("enable saver mode: %v", err) } got, err := store.GetDesktopPreferences(context.Background()) if err != nil { t.Fatalf("GetDesktopPreferences() error = %v", err) } if !got.AgentComposerDefaultsByAgentTarget["local:codex"].CodexSaverMode { t.Fatalf("defaults = %#v", got.AgentComposerDefaultsByAgentTarget) } if _, err := store.PatchAgentComposerDefaultsForTarget(context.Background(), "local:codex", preferencesbiz.AgentComposerDefaultsPatch{ preferencesbiz.AgentComposerDefaultsFieldCodexSaverMode: false, }); err != nil { t.Fatalf("disable saver mode: %v", err) } got, err = store.GetDesktopPreferences(context.Background()) if err != nil { t.Fatalf("GetDesktopPreferences() error = %v", err) } if got.AgentComposerDefaultsByAgentTarget["local:codex"].CodexSaverMode { t.Fatalf("defaults = %#v, want saver mode cleared", got.AgentComposerDefaultsByAgentTarget) } } func TestSQLiteStorePatchAgentComposerDefaultsForTargetSerializesConcurrentFields(t *testing.T) { t.Parallel() store := openTestSQLiteStore(t) if _, err := store.PutDesktopPreferences(context.Background(), preferencesbiz.DefaultDesktopPreferences()); err != nil { t.Fatalf("PutDesktopPreferences() error = %v", err) } permission := "full-access" model := "gpt-5" patches := []preferencesbiz.AgentComposerDefaultsPatch{ {preferencesbiz.AgentComposerDefaultsFieldPermissionModeID: &permission}, {preferencesbiz.AgentComposerDefaultsFieldModel: &model}, } var wait sync.WaitGroup errorsByPatch := make([]error, len(patches)) for index, patch := range patches { wait.Add(1) go func() { defer wait.Done() _, errorsByPatch[index] = store.PatchAgentComposerDefaultsForTarget(context.Background(), "local:codex", patch) }() } wait.Wait() for _, err := range errorsByPatch { if err != nil { t.Fatalf("concurrent patch error = %v", err) } } got, err := store.GetDesktopPreferences(context.Background()) if err != nil { t.Fatalf("GetDesktopPreferences() error = %v", err) } defaults := got.AgentComposerDefaultsByAgentTarget["local:codex"] if defaults.Model != model || defaults.PermissionModeID != permission { t.Fatalf("defaults = %#v", defaults) } } func TestSQLiteStorePatchAgentSessionLaunchModeSerializesConcurrentProjectsAndRejectsStaleReplacement(t *testing.T) { t.Parallel() store := openTestSQLiteStore(t) ctx := context.Background() stale := preferencesbiz.DefaultDesktopPreferences() stale.Locale = "en" if _, err := store.PutDesktopPreferences(ctx, stale); err != nil { t.Fatal(err) } patches := []struct { workspaceID string projectSectionKey string mode string }{ {workspaceID: "workspace-a", projectSectionKey: "project:/alpha", mode: "worktree"}, {workspaceID: "workspace-b", projectSectionKey: "project:/beta", mode: "local"}, } var wait sync.WaitGroup errorsByPatch := make([]error, len(patches)) for index, patch := range patches { wait.Add(1) go func() { defer wait.Done() _, errorsByPatch[index] = store.PatchAgentSessionLaunchMode( context.Background(), patch.workspaceID, patch.projectSectionKey, patch.mode, ) }() } wait.Wait() for _, err := range errorsByPatch { if err != nil { t.Fatalf("concurrent launch mode patch: %v", err) } } stale.Locale = "zh-CN" stale.AgentSessionLaunchModesByWorkspace = map[string]map[string]string{ "workspace-stale": {"project:/stale": "worktree"}, } if _, err := store.PutDesktopPreferences(ctx, stale); err != nil { t.Fatalf("put stale full preferences: %v", err) } got, err := store.GetDesktopPreferences(ctx) if err != nil { t.Fatal(err) } if got.AgentSessionLaunchModesByWorkspace["workspace-a"]["project:/alpha"] != "worktree" || got.AgentSessionLaunchModesByWorkspace["workspace-b"]["project:/beta"] != "local" || len(got.AgentSessionLaunchModesByWorkspace) != 2 { t.Fatalf("launch modes = %#v", got.AgentSessionLaunchModesByWorkspace) } if got.Locale != "zh-CN" { t.Fatalf("locale = %q, want zh-CN", got.Locale) } } func TestSQLiteStorePatchAgentSessionLaunchModeRejectsMissingPreferencesRow(t *testing.T) { t.Parallel() store := openTestSQLiteStore(t) _, err := store.PatchAgentSessionLaunchMode( context.Background(), "workspace-a", "project:/alpha", "worktree", ) if !errors.Is(err, ErrDesktopPreferencesNotInitialized) { t.Fatalf("PatchAgentSessionLaunchMode() error = %v, want %v", err, ErrDesktopPreferencesNotInitialized) } var rows int if err := store.readDB.QueryRowContext(context.Background(), `SELECT COUNT(*) FROM desktop_preferences`).Scan(&rows); err != nil { t.Fatalf("count desktop preferences rows: %v", err) } if rows != 0 { t.Fatalf("desktop preferences rows = %d, want 0", rows) } } func TestDesktopPreferencesFeatureFlagsRoundtrip(t *testing.T) { t.Parallel() store := openTestSQLiteStore(t) ctx := context.Background() in := preferencesbiz.DefaultDesktopPreferences() in.FeatureFlags = map[string]bool{"lab.enabled": true, "lab.workbenchShortcuts": true} in.WorkbenchShortcuts = preferencesbiz.DesktopWorkbenchShortcuts{ NewAgentConversation: "Meta+K", CaptureScreenshot: "Meta+Shift+P", } if _, err := store.PutDesktopPreferences(ctx, in); err != nil { t.Fatal(err) } got, err := store.GetDesktopPreferences(ctx) if err != nil { t.Fatal(err) } if !got.FeatureFlags["lab.enabled"] || !got.FeatureFlags["lab.workbenchShortcuts"] { t.Fatalf("flags not persisted: %v", got.FeatureFlags) } if got.WorkbenchShortcuts.NewAgentConversation != "Meta+K" || got.WorkbenchShortcuts.NewSameTypeWindow != "" || got.WorkbenchShortcuts.CaptureScreenshot != "Meta+Shift+P" { t.Fatalf("shortcuts wrong: %+v", got.WorkbenchShortcuts) } }