* fix(ssh): per-host skipEcdsaHostKey toggle + advanced algorithm overrides (#1027) #1027 reported an old Huawei S7706 (SSH banner `SSH-2.0--`, empty software-version field) where the legacy-algorithms toggle still couldn't get a connection through. The debug log shows the handshake makes it through every negotiation step, picks `ecdsa-sha2-nistp521` for the host key, then dies at: Handshake failed: signature verification failed i.e. ssh2's strict RFC verifier rejects the ECDSA signature the switch produces. OpenSSH on the same machine connects because its known_hosts is already pinned to an RSA fingerprint, so it never advertises ECDSA in the first place. This adds two layers of escape hatch: A. `host.skipEcdsaHostKey` (one-click) — drops every `ecdsa-sha2-*` from the offered host-key list. Forces the fallback to ssh-rsa / ssh-dss / ssh-ed25519 that those old stacks implement correctly. Wired through ssh / sftp / port-forwarding bridges, inheritable from the group default. B. `host.algorithms` (advanced) — per-category override lists (`kex`, `cipher`, `hmac`, `serverHostKey`, `compress`). When a category's array is non-empty, it fully replaces the negotiated list for that category. Exposed in a collapsible "Advanced algorithm overrides" panel on both host and group settings, inspired by Tabby's per-profile algorithm UI. Empty arrays normalize to "use default" so picking zero algorithms in a category doesn't bench the connection with "no matching algorithm". Overrides apply BEFORE `skipEcdsaHostKey` so the latter stays an unconditional kill switch even if the user explicitly puts `ecdsa-*` back into the host-key list. Behavior with default values (neither toggle set, no override list) is identical to before — zero change for hosts that aren't opted in. Tests cover the new options on both `buildAlgorithms` and `buildSftpAlgorithms`, plus group→host inheritance for both fields. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(ssh): address Codex review on #1116 — proper seed + jump-host carry + missing call sites Four review findings from the Codex pass on #1116, all real: 1. AlgorithmOverridesPanel was seeding the first customization in a category from `SUPPORTED_ALGORITHMS_BY_CATEGORY`, which contains legacy algorithms (CBC, arcfour, MD5, ssh-dss, group1-sha1...). Unchecking a single modern algorithm in a host that had legacy mode *off* would silently start offering those legacy algorithms. Now seeds from `effectiveDefaultAlgorithms(legacyEnabled)`, mirroring what `buildAlgorithms` actually emits at connect time. - New `effectiveDefaultAlgorithms` pure helper in `domain/sshAlgorithmList.ts` with its own test suite (also asserts the modern subset contains no SHA-1 KEX / CBC / arcfour / MD5) - `AlgorithmOverridesPanel` takes a `legacyEnabled` prop - `isChecked` now reflects the effective-default state for an untouched category, so unchecked rows visually represent algorithms the connection wouldn't currently advertise 2. The chain-mode jump-host loop in `sshBridge.cjs` was applying the target host's `skipEcdsaHostKey` / `algorithmOverrides` to every bastion hop, which is wrong when the bastion has its own settings. This was actually a pre-existing issue with `legacyAlgorithms` too — `NetcattyJumpHost` simply didn't carry any of these fields. - `NetcattyJumpHost` gains `legacyAlgorithms`, `skipEcdsaHostKey`, `algorithmOverrides` - `createTerminalSessionStarters` populates them from each jump host's own configuration - The jump-host bridge call now reads `jump.* ?? options.*` so a hop with its own setting wins, but unset hops still fall back to the target's settings (preserves historic chain-wide behavior when nothing is overridden) 3. `infrastructure/services/portForwardingService.ts` only forwarded `legacyAlgorithms` to the port-forwarding bridge, so a host that needed the ECDSA skip or advanced overrides could connect through the terminal but its auto-start tunnels would still hit the original handshake failure. - Forward `skipEcdsaHostKey` and `algorithmOverrides` at the target call site and at the jump-host map. 4. `application/state/sftp/useSftpHostCredentials.ts` built `NetcattySSHOptions` for `openSftp` without any of the algorithm fields — and on inspection it didn't even forward `legacyAlgorithms` to begin with, so SFTP panes for legacy-mode hosts were silently negotiating with the modern default list. Same gap at the jump-host map. - Forward all three fields at both the target and the jump-host map. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * polish(hosts): make the advanced-algorithm collapsible trigger a real button The first cut used a plain underlined caption for the "Advanced algorithm overrides" collapsible trigger. That blends into the surrounding helper text and doesn't read as an interactive control — users couldn't tell that the per-category checkbox editor was reachable at all. Match the project's existing collapsible-trigger pattern (see `SerialConnectModal`): full-width ghost Button, label on the left, ChevronDown / ChevronUp on the right that flips with open state, controlled via `useState`. Applied in both `HostDetailsPanel` and `GroupDetailsPanel`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * polish(hosts): rename Legacy Algorithms card to SSH Algorithms; split Backspace into its own section The card now carries three algorithm controls (Allow Legacy / Skip ECDSA / Advanced overrides) plus a Backspace Behavior dropdown that doesn't belong with the rest. Two cleanups: 1. Rename the card from "Legacy Algorithms" to "SSH Algorithms". The original title only described the first toggle; the section now covers the whole algorithm-negotiation surface, including the ECDSA host-key skip and the per-category override editor. - i18n key renamed `hostDetails.section.legacyAlgorithms` -> `hostDetails.section.sshAlgorithms` in zh-CN / en / ru - `HostDetailsPanel` references the new key 2. Move the Backspace Behavior control out of the algorithm card. - `HostDetailsPanel`: new dedicated "Terminal Behavior" card (TerminalSquare icon) placed between SSH Algorithms and Keepalive. New i18n key `hostDetails.section.terminalBehavior`. - `GroupDetailsPanel`: no separate Card scaffolding for group defaults — moved Backspace to the bottom of the SSH section (after Mosh) so it visually separates from the algorithm block above without introducing a new card heading. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(sftp): per-hop algorithm settings in SFTP chain (Codex review on #1116) `sshBridge.cjs`'s jump loop already reads `jump.* ?? options.*` for the three algorithm fields, but `sftpBridge.cjs#connectThroughChainForSftp` was missed in that change and still applied the target host's `options.legacyAlgorithms` / `skipEcdsaHostKey` / `algorithmOverrides` to every bastion. A bastion that needed the ECDSA skip or a custom algorithm list while the target didn't would still fail the SFTP handshake before reaching the target. Mirror the sshBridge fix: read each setting from the jump host first, falling back to the target options when the hop didn't override. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(ssh): keychain SSH exec + advanced editor seed honor inherited algorithm settings Two findings from a local Codex review pass on the branch: 1. The keychain "export public key to host" flow opens its own one-off SSH connection through `sshBridge#execCommand`, but the connectOpts built there didn't include any `algorithms`. ssh2 then negotiated with its built-in modern defaults regardless of what the host had set, so a host that needs the ECDSA skip (or legacy mode) would connect in the terminal but the keychain export would fail with the original signature-verification error. - `sshBridge.cjs#execCommand` now sets `algorithms` from `payload.legacyAlgorithms` / `skipEcdsaHostKey` / `algorithmOverrides`, mirroring `startSSHSession`. - `useKeychainBackend`'s `execCommand` typing gets the three new optional fields. - `KeychainManager` forwards the host's `legacyAlgorithms`, `skipEcdsaHostKey`, and `algorithms` when invoking the export. 2. The Advanced Algorithm Overrides editor seeded its first customization from `form.legacyAlgorithms`, which is the host's own field — it doesn't reflect a value the host *inherits* from its group's default. A user in a group with `legacyAlgorithms=true` editing a host that hadn't explicitly set the flag would see the editor seed in modern-only mode, and saving could silently drop the legacy algorithms the host actually needed. - `HostDetailsPanel` passes `form.legacyAlgorithms ?? groupDefaults?.legacyAlgorithms`. - `GroupDetailsPanel` adds an `inheritedLegacyAlgorithms` memo resolved from the parent group chain via `resolveGroupDefaults`, and the editor uses `form.legacyAlgorithms ?? inheritedLegacyAlgorithms`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(keychain): export-public-key honors group-inherited algorithm settings Second-pass Codex review on PR #1116 flagged that the keychain "export public key to host" flow now carries the three algorithm fields end-to-end, but only reads them from `exportHost.*` directly — which doesn't reflect values the host inherits from its group's defaults. A host that left `legacyAlgorithms` / `skipEcdsaHostKey` / `algorithms` unset but sat inside a group that turned them on would work fine in the terminal (the terminal starter applies group defaults before sending the IPC payload) but its keychain export would silently fall back to ssh2's modern defaults and hit the original signature-verification failure. Resolve the effective host with `applyGroupDefaults` + `resolveGroupDefaults` before the `execCommand` call, then read the algorithm fields off the effective host. Requires plumbing `groupConfigs` into `KeychainManager` (added as an optional prop, forwarded by `VaultView`). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(host-details): algorithm-overrides editor reads currently selected group's defaults Third-pass Codex review caught that the editor seed was reading from the `groupDefaults` prop, which is whatever group the host belonged to when the panel opened. If a user switched the host into a different group inside the panel and then opened the Advanced Algorithm Overrides collapsible before saving, the editor would seed from the old group's `legacyAlgorithms` flag and could save the wrong list. The panel already memoizes `effectiveGroupDefaults` from `form.group`/`defaultGroup`/`groupConfigs` for exactly this kind of re-resolution (used by theme/font effective lookups). Read the inherited flag from there instead of the stale prop. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(algorithms): trim UI cipher list to algorithms ssh2 actually supports Fourth-pass Codex review flagged that `SUPPORTED_CIPHER_ALGORITHMS` included `blowfish-cbc`, `cast128-cbc`, and the `arcfour*` family, but OpenSSL 3 disabled those primitives, so ssh2's `canUseCipher` filter drops them from `SUPPORTED_CIPHER` at startup. Selecting any of them in the Advanced Algorithm Overrides editor would make `ssh2.Client.connect()` throw `Unsupported algorithm` synchronously, turning the override into a "host now unreachable" footgun instead of a narrowing knob. Realigned each `SUPPORTED_*_ALGORITHMS` list to ssh2's actual `SUPPORTED_*` constants: - `SUPPORTED_CIPHER_ALGORITHMS`: dropped blowfish / cast128 / arcfour; added the no-suffix `aes128-gcm` / `aes256-gcm` variants ssh2 also accepts. - `SUPPORTED_KEX_ALGORITHMS`: added `diffie-hellman-group15-sha512` and `diffie-hellman-group17-sha512` (present in ssh2 but missing from the UI list); reordered to ssh2's canonical order. - `SUPPORTED_HMAC_ALGORITHMS`: reordered so the ETM/SHA-2 grouping matches ssh2's `DEFAULT_MAC` and lookups are predictable. Locked the invariant in `sshAlgorithmList.test.ts` with a new test that asserts every UI-offered algorithm is a member of ssh2's `SUPPORTED_*` for its category. A future ssh2 bump that drops an algorithm we still expose will fail this test instead of silently becoming a connect-time error for users. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(algorithms): run KEX override through the runtime fixed-DH filter Codex round-N flagged a gap in `applyAlgorithmOverrides`: when the user supplies a custom KEX list via the advanced editor, the previous implementation copied it verbatim into the negotiated `algorithms.kex` field. The default builder already passes its KEX list through `filterSupportedFixedDhKex` to drop fixed-DH groups the runtime doesn't support (notably `diffie-hellman-group1-sha1` on Electron/BoringSSL, which lacks modp2), but the override path bypassed that filter — so an Electron user enabling legacy mode and saving any KEX checkbox state would re-advertise group1-sha1 and the handshake would crash with "Unknown DH group" instead of failing fast. Apply the same `filterSupportedFixedDhKex` to the override list. New test in `sshAlgorithms.test.cjs` exercises a simulated BoringSSL runtime that lacks modp2 and asserts the override-filtered KEX no longer includes group1-sha1, while group14-sha1 / group-exchange-sha1 remain. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(algorithms): run HMAC override through the FIPS MD5 filter Codex flagged the same runtime-bypass pattern Round-N-1 fixed for KEX, now in HMAC: `applyLegacyHmacAlgorithms` gates `hmac-md5` behind `md5Supported()` so FIPS Node builds don't get it, but the UI's `effectiveDefaultAlgorithms(true)` seed adds `hmac-md5` / `hmac-md5-96` unconditionally. A user with legacy mode on who saves any HMAC checkbox change would push those MD5 entries through `applyAlgorithmOverrides`, which previously copied the override verbatim — bypassing the FIPS gate and making ssh2 throw "Unsupported algorithm" before negotiation. New `filterRuntimeUnsupportedHmac` helper applies the same `md5Supported()` gate to a user override. `applyAlgorithmOverrides` routes the HMAC override through it (mirrors how the same function already routes KEX overrides through `filterSupportedFixedDhKex`). New test simulates a FIPS-disabled MD5 runtime and asserts MD5 variants drop from the final HMAC list while SHA-1 / SHA-2 entries remain. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(ssh): jump-host overrides no longer inherit the leaf's algorithm overrides Codex flagged that the jump-loop fallback I added for chain convenience applied the target's per-host \`algorithmOverrides\` to every bastion whose own override wasn't set, which is wrong: a target restricted to e.g. \`serverHostKey: [\"ssh-rsa\"]\` would lock the hop to ssh-rsa too and break negotiation against an Ed25519-only bastion. The fallback IS still correct for \`legacyAlgorithms\` and \`skipEcdsaHostKey\` — those are append/safety toggles that widen the offered list, so propagating them to a bastion is safe and matches the historic chain-wide behavior of \`options.legacyAlgorithms\` (single-toggle convenience for a chain with one old leaf). Treat \`algorithmOverrides\` strictly per-host instead. Same change in both \`sshBridge.cjs\` and \`sftpBridge.cjs\` jump loops, with a comment block explaining the asymmetry so a future refactor doesn't "clean up" the distinction. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ui(host-details): surface inherited algorithm overrides in the advanced editor Codex flagged a real gap in the inheritance model: when a parent group has set algorithm overrides (e.g. \`algorithms.kex: [...]\`), a host or child group under it can't simply Reset a category back to NetCatty's defaults — \`applyGroupDefaults\` treats an unset host field as "inherit", so the local Reset falls back to the group's list rather than to ssh2's defaults. Cleanly distinguishing "reset to NetCatty defaults" from "inherit from group" needs a new schema field or sentinel, which is a non-trivial design change well beyond the scope of this PR. For now, surface the situation in the UI so the user understands why Reset doesn't behave the way they might expect and where to go to actually clear the restriction: - \`AlgorithmOverridesPanel\` accepts an optional \`inheritedFromGroup\` prop and, when populated, renders a blue inline notice listing the inherited categories and directing the user to the group's algorithm settings if they need to opt out. - \`HostDetailsPanel\` passes \`effectiveGroupDefaults?.algorithms\`. - \`GroupDetailsPanel\` adds a new \`inheritedAlgorithmOverrides\` memo that resolves the same way the existing \`inheritedLegacyAlgorithms\` does, and forwards it. - i18n strings added in zh-CN / en / ru. Follow-up (out of scope for this PR): if real users do hit this, introduce a \`host.algorithms = null\` (or explicit \`algorithmsOverride: boolean\`) sentinel and a Reset-to-defaults button that uses it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(ssh): jump-host skipEcdsaHostKey is per-host; panel seeds from inherited overrides Three findings from the latest Codex pass: 1 & 2. `skipEcdsaHostKey` on the leaf was still falling back onto jump hosts in both `sshBridge.cjs` and `sftpBridge.cjs`. I had classified it with `legacyAlgorithms` as a "safety widening" knob, but Codex is right that it actually *narrows* the offered host-key list by dropping every `ecdsa-sha2-*`. An ECDSA-only bastion (or one where the operator pinned ECDSA via known_hosts) would still negotiate when ECDSA is offered, but fails when the leaf's skip is propagated to it. Same fix as `algorithmOverrides` last round: strictly per-host. Only `legacyAlgorithms` keeps the chain-wide fallback (append-only — it widens the offer, can never break a hop that wasn't already failing). 3. `AlgorithmOverridesPanel` was seeding the first customization of a category from the NetCatty/legacy effective default, ignoring any list the host inherits from its group for OTHER categories. Because `applyGroupDefaults` treats `host.algorithms` as an all-or-nothing inherit boundary, the moment the user saved any host-local override `{ cipher: [...] }`, the group's `{ serverHostKey: ["ssh-rsa"] }` restriction silently dropped from the effective host. Now: - `toggleAlgorithm`'s first-click seed reads `inheritedFromGroup?.[category] ?? effectiveDefault[category]`, so customizing one category preserves the group's narrowing on the others. - `updateCategory` initializes `next` from `{ ...inheritedFromGroup, ...value }` so saved overrides carry the inherited categories alongside the host's own edits. - `isChecked` reflects the inherited list when there's no local value, so the visible checkbox state matches what would actually be advertised. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(host-details): Reset preserves the inherited list instead of silently widening Codex caught a follow-on bug from the previous "carry inheritance" fix: pressing Reset on a category that's inherited from the group deleted that category from \`host.algorithms\` while other host-local or carried-inherited categories remained. Because \`applyGroupDefaults\` treats \`host.algorithms\` as all-or-nothing, the moment any host-local entry exists the group's \`algorithms\` object stops being inherited as a whole — so the freshly-deleted category fell back to NetCatty defaults rather than the group's narrower list. Effective result: Reset on an inherited category *widened* the offer instead of restoring it. Reset now persists the inherited list verbatim onto the host when the group has an override for that category, so "Reset" means "use what this host would otherwise inherit" in all cases. Also tightened \`isCustomized\` to suppress the "customized" badge (and the per-category Reset button) when the host's stored list is identical to the inherited list — those rows haven't really been customized by the user. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ui(host-details): gate the inherited-notice / checkbox baseline on no host override Codex caught the asymmetry between the read and write sides of the panel. The write side (`updateCategory`, `toggleAlgorithm`, Reset) intentionally carries `inheritedFromGroup` onto the host on the first edit so the runtime's all-or-nothing inherit boundary in `applyGroupDefaults` doesn't silently widen the offer. But the read side (the inherited-notice banner and the `isChecked` baseline for unfilled categories) was applying inherited values *unconditionally* — including when the host already had any local `algorithms` object, which makes `applyGroupDefaults` stop inheriting from the group as a whole. Net effect on a host that only locally overrode `cipher`: the UI claimed the group's `serverHostKey` restriction was still in effect, while the runtime would actually use NetCatty's modern defaults for that category. Introduce `inheritedForDisplay`, defined as `value === undefined ? inheritedFromGroup : undefined`, and route the notice + the `isChecked` baseline through it. The write side keeps consulting the unconditional `inheritedFromGroup` so the carry-over still happens on first edit — that part is what makes the runtime behavior match what the UI used to advertise. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ui(host-details): legacy/skipEcdsa toggles reflect group-inherited value Codex caught that the Skip ECDSA toggle in both the host and group panels read `form.skipEcdsaHostKey` directly, so a host whose group turned the flag on (and `applyGroupDefaults` therefore applied it to the runtime SSH negotiation) still saw the toggle rendered as off. Worse, clicking it computed `!form.skipEcdsaHostKey` — which is `!undefined` = `true` — so the first click could not actually disable the inherited setting on a per-host basis; it would just store `true` explicitly, the same effective state. The Allow Legacy Algorithms toggle had the same pre-existing issue. Fix both at once: each toggle now derives its enabled state from `form.<field> ?? <inherited>` and the onToggle handler flips that same effective value, so the toggle accurately represents what the runtime would do and a single click off correctly stores `false` (which `applyGroupDefaults` then leaves alone, breaking inheritance for this host). - `HostDetailsPanel` reads from `effectiveGroupDefaults`. - `GroupDetailsPanel` adds an `inheritedSkipEcdsaHostKey` memo alongside the existing `inheritedLegacyAlgorithms` and uses both. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
240 lines
7.8 KiB
TypeScript
240 lines
7.8 KiB
TypeScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { applyGroupDefaults, resolveGroupDefaults, sanitizeGroupConfig } from "./groupConfig.ts";
|
|
import { resolveTelnetPassword, resolveTelnetUsername } from "./host.ts";
|
|
import type { GroupConfig, Host } from "./models.ts";
|
|
|
|
const host = (overrides: Partial<Host> = {}): Host => ({
|
|
id: "host-1",
|
|
label: "Host",
|
|
hostname: "example.com",
|
|
username: "root",
|
|
tags: [],
|
|
os: "linux",
|
|
...overrides,
|
|
});
|
|
|
|
test("applyGroupDefaults lets a host proxy profile override a group custom proxy", () => {
|
|
const groupDefaults: Partial<GroupConfig> = {
|
|
proxyConfig: { type: "http", host: "group-proxy.example.com", port: 3128 },
|
|
};
|
|
|
|
const result = applyGroupDefaults(host({ proxyProfileId: "proxy-1" }), groupDefaults);
|
|
|
|
assert.equal(result.proxyProfileId, "proxy-1");
|
|
assert.equal(result.proxyConfig, undefined);
|
|
});
|
|
|
|
test("applyGroupDefaults lets a host custom proxy override a group proxy profile", () => {
|
|
const groupDefaults: Partial<GroupConfig> = {
|
|
proxyProfileId: "group-proxy",
|
|
};
|
|
const customProxy = { type: "socks5" as const, host: "host-proxy.example.com", port: 1080 };
|
|
|
|
const result = applyGroupDefaults(host({ proxyConfig: customProxy }), groupDefaults);
|
|
|
|
assert.equal(result.proxyProfileId, undefined);
|
|
assert.deepEqual(result.proxyConfig, customProxy);
|
|
});
|
|
|
|
test("resolveGroupDefaults treats saved and custom proxies as one inherited setting", () => {
|
|
const resolved = resolveGroupDefaults("prod/api", [
|
|
{
|
|
path: "prod",
|
|
proxyConfig: { type: "http", host: "parent-proxy.example.com", port: 3128 },
|
|
},
|
|
{
|
|
path: "prod/api",
|
|
proxyProfileId: "child-proxy",
|
|
},
|
|
]);
|
|
|
|
assert.equal(resolved.proxyProfileId, "child-proxy");
|
|
assert.equal(resolved.proxyConfig, undefined);
|
|
});
|
|
|
|
test("applyGroupDefaults keeps a missing host proxy profile instead of using group proxy", () => {
|
|
const groupDefaults: Partial<GroupConfig> = {
|
|
proxyProfileId: "group-proxy",
|
|
};
|
|
|
|
const result = applyGroupDefaults(
|
|
host({ proxyProfileId: "missing-proxy" }),
|
|
groupDefaults,
|
|
{ validProxyProfileIds: new Set(["group-proxy"]) },
|
|
);
|
|
|
|
assert.equal(result.proxyProfileId, "missing-proxy");
|
|
assert.equal(result.proxyConfig, undefined);
|
|
});
|
|
|
|
test("applyGroupDefaults keeps a missing host proxy profile when no group fallback exists", () => {
|
|
const result = applyGroupDefaults(
|
|
host({ proxyProfileId: "missing-proxy" }),
|
|
{},
|
|
{ validProxyProfileIds: new Set(["group-proxy"]) },
|
|
);
|
|
|
|
assert.equal(result.proxyProfileId, "missing-proxy");
|
|
assert.equal(result.proxyConfig, undefined);
|
|
});
|
|
|
|
test("applyGroupDefaults keeps a missing host proxy profile instead of using group custom proxy", () => {
|
|
const groupProxy = { type: "http" as const, host: "group-proxy.example.com", port: 3128 };
|
|
const result = applyGroupDefaults(
|
|
host({ proxyProfileId: "missing-proxy" }),
|
|
{ proxyConfig: groupProxy },
|
|
{ validProxyProfileIds: new Set(["group-proxy"]) },
|
|
);
|
|
|
|
assert.equal(result.proxyProfileId, "missing-proxy");
|
|
assert.equal(result.proxyConfig, undefined);
|
|
});
|
|
|
|
test("resolveGroupDefaults keeps a missing group proxy marker when there is no fallback", () => {
|
|
const resolved = resolveGroupDefaults(
|
|
"prod",
|
|
[{ path: "prod", proxyProfileId: "missing-proxy" }],
|
|
{ validProxyProfileIds: new Set(["group-proxy"]) },
|
|
);
|
|
|
|
assert.equal(resolved.proxyProfileId, "missing-proxy");
|
|
});
|
|
|
|
test("applyGroupDefaults inherits a missing group proxy marker so connect paths can fail", () => {
|
|
const result = applyGroupDefaults(
|
|
host({ group: "prod" }),
|
|
{ proxyProfileId: "missing-proxy" },
|
|
{ validProxyProfileIds: new Set(["group-proxy"]) },
|
|
);
|
|
|
|
assert.equal(result.proxyProfileId, "missing-proxy");
|
|
assert.equal(result.proxyConfig, undefined);
|
|
});
|
|
|
|
test("resolveGroupDefaults keeps missing child proxy profiles instead of using parent proxy", () => {
|
|
const resolved = resolveGroupDefaults(
|
|
"prod/api",
|
|
[
|
|
{
|
|
path: "prod",
|
|
proxyConfig: { type: "http", host: "parent-proxy.example.com", port: 3128 },
|
|
},
|
|
{
|
|
path: "prod/api",
|
|
proxyProfileId: "missing-proxy",
|
|
},
|
|
],
|
|
{ validProxyProfileIds: new Set(["group-proxy"]) },
|
|
);
|
|
|
|
assert.equal(resolved.proxyProfileId, "missing-proxy");
|
|
assert.equal(resolved.proxyConfig, undefined);
|
|
});
|
|
|
|
test("applyGroupDefaults preserves explicitly cleared telnet credentials", () => {
|
|
const result = applyGroupDefaults(
|
|
host({
|
|
username: "ssh-user",
|
|
password: "ssh-password",
|
|
telnetUsername: "",
|
|
telnetPassword: "",
|
|
}),
|
|
{
|
|
telnetUsername: "group-telnet-user",
|
|
telnetPassword: "group-telnet-password",
|
|
},
|
|
);
|
|
|
|
assert.equal(result.telnetUsername, "");
|
|
assert.equal(result.telnetPassword, "");
|
|
assert.equal(resolveTelnetUsername(result), "");
|
|
assert.equal(resolveTelnetPassword(result), "");
|
|
});
|
|
|
|
test("applyGroupDefaults still inherits telnet credentials when host fields are unset", () => {
|
|
const result = applyGroupDefaults(
|
|
host({
|
|
username: "ssh-user",
|
|
password: "ssh-password",
|
|
}),
|
|
{
|
|
telnetUsername: "group-telnet-user",
|
|
telnetPassword: "group-telnet-password",
|
|
},
|
|
);
|
|
|
|
assert.equal(result.telnetUsername, "group-telnet-user");
|
|
assert.equal(result.telnetPassword, "group-telnet-password");
|
|
assert.equal(resolveTelnetUsername(result), "group-telnet-user");
|
|
assert.equal(resolveTelnetPassword(result), "group-telnet-password");
|
|
});
|
|
|
|
test("applyGroupDefaults continues to inherit empty ssh username from the group", () => {
|
|
const result = applyGroupDefaults(
|
|
host({
|
|
username: "",
|
|
}),
|
|
{
|
|
username: "group-ssh-user",
|
|
},
|
|
);
|
|
|
|
assert.equal(result.username, "group-ssh-user");
|
|
});
|
|
|
|
test("sanitizeGroupConfig migrates a deprecated fontFamily and clears the override flag", () => {
|
|
// Regression guard for codex P2 review on PR #940: groups saved with
|
|
// pingfang-sc / microsoft-yahei / comic-sans-ms must shed the
|
|
// override so member hosts inherit the global default instead of
|
|
// silently falling through to fonts[0] under an enabled override.
|
|
const before: GroupConfig = {
|
|
path: "team",
|
|
fontFamily: "pingfang-sc",
|
|
fontFamilyOverride: true,
|
|
};
|
|
const after = sanitizeGroupConfig(before);
|
|
assert.equal(after.fontFamily, undefined);
|
|
assert.equal(after.fontFamilyOverride, false);
|
|
});
|
|
|
|
test("sanitizeGroupConfig keeps a still-valid fontFamily untouched", () => {
|
|
const before: GroupConfig = {
|
|
path: "team",
|
|
fontFamily: "jetbrains-mono",
|
|
fontFamilyOverride: true,
|
|
};
|
|
const after = sanitizeGroupConfig(before);
|
|
assert.equal(after.fontFamily, "jetbrains-mono");
|
|
assert.equal(after.fontFamilyOverride, true);
|
|
});
|
|
|
|
test("applyGroupDefaults inherits skipEcdsaHostKey from the group when host has no value", () => {
|
|
const result = applyGroupDefaults(host(), { skipEcdsaHostKey: true });
|
|
assert.equal(result.skipEcdsaHostKey, true);
|
|
});
|
|
|
|
test("applyGroupDefaults keeps host-level skipEcdsaHostKey instead of group default", () => {
|
|
const result = applyGroupDefaults(
|
|
host({ skipEcdsaHostKey: false }),
|
|
{ skipEcdsaHostKey: true },
|
|
);
|
|
assert.equal(result.skipEcdsaHostKey, false);
|
|
});
|
|
|
|
test("applyGroupDefaults inherits algorithm overrides from the group", () => {
|
|
const overrides = { serverHostKey: ["ssh-rsa", "ssh-dss"] };
|
|
const result = applyGroupDefaults(host(), { algorithms: overrides });
|
|
assert.deepEqual(result.algorithms, overrides);
|
|
});
|
|
|
|
test("applyGroupDefaults keeps host algorithm overrides instead of inheriting", () => {
|
|
const hostOverrides = { kex: ["curve25519-sha256"] };
|
|
const groupOverrides = { kex: ["diffie-hellman-group14-sha256"] };
|
|
const result = applyGroupDefaults(
|
|
host({ algorithms: hostOverrides }),
|
|
{ algorithms: groupOverrides },
|
|
);
|
|
assert.deepEqual(result.algorithms, hostOverrides);
|
|
});
|