mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-29 14:45:24 +00:00
feat: rebuild desktop sync as linked server accounts, fix packaged plugin bundles and UI scaling
This commit is contained in:
1 parent
7c02daccf4
commit
6cd9a2846d
242 files changed
+39127
-8558
No files matched your search
@@ -44,7 +44,6 @@ const UNGATED: Record<string, string> = {
|
||||
"events.emit": "namespace check: plugin.<id>.* only without events:core",
|
||||
"events.on": "namespace check: plugin.* only without events:core",
|
||||
"sync.registerEntity": "adds the plugin's own table to remote sync",
|
||||
"sync.recordTombstone": "records a delete of the plugin's own row",
|
||||
"registry.provide": "the plugin's own registry key",
|
||||
"registry.consume": "reads what another plugin chose to publish",
|
||||
"registry.revoke": "removes the plugin's own registry key",
|
||||
|
||||
@@ -9,8 +9,7 @@ import {
|
||||
resetSyncRegistry,
|
||||
unregisterByOwner,
|
||||
} from "../../plugins/sync-registry.js";
|
||||
import { registerCoreSyncEntities } from "../../database/routes/sync-entities.js";
|
||||
import { SYNCED_ENTITY_TYPES } from "../../../../electron/remote-sync-entities.cjs";
|
||||
import { registerCoreSyncEntities } from "../../sync/entities.js";
|
||||
|
||||
beforeEach(() => {
|
||||
resetSyncRegistry();
|
||||
@@ -91,35 +90,47 @@ describe("sync registry", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("entity aliases", () => {
|
||||
it("finds an entity by a name core uses for it", () => {
|
||||
registerEntity("demo", {
|
||||
type: "snippets",
|
||||
table: {},
|
||||
answersTo: ["commandSnippet"],
|
||||
});
|
||||
|
||||
expect(getEntity("commandSnippet")?.type).toBe("snippets");
|
||||
expect(getEntity("snippets")?.type).toBe("snippets");
|
||||
expect(getEntity("nothing")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("keeps core-only options away from plugins", () => {
|
||||
registerEntity(
|
||||
"demo",
|
||||
{ type: "thing", table: {} },
|
||||
{ readOnly: true, load: async () => [] },
|
||||
);
|
||||
|
||||
expect(getEntity("thing")?.readOnly).toBeUndefined();
|
||||
expect(getEntity("thing")?.load).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("core sync entities", () => {
|
||||
beforeEach(() => {
|
||||
registerCoreSyncEntities();
|
||||
});
|
||||
|
||||
// The frozen array also lists networkTopology, snippets/snippetFolders,
|
||||
// dashboardServiceLinks/homepageItems and vaultProfiles, now registered by
|
||||
// the network-topology, snippets, homepage and vault plugins rather than core.
|
||||
const PLUGIN_OWNED_TYPES = new Set([
|
||||
"networkTopology",
|
||||
"snippets",
|
||||
"snippetFolders",
|
||||
"dashboardServiceLinks",
|
||||
"homepageItems",
|
||||
"vaultProfiles",
|
||||
]);
|
||||
|
||||
it("registers every core-owned entity the Electron client knows about", () => {
|
||||
const coreTypes = new Set(listEntityTypes());
|
||||
for (const type of SYNCED_ENTITY_TYPES) {
|
||||
if (PLUGIN_OWNED_TYPES.has(type)) continue;
|
||||
expect(coreTypes.has(type), type).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps the dependency order the frozen Electron array encodes", () => {
|
||||
expect(listEntityTypes()).toEqual(
|
||||
[...SYNCED_ENTITY_TYPES].filter((type) => !PLUGIN_OWNED_TYPES.has(type)),
|
||||
);
|
||||
it("registers what core syncs, under the wire names 2.8 used", () => {
|
||||
expect(listEntityTypes()).toEqual([
|
||||
"accountProfile",
|
||||
"sshCredentials",
|
||||
"sharedCredentials",
|
||||
"sshFolders",
|
||||
"hosts",
|
||||
"sharedHosts",
|
||||
"userPreferences",
|
||||
"pluginUserSettings",
|
||||
]);
|
||||
});
|
||||
|
||||
it("owns all of them as core", () => {
|
||||
@@ -128,15 +139,18 @@ describe("core sync entities", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("sorts every reference target before the entity that points at it", () => {
|
||||
it("sorts every column reference target before the entity that points at it", () => {
|
||||
const order = new Map(
|
||||
listEntities().map((entity) => [entity.type, entity.order]),
|
||||
);
|
||||
|
||||
for (const entity of listEntities()) {
|
||||
for (const reference of entity.references ?? []) {
|
||||
// A self-reference is ordered within the entity by orderSyncRows.
|
||||
// Rows pointing at themselves are ordered within the entity, and a
|
||||
// JSON reference that arrives first is written again once the
|
||||
// target exists.
|
||||
if (reference.entityType === entity.type) continue;
|
||||
if (reference.field.includes(".")) continue;
|
||||
expect(
|
||||
order.get(reference.entityType),
|
||||
`${entity.type} -> ${reference.entityType}`,
|
||||
@@ -145,21 +159,51 @@ describe("core sync entities", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("marks the one singleton and nothing else", () => {
|
||||
it("marks the singletons and nothing else", () => {
|
||||
const singletons = listEntities()
|
||||
.filter((entity) => entity.singleton)
|
||||
.map((entity) => entity.type);
|
||||
|
||||
expect(singletons.sort()).toEqual(["userPreferences"]);
|
||||
expect(singletons.sort()).toEqual(["accountProfile", "userPreferences"]);
|
||||
});
|
||||
|
||||
it("keeps the read-only fields that must survive a sync payload", () => {
|
||||
expect(getEntity("hosts")?.readOnlyFields).toEqual(["connectionOrigin"]);
|
||||
it("only sends shared copies and the account one way", () => {
|
||||
const readOnly = listEntities()
|
||||
.filter((entity) => entity.readOnly)
|
||||
.map((entity) => entity.type);
|
||||
|
||||
expect(readOnly.sort()).toEqual([
|
||||
"accountProfile",
|
||||
"sharedCredentials",
|
||||
"sharedHosts",
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps device-only host fields off the wire", () => {
|
||||
expect(getEntity("hosts")?.readOnlyFields).toEqual(
|
||||
expect.arrayContaining(["connectionOrigin", "localOnly", "sharedSource"]),
|
||||
);
|
||||
expect(getEntity("userPreferences")?.readOnlyFields).toEqual([
|
||||
"storageMode",
|
||||
]);
|
||||
});
|
||||
|
||||
it("leaves local-only and shared rows out of what a device sends", () => {
|
||||
const hosts = getEntity("hosts")!;
|
||||
expect(hosts.shouldSync?.({ localOnly: true })).toBe(false);
|
||||
expect(hosts.shouldSync?.({ sharedSource: "{}" })).toBe(false);
|
||||
expect(hosts.shouldSync?.({ localOnly: false, sharedSource: null })).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it("encrypts host and credential secrets", () => {
|
||||
expect(getEntity("hosts")?.encryptedFields).toContain("password");
|
||||
expect(getEntity("sshCredentials")?.encryptedFields).toContain(
|
||||
"privateKey",
|
||||
);
|
||||
});
|
||||
|
||||
it("is idempotent, because several modules prime it", () => {
|
||||
const before = listEntityTypes();
|
||||
registerCoreSyncEntities();
|
||||
|
||||
@@ -592,8 +592,7 @@ function upgradeChecks(current: () => Booted, bootIndex: number) {
|
||||
|
||||
it("syncs every entity type 2.8 synced, under the same name", async () => {
|
||||
const { listEntityTypes } = await import("../../plugins/sync-registry.js");
|
||||
const { registerCoreSyncEntities } =
|
||||
await import("../../database/routes/sync-entities.js");
|
||||
const { registerCoreSyncEntities } = await import("../../sync/entities.js");
|
||||
registerCoreSyncEntities();
|
||||
const known = new Set(listEntityTypes());
|
||||
for (const type of SYNCED_ENTITY_TYPES_28) {
|
||||
|
||||
Reference in new issue
Block a user