[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:
622
domain/syncMerge.test.ts
Normal file
622
domain/syncMerge.test.ts
Normal file
@@ -0,0 +1,622 @@
|
||||
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);
|
||||
});
|
||||
Reference in New Issue
Block a user