fix desktop servers

Signed-off-by: dongjiang <dongjiang1989@126.com>
pull/18049/head
dongjiang 2026-03-18 15:06:16 +08:00 committed by dongjiang
parent 84e62fc662
commit 2760a0b6ab
3 changed files with 426 additions and 7 deletions

View File

@ -0,0 +1,259 @@
import { describe, expect, test } from "bun:test"
// Import types inline to avoid dependency issues in test environment
type ServerConnectionHttpBase = {
url: string
username?: string
password?: string
}
type ServerConnectionHttp = {
type: "http"
displayName?: string
http: ServerConnectionHttpBase
}
type ServerConnectionSidecar = {
type: "sidecar"
variant: "base" | "wsl"
distro?: string
http: ServerConnectionHttpBase
}
type ServerConnectionSsh = {
type: "ssh"
host: string
http: ServerConnectionHttpBase
}
type ServerConnectionAny = ServerConnectionHttp | ServerConnectionSidecar | ServerConnectionSsh
// Mock the dependencies that would normally come from SolidJS and other modules
// This test verifies the isLocal logic without requiring full SolidJS runtime
describe("isLocal server detection (Issue #13518, #18040)", () => {
// Simulate the isLocal logic from server.tsx:214-216
// After fix: only sidecar base connections are considered local
function isLocalFixed(conn: ServerConnectionAny | undefined): boolean {
return conn?.type === "sidecar" && conn.variant === "base"
}
// Simulate the OLD buggy logic for comparison
// Before fix: HTTP connections to localhost were also considered local
function isLocalBuggy(conn: ServerConnectionAny | undefined): boolean {
if (!conn) return false
if (conn.type === "sidecar" && conn.variant === "base") return true
if (conn.type === "http") {
const host = conn.http.url.replace(/^https?:\/\//, "").split(":")[0]
if (host === "localhost" || host === "127.0.0.1") return true
}
return false
}
describe("sidecar connections", () => {
test("sidecar base should be local", () => {
const conn: ServerConnectionSidecar = {
type: "sidecar",
variant: "base",
http: { url: "http://localhost:4096" },
}
expect(isLocalFixed(conn)).toBe(true)
expect(isLocalBuggy(conn)).toBe(true) // Both agree on this
})
test("sidecar WSL should NOT be local", () => {
const conn: ServerConnectionSidecar = {
type: "sidecar",
variant: "wsl",
distro: "Ubuntu-22.04",
http: { url: "http://localhost:4096" },
}
expect(isLocalFixed(conn)).toBe(false)
expect(isLocalBuggy(conn)).toBe(false) // Both agree on this
})
})
describe("HTTP connections to localhost", () => {
test("HTTP localhost:4096 should NOT be local (port-forwarded remote)", () => {
const conn: ServerConnectionHttp = {
type: "http",
http: { url: "http://localhost:4096" },
}
expect(isLocalFixed(conn)).toBe(false) // Fixed: uses web dialog
expect(isLocalBuggy(conn)).toBe(true) // Bug: uses native file picker
})
test("HTTP 127.0.0.1:4096 should NOT be local (port-forwarded remote)", () => {
const conn: ServerConnectionHttp = {
type: "http",
http: { url: "http://127.0.0.1:4096" },
}
expect(isLocalFixed(conn)).toBe(false) // Fixed: uses web dialog
expect(isLocalBuggy(conn)).toBe(true) // Bug: uses native file picker
})
test("HTTP localhost with custom port should NOT be local", () => {
const conn: ServerConnectionHttp = {
type: "http",
http: { url: "http://localhost:1455" },
}
expect(isLocalFixed(conn)).toBe(false)
expect(isLocalBuggy(conn)).toBe(true)
})
})
describe("HTTP connections to remote servers", () => {
test("HTTP remote IP should NOT be local", () => {
const conn: ServerConnectionHttp = {
type: "http",
http: { url: "http://192.168.0.40:1455" },
}
expect(isLocalFixed(conn)).toBe(false)
expect(isLocalBuggy(conn)).toBe(false) // Both agree on this
})
test("HTTP remote hostname should NOT be local", () => {
const conn: ServerConnectionHttp = {
type: "http",
http: { url: "http://remote-server.example.com:4096" },
}
expect(isLocalFixed(conn)).toBe(false)
expect(isLocalBuggy(conn)).toBe(false)
})
test("HTTP with credentials should NOT be local", () => {
const conn: ServerConnectionHttp = {
type: "http",
displayName: "Remote VM",
http: {
url: "http://192.168.1.100:4096",
username: "opencode",
password: "secret",
},
}
expect(isLocalFixed(conn)).toBe(false)
expect(isLocalBuggy(conn)).toBe(false)
})
})
describe("SSH connections", () => {
test("SSH remote should NOT be local", () => {
const conn: ServerConnectionSsh = {
type: "ssh",
host: "remote.example.com",
http: { url: "http://localhost:4097" },
}
expect(isLocalFixed(conn)).toBe(false)
expect(isLocalBuggy(conn)).toBe(false)
})
test("SSH with IP should NOT be local", () => {
const conn: ServerConnectionSsh = {
type: "ssh",
host: "192.168.1.100",
http: { url: "http://localhost:4097" },
}
expect(isLocalFixed(conn)).toBe(false)
expect(isLocalBuggy(conn)).toBe(false)
})
})
describe("undefined connection", () => {
test("undefined should NOT be local", () => {
expect(isLocalFixed(undefined)).toBe(false)
expect(isLocalBuggy(undefined)).toBe(false)
})
})
describe("real-world scenarios from issues", () => {
test("Issue #13518: RHEL VM on localhost:4096 via port forwarding", () => {
// User connects to VM via port forwarding: localhost:4096 -> VM:4096
const conn: ServerConnectionHttp = {
type: "http",
http: { url: "http://localhost:4096" },
}
// Fixed behavior: should use web dialog (not local file picker)
expect(isLocalFixed(conn)).toBe(false)
// This means DialogSelectDirectory will be used instead of native picker
})
test("Issue #18040: Ubuntu server at 192.168.0.40:1455", () => {
// User connects to remote Ubuntu server directly
const conn: ServerConnectionHttp = {
type: "http",
http: { url: "http://192.168.0.40:1455" },
}
expect(isLocalFixed(conn)).toBe(false)
// Should use web dialog to browse remote filesystem
})
test("Local desktop app with default sidecar", () => {
// User runs desktop app with default local server
const conn: ServerConnectionSidecar = {
type: "sidecar",
variant: "base",
http: { url: "http://localhost:4096" },
}
expect(isLocalFixed(conn)).toBe(true)
// Should use native file picker for better UX
})
})
describe("behavior comparison", () => {
test("fix only affects localhost HTTP connections", () => {
const testCases: Array<{
name: string
conn: ServerConnectionAny
buggyResult: boolean
fixedResult: boolean
}> = [
{
name: "sidecar base",
conn: { type: "sidecar", variant: "base", http: { url: "http://localhost:4096" } },
buggyResult: true,
fixedResult: true,
},
{
name: "sidecar WSL",
conn: {
type: "sidecar",
variant: "wsl",
distro: "Ubuntu",
http: { url: "http://localhost:4096" },
},
buggyResult: false,
fixedResult: false,
},
{
name: "HTTP localhost",
conn: { type: "http", http: { url: "http://localhost:4096" } },
buggyResult: true, // BUG: incorrectly local
fixedResult: false, // FIXED: correctly remote
},
{
name: "HTTP 127.0.0.1",
conn: { type: "http", http: { url: "http://127.0.0.1:4096" } },
buggyResult: true, // BUG: incorrectly local
fixedResult: false, // FIXED: correctly remote
},
{
name: "HTTP remote IP",
conn: { type: "http", http: { url: "http://192.168.0.40:1455" } },
buggyResult: false,
fixedResult: false,
},
{
name: "SSH remote",
conn: { type: "ssh", host: "remote.com", http: { url: "http://localhost:4097" } },
buggyResult: false,
fixedResult: false,
},
]
for (const tc of testCases) {
expect(isLocalBuggy(tc.conn)).toBe(tc.buggyResult)
expect(isLocalFixed(tc.conn)).toBe(tc.fixedResult)
}
})
})
})

View File

@ -0,0 +1,166 @@
import { describe, expect, test } from "bun:test"
import { normalizeServerUrl, serverName, ServerConnection } from "./server"
describe("normalizeServerUrl", () => {
test("adds http protocol when missing", () => {
expect(normalizeServerUrl("localhost:4096")).toBe("http://localhost:4096")
expect(normalizeServerUrl("192.168.1.100:4096")).toBe("http://192.168.1.100:4096")
})
test("preserves existing protocol", () => {
expect(normalizeServerUrl("http://localhost:4096")).toBe("http://localhost:4096")
expect(normalizeServerUrl("https://example.com:4096")).toBe("https://example.com:4096")
})
test("removes trailing slashes", () => {
expect(normalizeServerUrl("http://localhost:4096/")).toBe("http://localhost:4096")
expect(normalizeServerUrl("http://localhost:4096///")).toBe("http://localhost:4096")
})
test("handles whitespace", () => {
expect(normalizeServerUrl(" localhost:4096 ")).toBe("http://localhost:4096")
})
test("returns undefined for empty input", () => {
expect(normalizeServerUrl("")).toBeUndefined()
expect(normalizeServerUrl(" ")).toBeUndefined()
})
})
describe("serverName", () => {
test("returns displayName when available", () => {
const conn: ServerConnection.Http = {
type: "http",
displayName: "My Remote Server",
http: { url: "http://192.168.1.100:4096" },
}
expect(serverName(conn)).toBe("My Remote Server")
})
test("extracts host from URL when no displayName", () => {
const conn: ServerConnection.Http = {
type: "http",
http: { url: "http://192.168.1.100:4096" },
}
expect(serverName(conn)).toBe("192.168.1.100:4096")
})
test("ignores displayName when ignoreDisplayName is true", () => {
const conn: ServerConnection.Http = {
type: "http",
displayName: "My Remote Server",
http: { url: "http://192.168.1.100:4096" },
}
expect(serverName(conn, true)).toBe("192.168.1.100:4096")
})
test("handles sidecar connections", () => {
const conn: ServerConnection.Sidecar = {
type: "sidecar",
variant: "base",
http: { url: "http://localhost:4096" },
}
expect(serverName(conn)).toBe("localhost:4096")
})
test("handles SSH connections", () => {
const conn: ServerConnection.Ssh = {
type: "ssh",
host: "remote.example.com",
http: { url: "http://localhost:4097" },
}
expect(serverName(conn)).toBe("localhost:4097")
})
test("returns empty string for undefined", () => {
expect(serverName()).toBe("")
})
})
describe("ServerConnection.key", () => {
test("generates key for HTTP connection", () => {
const conn: ServerConnection.Http = {
type: "http",
http: { url: "http://192.168.1.100:4096" },
}
expect(ServerConnection.key(conn)).toBe("http://192.168.1.100:4096")
})
test("generates key for sidecar base connection", () => {
const conn: ServerConnection.Sidecar = {
type: "sidecar",
variant: "base",
http: { url: "http://localhost:4096" },
}
expect(ServerConnection.key(conn)).toBe("sidecar")
})
test("generates key for sidecar WSL connection", () => {
const conn: ServerConnection.Sidecar = {
type: "sidecar",
variant: "wsl",
distro: "Ubuntu-22.04",
http: { url: "http://localhost:4096" },
}
expect(ServerConnection.key(conn)).toBe("wsl:Ubuntu-22.04")
})
test("generates key for SSH connection", () => {
const conn: ServerConnection.Ssh = {
type: "ssh",
host: "remote.example.com",
http: { url: "http://localhost:4097" },
}
expect(ServerConnection.key(conn)).toBe("ssh:remote.example.com")
})
})
describe("ServerConnection types", () => {
test("HTTP connection structure", () => {
const conn: ServerConnection.Http = {
type: "http",
displayName: "Remote",
http: {
url: "http://192.168.1.100:4096",
username: "user",
password: "pass",
},
}
expect(conn.type).toBe("http")
expect(conn.http.url).toBe("http://192.168.1.100:4096")
expect(conn.http.username).toBe("user")
expect(conn.http.password).toBe("pass")
})
test("Sidecar base connection structure", () => {
const conn: ServerConnection.Sidecar = {
type: "sidecar",
variant: "base",
http: { url: "http://localhost:4096" },
}
expect(conn.type).toBe("sidecar")
expect(conn.variant).toBe("base")
})
test("Sidecar WSL connection structure", () => {
const conn: ServerConnection.Sidecar = {
type: "sidecar",
variant: "wsl",
distro: "Ubuntu",
http: { url: "http://localhost:4096" },
}
expect(conn.type).toBe("sidecar")
expect(conn.variant).toBe("wsl")
expect(conn.distro).toBe("Ubuntu")
})
test("SSH connection structure", () => {
const conn: ServerConnection.Ssh = {
type: "ssh",
host: "192.168.1.100",
http: { url: "http://localhost:4097" },
}
expect(conn.type).toBe("ssh")
expect(conn.host).toBe("192.168.1.100")
})
})

View File

@ -24,15 +24,9 @@ export function serverName(conn?: ServerConnection.Any, ignoreDisplayName = fals
function projectsKey(key: ServerConnection.Key) {
if (!key) return ""
if (key === "sidecar") return "local"
if (isLocalHost(key)) return "local"
return key
}
function isLocalHost(url: string) {
const host = url.replace(/^https?:\/\//, "").split(":")[0]
if (host === "localhost" || host === "127.0.0.1") return "local"
}
export namespace ServerConnection {
type Base = { displayName?: string }
@ -213,7 +207,7 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
)
const isLocal = createMemo(() => {
const c = current()
return (c?.type === "sidecar" && c.variant === "base") || (c?.type === "http" && isLocalHost(c.http.url))
return c?.type === "sidecar" && c.variant === "base"
})
return {