Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
794 lines
29 KiB
Go
794 lines
29 KiB
Go
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)
|
|
}
|
|
}
|