源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
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
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
This commit is contained in:
@@ -0,0 +1,793 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user