feat: redesign user host defaults system

This commit is contained in:
LukeGus committed 2026-09-30 06:19:56 -05:00
1 parent 3028ab1176
commit a4a4d4f27b
144 files changed
+24609 -2555

No files matched your search

@@ -0,0 +1,121 @@
import { describe, expect, it, vi } from "vitest";
vi.mock("../../../database/repositories/factory.js", () => ({}));
import type { PluginManifest } from "@termix/plugin-sdk/manifest";
import {
buildCatalog,
catalogNamespaces,
isDefaultableField,
} from "../../../hosts/defaults/catalog.js";
import {
defaultValuesEqual,
normalizeAuthDefault,
normalizeSocks5Default,
parseDefaultOverrides,
splitDefaultKey,
stableStringify,
} from "../../../../types/host-defaults.js";
const manifest = {
id: "fixture",
contributes: {
settings: {
host: {
enableKey: "enabled",
enableDefault: true,
fields: [
{ key: "mode", type: "select", default: "a", options: [] },
{ key: "token", type: "secret" },
{ key: "config", type: "json", secretKeys: ["password"] },
{ key: "mac", type: "string", defaultable: false },
{
key: "snippet",
type: "number",
defaultLevels: ["user", "folder"],
},
{ key: "font", type: "number", default: 14, personal: true },
],
},
},
},
} as unknown as PluginManifest;
describe("the catalog", () => {
it("holds core's keys and each defaultable plugin field", () => {
const catalog = buildCatalog([manifest]);
expect(catalog.has("core.sshPort")).toBe(true);
expect(catalog.has("core.auth")).toBe(true);
expect(catalog.has("fixture.enabled")).toBe(true);
expect(catalog.has("fixture.mode")).toBe(true);
expect(catalog.has("fixture.token")).toBe(false);
expect(catalog.has("fixture.config")).toBe(false);
expect(catalog.has("fixture.mac")).toBe(false);
expect(catalog.get("fixture.snippet")?.levels).toEqual(["user", "folder"]);
expect(catalog.get("fixture.font")?.personal).toBe(true);
expect(catalog.get("fixture.enabled")?.builtin).toBe(true);
expect(catalogNamespaces(catalog)).toEqual(["core", "fixture"]);
});
it("reads an unset plugin value as its manifest default", () => {
const mode = buildCatalog([manifest]).get("fixture.mode")!;
expect(mode.normalize(null)).toBe("a");
expect(mode.normalize("")).toBe("a");
expect(mode.normalize("b")).toBe("b");
});
it("never lets a secret be a default", () => {
expect(isDefaultableField({ key: "s", type: "secret" })).toBe(false);
expect(
isDefaultableField({ key: "s", type: "secret", defaultable: true }),
).toBe(false);
});
});
describe("value shapes", () => {
it("keeps only the auth fields its type uses", () => {
expect(
normalizeAuthDefault({
authType: "agent",
credentialId: 4,
agentSocketPath: "/tmp/agent",
}),
).toEqual({
authType: "agent",
credentialId: null,
overrideCredentialUsername: false,
agentSocketPath: "/tmp/agent",
agentIdentity: null,
});
expect(normalizeAuthDefault(null)).toBeNull();
});
it("drops a proxy's settings while it is off, and chain passwords always", () => {
expect(
normalizeSocks5Default({ useSocks5: false, socks5Host: "x" }).socks5Host,
).toBeNull();
expect(
normalizeSocks5Default({
useSocks5: true,
socks5ProxyChain: [{ host: "a", password: "p" }],
}).socks5ProxyChain,
).toEqual([{ host: "a" }]);
});
it("compares objects regardless of key order", () => {
expect(stableStringify({ b: 1, a: 2 })).toBe(
stableStringify({ a: 2, b: 1 }),
);
expect(defaultValuesEqual([{ a: 1 }], [{ a: 1 }])).toBe(true);
});
it("parses overrides and splits keys", () => {
expect(parseDefaultOverrides('{"core":["b","a","a"],"x":"no"}')).toEqual({
core: ["a", "b"],
});
expect(parseDefaultOverrides(null)).toBeNull();
expect(splitDefaultKey("ssh-terminal.fontSize")).toEqual([
"ssh-terminal",
"fontSize",
]);
});
});
@@ -0,0 +1,87 @@
import { describe, expect, it } from "vitest";
import {
readCoreDefault,
writeCoreDefault,
} from "../../../hosts/defaults/core-columns.js";
describe("core defaults in column form", () => {
it("reads the SSH options a row keeps as JSON", () => {
const row = {
sshOptions: JSON.stringify({ keepaliveInterval: 30 }),
};
expect(readCoreDefault("keepaliveInterval", row)).toBe(30);
expect(readCoreDefault("keepaliveCountMax", row)).toBe(5);
expect(readCoreDefault("allowLegacyAlgorithms", row)).toBe(true);
});
it("writes one SSH option and keeps the others", () => {
const row = {
sshOptions: JSON.stringify({
keepaliveInterval: 30,
agentForwarding: true,
}),
};
const patch = writeCoreDefault("keepaliveInterval", 90, row);
expect(JSON.parse(patch.sshOptions as string)).toEqual({
keepaliveInterval: 90,
agentForwarding: true,
});
});
it("moves an SSH host's port with its SSH port, but not a plugin protocol's", () => {
expect(
writeCoreDefault("sshPort", 2222, { connectionType: "ssh" }),
).toEqual({ sshPort: 2222, port: 2222 });
expect(
writeCoreDefault("sshPort", 2222, { connectionType: "rdp" }),
).toEqual({ sshPort: 2222 });
});
it("writes a credential login and clears any key the host held", () => {
const patch = writeCoreDefault(
"auth",
{ authType: "credential", credentialId: 7 },
{ sshOptions: null, password: "kept" },
);
expect(patch).toMatchObject({
authType: "credential",
credentialId: 7,
key: null,
keyPassword: null,
});
expect(patch).not.toHaveProperty("password");
expect(
writeCoreDefault("auth", { authType: "agent" }, {}).password,
).toBeNull();
});
it("round-trips a proxy and keeps the host's own chain passwords", () => {
const row = {
socks5ProxyChain: JSON.stringify([{ host: "a", password: "mine" }]),
};
const patch = writeCoreDefault(
"socks5",
{ useSocks5: true, socks5ProxyChain: [{ host: "a" }] },
row,
);
expect(JSON.parse(patch.socks5ProxyChain as string)).toEqual([
{ host: "a", password: "mine" },
]);
expect(readCoreDefault("socks5", { ...row, ...patch })).toMatchObject({
useSocks5: true,
socks5ProxyChain: [{ host: "a" }],
});
});
it("stores empty lists as null and booleans as the column expects", () => {
expect(writeCoreDefault("jumpHosts", [], {})).toEqual({ jumpHosts: null });
expect(writeCoreDefault("forceKeyboardInteractive", true, {})).toEqual({
forceKeyboardInteractive: "true",
});
expect(
readCoreDefault("forceKeyboardInteractive", {
forceKeyboardInteractive: "false",
}),
).toBe(false);
});
});
@@ -0,0 +1,324 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
/**
* The materialize pass against in-memory tables: hosts, plugin host rows and
* the defaults levels.
*/
type Row = Record<string, unknown> & { id: number; userId: string };
const db = vi.hoisted(() => ({
hosts: [] as Array<Record<string, unknown> & { id: number; userId: string }>,
pluginRows: [] as Array<{
pluginId: string;
scope: string;
scopeId: string;
key: string;
value: string | null;
encrypted: boolean;
}>,
defaults: [] as Array<Record<string, unknown>>,
folders: [] as Array<{ id: number; userId: string; name: string }>,
events: [] as unknown[],
notified: [] as unknown[],
}));
const manifest = vi.hoisted(() => ({
id: "term",
contributes: {
settings: {
host: {
enableKey: "enabled",
enableDefault: true,
fields: [
{ key: "fontSize", type: "number", default: 14 },
{ key: "mac", type: "string", defaultable: false },
],
},
},
},
}));
vi.mock("../../../database/routes/host-plugin-settings.js", () => ({
hostSettingsPlugins: () => [manifest],
}));
vi.mock("../../../plugins/events.js", () => ({
TOPICS: { hostUpdated: "host.updated" },
pluginEvents: { emit: (...args: unknown[]) => db.events.push(args) },
}));
vi.mock("../../../sync/server/feed.js", () => ({ markChanged: () => {} }));
vi.mock("../../../utils/shared-host-secrets-manager.js", () => ({
SharedHostSecretsManager: {
getInstance: () => ({ resyncHost: async () => {} }),
},
}));
vi.mock("../../../plugins/settings.js", async (importOriginal) => ({
...(await importOriginal<typeof import("../../../plugins/settings.js")>()),
notifySettingChange: (...args: unknown[]) => db.notified.push(args),
}));
vi.mock("../../../database/repositories/factory.js", () => ({
createCurrentSettingsRepository: () => ({ get: async () => null }),
createCurrentHostDefaultsRepository: () => ({
listHosts: async (filter: {
hostIds?: number[];
userIds?: string[];
all?: boolean;
}) =>
db.hosts.filter(
(host) =>
filter.all ||
filter.hostIds?.includes(host.id) ||
filter.userIds?.includes(host.userId),
),
listForUsers: async (userIds: string[]) =>
db.defaults.filter(
(row) =>
row.level === "admin" || userIds.includes(row.userId as string),
),
listFolders: async (userIds: string[]) =>
db.folders.filter((folder) => userIds.includes(folder.userId)),
listHostPlacement: async (userIds: string[]) =>
db.hosts
.filter((host) => userIds.includes(host.userId))
.map((host) => ({
id: host.id,
userId: host.userId,
folder: (host.folder as string | null) ?? null,
parentHostId: (host.parentHostId as number | null) ?? null,
})),
updateHosts: async (patches: Array<{ id: number; values: object }>) => {
for (const patch of patches) {
const host = db.hosts.find((row) => row.id === patch.id)!;
Object.assign(host, patch.values);
}
},
}),
createCurrentPluginSettingsRepository: () => ({
getAllForScopeIds: async (_scope: string, ids: string[]) =>
db.pluginRows.filter((row) => ids.includes(row.scopeId)),
set: async (
pluginId: string,
scope: string,
scopeId: string,
key: string,
value: string,
) => {
const existing = db.pluginRows.find(
(row) =>
row.pluginId === pluginId &&
row.scopeId === scopeId &&
row.key === key,
);
if (existing) existing.value = value;
else
db.pluginRows.push({
pluginId,
scope,
scopeId,
key,
value,
encrypted: false,
});
},
delete: async (
pluginId: string,
_scope: string,
scopeId: string,
key: string,
) => {
db.pluginRows = db.pluginRows.filter(
(row) =>
!(
row.pluginId === pluginId &&
row.scopeId === scopeId &&
row.key === key
),
);
return true;
},
}),
}));
const { materializeHosts } =
await import("../../../hosts/defaults/materialize.js");
function host(partial: Partial<Row> & { id: number }): Row {
return {
userId: "u1",
connectionType: "ssh",
port: 22,
sshPort: 22,
username: "root",
authType: "password",
folder: null,
parentHostId: null,
statusCheckEnabled: true,
defaultOverrides: null,
sharedSource: null,
...partial,
};
}
function setDefault(
level: "admin" | "user" | "folder",
key: string,
value: unknown,
extra: Record<string, unknown> = {},
) {
const [namespace, name] = key.split(".");
db.defaults.push({
level,
namespace,
key: name,
value: JSON.stringify(value),
userId: level === "admin" ? null : "u1",
folderId: null,
...extra,
});
}
function pluginValue(hostId: number, key: string): unknown {
const row = db.pluginRows.find(
(entry) => entry.scopeId === String(hostId) && entry.key === key,
);
return row ? JSON.parse(row.value as string) : undefined;
}
beforeEach(() => {
db.hosts = [];
db.pluginRows = [];
db.defaults = [];
db.folders = [];
db.events = [];
db.notified = [];
});
describe("materializeHosts", () => {
it("classifies a host from before host defaults: matching values follow them", async () => {
db.hosts = [host({ id: 1, sshPort: 22 }), host({ id: 2, sshPort: 2222 })];
db.pluginRows.push({
pluginId: "term",
scope: "host",
scopeId: "2",
key: "fontSize",
value: "18",
encrypted: false,
});
const result = await materializeHosts({ all: true });
expect(result.changedHostIds).toEqual([]);
const first = JSON.parse(db.hosts[0].defaultOverrides as string);
const second = JSON.parse(db.hosts[1].defaultOverrides as string);
expect(first.core).not.toContain("sshPort");
expect(second.core).toContain("sshPort");
expect(first.term).toEqual([]);
expect(second.term).toEqual(["fontSize"]);
// Nothing a user sees changed.
expect(db.hosts[1].sshPort).toBe(2222);
});
it("writes a new default into every host that follows it, and no other", async () => {
db.hosts = [
host({ id: 1, defaultOverrides: JSON.stringify({ core: [], term: [] }) }),
host({
id: 2,
sshPort: 2200,
defaultOverrides: JSON.stringify({ core: ["sshPort"], term: [] }),
}),
];
setDefault("user", "core.sshPort", 2022);
setDefault("admin", "term.fontSize", 16);
const result = await materializeHosts({ userIds: ["u1"] });
expect(result.changedHostIds).toEqual([1, 2]);
expect(db.hosts[0].sshPort).toBe(2022);
expect(db.hosts[0].port).toBe(2022);
expect(db.hosts[1].sshPort).toBe(2200);
expect(pluginValue(1, "fontSize")).toBe(16);
expect(pluginValue(2, "fontSize")).toBe(16);
expect(db.events).toHaveLength(2);
expect(db.notified).toEqual([["term", "fontSize", 16]]);
});
it("removes the row when a key goes back to the manifest default", async () => {
db.hosts = [
host({ id: 1, defaultOverrides: JSON.stringify({ core: [], term: [] }) }),
];
db.pluginRows.push({
pluginId: "term",
scope: "host",
scopeId: "1",
key: "fontSize",
value: "16",
encrypted: false,
});
await materializeHosts({ all: true });
expect(pluginValue(1, "fontSize")).toBeUndefined();
});
it("follows the deepest folder, and a sub-host follows its parent's", async () => {
db.folders = [
{ id: 10, userId: "u1", name: "Prod" },
{ id: 11, userId: "u1", name: "Prod / Web" },
];
const classified = JSON.stringify({ core: [], term: [] });
db.hosts = [
host({ id: 1, folder: "Prod / Web", defaultOverrides: classified }),
host({ id: 2, parentHostId: 1, defaultOverrides: classified }),
host({ id: 3, folder: "Prod", defaultOverrides: classified }),
];
setDefault("folder", "core.sshPort", 2100, { folderId: 10 });
setDefault("folder", "core.sshPort", 2200, { folderId: 11 });
await materializeHosts({ all: true });
expect(db.hosts.map((row) => row.sshPort)).toEqual([2200, 2200, 2100]);
});
it("never writes a jump chain through the host itself", async () => {
db.folders = [{ id: 10, userId: "u1", name: "Lab" }];
db.hosts = [
host({
id: 1,
folder: "Lab",
defaultOverrides: JSON.stringify({ core: [], term: [] }),
}),
];
setDefault("folder", "core.jumpHosts", [{ hostId: 1 }, { hostId: 9 }], {
folderId: 10,
});
await materializeHosts({ all: true });
expect(JSON.parse(db.hosts[0].jumpHosts as string)).toEqual([
{ hostId: 9 },
]);
});
it("previews without writing", async () => {
db.hosts = [
host({ id: 1, defaultOverrides: JSON.stringify({ core: [], term: [] }) }),
];
const result = await materializeHosts(
{ all: true },
{
dryRun: true,
overlay: {
level: "admin",
set: new Map([["core.sshPort", 2022]]),
unset: new Set(),
},
},
);
expect(result.changedHostIds).toEqual([1]);
expect(db.hosts[0].sshPort).toBe(22);
});
it("leaves a desktop's copy of a shared host alone", async () => {
db.hosts = [host({ id: 1, sharedSource: "{}" })];
setDefault("admin", "core.sshPort", 2022);
await materializeHosts({ all: true });
expect(db.hosts[0].sshPort).toBe(22);
expect(db.hosts[0].defaultOverrides).toBeNull();
});
});
@@ -0,0 +1,132 @@
import { describe, expect, it, vi } from "vitest";
vi.mock("../../../database/repositories/factory.js", () => ({}));
import {
effectiveFolderPath,
folderChainPaths,
isOwnValue,
resolveKey,
withoutSelfJump,
type HostLevels,
} from "../../../hosts/defaults/resolve.js";
import { coreCatalogEntries } from "../../../hosts/defaults/catalog.js";
const entry = (key: string) =>
coreCatalogEntries().find((info) => info.key === key)!;
function levels(partial: Partial<HostLevels> = {}): HostLevels {
return {
admin: new Map(),
user: new Map(),
folders: [],
...partial,
};
}
describe("folderChainPaths", () => {
it("lists every ancestor, root first", () => {
expect(folderChainPaths("A / B / C")).toEqual(["A", "A / B", "A / B / C"]);
expect(folderChainPaths(null)).toEqual([]);
expect(folderChainPaths("")).toEqual([]);
});
});
describe("effectiveFolderPath", () => {
it("takes a sub-host's folder from its nearest placed ancestor", () => {
const placement = new Map([
[1, { folder: "Prod", parentHostId: null }],
[2, { folder: null, parentHostId: 1 }],
[3, { folder: null, parentHostId: 2 }],
]);
expect(effectiveFolderPath(3, placement)).toBe("Prod");
expect(effectiveFolderPath(1, placement)).toBe("Prod");
});
it("stops on a parent cycle", () => {
const placement = new Map([
[1, { folder: null, parentHostId: 2 }],
[2, { folder: null, parentHostId: 1 }],
]);
expect(effectiveFolderPath(1, placement)).toBeNull();
});
});
describe("resolveKey", () => {
it("prefers the deepest folder, then the user, then the server, then built-in", () => {
const port = entry("sshPort");
expect(resolveKey(port, levels())).toEqual({
value: 22,
source: { level: "builtin" },
});
expect(
resolveKey(port, levels({ admin: new Map([["core.sshPort", 2200]]) }))
?.source.level,
).toBe("admin");
const all = levels({
admin: new Map([["core.sshPort", 2200]]),
user: new Map([["core.sshPort", 2201]]),
folders: [
{ folderId: 1, name: "A", values: new Map([["core.sshPort", 2202]]) },
{
folderId: 2,
name: "A / B",
values: new Map([["core.sshPort", 2203]]),
},
],
});
expect(resolveKey(port, all)).toEqual({
value: 2203,
source: { level: "folder", folderId: 2, folderName: "A / B" },
});
all.folders[1].values.clear();
expect(resolveKey(port, all)?.value).toBe(2202);
});
it("skips a level the key cannot be set at", () => {
const jumps = entry("jumpHosts");
const result = resolveKey(
jumps,
levels({ admin: new Map([["core.jumpHosts", [{ hostId: 5 }]]]) }),
);
expect(result?.source.level).toBe("builtin");
expect(result?.value).toEqual([]);
});
it("resolves nothing for a key with no built-in value and no level", () => {
expect(resolveKey(entry("username"), levels())).toBeUndefined();
});
});
describe("isOwnValue", () => {
it("compares normalized values", () => {
const port = entry("sshPort");
const resolved = { value: 22, source: { level: "builtin" as const } };
expect(isOwnValue(port, "22", resolved)).toBe(false);
expect(isOwnValue(port, null, resolved)).toBe(false);
expect(isOwnValue(port, 2222, resolved)).toBe(true);
});
it("treats any value as the host's own when nothing resolves", () => {
const username = entry("username");
expect(isOwnValue(username, "root", undefined)).toBe(true);
expect(isOwnValue(username, "", undefined)).toBe(false);
});
it("treats keepalive left unset as the built-in value", () => {
const keepalive = entry("keepaliveInterval");
expect(
isOwnValue(keepalive, undefined, {
value: 60,
source: { level: "builtin" },
}),
).toBe(false);
});
});
describe("withoutSelfJump", () => {
it("drops the host itself from an inherited jump chain", () => {
expect(withoutSelfJump([{ hostId: 1 }, { hostId: 2 }], 2)).toEqual([
{ hostId: 1 },
]);
});
});
@@ -0,0 +1,115 @@
import { describe, expect, it, vi } from "vitest";
import type { PluginManifest } from "@termix/plugin-sdk/manifest";
vi.mock("../../../database/repositories/factory.js", () => ({}));
vi.mock("../../../database/routes/host-plugin-settings.js", () => ({
hostSettingsPlugins: () => [],
}));
const { validateLevelChange } =
await import("../../../hosts/defaults/service.js");
const { buildCatalog } = await import("../../../hosts/defaults/catalog.js");
const catalog = buildCatalog([
{
id: "fx",
contributes: {
settings: {
host: {
fields: [
{ key: "size", type: "number", min: 8, max: 36, default: 14 },
{ key: "snippet", type: "number", defaultLevels: ["user"] },
],
},
},
},
} as unknown as PluginManifest,
]);
const context = {
actorId: "u1",
ownerId: "u1",
canUseCredential: async (id: number) => id === 7,
canUseHost: async (id: number) => id === 3,
};
describe("validateLevelChange", () => {
it("accepts known keys and normalizes their values", async () => {
const change = await validateLevelChange(
{ level: "user", userId: "u1" },
{ set: { "core.sshPort": "2022", "fx.size": "16" }, unset: ["fx.x"] },
context,
catalog,
);
expect(change.errors).toEqual({});
expect(change.set.get("core.sshPort")).toBe(2022);
expect(change.set.get("fx.size")).toBe(16);
expect([...change.unset]).toEqual(["fx.x"]);
});
it("refuses unknown keys, out of range values and levels a key cannot use", async () => {
const change = await validateLevelChange(
{ level: "admin" },
{
set: {
"core.name": "x",
"fx.size": 99,
"fx.snippet": 1,
"core.jumpHosts": [{ hostId: 3 }],
},
},
context,
catalog,
);
expect(Object.keys(change.errors).sort()).toEqual([
"core.jumpHosts",
"core.name",
"fx.size",
"fx.snippet",
]);
});
it("never takes a password or key login, or a credential at the server level", async () => {
const password = await validateLevelChange(
{ level: "user", userId: "u1" },
{ set: { "core.auth": { authType: "password" } } },
context,
catalog,
);
expect(password.errors["core.auth"]).toMatch(/cannot be a default/);
const serverCredential = await validateLevelChange(
{ level: "admin" },
{ set: { "core.auth": { authType: "credential", credentialId: 7 } } },
context,
catalog,
);
expect(serverCredential.errors["core.auth"]).toMatch(/server default/);
const ownCredential = await validateLevelChange(
{ level: "folder", folderId: 1, userId: "u1" },
{ set: { "core.auth": { authType: "credential", credentialId: 7 } } },
context,
catalog,
);
expect(ownCredential.errors).toEqual({});
const otherCredential = await validateLevelChange(
{ level: "user", userId: "u1" },
{ set: { "core.auth": { authType: "credential", credentialId: 8 } } },
context,
catalog,
);
expect(otherCredential.errors["core.auth"]).toMatch(/not available/);
});
it("checks every jump host in a chain", async () => {
const change = await validateLevelChange(
{ level: "user", userId: "u1" },
{ set: { "core.jumpHosts": [{ hostId: 3 }, { hostId: 4 }] } },
context,
catalog,
);
expect(change.errors["core.jumpHosts"]).toBeDefined();
});
});
@@ -0,0 +1,150 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
const db = vi.hoisted(() => ({
defaults: [] as Array<Record<string, unknown>>,
}));
vi.mock("../../../database/routes/host-plugin-settings.js", () => ({
hostSettingsPlugins: () => [],
}));
vi.mock("../../../database/repositories/factory.js", () => ({
createCurrentSettingsRepository: () => ({ get: async () => null }),
createCurrentHostDefaultsRepository: () => ({
listForUsers: async () => db.defaults,
listFolders: async () => [],
listHostPlacement: async () => [],
}),
}));
const { applyHostDefaultsToWrite } =
await import("../../../hosts/defaults/write-rules.js");
const { applyOverrideChange } =
await import("../../../hosts/defaults/overrides.js");
function userDefault(key: string, value: unknown) {
db.defaults.push({
level: "user",
userId: "u1",
folderId: null,
namespace: "core",
key,
value: JSON.stringify(value),
});
}
beforeEach(() => {
db.defaults = [];
});
describe("applyHostDefaultsToWrite", () => {
it("takes the editor's list as the answer and fills what it inherits", async () => {
userDefault("sshPort", 2022);
const columns: Record<string, unknown> = {
sshPort: 9999,
port: 9999,
connectionType: "ssh",
};
const overrides = await applyHostDefaultsToWrite({
ownerId: "u1",
hostId: null,
columns,
body: { defaultOverrides: { core: [], term: ["fontSize"] } },
});
expect(columns.sshPort).toBe(2022);
expect(columns.port).toBe(2022);
expect(overrides.core).toEqual([]);
expect(overrides.term).toEqual(["fontSize"]);
});
it("inherits what a create leaves out, and keeps what it sets", async () => {
userDefault("sshPort", 2022);
userDefault("statusCheckEnabled", false);
const columns: Record<string, unknown> = {
sshPort: 22,
port: 22,
statusCheckEnabled: 1,
username: "deploy",
connectionType: "ssh",
};
const overrides = await applyHostDefaultsToWrite({
ownerId: "u1",
hostId: null,
columns,
body: { ip: "10.0.0.1", port: 22, username: "deploy" },
});
expect(columns.statusCheckEnabled).toBe(false);
// The port was sent and differs from the default, so it is the host's.
expect(overrides.core).toContain("sshPort");
expect(columns.sshPort).toBe(22);
expect(overrides.core).toContain("username");
});
it("marks a changed key on an update without a list, and keeps the rest", async () => {
userDefault("sshPort", 2022);
const stored = {
id: 5,
userId: "u1",
sshPort: 2022,
port: 2022,
statusCheckEnabled: true,
username: "root",
defaultOverrides: JSON.stringify({ core: ["username"] }),
};
const columns: Record<string, unknown> = {
sshPort: 2022,
port: 2022,
statusCheckEnabled: 0,
username: "root",
};
const overrides = await applyHostDefaultsToWrite({
ownerId: "u1",
hostId: 5,
columns,
body: {},
stored: stored as never,
});
expect(overrides.core).toEqual(["statusCheckEnabled", "username"]);
});
it("keeps a locked key's state for a shared editor", async () => {
const overrides = await applyHostDefaultsToWrite({
ownerId: "u1",
hostId: 5,
columns: {},
body: { defaultOverrides: { core: [] } },
stored: {
id: 5,
userId: "u1",
defaultOverrides: JSON.stringify({ core: ["auth"] }),
} as never,
lockedKeys: ["auth"],
});
expect(overrides.core).toContain("auth");
});
});
describe("applyOverrideChange", () => {
it("adds and removes keys, per namespace", () => {
const next = applyOverrideChange(
{ core: ["sshPort"], term: ["fontSize", "theme"] },
{ own: [["core", "username"]], inherit: [["term", "theme"]] },
);
expect(next).toEqual({ core: ["sshPort", "username"], term: ["fontSize"] });
});
it("leaves a namespace nobody classified yet to its first pass", () => {
expect(applyOverrideChange({ core: [] }, { own: [["term", "x"]] })).toEqual(
{ core: [] },
);
expect(applyOverrideChange(null, { own: [["core", "x"]] })).toEqual({});
});
it("resets everything, known namespaces included", () => {
expect(
applyOverrideChange({ core: ["a"] }, { inheritAll: true }, [
"core",
"term",
]),
).toEqual({ core: [], term: [] });
});
});