Skip to content

Commit d7bee0f

Browse files
authored
refactor: replace window lifecycle and SSH tunnel notifications with direct calls (#291)
* refactor: replace editor and AI notifications with direct calls * refactor: replace SSH tunnel and window lifecycle notifications with direct calls * test: add regression tests for notification refactor Phases 3-7 * docs: remove notification-refactor.md
1 parent ca42174 commit d7bee0f

9 files changed

Lines changed: 1031 additions & 248 deletions

TableProTests/Core/Database/ConnectionHealthMonitorTests.swift

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -115,12 +115,6 @@ struct ConnectionHealthMonitorTests {
115115
#expect(count == 0)
116116
}
117117

118-
@Test("connectionHealthStateChanged notification name exists")
119-
func notificationNameExists() {
120-
let name = Notification.Name.connectionHealthStateChanged
121-
#expect(name.rawValue == "connectionHealthStateChanged")
122-
}
123-
124118
@Test("Staggered initial delay — no ping fires immediately")
125119
func staggeredInitialDelay() async {
126120
var pingCount = 0
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
//
2+
// AIChatViewModelActionTests.swift
3+
// TableProTests
4+
//
5+
// Tests for AI action dispatch methods on AIChatViewModel.
6+
//
7+
8+
import Foundation
9+
import Testing
10+
11+
@testable import TablePro
12+
13+
@Suite("AIChatViewModel Action Dispatch")
14+
@MainActor
15+
struct AIChatViewModelActionTests {
16+
17+
// MARK: - handleFixError
18+
19+
@Test("handleFixError with default connection uses SQL query language")
20+
func fixErrorDefaultConnection() {
21+
let vm = AIChatViewModel()
22+
vm.connection = TestFixtures.makeConnection(type: .mysql)
23+
24+
vm.handleFixError(query: "SELECT * FROM users", error: "Table not found")
25+
26+
#expect(vm.messages.count >= 1)
27+
let userMessage = vm.messages.first { $0.role == .user }
28+
#expect(userMessage != nil)
29+
#expect(userMessage?.content.contains("SQL query") == true)
30+
#expect(userMessage?.content.contains("```sql") == true)
31+
}
32+
33+
@Test("handleFixError with MongoDB connection uses JavaScript language")
34+
func fixErrorMongoDBConnection() {
35+
let vm = AIChatViewModel()
36+
vm.connection = TestFixtures.makeConnection(type: .mongodb)
37+
38+
vm.handleFixError(query: "db.users.find({})", error: "SyntaxError")
39+
40+
let userMessage = vm.messages.first { $0.role == .user }
41+
#expect(userMessage != nil)
42+
#expect(userMessage?.content.contains("MongoDB query") == true)
43+
#expect(userMessage?.content.contains("```javascript") == true)
44+
}
45+
46+
@Test("handleFixError with Redis connection uses bash language")
47+
func fixErrorRedisConnection() {
48+
let vm = AIChatViewModel()
49+
vm.connection = TestFixtures.makeConnection(type: .redis)
50+
51+
vm.handleFixError(query: "GET mykey", error: "WRONGTYPE")
52+
53+
let userMessage = vm.messages.first { $0.role == .user }
54+
#expect(userMessage != nil)
55+
#expect(userMessage?.content.contains("Redis command") == true)
56+
#expect(userMessage?.content.contains("```bash") == true)
57+
}
58+
59+
@Test("handleFixError includes query and error text verbatim")
60+
func fixErrorIncludesVerbatimText() {
61+
let vm = AIChatViewModel()
62+
vm.connection = TestFixtures.makeConnection(type: .mysql)
63+
64+
let query = "SELECT * FROM orders WHERE id = 999"
65+
let error = "ERROR 1146: Table 'orders' doesn't exist"
66+
67+
vm.handleFixError(query: query, error: error)
68+
69+
let userMessage = vm.messages.first { $0.role == .user }
70+
#expect(userMessage?.content.contains(query) == true)
71+
#expect(userMessage?.content.contains(error) == true)
72+
}
73+
74+
// MARK: - handleExplainSelection
75+
76+
@Test("handleExplainSelection with non-empty text creates user message")
77+
func explainSelectionNonEmpty() {
78+
let vm = AIChatViewModel()
79+
vm.connection = TestFixtures.makeConnection(type: .mysql)
80+
81+
let selectedText = "SELECT u.name, COUNT(o.id) FROM users u JOIN orders o ON u.id = o.user_id GROUP BY u.name"
82+
83+
vm.handleExplainSelection(selectedText)
84+
85+
let userMessage = vm.messages.first { $0.role == .user }
86+
#expect(userMessage != nil)
87+
#expect(userMessage?.content.contains("Explain this SQL query") == true)
88+
#expect(userMessage?.content.contains(selectedText) == true)
89+
#expect(userMessage?.content.contains("```sql") == true)
90+
}
91+
92+
@Test("handleExplainSelection with empty text is a no-op")
93+
func explainSelectionEmpty() {
94+
let vm = AIChatViewModel()
95+
vm.connection = TestFixtures.makeConnection(type: .mysql)
96+
97+
let countBefore = vm.messages.count
98+
99+
vm.handleExplainSelection("")
100+
101+
// No new messages should be added
102+
#expect(vm.messages.count == countBefore)
103+
}
104+
105+
// MARK: - handleOptimizeSelection
106+
107+
@Test("handleOptimizeSelection with non-empty text creates user message")
108+
func optimizeSelectionNonEmpty() {
109+
let vm = AIChatViewModel()
110+
vm.connection = TestFixtures.makeConnection(type: .mysql)
111+
112+
let selectedText = "SELECT * FROM users WHERE name LIKE '%john%'"
113+
114+
vm.handleOptimizeSelection(selectedText)
115+
116+
let userMessage = vm.messages.first { $0.role == .user }
117+
#expect(userMessage != nil)
118+
#expect(userMessage?.content.contains("Optimize this SQL query") == true)
119+
#expect(userMessage?.content.contains(selectedText) == true)
120+
#expect(userMessage?.content.contains("```sql") == true)
121+
}
122+
123+
@Test("handleOptimizeSelection with empty text is a no-op")
124+
func optimizeSelectionEmpty() {
125+
let vm = AIChatViewModel()
126+
vm.connection = TestFixtures.makeConnection(type: .mysql)
127+
128+
let countBefore = vm.messages.count
129+
130+
vm.handleOptimizeSelection("")
131+
132+
// No new messages should be added
133+
#expect(vm.messages.count == countBefore)
134+
}
135+
136+
// MARK: - startNewConversation clears state
137+
138+
@Test("Action methods clear previous messages via startNewConversation")
139+
func actionClearsPreviousMessages() {
140+
let vm = AIChatViewModel()
141+
vm.connection = TestFixtures.makeConnection(type: .mysql)
142+
143+
vm.handleExplainSelection("SELECT 1")
144+
145+
let firstCount = vm.messages.filter { $0.role == .user }.count
146+
#expect(firstCount >= 1)
147+
148+
vm.handleOptimizeSelection("SELECT 2")
149+
150+
// After second action, startNewConversation should have cleared,
151+
// so there should be exactly 1 user message (from the second action).
152+
// There may also be assistant/error messages from startStreaming.
153+
let userMessages = vm.messages.filter { $0.role == .user }
154+
#expect(userMessages.count == 1)
155+
#expect(userMessages.first?.content.contains("SELECT 2") == true)
156+
}
157+
}
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
//
2+
// CommandActionsDispatchTests.swift
3+
// TableProTests
4+
//
5+
// Tests that MainContentCommandActions correctly forwards calls
6+
// to MainContentCoordinator and its sub-handlers.
7+
//
8+
9+
import Foundation
10+
import SwiftUI
11+
import Testing
12+
@testable import TablePro
13+
14+
@MainActor @Suite("CommandActions Dispatch")
15+
struct CommandActionsDispatchTests {
16+
// MARK: - Helpers
17+
18+
private func makeSUT() -> (MainContentCommandActions, MainContentCoordinator) {
19+
let connection = TestFixtures.makeConnection()
20+
let state = SessionStateFactory.create(connection: connection, payload: nil)
21+
let coordinator = state.coordinator
22+
23+
var selectedRowIndices: Set<Int> = []
24+
var selectedTables: Set<TableInfo> = []
25+
var pendingTruncates: Set<String> = []
26+
var pendingDeletes: Set<String> = []
27+
var tableOperationOptions: [String: TableOperationOptions] = [:]
28+
var editingCell: CellPosition? = nil
29+
let rightPanelState = RightPanelState()
30+
31+
let actions = MainContentCommandActions(
32+
coordinator: coordinator,
33+
filterStateManager: state.filterStateManager,
34+
connection: connection,
35+
selectedRowIndices: Binding(get: { selectedRowIndices }, set: { selectedRowIndices = $0 }),
36+
selectedTables: Binding(get: { selectedTables }, set: { selectedTables = $0 }),
37+
pendingTruncates: Binding(get: { pendingTruncates }, set: { pendingTruncates = $0 }),
38+
pendingDeletes: Binding(get: { pendingDeletes }, set: { pendingDeletes = $0 }),
39+
tableOperationOptions: Binding(
40+
get: { tableOperationOptions },
41+
set: { tableOperationOptions = $0 }
42+
),
43+
rightPanelState: rightPanelState,
44+
editingCell: Binding(get: { editingCell }, set: { editingCell = $0 })
45+
)
46+
47+
return (actions, coordinator)
48+
}
49+
50+
// MARK: - loadQueryIntoEditor
51+
52+
@Test("loadQueryIntoEditor forwards query to coordinator and updates tab")
53+
func loadQueryIntoEditor_forwardsToCoordinator() {
54+
let (actions, coordinator) = makeSUT()
55+
coordinator.tabManager.addTab(databaseName: "testdb")
56+
57+
actions.loadQueryIntoEditor("SELECT 1")
58+
59+
let tab = coordinator.tabManager.selectedTab
60+
#expect(tab?.query == "SELECT 1")
61+
}
62+
63+
// MARK: - insertQueryFromAI
64+
65+
@Test("insertQueryFromAI forwards query to coordinator and updates tab")
66+
func insertQueryFromAI_forwardsToCoordinator() {
67+
let (actions, coordinator) = makeSUT()
68+
coordinator.tabManager.addTab(databaseName: "testdb")
69+
70+
actions.insertQueryFromAI("SELECT 2")
71+
72+
let tab = coordinator.tabManager.selectedTab
73+
#expect(tab?.query == "SELECT 2")
74+
}
75+
76+
@Test("insertQueryFromAI appends to existing query")
77+
func insertQueryFromAI_appendsToExisting() {
78+
let (actions, coordinator) = makeSUT()
79+
coordinator.tabManager.addTab(databaseName: "testdb")
80+
81+
// Set an initial query on the tab
82+
if let idx = coordinator.tabManager.selectedTabIndex {
83+
coordinator.tabManager.tabs[idx].query = "SELECT 1"
84+
}
85+
86+
actions.insertQueryFromAI("SELECT 2")
87+
88+
let tab = coordinator.tabManager.selectedTab
89+
#expect(tab?.query == "SELECT 1\n\nSELECT 2")
90+
}
91+
92+
// MARK: - copySelectedRows (structure mode)
93+
94+
@Test("copySelectedRows in structure mode calls structureActions.copyRows")
95+
func copySelectedRows_structureMode_callsStructureActions() {
96+
let (actions, coordinator) = makeSUT()
97+
coordinator.tabManager.addTab(databaseName: "testdb")
98+
99+
// Enable structure mode on the selected tab
100+
if let idx = coordinator.tabManager.selectedTabIndex {
101+
coordinator.tabManager.tabs[idx].showStructure = true
102+
}
103+
104+
// Install a spy handler
105+
let handler = StructureViewActionHandler()
106+
var copyRowsCalled = false
107+
handler.copyRows = { copyRowsCalled = true }
108+
coordinator.structureActions = handler
109+
110+
actions.copySelectedRows()
111+
112+
#expect(copyRowsCalled)
113+
}
114+
115+
// MARK: - pasteRows (structure mode)
116+
117+
@Test("pasteRows in structure mode calls structureActions.pasteRows")
118+
func pasteRows_structureMode_callsStructureActions() {
119+
let (actions, coordinator) = makeSUT()
120+
coordinator.tabManager.addTab(databaseName: "testdb")
121+
122+
// Enable structure mode on the selected tab
123+
if let idx = coordinator.tabManager.selectedTabIndex {
124+
coordinator.tabManager.tabs[idx].showStructure = true
125+
}
126+
127+
// Install a spy handler
128+
let handler = StructureViewActionHandler()
129+
var pasteRowsCalled = false
130+
handler.pasteRows = { pasteRowsCalled = true }
131+
coordinator.structureActions = handler
132+
133+
actions.pasteRows()
134+
135+
#expect(pasteRowsCalled)
136+
}
137+
}

0 commit comments

Comments
 (0)