Files
NetMesh/domain/syncMerge.test.ts
zhaolei 3c72efcb7f
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
[Init] Initial commit - NetMesh terminal manager
2026-09-13 18:24:01 +08:00

623 lines
16 KiB
TypeScript

import test from "node:test";
import assert from "node:assert/strict";
import { mergeSyncPayloads } from "./syncMerge.ts";
import { withSyncReliabilityMeta } from "./syncReliability.ts";
import type { SyncPayload } from "./sync.ts";
function payload(overrides: Partial<SyncPayload> = {}): SyncPayload {
return {
hosts: [],
keys: [],
identities: [],
snippets: [],
customGroups: [],
snippetPackages: [],
notes: [],
noteGroups: [],
portForwardingRules: [],
groupConfigs: [],
settings: undefined,
syncedAt: 0,
...overrides,
};
}
const knownHosts = (n: number): SyncPayload["knownHosts"] =>
Array.from({ length: n }, (_, i) => ({
id: `kh-${i}`,
hostname: `host-${i}.example.com`,
port: 22,
keyType: "ssh-ed25519",
publicKey: `SHA256:${i}`,
discoveredAt: 1,
}));
test("mergeSyncPayloads does not carry legacy known hosts forward", () => {
const result = mergeSyncPayloads(
payload({ knownHosts: knownHosts(2) }),
payload(),
payload({ knownHosts: knownHosts(3) }),
);
assert.equal("knownHosts" in result.payload, false);
});
test("mergeSyncPayloads merges reusable proxy profiles by id", () => {
const localProfile = {
id: "proxy-local",
label: "Local Proxy",
config: { type: "http", host: "local.example.com", port: 3128 },
createdAt: 1,
updatedAt: 1,
};
const remoteProfile = {
id: "proxy-remote",
label: "Remote Proxy",
config: { type: "socks5", host: "remote.example.com", port: 1080 },
createdAt: 2,
updatedAt: 2,
};
const result = mergeSyncPayloads(
payload(),
payload({ proxyProfiles: [localProfile] } as Partial<SyncPayload>),
payload({ proxyProfiles: [remoteProfile] } as Partial<SyncPayload>),
);
assert.deepEqual(result.payload.proxyProfiles?.map((item) => item.id).sort(), [
"proxy-local",
"proxy-remote",
]);
});
test("mergeSyncPayloads keeps local and remote notes", () => {
const result = mergeSyncPayloads(
payload(),
payload({
notes: [{
id: "local",
title: "Local",
content: "",
createdAt: 1,
updatedAt: 2,
}],
noteGroups: ["Local"],
}),
payload({
notes: [{
id: "remote",
title: "Remote",
content: "",
createdAt: 1,
updatedAt: 3,
}],
noteGroups: ["Remote"],
}),
);
assert.deepEqual(result.payload.notes?.map((note) => note.id).sort(), ["local", "remote"]);
assert.deepEqual(result.payload.noteGroups?.sort(), ["Local", "Remote"]);
});
test("mergeSyncPayloads preserves proxy profiles when remote payload predates them", () => {
const proxy = {
id: "proxy-1",
label: "Office Proxy",
config: { type: "http", host: "proxy.example.com", port: 3128 },
createdAt: 1,
};
const result = mergeSyncPayloads(
payload({ proxyProfiles: [proxy] } as Partial<SyncPayload>),
payload({ proxyProfiles: [proxy] } as Partial<SyncPayload>),
payload(),
);
assert.deepEqual(result.payload.proxyProfiles, [proxy]);
});
test("mergeSyncPayloads keeps missing proxy references visible to connection guards", () => {
const result = mergeSyncPayloads(
payload({
hosts: [{
id: "host-1",
label: "Host",
hostname: "example.com",
username: "root",
tags: [],
os: "linux",
proxyProfileId: "proxy-1",
}],
proxyProfiles: [{
id: "proxy-1",
label: "Old Proxy",
config: { type: "http", host: "old.example.com", port: 3128 },
createdAt: 1,
}],
groupConfigs: [{ path: "prod", proxyProfileId: "proxy-1" }],
}),
payload({
hosts: [{
id: "host-1",
label: "Host",
hostname: "example.com",
username: "root",
tags: [],
os: "linux",
proxyProfileId: "proxy-1",
}],
proxyProfiles: [],
groupConfigs: [{ path: "prod", proxyProfileId: "proxy-1" }],
}),
payload({
hosts: [{
id: "host-1",
label: "Host",
hostname: "example.com",
username: "root",
tags: [],
os: "linux",
proxyProfileId: "proxy-1",
}],
proxyProfiles: [],
groupConfigs: [{ path: "prod", proxyProfileId: "proxy-1" }],
}),
);
assert.equal(result.payload.hosts[0]?.proxyProfileId, "proxy-1");
assert.equal(result.payload.groupConfigs?.[0]?.proxyProfileId, "proxy-1");
});
test("mergeSyncPayloads honors remote deletion records when base is unavailable", () => {
const result = mergeSyncPayloads(
null,
payload({
hosts: [{
id: "host-1",
label: "Stale local copy",
hostname: "old.example.com",
username: "root",
tags: [],
os: "linux",
}],
}),
payload({
syncMeta: {
schemaVersion: 1,
generatedAt: 123,
localChanged: true,
deletions: [{
entityType: "hosts",
id: "host-1",
deletedAt: 123,
deviceId: "remote-device",
}],
changeSummary: {
hasLocalChanges: true,
hasRemoteChanges: false,
hasConflicts: false,
byEntity: {},
conflicts: [],
},
},
}),
);
assert.deepEqual(result.payload.hosts, []);
assert.equal(result.summary.deleted.remote, 1);
});
test("mergeSyncPayloads carries deletion records forward after applying a tombstone", () => {
const remote = payload({
syncMeta: {
schemaVersion: 1,
generatedAt: 123,
localChanged: true,
deletions: [{
entityType: "hosts",
id: "host-1",
deletedAt: 123,
deviceId: "remote-device",
}],
changeSummary: {
hasLocalChanges: true,
hasRemoteChanges: false,
hasConflicts: false,
byEntity: {},
conflicts: [],
},
},
});
const result = mergeSyncPayloads(
null,
payload({
hosts: [{
id: "host-1",
label: "Stale local copy",
hostname: "old.example.com",
username: "root",
tags: [],
os: "linux",
}],
}),
remote,
);
const enriched = withSyncReliabilityMeta(result.payload, null, {
deviceId: "local-device",
now: 456,
});
assert.deepEqual(enriched.syncMeta?.deletions, [{
entityType: "hosts",
id: "host-1",
deletedAt: 123,
deviceId: "remote-device",
}]);
});
test("mergeSyncPayloads adopts cloud settings on the first merge without a base", () => {
const result = mergeSyncPayloads(
null,
payload({
settings: {
theme: "dark",
terminalFontSize: 14,
terminalSettings: {
cursorBlink: true,
copyOnSelect: false,
},
},
}),
payload({
settings: {
theme: "light",
terminalFontSize: 18,
terminalSettings: {
cursorBlink: false,
copyOnSelect: true,
},
},
}),
);
assert.deepEqual(result.payload.settings, {
theme: "light",
terminalFontSize: 18,
terminalSettings: {
cursorBlink: false,
copyOnSelect: true,
},
});
});
test("mergeSyncPayloads preserves one-sided settings on the first merge", () => {
const result = mergeSyncPayloads(
null,
payload({
settings: {
customCSS: ".terminal { opacity: 0.9; }",
terminalSettings: { copyOnSelect: true },
},
}),
payload({
settings: {
theme: "system",
terminalSettings: { cursorBlink: false },
},
}),
);
assert.deepEqual(result.payload.settings, {
customCSS: ".terminal { opacity: 0.9; }",
theme: "system",
terminalSettings: {
copyOnSelect: true,
cursorBlink: false,
},
});
});
test("mergeSyncPayloads honors empty cloud setting maps as resets on the first merge", () => {
const result = mergeSyncPayloads(
null,
payload({
settings: {
customKeyBindings: {
copy: { mac: "meta+c", pc: "ctrl+c" },
},
ai: {
agentModelMap: { codex: "gpt-local" },
agentProviderMap: { codex: "openai-local" },
activeModelId: "local-model",
},
},
}),
payload({
settings: {
customKeyBindings: {},
ai: {
agentModelMap: {},
agentProviderMap: {},
activeProviderId: "cloud-provider",
},
},
}),
);
assert.deepEqual(result.payload.settings, {
customKeyBindings: {},
ai: {
agentModelMap: {},
agentProviderMap: {},
activeModelId: "local-model",
activeProviderId: "cloud-provider",
},
});
});
test("mergeSyncPayloads retains unique nested setting entries while cloud wins duplicate ids", () => {
const result = mergeSyncPayloads(
null,
payload({
settings: {
ai: {
providers: [
{ id: "shared", name: "Local shared" },
{ id: "local-only", name: "Local only" },
],
},
},
}),
payload({
settings: {
ai: {
providers: [
{ id: "shared", name: "Cloud shared" },
{ id: "cloud-only", name: "Cloud only" },
],
},
},
}),
);
assert.deepEqual(result.payload.settings?.ai?.providers, [
{ id: "shared", name: "Cloud shared" },
{ id: "cloud-only", name: "Cloud only" },
{ id: "local-only", name: "Local only" },
]);
});
test("mergeSyncPayloads keeps local settings conflict policy when a base exists", () => {
const base = payload({
settings: {
theme: "system",
terminalSettings: { cursorBlink: true },
},
});
const result = mergeSyncPayloads(
base,
payload({
settings: {
theme: "dark",
terminalSettings: { cursorBlink: false },
},
}),
payload({
settings: {
theme: "light",
terminalSettings: { cursorBlink: true },
},
}),
);
assert.deepEqual(result.payload.settings, {
theme: "dark",
terminalSettings: { cursorBlink: false },
});
});
test("mergeSyncPayloads carries plugin sidecars through without dropping remote baselines", () => {
const local: SyncPayload = {
hosts: [],
keys: [],
identities: [],
snippets: [],
customGroups: [],
syncedAt: 1,
pluginSidecars: {
version: 1,
entries: [{
pluginId: "com.local.plugin",
kind: "settings",
key: "com.local.plugin.theme\0application\0application",
value: "dark",
updatedAt: 5,
}],
},
};
const remote: SyncPayload = {
hosts: [],
keys: [],
identities: [],
snippets: [],
customGroups: [],
syncedAt: 2,
pluginSidecars: {
version: 1,
entries: [{
pluginId: "com.remote.plugin",
kind: "account_baseline",
key: "account",
value: { id: "acct-r" },
updatedAt: 9,
}],
},
};
const result = mergeSyncPayloads(null, local, remote);
assert.ok(result.payload.pluginSidecars);
assert.equal(result.payload.pluginSidecars!.entries.length, 2);
assert.ok(result.payload.pluginSidecars!.entries.some((e) => e.pluginId === "com.local.plugin"));
assert.ok(result.payload.pluginSidecars!.entries.some((e) => e.kind === "account_baseline"));
});
test("mergeSyncPayloads propagates local sidecar setting resets via three-way merge", () => {
const entry = {
pluginId: "com.example.p",
kind: "settings" as const,
key: "com.example.p.theme\0application\0application",
value: "dark",
updatedAt: 1,
};
const base: SyncPayload = {
hosts: [], keys: [], identities: [], snippets: [], customGroups: [],
syncedAt: 1,
pluginSidecars: { version: 1, entries: [entry] },
};
const local: SyncPayload = {
hosts: [], keys: [], identities: [], snippets: [], customGroups: [],
syncedAt: 2,
// user reset: explicit empty sidecar bundle (omitted field = legacy/unsupported)
pluginSidecars: { version: 1, entries: [] },
};
const remote: SyncPayload = {
hosts: [], keys: [], identities: [], snippets: [], customGroups: [],
syncedAt: 2,
pluginSidecars: { version: 1, entries: [entry] },
};
const result = mergeSyncPayloads(base, local, remote);
assert.equal(
result.payload.pluginSidecars?.entries?.some((e) => e.key === entry.key) ?? false,
false,
"local reset must not be resurrected from base/remote",
);
assert.ok(
Object.prototype.hasOwnProperty.call(result.payload, "pluginSidecars"),
"explicit empty sidecar field must remain so apply clears remote/local DB",
);
assert.deepEqual(result.payload.pluginSidecars, { version: 1, entries: [] });
});
test("mergeSyncPayloads preserves sidecars when remote legacy payload omits the field", () => {
const entry = {
pluginId: "com.example.p",
kind: "settings" as const,
key: "com.example.p.theme\0application\0application",
value: "dark",
updatedAt: 1,
};
const base: SyncPayload = {
hosts: [], keys: [], identities: [], snippets: [], customGroups: [],
syncedAt: 1,
pluginSidecars: { version: 1, entries: [entry] },
};
const local: SyncPayload = {
hosts: [], keys: [], identities: [], snippets: [], customGroups: [],
syncedAt: 2,
pluginSidecars: { version: 1, entries: [entry] },
};
const remote: SyncPayload = {
hosts: [], keys: [], identities: [], snippets: [], customGroups: [],
syncedAt: 2,
// legacy remote omits pluginSidecars entirely
};
const result = mergeSyncPayloads(base, local, remote);
assert.ok(result.payload.pluginSidecars?.entries?.some((e) => e.key === entry.key));
});
test("mergeSyncPayloads treats missing optional arrays as legacy payloads, not deletions", () => {
const identity = {
id: "identity-1",
label: "Prod",
username: "root",
authMethod: "password" as const,
created: 1,
};
const rule = {
id: "rule-1",
name: "Web",
hostId: "host-1",
type: "local" as const,
localHost: "127.0.0.1",
localPort: 8080,
remoteHost: "127.0.0.1",
remotePort: 80,
enabled: true,
createdAt: 1,
};
const base = payload({
identities: [identity],
snippetPackages: ["ops"],
portForwardingRules: [rule],
groupConfigs: [{ path: "prod", username: "root" }],
});
const local = payload({
identities: [identity],
snippetPackages: ["ops"],
portForwardingRules: [rule],
groupConfigs: [{ path: "prod", username: "root" }],
});
const remote = payload();
delete remote.identities;
delete remote.snippetPackages;
delete remote.portForwardingRules;
delete remote.groupConfigs;
const result = mergeSyncPayloads(base, local, remote);
assert.deepEqual(result.payload.identities, [identity]);
assert.deepEqual(result.payload.snippetPackages, ["ops"]);
assert.deepEqual(result.payload.portForwardingRules, [rule]);
assert.deepEqual(result.payload.groupConfigs, [{ path: "prod", username: "root" }]);
});
const hostWithTelemetry = (lastConnectedAt?: number) => ({
id: "host-1",
label: "prod",
hostname: "prod.example.com",
username: "root",
tags: [],
os: "linux" as const,
protocol: "ssh" as const,
...(lastConnectedAt === undefined ? {} : { lastConnectedAt }),
});
test("mergeSyncPayloads normalizes lastConnectedAt across base and remote so sanitized local does not shadow remote edits", () => {
// Legacy base + old-device remote still carry the legacy telemetry field;
// the upgraded local payload strips it. A remote edit to the same host
// must not be classified as a local-modified conflict and overwritten.
const base = payload({
hosts: [hostWithTelemetry(1_000)],
});
// Local is otherwise unchanged — it only lost the telemetry field to the
// new sanitize-on-build behavior.
const local = payload({
hosts: [hostWithTelemetry()],
});
const remote = payload({
hosts: [{ ...hostWithTelemetry(1_000), label: "prod-renamed-by-remote" }],
});
const result = mergeSyncPayloads(base, local, remote);
// Remote edit survives: the telemetry-only local delta must not classify
// this host as locally modified and shadow the remote rename.
assert.equal(result.payload.hosts[0]?.label, "prod-renamed-by-remote");
assert.equal(result.summary.modified.local, 0);
assert.equal(result.summary.modified.remote, 1);
});
test("mergeSyncPayloads keeps host telemetry off merged payloads", () => {
const base = payload({ hosts: [hostWithTelemetry(1_000)] });
const unchanged = mergeSyncPayloads(
base,
payload({ hosts: [hostWithTelemetry(2_000)] }),
payload({ hosts: [hostWithTelemetry(1_000)] }),
);
assert.equal(unchanged.payload.hosts[0]?.lastConnectedAt, undefined);
assert.equal(unchanged.summary.modified.local, 0);
});