[Init] Initial commit - NetMesh terminal manager
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
This commit is contained in:
729
domain/host.test.ts
Normal file
729
domain/host.test.ts
Normal file
@@ -0,0 +1,729 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import type { Host } from "./models.ts";
|
||||
import {
|
||||
classifyDistroId,
|
||||
detectVendorFromSshVersion,
|
||||
getHostAddressForClipboard,
|
||||
hostsEqualForIdentityReuse,
|
||||
migrateHostsFromLegacyLineTimestamps,
|
||||
normalizeDistroId,
|
||||
normalizePrimaryTelnetState,
|
||||
preserveConcurrentHostLineTimestampUpdate,
|
||||
resolveHostKeepalive,
|
||||
resolveTelnetPort,
|
||||
resolveTelnetPassword,
|
||||
resolveTelnetUsername,
|
||||
sanitizeHost,
|
||||
shouldProbeSessionCwd,
|
||||
shouldSuggestNetworkDeviceMode,
|
||||
upsertHostById,
|
||||
} from "./host.ts";
|
||||
|
||||
const makeHost = (overrides: Partial<Host> = {}): Host => ({
|
||||
id: "host-1",
|
||||
label: "Primary Host",
|
||||
hostname: "127.0.0.1",
|
||||
port: 22,
|
||||
username: "root",
|
||||
authMethod: "password",
|
||||
tags: [],
|
||||
os: "linux",
|
||||
createdAt: 1,
|
||||
protocol: "ssh",
|
||||
...overrides,
|
||||
});
|
||||
|
||||
test("upsertHostById updates an existing host in place", () => {
|
||||
const existing = makeHost();
|
||||
const updated = makeHost({ label: "Updated Host" });
|
||||
|
||||
assert.deepEqual(upsertHostById([existing], updated), [updated]);
|
||||
});
|
||||
|
||||
test("shouldSuggestNetworkDeviceMode offers the switch for an auto-detected vendor", () => {
|
||||
assert.equal(
|
||||
shouldSuggestNetworkDeviceMode({ host: makeHost(), detectedDistro: "huawei", alreadyHandled: false }),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test("shouldSuggestNetworkDeviceMode stays silent once already enabled", () => {
|
||||
assert.equal(
|
||||
shouldSuggestNetworkDeviceMode({
|
||||
host: makeHost({ deviceType: "network" }),
|
||||
detectedDistro: "cisco",
|
||||
alreadyHandled: false,
|
||||
}),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test("shouldSuggestNetworkDeviceMode stays silent after it was already handled", () => {
|
||||
assert.equal(
|
||||
shouldSuggestNetworkDeviceMode({ host: makeHost(), detectedDistro: "cisco", alreadyHandled: true }),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test("shouldSuggestNetworkDeviceMode ignores ordinary linux distros", () => {
|
||||
assert.equal(
|
||||
shouldSuggestNetworkDeviceMode({ host: makeHost(), detectedDistro: "ubuntu", alreadyHandled: false }),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test("shouldSuggestNetworkDeviceMode does not fire on a substring-only vendor keyword", () => {
|
||||
assert.equal(
|
||||
shouldSuggestNetworkDeviceMode({ host: makeHost(), detectedDistro: "cisco-lab-server", alreadyHandled: false }),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test("shouldSuggestNetworkDeviceMode still fires for a plain SSH session", () => {
|
||||
assert.equal(
|
||||
shouldSuggestNetworkDeviceMode({
|
||||
host: makeHost(),
|
||||
detectedDistro: "cisco",
|
||||
alreadyHandled: false,
|
||||
effectiveProtocol: "ssh",
|
||||
}),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test("shouldSuggestNetworkDeviceMode stays silent for non-SSH sessions (mosh/et/serial/telnet)", () => {
|
||||
for (const effectiveProtocol of ["mosh", "et", "serial", "telnet", "local"]) {
|
||||
assert.equal(
|
||||
shouldSuggestNetworkDeviceMode({
|
||||
host: makeHost(),
|
||||
detectedDistro: "cisco",
|
||||
alreadyHandled: false,
|
||||
effectiveProtocol,
|
||||
}),
|
||||
false,
|
||||
`expected no suggestion for ${effectiveProtocol}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("upsertHostById appends a duplicated host with a fresh id", () => {
|
||||
const existing = makeHost({
|
||||
id: "serial-original",
|
||||
label: "Serial Config",
|
||||
protocol: "serial",
|
||||
hostname: "/dev/ttyUSB0",
|
||||
port: 115200,
|
||||
serialConfig: {
|
||||
path: "/dev/ttyUSB0",
|
||||
baudRate: 115200,
|
||||
dataBits: 8,
|
||||
stopBits: 1,
|
||||
parity: "none",
|
||||
flowControl: "none",
|
||||
localEcho: false,
|
||||
lineMode: false,
|
||||
},
|
||||
});
|
||||
const duplicate = makeHost({
|
||||
...existing,
|
||||
id: "serial-duplicate",
|
||||
label: "Serial Config (copy)",
|
||||
});
|
||||
|
||||
assert.deepEqual(upsertHostById([existing], duplicate), [existing, duplicate]);
|
||||
});
|
||||
|
||||
test("telnet credential helpers preserve explicitly cleared values", () => {
|
||||
const host = makeHost({
|
||||
username: "ssh-user",
|
||||
password: "ssh-password",
|
||||
telnetUsername: "",
|
||||
telnetPassword: "",
|
||||
});
|
||||
|
||||
assert.equal(resolveTelnetUsername(host), "");
|
||||
assert.equal(resolveTelnetPassword(host), "");
|
||||
});
|
||||
|
||||
test("telnet credential helpers fall back only when telnet fields are unset", () => {
|
||||
const host = makeHost({
|
||||
username: " ssh-user ",
|
||||
password: "ssh-password",
|
||||
telnetUsername: undefined,
|
||||
telnetPassword: undefined,
|
||||
});
|
||||
|
||||
assert.equal(resolveTelnetUsername(host), "ssh-user");
|
||||
assert.equal(resolveTelnetPassword(host), "ssh-password");
|
||||
});
|
||||
|
||||
test("normalizePrimaryTelnetState enables primary telnet without materializing a port", () => {
|
||||
const result = normalizePrimaryTelnetState(makeHost({
|
||||
protocol: "telnet",
|
||||
telnetEnabled: false,
|
||||
telnetPort: undefined,
|
||||
port: undefined,
|
||||
}));
|
||||
|
||||
assert.equal(result.telnetEnabled, true);
|
||||
assert.equal(result.telnetPort, undefined);
|
||||
assert.equal(result.port, undefined);
|
||||
});
|
||||
|
||||
test("normalizePrimaryTelnetState leaves optional telnet hosts unchanged", () => {
|
||||
const result = normalizePrimaryTelnetState(makeHost({
|
||||
protocol: "ssh",
|
||||
telnetEnabled: false,
|
||||
telnetPort: undefined,
|
||||
}));
|
||||
|
||||
assert.equal(result.telnetEnabled, false);
|
||||
assert.equal(result.telnetPort, undefined);
|
||||
});
|
||||
|
||||
test("migrateHostsFromLegacyLineTimestamps preserves the old global opt-in", () => {
|
||||
const host = makeHost();
|
||||
|
||||
assert.deepEqual(migrateHostsFromLegacyLineTimestamps([host], true), [
|
||||
{ ...host, showLineTimestamps: true },
|
||||
]);
|
||||
});
|
||||
|
||||
test("migrateHostsFromLegacyLineTimestamps does not override explicit host choices", () => {
|
||||
const enabled = makeHost({ id: "enabled", showLineTimestamps: true });
|
||||
const disabled = makeHost({ id: "disabled", showLineTimestamps: false });
|
||||
|
||||
assert.deepEqual(migrateHostsFromLegacyLineTimestamps([enabled, disabled], true), [enabled, disabled]);
|
||||
});
|
||||
|
||||
test("migrateHostsFromLegacyLineTimestamps fills only missing host choices", () => {
|
||||
const inherited = makeHost({ id: "inherited" });
|
||||
const disabled = makeHost({ id: "disabled", showLineTimestamps: false });
|
||||
|
||||
assert.deepEqual(migrateHostsFromLegacyLineTimestamps([inherited, disabled], true), [
|
||||
{ ...inherited, showLineTimestamps: true },
|
||||
disabled,
|
||||
]);
|
||||
});
|
||||
|
||||
test("sanitizeHost preserves valid custom host icon fields", () => {
|
||||
const sanitized = sanitizeHost(makeHost({
|
||||
iconMode: "custom",
|
||||
iconId: "database",
|
||||
iconColor: "blue",
|
||||
}));
|
||||
|
||||
assert.equal(sanitized.iconMode, "custom");
|
||||
assert.equal(sanitized.iconId, "database");
|
||||
assert.equal(sanitized.iconColor, "blue");
|
||||
});
|
||||
|
||||
test("sanitizeHost preserves valid unavailable plugin connection configuration", () => {
|
||||
const providerId = "com.example.transport.connection";
|
||||
const sanitized = sanitizeHost(makeHost({
|
||||
protocol: `plugin:${providerId}`,
|
||||
pluginConnection: {
|
||||
providerId,
|
||||
configuration: { endpoint: "example", options: [1, 2] },
|
||||
credentialId: "credential-reference-1234",
|
||||
},
|
||||
}));
|
||||
assert.deepEqual(sanitized.pluginConnection, {
|
||||
providerId,
|
||||
configuration: { endpoint: "example", options: [1, 2] },
|
||||
credentialId: "credential-reference-1234",
|
||||
});
|
||||
});
|
||||
|
||||
test("sanitizeHost removes hidden built-in credentials and transport state from plugin hosts", () => {
|
||||
const providerId = "com.example.transport.connection";
|
||||
const sanitized = sanitizeHost(makeHost({
|
||||
protocol: `plugin:${providerId}`,
|
||||
pluginConnection: {
|
||||
providerId,
|
||||
configuration: { endpoint: "example" },
|
||||
credentialId: "credential-reference-1234",
|
||||
},
|
||||
port: 22,
|
||||
identityId: "identity-1",
|
||||
identityFileId: "key-1",
|
||||
identityFilePaths: ["~/.ssh/id_work"],
|
||||
password: "saved-secret",
|
||||
savePassword: true,
|
||||
authMethod: "key",
|
||||
authPolicyVersion: 1,
|
||||
requiresMfa: true,
|
||||
useSshAgent: true,
|
||||
identityAgent: "~/.ssh/agent.sock",
|
||||
identitiesOnly: true,
|
||||
addKeysToAgent: "confirm",
|
||||
useKeychain: true,
|
||||
agentForwarding: true,
|
||||
x11Forwarding: true,
|
||||
proxyProfileId: "proxy-1",
|
||||
proxyConfig: { type: "http", host: "proxy.example", port: 8080 },
|
||||
hostChain: { type: "hosts", hostIds: ["jump-1"] },
|
||||
moshEnabled: true,
|
||||
moshServerPath: "/usr/bin/mosh-server",
|
||||
etEnabled: true,
|
||||
etPort: 2022,
|
||||
telnetEnabled: true,
|
||||
telnetPort: 23,
|
||||
telnetIdentityId: "telnet-identity",
|
||||
telnetUsername: "legacy-user",
|
||||
telnetPassword: "legacy-secret",
|
||||
sftpSudo: true,
|
||||
legacyAlgorithms: true,
|
||||
skipEcdsaHostKey: true,
|
||||
algorithms: { kex: ["diffie-hellman-group14-sha1"] },
|
||||
keepaliveOverride: true,
|
||||
keepaliveInterval: 10,
|
||||
keepaliveCountMax: 3,
|
||||
sshTcpConnectTimeoutSeconds: 15,
|
||||
sshAuthReadyTimeoutSeconds: 20,
|
||||
}));
|
||||
|
||||
assert.deepEqual(sanitized.pluginConnection, {
|
||||
providerId,
|
||||
configuration: { endpoint: "example" },
|
||||
credentialId: "credential-reference-1234",
|
||||
});
|
||||
for (const field of [
|
||||
"port", "identityId", "identityFileId", "identityFilePaths", "password", "savePassword",
|
||||
"authMethod", "authPolicyVersion", "requiresMfa", "useSshAgent", "identityAgent",
|
||||
"identitiesOnly", "addKeysToAgent", "useKeychain", "agentForwarding", "x11Forwarding",
|
||||
"proxyProfileId", "proxyConfig", "hostChain", "moshEnabled", "moshServerPath", "etEnabled",
|
||||
"etPort", "telnetEnabled", "telnetPort", "telnetIdentityId", "telnetUsername",
|
||||
"telnetPassword", "sftpSudo", "legacyAlgorithms", "skipEcdsaHostKey", "algorithms",
|
||||
"keepaliveOverride", "keepaliveInterval", "keepaliveCountMax", "sshTcpConnectTimeoutSeconds",
|
||||
"sshAuthReadyTimeoutSeconds",
|
||||
]) {
|
||||
assert.equal(field in sanitized, false, `${field} should be removed`);
|
||||
}
|
||||
});
|
||||
|
||||
test("sanitizeHost keeps legacy empty-password hosts on automatic authentication", () => {
|
||||
const sanitized = sanitizeHost(makeHost({
|
||||
password: undefined,
|
||||
authMethod: "password",
|
||||
authPolicyVersion: undefined,
|
||||
}));
|
||||
|
||||
assert.equal(sanitized.authMethod, undefined);
|
||||
assert.equal(sanitized.authPolicyVersion, 1);
|
||||
});
|
||||
|
||||
test("sanitizeHost keeps legacy agent and key hosts on their prior authentication path", () => {
|
||||
for (const legacySettings of [
|
||||
{ useSshAgent: true, password: "saved-secret" },
|
||||
{ identityFilePaths: ["~/.ssh/id_work"], password: "saved-secret" },
|
||||
]) {
|
||||
const sanitized = sanitizeHost(makeHost({
|
||||
authMethod: "password",
|
||||
authPolicyVersion: undefined,
|
||||
...legacySettings,
|
||||
}));
|
||||
|
||||
assert.equal(sanitized.authMethod, undefined);
|
||||
assert.equal(sanitized.authPolicyVersion, 1);
|
||||
}
|
||||
});
|
||||
|
||||
test("sanitizeHost preserves an explicit password-only choice", () => {
|
||||
const sanitized = sanitizeHost(makeHost({
|
||||
password: undefined,
|
||||
authMethod: "password",
|
||||
authPolicyVersion: 1,
|
||||
}));
|
||||
|
||||
assert.equal(sanitized.authMethod, "password");
|
||||
assert.equal(sanitized.authPolicyVersion, 1);
|
||||
});
|
||||
|
||||
test("sanitizeHost preserves a legacy no-save password-only choice", () => {
|
||||
const sanitized = sanitizeHost(makeHost({
|
||||
password: undefined,
|
||||
savePassword: false,
|
||||
authMethod: "password",
|
||||
authPolicyVersion: undefined,
|
||||
}));
|
||||
|
||||
assert.equal(sanitized.authMethod, "password");
|
||||
assert.equal(sanitized.savePassword, false);
|
||||
assert.equal(sanitized.authPolicyVersion, 1);
|
||||
});
|
||||
|
||||
test("sanitizeHost keeps legacy no-save agent and key hosts on their prior authentication path", () => {
|
||||
for (const legacySettings of [
|
||||
{ useSshAgent: true },
|
||||
{ identityFileId: "selected-key" },
|
||||
{ identityFilePaths: ["~/.ssh/id_work"] },
|
||||
]) {
|
||||
const sanitized = sanitizeHost(makeHost({
|
||||
password: undefined,
|
||||
savePassword: false,
|
||||
authMethod: "password",
|
||||
authPolicyVersion: undefined,
|
||||
...legacySettings,
|
||||
}));
|
||||
|
||||
assert.equal(sanitized.authMethod, undefined);
|
||||
assert.equal(sanitized.authPolicyVersion, 1);
|
||||
}
|
||||
});
|
||||
|
||||
test("sanitizeHost preserves a legacy whitespace-only password", () => {
|
||||
const sanitized = sanitizeHost(makeHost({
|
||||
password: " ",
|
||||
authMethod: "password",
|
||||
authPolicyVersion: undefined,
|
||||
}));
|
||||
|
||||
assert.equal(sanitized.authMethod, "password");
|
||||
assert.equal(sanitized.password, " ");
|
||||
assert.equal(sanitized.authPolicyVersion, 1);
|
||||
});
|
||||
|
||||
test("sanitizeHost preserves an inferred legacy saved password", () => {
|
||||
const sanitized = sanitizeHost(makeHost({
|
||||
password: "saved-secret",
|
||||
authMethod: undefined,
|
||||
authPolicyVersion: undefined,
|
||||
}));
|
||||
|
||||
assert.equal(sanitized.authMethod, "password");
|
||||
assert.equal(sanitized.authPolicyVersion, 1);
|
||||
});
|
||||
|
||||
test("sanitizeHost preserves inferred legacy identity file paths", () => {
|
||||
const sanitized = sanitizeHost(makeHost({
|
||||
password: "fallback-secret",
|
||||
authMethod: undefined,
|
||||
authPolicyVersion: undefined,
|
||||
identityFilePaths: ["~/.ssh/id_work"],
|
||||
}));
|
||||
|
||||
assert.equal(sanitized.authMethod, "key");
|
||||
assert.equal(sanitized.authPolicyVersion, 1);
|
||||
});
|
||||
|
||||
test("sanitizeHost preserves automatic host icon color fields", () => {
|
||||
const sanitized = sanitizeHost(makeHost({
|
||||
iconMode: "auto",
|
||||
iconColor: "violet",
|
||||
}));
|
||||
|
||||
assert.equal(sanitized.iconMode, "auto");
|
||||
assert.equal(sanitized.iconId, undefined);
|
||||
assert.equal(sanitized.iconColor, "violet");
|
||||
});
|
||||
|
||||
test("sanitizeHost removes invalid custom host icon fields", () => {
|
||||
const sanitized = sanitizeHost(makeHost({
|
||||
iconMode: "custom",
|
||||
iconId: "bad",
|
||||
iconColor: "blue",
|
||||
} as unknown as Partial<Host>));
|
||||
|
||||
assert.equal(sanitized.iconMode, undefined);
|
||||
assert.equal(sanitized.iconId, undefined);
|
||||
assert.equal(sanitized.iconColor, undefined);
|
||||
});
|
||||
|
||||
test("sanitizeHost trims leading whitespace before extracting the hostname", () => {
|
||||
const sanitized = sanitizeHost(makeHost({
|
||||
hostname: " 127.0.0.1",
|
||||
}));
|
||||
|
||||
assert.equal(sanitized.hostname, "127.0.0.1");
|
||||
});
|
||||
|
||||
test("sanitizeHost preserves valid per-host SSH timeouts and removes invalid values", () => {
|
||||
const sanitized = sanitizeHost(makeHost({
|
||||
sshTcpConnectTimeoutSeconds: 45.4,
|
||||
sshAuthReadyTimeoutSeconds: 3601,
|
||||
}));
|
||||
|
||||
assert.equal(sanitized.sshTcpConnectTimeoutSeconds, 45);
|
||||
assert.equal(sanitized.sshAuthReadyTimeoutSeconds, undefined);
|
||||
});
|
||||
|
||||
test("preserves a concurrent terminal timestamp toggle when host details did not edit it", () => {
|
||||
const openedHost = makeHost({ showLineTimestamps: false });
|
||||
const latestHost = makeHost({ showLineTimestamps: true });
|
||||
const draft = makeHost({ label: "Edited label", showLineTimestamps: false });
|
||||
|
||||
assert.deepEqual(
|
||||
preserveConcurrentHostLineTimestampUpdate({ draft, openedHost, latestHost }),
|
||||
{ ...draft, showLineTimestamps: true },
|
||||
);
|
||||
});
|
||||
|
||||
test("keeps host details timestamp value when the details form edits it", () => {
|
||||
const openedHost = makeHost({ showLineTimestamps: false });
|
||||
const latestHost = makeHost({ showLineTimestamps: false });
|
||||
const draft = makeHost({ showLineTimestamps: true });
|
||||
|
||||
assert.equal(
|
||||
preserveConcurrentHostLineTimestampUpdate({ draft, openedHost, latestHost }).showLineTimestamps,
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test("preserves a concurrent SFTP follow-terminal-directory toggle when host details did not edit it", () => {
|
||||
const openedHost = makeHost({ sftpFollowTerminalCwd: undefined });
|
||||
const latestHost = makeHost({ sftpFollowTerminalCwd: true });
|
||||
const draft = makeHost({ label: "Edited label", sftpFollowTerminalCwd: undefined });
|
||||
|
||||
assert.deepEqual(
|
||||
preserveConcurrentHostLineTimestampUpdate({ draft, openedHost, latestHost }),
|
||||
{ ...draft, sftpFollowTerminalCwd: true },
|
||||
);
|
||||
});
|
||||
|
||||
test("keeps host details SFTP follow-terminal-directory value when the details form edits it", () => {
|
||||
const openedHost = makeHost({ sftpFollowTerminalCwd: false });
|
||||
const latestHost = makeHost({ sftpFollowTerminalCwd: false });
|
||||
const draft = makeHost({ sftpFollowTerminalCwd: true });
|
||||
|
||||
assert.equal(
|
||||
preserveConcurrentHostLineTimestampUpdate({ draft, openedHost, latestHost }).sftpFollowTerminalCwd,
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test("normalizePrimaryTelnetState preserves an explicit telnet port", () => {
|
||||
const result = normalizePrimaryTelnetState(makeHost({
|
||||
protocol: "telnet",
|
||||
telnetEnabled: false,
|
||||
telnetPort: 2325,
|
||||
}));
|
||||
|
||||
assert.equal(result.telnetEnabled, true);
|
||||
assert.equal(result.telnetPort, 2325);
|
||||
});
|
||||
|
||||
test("resolveTelnetPort ignores ssh ports for optional telnet", () => {
|
||||
assert.equal(resolveTelnetPort(makeHost({
|
||||
protocol: "ssh",
|
||||
port: 2222,
|
||||
telnetPort: undefined,
|
||||
})), 23);
|
||||
});
|
||||
|
||||
test("resolveTelnetPort uses primary telnet port fallback", () => {
|
||||
assert.equal(resolveTelnetPort(makeHost({
|
||||
protocol: "telnet",
|
||||
port: 2325,
|
||||
telnetPort: undefined,
|
||||
})), 2325);
|
||||
});
|
||||
|
||||
test("sanitizeHost migrates a deprecated fontFamily and clears the override flag", () => {
|
||||
// Regression guard for codex P2 review on PR #940: hosts saved with
|
||||
// pingfang-sc / microsoft-yahei / comic-sans-ms in fontFamily must
|
||||
// have the override dropped so they fall back to the global default
|
||||
// instead of silently rendering the wrong font while still claiming
|
||||
// an override is active.
|
||||
const before = makeHost({
|
||||
fontFamily: "comic-sans-ms",
|
||||
fontFamilyOverride: true,
|
||||
});
|
||||
const after = sanitizeHost(before);
|
||||
assert.equal(after.fontFamily, undefined);
|
||||
assert.equal(after.fontFamilyOverride, false);
|
||||
});
|
||||
|
||||
test("sanitizeHost keeps a still-valid fontFamily untouched", () => {
|
||||
const before = makeHost({
|
||||
fontFamily: "fira-code",
|
||||
fontFamilyOverride: true,
|
||||
});
|
||||
const after = sanitizeHost(before);
|
||||
assert.equal(after.fontFamily, "fira-code");
|
||||
assert.equal(after.fontFamilyOverride, true);
|
||||
});
|
||||
|
||||
test("detectVendorFromSshVersion recognizes legacy Huawei VRP dash banner", () => {
|
||||
assert.equal(detectVendorFromSshVersion("-"), "huawei");
|
||||
assert.equal(detectVendorFromSshVersion("SSH-2.0--"), "huawei");
|
||||
});
|
||||
|
||||
test("detectVendorFromSshVersion recognizes Ruijie RGOS banner", () => {
|
||||
assert.equal(detectVendorFromSshVersion("RGOS_SSH"), "ruijie");
|
||||
assert.equal(detectVendorFromSshVersion("SSH-2.0-RGOS_SSH"), "ruijie");
|
||||
});
|
||||
|
||||
test("detectVendorFromSshVersion recognizes H3C and Comware banners", () => {
|
||||
assert.equal(detectVendorFromSshVersion("H3C-Comware-7.1.064"), "h3c");
|
||||
assert.equal(detectVendorFromSshVersion("SSH-2.0-Comware-7.1.064"), "h3c");
|
||||
assert.equal(detectVendorFromSshVersion("SSH-2.0-3Com OS"), "h3c");
|
||||
});
|
||||
|
||||
test("detectVendorFromSshVersion maps mpSSH banners to HPE iLO", () => {
|
||||
assert.equal(detectVendorFromSshVersion("SSH-2.0-mpSSH_0.2.1"), "hpe");
|
||||
});
|
||||
|
||||
test("normalizeDistroId maps Alibaba Cloud Linux os-release ID to alinux", () => {
|
||||
// /etc/os-release ID="alinux" — the canonical signal from Alibaba Cloud
|
||||
// Linux 3 (issue #1200). Regression guard: 'alinux'.includes('linux') is
|
||||
// true, so without a dedicated branch this would fall through to the
|
||||
// generic 'linux' icon (the bug the issue reports).
|
||||
assert.equal(normalizeDistroId("alinux"), "alinux");
|
||||
assert.notEqual(normalizeDistroId("alinux"), "linux");
|
||||
});
|
||||
|
||||
test("normalizeDistroId maps legacy Aliyun Linux IDs to alinux", () => {
|
||||
// Older releases branded the distro as "Aliyun Linux" with ID=aliyun.
|
||||
assert.equal(normalizeDistroId("aliyun"), "alinux");
|
||||
});
|
||||
|
||||
test("normalizeDistroId matches Alibaba Cloud Linux PRETTY_NAME/NAME fallback", () => {
|
||||
// When ID is absent the detector falls back to NAME / PRETTY_NAME text.
|
||||
assert.equal(normalizeDistroId("Alibaba Cloud Linux"), "alinux");
|
||||
assert.equal(
|
||||
normalizeDistroId("Alibaba Cloud Linux 3.2104 U13.1 (OpenAnolis Edition)"),
|
||||
"alinux",
|
||||
);
|
||||
});
|
||||
|
||||
test("normalizeDistroId maps openEuler before the generic Linux fallback", () => {
|
||||
assert.equal(normalizeDistroId("openeuler"), "openeuler");
|
||||
assert.equal(normalizeDistroId("openEuler"), "openeuler");
|
||||
assert.notEqual(normalizeDistroId("openeuler"), "linux");
|
||||
});
|
||||
|
||||
test("normalizeDistroId maps Darwin and macOS labels to macos", () => {
|
||||
assert.equal(normalizeDistroId("Darwin"), "macos");
|
||||
assert.equal(normalizeDistroId("Darwin Kernel Version 24.5.0"), "macos");
|
||||
assert.equal(normalizeDistroId("macOS"), "macos");
|
||||
assert.equal(normalizeDistroId("Mac OS X"), "macos");
|
||||
});
|
||||
|
||||
test("normalizeDistroId maps FreeBSD uname output to freebsd", () => {
|
||||
assert.equal(normalizeDistroId("FreeBSD"), "freebsd");
|
||||
assert.equal(
|
||||
normalizeDistroId("FreeBSD host.example.com 14.3-RELEASE-p1 GENERIC amd64"),
|
||||
"freebsd",
|
||||
);
|
||||
});
|
||||
|
||||
test("classifyDistroId limits Linux-like runtime support to implemented POSIX platforms", () => {
|
||||
assert.equal(classifyDistroId("macos"), "linux-like");
|
||||
assert.equal(classifyDistroId("Darwin"), "linux-like");
|
||||
assert.equal(classifyDistroId("freebsd"), "other");
|
||||
});
|
||||
|
||||
test("shouldProbeSessionCwd allows the probe on a plain Linux host", () => {
|
||||
assert.equal(
|
||||
shouldProbeSessionCwd({ isNetworkDevice: false, remoteSshVersion: "OpenSSH_9.6" }),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test("shouldProbeSessionCwd skips the probe on an already-classified network device", () => {
|
||||
// Reconnect / manual deviceType='network': host.distro already says network.
|
||||
assert.equal(
|
||||
shouldProbeSessionCwd({ isNetworkDevice: true, remoteSshVersion: "OpenSSH_9.6" }),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test("shouldProbeSessionCwd skips the probe when the SSH banner reveals a network vendor", () => {
|
||||
// First connect to a brand-new Huawei VRP: host.distro not persisted yet, so
|
||||
// isNetworkDevice is still false — the banner is the only signal (#1043).
|
||||
assert.equal(
|
||||
shouldProbeSessionCwd({ isNetworkDevice: false, remoteSshVersion: "-" }),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
shouldProbeSessionCwd({ isNetworkDevice: false, remoteSshVersion: "SSH-1.99--" }),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
const GLOBAL_KEEPALIVE = { keepaliveInterval: 30, keepaliveCountMax: 10 };
|
||||
|
||||
test("resolveHostKeepalive falls back to global when override is not set", () => {
|
||||
const host = makeHost();
|
||||
assert.deepEqual(
|
||||
resolveHostKeepalive(host, GLOBAL_KEEPALIVE),
|
||||
{ interval: 30, countMax: 10, source: "global" },
|
||||
);
|
||||
});
|
||||
|
||||
test("resolveHostKeepalive falls back to global when override is explicitly false", () => {
|
||||
const host = makeHost({
|
||||
keepaliveOverride: false,
|
||||
keepaliveInterval: 0,
|
||||
keepaliveCountMax: 3,
|
||||
});
|
||||
// Override flag is the gate; the host's stored values stay parked and
|
||||
// unused so toggling the flag back on later restores them.
|
||||
assert.deepEqual(
|
||||
resolveHostKeepalive(host, GLOBAL_KEEPALIVE),
|
||||
{ interval: 30, countMax: 10, source: "global" },
|
||||
);
|
||||
});
|
||||
|
||||
test("resolveHostKeepalive uses host values when override is true", () => {
|
||||
const host = makeHost({
|
||||
keepaliveOverride: true,
|
||||
keepaliveInterval: 0,
|
||||
keepaliveCountMax: 3,
|
||||
});
|
||||
assert.deepEqual(
|
||||
resolveHostKeepalive(host, GLOBAL_KEEPALIVE),
|
||||
{ interval: 0, countMax: 3, source: "host" },
|
||||
);
|
||||
});
|
||||
|
||||
test("resolveHostKeepalive lets each field fall back independently", () => {
|
||||
// Override on, but only `interval` set on the host: inherit global countMax.
|
||||
assert.deepEqual(
|
||||
resolveHostKeepalive(
|
||||
makeHost({ keepaliveOverride: true, keepaliveInterval: 5 }),
|
||||
GLOBAL_KEEPALIVE,
|
||||
),
|
||||
{ interval: 5, countMax: 10, source: "host" },
|
||||
);
|
||||
// Override on, but only countMax set: inherit global interval.
|
||||
assert.deepEqual(
|
||||
resolveHostKeepalive(
|
||||
makeHost({ keepaliveOverride: true, keepaliveCountMax: 50 }),
|
||||
GLOBAL_KEEPALIVE,
|
||||
),
|
||||
{ interval: 30, countMax: 50, source: "host" },
|
||||
);
|
||||
});
|
||||
|
||||
test("hostsEqualForIdentityReuse is true for shallow-identical field values", () => {
|
||||
const a = makeHost({ showLineTimestamps: true });
|
||||
const b = { ...a };
|
||||
assert.equal(hostsEqualForIdentityReuse(a, b), true);
|
||||
assert.equal(hostsEqualForIdentityReuse(a, makeHost({ showLineTimestamps: false })), false);
|
||||
});
|
||||
|
||||
test("getHostAddressForClipboard returns the trimmed hostname for vault one-click copy", () => {
|
||||
assert.equal(getHostAddressForClipboard(makeHost({ hostname: " 10.0.0.12 " })), "10.0.0.12");
|
||||
assert.equal(getHostAddressForClipboard(makeHost({ hostname: "db.internal" })), "db.internal");
|
||||
assert.equal(getHostAddressForClipboard({ hostname: "" }), "");
|
||||
});
|
||||
|
||||
test("getHostAddressForClipboard omits synthesized plugin hostnames", () => {
|
||||
assert.equal(
|
||||
getHostAddressForClipboard(
|
||||
makeHost({
|
||||
protocol: "plugin:com.example.transport.connection",
|
||||
hostname: "My Plugin Label",
|
||||
pluginConnection: {
|
||||
providerId: "com.example.transport.connection",
|
||||
configuration: { endpoint: "opaque-target" },
|
||||
},
|
||||
}),
|
||||
),
|
||||
"",
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user