Skip to content

Commit 636e6af

Browse files
committed
fix: exclude transient fields from unsaved changes detection
After ping status update, editing server form would incorrectly detect unsaved changes. This fix excludes transient runtime fields (PingStatus, PingLatency) and metadata fields from the comparison logic. - Added struct tags to mark transient and metadata fields in domain.Server - Updated serversDiffer() to skip fields based on struct tags - Fixed goconst warnings by using existing status constants - Added comprehensive tests for the change detection logic
1 parent d6433da commit 636e6af

File tree

4 files changed

+160
-19
lines changed

4 files changed

+160
-19
lines changed

internal/adapters/ui/server_form.go

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2039,22 +2039,30 @@ func (sf *ServerForm) serversDiffer(a, b domain.Server) bool {
20392039
valB := reflect.ValueOf(b)
20402040
typeA := valA.Type()
20412041

2042-
// Fields to skip during comparison (lazyssh metadata fields)
2042+
// Special fields to skip that don't have tags
20432043
skipFields := map[string]bool{
2044-
"Aliases": true, // Computed field
2045-
"LastSeen": true, // Metadata field
2046-
"PinnedAt": true, // Metadata field
2047-
"SSHCount": true, // Metadata field
2044+
"Aliases": true, // Computed field (derived from Host)
20482045
}
20492046

20502047
// Iterate through all fields
20512048
for i := 0; i < valA.NumField(); i++ {
20522049
fieldA := valA.Field(i)
20532050
fieldB := valB.Field(i)
2054-
fieldName := typeA.Field(i).Name
2051+
field := typeA.Field(i)
2052+
fieldName := field.Name
20552053

2056-
// Skip unexported fields and metadata fields
2057-
if !fieldA.CanInterface() || skipFields[fieldName] {
2054+
// Skip unexported fields
2055+
if !fieldA.CanInterface() {
2056+
continue
2057+
}
2058+
2059+
// Skip special fields
2060+
if skipFields[fieldName] {
2061+
continue
2062+
}
2063+
2064+
// Check for lazyssh struct tags to skip metadata and transient fields
2065+
if tag := field.Tag.Get("lazyssh"); tag == "metadata" || tag == "transient" {
20582066
continue
20592067
}
20602068

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
// Copyright 2025.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package ui
16+
17+
import (
18+
"testing"
19+
"time"
20+
21+
"github.com/Adembc/lazyssh/internal/core/domain"
22+
)
23+
24+
func TestServersDifferIgnoresTransientFields(t *testing.T) {
25+
sf := &ServerForm{}
26+
27+
// Create two servers with identical config but different transient fields
28+
server1 := domain.Server{
29+
Alias: "test-server",
30+
Host: "example.com",
31+
User: "testuser",
32+
Port: 22,
33+
PingStatus: "up",
34+
PingLatency: 100 * time.Millisecond,
35+
LastSeen: time.Now(),
36+
PinnedAt: time.Now(),
37+
SSHCount: 5,
38+
}
39+
40+
server2 := domain.Server{
41+
Alias: "test-server",
42+
Host: "example.com",
43+
User: "testuser",
44+
Port: 22,
45+
PingStatus: "down", // Different transient field
46+
PingLatency: 200 * time.Millisecond, // Different transient field
47+
LastSeen: time.Now().Add(1 * time.Hour), // Different metadata field
48+
PinnedAt: time.Now().Add(2 * time.Hour), // Different metadata field
49+
SSHCount: 10, // Different metadata field
50+
}
51+
52+
// Should not detect differences since only transient/metadata fields differ
53+
if sf.serversDiffer(server1, server2) {
54+
t.Error("serversDiffer should ignore transient and metadata fields")
55+
}
56+
57+
// Now change a real config field
58+
server2.Port = 2222
59+
60+
// Should detect the difference now
61+
if !sf.serversDiffer(server1, server2) {
62+
t.Error("serversDiffer should detect differences in non-transient fields")
63+
}
64+
}
65+
66+
func TestServersDifferDetectsRealChanges(t *testing.T) {
67+
sf := &ServerForm{}
68+
69+
server1 := domain.Server{
70+
Alias: "test-server",
71+
Host: "example.com",
72+
User: "testuser",
73+
Port: 22,
74+
}
75+
76+
testCases := []struct {
77+
name string
78+
modify func(*domain.Server)
79+
expect bool
80+
}{
81+
{
82+
name: "No changes",
83+
modify: func(s *domain.Server) {},
84+
expect: false,
85+
},
86+
{
87+
name: "Changed Host",
88+
modify: func(s *domain.Server) { s.Host = "different.com" },
89+
expect: true,
90+
},
91+
{
92+
name: "Changed User",
93+
modify: func(s *domain.Server) { s.User = "otheruser" },
94+
expect: true,
95+
},
96+
{
97+
name: "Changed Port",
98+
modify: func(s *domain.Server) { s.Port = 2222 },
99+
expect: true,
100+
},
101+
{
102+
name: "Added IdentityFile",
103+
modify: func(s *domain.Server) { s.IdentityFiles = []string{"~/.ssh/id_rsa"} },
104+
expect: true,
105+
},
106+
{
107+
name: "Changed ProxyJump",
108+
modify: func(s *domain.Server) { s.ProxyJump = "jumphost" },
109+
expect: true,
110+
},
111+
{
112+
name: "Changed only PingStatus (transient)",
113+
modify: func(s *domain.Server) { s.PingStatus = "checking" },
114+
expect: false,
115+
},
116+
{
117+
name: "Changed only LastSeen (metadata)",
118+
modify: func(s *domain.Server) { s.LastSeen = time.Now().Add(1 * time.Hour) },
119+
expect: false,
120+
},
121+
}
122+
123+
for _, tc := range testCases {
124+
t.Run(tc.name, func(t *testing.T) {
125+
server2 := server1 // Copy
126+
tc.modify(&server2)
127+
result := sf.serversDiffer(server1, server2)
128+
if result != tc.expect {
129+
t.Errorf("Expected %v but got %v for test case %s", tc.expect, result, tc.name)
130+
}
131+
})
132+
}
133+
}

internal/adapters/ui/utils.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ func formatServerLine(s domain.Server, width int) (primary, secondary string) {
8989
pingIndicator := ""
9090
if s.PingStatus != "" {
9191
switch s.PingStatus {
92-
case "up":
92+
case statusUp:
9393
if s.PingLatency > 0 {
9494
ms := s.PingLatency.Milliseconds()
9595
var statusText string
@@ -106,9 +106,9 @@ func formatServerLine(s domain.Server, width int) (primary, secondary string) {
106106
} else {
107107
pingIndicator = "[#4AF626]● UP [-]"
108108
}
109-
case "down":
109+
case statusDown:
110110
pingIndicator = "[#FF6B6B]● DOWN[-]"
111-
case "checking":
111+
case statusChecking:
112112
pingIndicator = "[#FFB86C]● ... [-]"
113113
}
114114
}
@@ -133,11 +133,11 @@ func formatServerLine(s domain.Server, width int) (primary, secondary string) {
133133
// Medium screen: show only dot
134134
simplePingIndicator := ""
135135
switch s.PingStatus {
136-
case "up":
136+
case statusUp:
137137
simplePingIndicator = "[#4AF626]●[-]"
138-
case "down":
138+
case statusDown:
139139
simplePingIndicator = "[#FF6B6B]●[-]"
140-
case "checking":
140+
case statusChecking:
141141
simplePingIndicator = "[#FFB86C]●[-]"
142142
}
143143
paddingLen := width - mainTextLen - 1 // 1 for dot, no margin

internal/core/domain/server.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,11 @@ type Server struct {
2424
Port int
2525
IdentityFiles []string
2626
Tags []string
27-
LastSeen time.Time
28-
PinnedAt time.Time
29-
SSHCount int
30-
PingStatus string // "up", "down", "checking", or ""
31-
PingLatency time.Duration // ping latency
27+
LastSeen time.Time `lazyssh:"metadata"`
28+
PinnedAt time.Time `lazyssh:"metadata"`
29+
SSHCount int `lazyssh:"metadata"`
30+
PingStatus string `lazyssh:"transient"` // "up", "down", "checking", or ""
31+
PingLatency time.Duration `lazyssh:"transient"` // ping latency
3232

3333
// Additional SSH config fields
3434
// Connection and proxy settings

0 commit comments

Comments
 (0)