import test from "node:test"; import assert from "node:assert/strict"; import { EncryptionService } from "../EncryptionService.ts"; import { clearProviderMergeStateImpl, commitRemoteInspectionImpl, } from "./authMethods.ts"; import { selectConvergentSyncToProviderResult, syncToProviderImpl, uploadToProviderImpl, } from "./providerSyncMethods.ts"; import { clearSyncBaseImpl, loadSyncSnapshotsImpl, saveSyncBaseImpl, syncAllProvidersImpl, } from "./syncAllStorageMethods.ts"; import type { CloudProvider, SyncedFile, SyncPayload, SyncResult, } from "../../../domain/sync.ts"; import { setConvergentSyncLocalConfig } from "../convergentSyncConfig.ts"; function payload(hostId: string): SyncPayload { return payloadWithHosts([hostId]); } function payloadWithHosts(hostIds: string[]): SyncPayload { return { hosts: hostIds.map((hostId) => ({ id: hostId, label: hostId, hostname: `${hostId}.example.com`, port: 22, username: "root", tags: [], os: "linux", })), keys: [], identities: [], proxyProfiles: [], snippets: [], customGroups: [], snippetPackages: [], portForwardingRules: [], groupConfigs: [], settings: undefined, syncedAt: 0, }; } function remoteFile(provider: CloudProvider, version: number, updatedAt: number): SyncedFile { return { meta: { version, updatedAt, deviceId: `${provider}-device`, deviceName: provider, appVersion: "0.0.0", iv: "", salt: "", algorithm: "AES-256-GCM", kdf: "PBKDF2", kdfIterations: 1, }, payload: provider, }; } test("provider identity changes clear v1 base, v2 baseline, and remote anchor together", () => { const removed: string[] = []; const manager = { syncBaseKey: (provider: CloudProvider) => `base:${provider}`, convergentProviderBaselineKey: (provider: CloudProvider) => `convergent:${provider}`, removeFromStorage: (key: string) => removed.push(key), clearSyncAnchor: (provider: CloudProvider) => removed.push(`anchor:${provider}`), }; clearProviderMergeStateImpl.call(manager, "github"); assert.deepEqual(removed, ["base:github", "convergent:github", "anchor:github"]); }); test("clearing all merge bases also removes every convergent provider baseline", () => { const removed = new Set(); const providers: CloudProvider[] = ["github", "google", "onedrive", "webdav", "s3"]; const manager = { removeFromStorage: (key: string) => removed.add(key), syncBaseKey: (provider?: CloudProvider) => `base:${provider ?? "default"}`, syncSnapshotsKey: (provider?: CloudProvider) => `snapshots:${provider ?? "default"}`, convergentProviderBaselineKey: (provider: CloudProvider) => `convergent:${provider}`, clearSyncAnchor: () => {}, }; clearSyncBaseImpl.call(manager); for (const provider of providers) { assert.equal(removed.has(`convergent:${provider}`), true); } }); test("syncAllProviders uses the newest cloud payload without merging other remotes when cloud wins", async () => { const originalDecryptPayload = EncryptionService.decryptPayload; const originalEncryptPayload = EncryptionService.encryptPayload; const githubRemote = remoteFile("github", 3, 300); const googleRemote = remoteFile("google", 2, 200); const githubPayload = payload("github-winner"); const localPayload = payload("local"); const uploaded: Array<{ provider: CloudProvider; payload: SyncPayload }> = []; const committed: CloudProvider[] = []; EncryptionService.decryptPayload = async (file: SyncedFile) => { if (file === githubRemote) return githubPayload; return payload("google-loser"); }; EncryptionService.encryptPayload = async (outgoing: SyncPayload) => ({ ...remoteFile("github", 4, 400), payload: JSON.stringify(outgoing), }); try { const manager = { masterPassword: "pw", adapters: new Map(), state: { securityState: "UNLOCKED", providers: { github: { enabled: true, connected: true, status: "connected" }, google: { enabled: true, connected: true, status: "connected" }, onedrive: { enabled: false, connected: false, status: "disconnected" }, webdav: { enabled: false, connected: false, status: "disconnected" }, s3: { enabled: false, connected: false, status: "disconnected" }, }, lastError: null, syncState: "IDLE", syncStrategy: "preferCloud", localVersion: 1, deviceId: "local-device", deviceName: "Local", }, getConnectedAdapter: async (provider: CloudProvider) => ({ provider }), updateProviderStatus: () => {}, emit: () => {}, checkProviderConflict: async (provider: CloudProvider) => ({ conflict: true, remoteFile: provider === "github" ? githubRemote : googleRemote, }), loadSyncBase: async () => payload("base"), commitRemoteInspection: async (provider: CloudProvider) => { committed.push(provider); }, uploadToProvider: async (provider: CloudProvider, _adapter: unknown, _file: SyncedFile, outgoing: SyncPayload) => { uploaded.push({ provider, payload: outgoing }); return { success: true, provider, action: "upload" as const, version: 4 }; }, exitBlockedState: () => {}, notifyStateChange: () => {}, }; const results = await syncAllProvidersImpl.call(manager, localPayload); assert.equal(results.get("github")?.action, "download"); assert.deepEqual(results.get("github")?.mergedPayload, githubPayload); assert.equal(results.get("github")?.remoteFile, githubRemote); assert.equal(uploaded.length, 1); assert.equal(uploaded[0].provider, "google"); assert.equal(uploaded[0].payload.hosts[0]?.id, "github-winner"); assert.equal(uploaded[0].payload.syncMeta?.schemaVersion, 1); assert.deepEqual(committed, []); } finally { EncryptionService.decryptPayload = originalDecryptPayload; EncryptionService.encryptPayload = originalEncryptPayload; } }); test("syncToProvider uses the checked remote as metadata base when no stored base exists", async () => { const originalDecryptPayload = EncryptionService.decryptPayload; const originalEncryptPayload = EncryptionService.encryptPayload; const checkedRemote = remoteFile("github", 3, 300); const remotePayload = payloadWithHosts(["kept", "deleted-on-local"]); const localPayload = payload("kept"); let uploadedPayload: SyncPayload | undefined; EncryptionService.decryptPayload = async (file: SyncedFile) => { assert.equal(file, checkedRemote); return remotePayload; }; EncryptionService.encryptPayload = async (outgoing: SyncPayload) => ({ ...remoteFile("github", 4, 400), payload: JSON.stringify(outgoing), }); try { const manager = { masterPassword: "pw", adapters: new Map(), providerDecryptSeq: { github: 0 }, state: { securityState: "UNLOCKED", providers: { github: { enabled: true, connected: true, status: "connected" }, }, lastError: null, syncState: "IDLE", syncStrategy: "smartMerge", localVersion: 1, deviceId: "local-device", deviceName: "Local", }, getConnectedAdapter: async () => ({ provider: "github" }), updateProviderStatus: () => {}, emit: () => {}, checkProviderConflict: async () => ({ conflict: false, remoteFile: checkedRemote }), loadSyncBase: async () => null, uploadToProvider: async (provider: CloudProvider, _adapter: unknown, _file: SyncedFile, outgoing: SyncPayload) => { uploadedPayload = outgoing; return { success: true, provider, action: "upload" as const, version: 4 }; }, exitBlockedState: () => {}, }; const result = await syncToProviderImpl.call(manager, "github", localPayload); assert.equal(result.success, true); assert.deepEqual(uploadedPayload?.syncMeta?.deletions, [{ entityType: "hosts", id: "deleted-on-local", deletedAt: uploadedPayload?.syncMeta?.generatedAt, deviceId: "local-device", }]); } finally { EncryptionService.decryptPayload = originalDecryptPayload; EncryptionService.encryptPayload = originalEncryptPayload; } }); test("syncToProvider refuses to downgrade a checked convergent remote", async () => { const checkedRemote = remoteFile("github", 3, 300); checkedRemote.meta.syncSchemaVersion = 2; let encrypted = false; const originalEncryptPayload = EncryptionService.encryptPayload; EncryptionService.encryptPayload = async () => { encrypted = true; return checkedRemote; }; try { const manager = { masterPassword: "pw", adapters: new Map(), state: { securityState: "UNLOCKED", providers: { github: { status: "connected" } }, lastError: null, syncState: "IDLE", syncStrategy: "smartMerge", localVersion: 1, deviceId: "local-device", deviceName: "Local", }, getConnectedAdapter: async () => ({ provider: "github" }), updateProviderStatus: () => {}, emit: () => {}, checkProviderConflict: async () => ({ conflict: false, remoteFile: checkedRemote }), addSyncHistoryEntry: () => {}, }; const result = await syncToProviderImpl.call(manager, "github", payload("local")); assert.equal(result.success, false); assert.equal(encrypted, false); assert.match(result.error ?? "", /Enable or migrate convergent sync/); } finally { EncryptionService.encryptPayload = originalEncryptPayload; } }); test("syncToProvider aborts an upload when the master key changes after encryption", async () => { const originalEncryptPayload = EncryptionService.encryptPayload; const localPayload = payload("local"); let generation = 0; let uploaded = false; EncryptionService.encryptPayload = async (outgoing: SyncPayload) => { generation += 1; return { ...remoteFile("github", 2, 200), payload: JSON.stringify(outgoing), }; }; try { const manager = { masterPassword: "old-master-password", adapters: new Map(), state: { securityState: "UNLOCKED", providers: { github: { enabled: true, connected: true, status: "connected" }, }, lastError: null, syncState: "IDLE", syncStrategy: "smartMerge", localVersion: 1, deviceId: "local-device", deviceName: "Local", }, getSyncSecurityGeneration: () => 0, assertSyncSecurityGeneration: (expected: number) => { if (generation !== expected) { throw new Error("Sync cancelled because master key changed"); } }, getConnectedAdapter: async () => ({ provider: "github" }), updateProviderStatus: () => {}, emit: () => {}, checkProviderConflict: async () => ({ conflict: false }), loadSyncBase: async () => null, uploadToProvider: async (provider: CloudProvider) => { uploaded = true; return { success: true, provider, action: "upload" as const, version: 2 }; }, exitBlockedState: () => {}, addSyncHistoryEntry: () => {}, }; const result = await syncToProviderImpl.call(manager, "github", localPayload); assert.equal(uploaded, false); assert.equal(result.success, false); assert.match(result.error ?? "", /master key changed/); } finally { EncryptionService.encryptPayload = originalEncryptPayload; } }); test("uploadToProvider skips local commits when the master key changes during upload", async () => { let generation = 0; let savedAnchor = false; let savedBase = false; let savedProvider = false; const file = remoteFile("github", 2, 200); const manager = { providerDecryptSeq: { github: 0 }, state: { providers: { github: { enabled: true, connected: true, status: "syncing" }, }, lastError: null, syncState: "SYNCING", localVersion: 1, localUpdatedAt: 100, remoteVersion: 1, remoteUpdatedAt: 100, deviceName: "Local", }, assertSyncSecurityGeneration: (expected: number) => { if (generation !== expected) { throw new Error("Sync cancelled because master key changed"); } }, saveSyncConfig: () => {}, saveSyncBase: async () => { savedBase = true; }, saveSyncAnchor: async () => { savedAnchor = true; }, saveProviderConnection: async () => { savedProvider = true; }, notifyStateChange: () => {}, addSyncHistoryEntry: () => {}, updateProviderStatus: () => {}, emit: () => {}, }; const adapter = { upload: async () => { generation += 1; return "resource-id"; }, }; const result = await uploadToProviderImpl.call( manager, "github", adapter, file, payload("local"), 0, ); assert.equal(result.success, false); assert.match(result.error ?? "", /master key changed/); assert.equal(savedBase, false); assert.equal(savedAnchor, false); assert.equal(savedProvider, false); }); test("syncAllProviders uses the checked remote as metadata base when provider base is missing", async () => { const originalDecryptPayload = EncryptionService.decryptPayload; const originalEncryptPayload = EncryptionService.encryptPayload; const checkedRemote = remoteFile("github", 3, 300); const remotePayload = payloadWithHosts(["kept", "deleted-on-local"]); const localPayload = payload("kept"); let uploadedPayload: SyncPayload | undefined; EncryptionService.decryptPayload = async (file: SyncedFile) => { assert.equal(file, checkedRemote); return remotePayload; }; EncryptionService.encryptPayload = async (outgoing: SyncPayload) => ({ ...remoteFile("github", 4, 400), payload: JSON.stringify(outgoing), }); try { const manager = { masterPassword: "pw", adapters: new Map(), state: { securityState: "UNLOCKED", providers: { github: { enabled: true, connected: true, status: "connected" }, google: { enabled: false, connected: false, status: "disconnected" }, onedrive: { enabled: false, connected: false, status: "disconnected" }, webdav: { enabled: false, connected: false, status: "disconnected" }, s3: { enabled: false, connected: false, status: "disconnected" }, }, lastError: null, syncState: "IDLE", syncStrategy: "smartMerge", localVersion: 1, deviceId: "local-device", deviceName: "Local", }, getConnectedAdapter: async () => ({ provider: "github" }), updateProviderStatus: () => {}, emit: () => {}, checkProviderConflict: async () => ({ conflict: false, remoteFile: checkedRemote }), loadSyncBase: async () => null, uploadToProvider: async (provider: CloudProvider, _adapter: unknown, _file: SyncedFile, outgoing: SyncPayload) => { uploadedPayload = outgoing; return { success: true, provider, action: "upload" as const, version: 4 }; }, exitBlockedState: () => {}, notifyStateChange: () => {}, }; const results = await syncAllProvidersImpl.call(manager, localPayload); assert.equal(results.get("github")?.success, true); assert.deepEqual(uploadedPayload?.syncMeta?.deletions, [{ entityType: "hosts", id: "deleted-on-local", deletedAt: uploadedPayload?.syncMeta?.generatedAt, deviceId: "local-device", }]); } finally { EncryptionService.decryptPayload = originalDecryptPayload; EncryptionService.encryptPayload = originalEncryptPayload; } }); test("commitRemoteInspection saves the comparison base before advancing the remote anchor", async () => { const calls: string[] = []; const file = remoteFile("github", 5, 500); const incoming = payload("cloud"); const manager = { providerDecryptSeq: { github: 0 }, state: { providers: { github: { resourceId: "old", lastSync: 0, lastSyncVersion: 0 }, }, localVersion: 0, localUpdatedAt: 0, remoteVersion: 0, remoteUpdatedAt: 0, }, getConnectedAdapter: async () => ({ resourceId: "remote-resource" }), saveSyncConfig: () => calls.push("config"), saveSyncBase: async () => calls.push("base"), saveSyncAnchor: async () => calls.push("anchor"), saveProviderConnection: async () => calls.push("connection"), addSyncHistoryEntry: () => calls.push("history"), notifyStateChange: () => calls.push("notify"), }; await commitRemoteInspectionImpl.call(manager, "github", file, incoming, { recordDownload: true, }); assert.deepEqual(calls, ["base", "config", "anchor", "connection", "history", "notify"]); }); test("commitRemoteInspection does not advance the remote anchor when saving the base fails", async () => { const calls: string[] = []; const manager = { providerDecryptSeq: { github: 0 }, state: { providers: { github: { resourceId: "remote-resource", lastSync: 0, lastSyncVersion: 0 }, }, localVersion: 0, localUpdatedAt: 0, remoteVersion: 0, remoteUpdatedAt: 0, }, getConnectedAdapter: async () => ({ resourceId: "remote-resource" }), saveSyncConfig: () => calls.push("config"), saveSyncBase: async () => { calls.push("base"); throw new Error("base failed"); }, saveSyncAnchor: async () => calls.push("anchor"), saveProviderConnection: async () => calls.push("connection"), addSyncHistoryEntry: () => calls.push("history"), notifyStateChange: () => calls.push("notify"), }; await assert.rejects( () => commitRemoteInspectionImpl.call(manager, "github", remoteFile("github", 5, 500), payload("cloud")), /base failed/, ); assert.deepEqual(calls, ["base"]); }); test("saveSyncBase reports storage failures so callers do not advance anchors", async () => { const originalWarn = console.warn; const manager = { state: { unlockedKey: { derivedKey: await crypto.subtle.generateKey( { name: "AES-GCM", length: 256 }, true, ["encrypt", "decrypt"], ), }, }, syncBaseKey: () => "sync-base", saveToStorage: () => { throw new Error("storage full"); }, }; console.warn = () => {}; try { await assert.rejects( () => saveSyncBaseImpl.call(manager, payload("cloud"), "github"), /storage full/, ); } finally { console.warn = originalWarn; } }); test("saveSyncBase reports a missing local encryption key", async () => { const manager = { state: { unlockedKey: null }, syncBaseKey: () => "sync-base", saveToStorage: () => {}, }; await assert.rejects( () => saveSyncBaseImpl.call(manager, payload("cloud"), "github"), /Sync base encryption key is unavailable/, ); }); test("saveSyncBase keeps a bounded encrypted snapshot history before replacing the base", async () => { const stored = new Map(); const key = await crypto.subtle.generateKey( { name: "AES-GCM", length: 256 }, true, ["encrypt", "decrypt"], ); const manager = { state: { unlockedKey: { derivedKey: key } }, syncBaseKey: (provider?: CloudProvider) => `base-${provider ?? "default"}`, syncSnapshotsKey: (provider?: CloudProvider) => `snapshots-${provider ?? "default"}`, saveToStorage: (storageKey: string, value: string) => stored.set(storageKey, value), loadFromStorage: (storageKey: string) => stored.get(storageKey), }; await saveSyncBaseImpl.call(manager, payload("base-0"), "github"); for (let i = 1; i <= 7; i += 1) { await saveSyncBaseImpl.call(manager, payload(`base-${i}`), "github"); } const snapshots = await loadSyncSnapshotsImpl.call(manager, "github"); assert.equal(snapshots.length, 5); assert.deepEqual( snapshots.map((snapshot) => snapshot.payload.hosts[0]?.id), ["base-6", "base-5", "base-4", "base-3", "base-2"], ); }); test("syncAllProviders builds provider-specific sync metadata from each provider base", async () => { const originalEncryptPayload = EncryptionService.encryptPayload; const uploaded: Array<{ provider: CloudProvider; payload: SyncPayload }> = []; const baseByProvider = { github: payload("shared"), google: payload("deleted-on-local"), } as Partial>; const localPayload = payload("shared"); EncryptionService.encryptPayload = async (outgoing: SyncPayload) => ({ ...remoteFile("github", 4, 400), payload: JSON.stringify(outgoing), }); try { const manager = { masterPassword: "pw", adapters: new Map(), state: { securityState: "UNLOCKED", providers: { github: { enabled: true, connected: true, status: "connected" }, google: { enabled: true, connected: true, status: "connected" }, onedrive: { enabled: false, connected: false, status: "disconnected" }, webdav: { enabled: false, connected: false, status: "disconnected" }, s3: { enabled: false, connected: false, status: "disconnected" }, }, lastError: null, syncState: "IDLE", syncStrategy: "smartMerge", localVersion: 1, deviceId: "local-device", deviceName: "Local", }, getConnectedAdapter: async (provider: CloudProvider) => ({ provider }), updateProviderStatus: () => {}, emit: () => {}, checkProviderConflict: async () => ({ conflict: false, remoteFile: null }), loadSyncBase: async (provider: CloudProvider) => baseByProvider[provider] ?? null, uploadToProvider: async (provider: CloudProvider, _adapter: unknown, _file: SyncedFile, outgoing: SyncPayload) => { uploaded.push({ provider, payload: outgoing }); return { success: true, provider, action: "upload" as const, version: 4 }; }, exitBlockedState: () => {}, notifyStateChange: () => {}, }; await syncAllProvidersImpl.call(manager, localPayload); assert.equal(uploaded.length, 2); assert.deepEqual(uploaded.find((entry) => entry.provider === "github")?.payload.syncMeta?.deletions, []); assert.deepEqual(uploaded.find((entry) => entry.provider === "google")?.payload.syncMeta?.deletions, [{ entityType: "hosts", id: "deleted-on-local", deletedAt: uploaded.find((entry) => entry.provider === "google")?.payload.syncMeta?.generatedAt, deviceId: "local-device", }]); } finally { EncryptionService.encryptPayload = originalEncryptPayload; } }); test("syncAllProviders upload-local override overwrites remote without decrypting when password differs", async () => { const originalDecryptPayload = EncryptionService.decryptPayload; const originalEncryptPayload = EncryptionService.encryptPayload; const checkedRemote = remoteFile("github", 5, 500); const localPayload = payload("local-after-reinstall"); const uploaded: Array<{ provider: CloudProvider; payload: SyncPayload }> = []; const encryptBaseVersions: number[] = []; let decryptCalls = 0; EncryptionService.decryptPayload = async () => { decryptCalls += 1; throw new Error("OperationError: unable to authenticate data"); }; EncryptionService.encryptPayload = async ( outgoing: SyncPayload, _password: string, _deviceId: string, _deviceName: string, _appVersion: string, existingVersion?: number, ) => { encryptBaseVersions.push(existingVersion ?? 0); return { ...remoteFile("github", (existingVersion ?? 0) + 1, 600), payload: JSON.stringify(outgoing), }; }; try { const manager = { masterPassword: "new-master-password", adapters: new Map(), state: { securityState: "UNLOCKED", providers: { github: { enabled: true, connected: true, status: "connected" }, google: { enabled: false, connected: false, status: "disconnected" }, onedrive: { enabled: false, connected: false, status: "disconnected" }, webdav: { enabled: false, connected: false, status: "disconnected" }, s3: { enabled: false, connected: false, status: "disconnected" }, }, lastError: null, syncState: "IDLE", syncStrategy: "smartMerge", localVersion: 1, deviceId: "local-device", deviceName: "Local", }, getConnectedAdapter: async (provider: CloudProvider) => ({ provider }), updateProviderStatus: () => {}, emit: () => {}, checkProviderConflict: async () => ({ conflict: true, remoteFile: checkedRemote }), loadSyncBase: async () => null, uploadToProvider: async (provider: CloudProvider, _adapter: unknown, _file: SyncedFile, outgoing: SyncPayload) => { uploaded.push({ provider, payload: outgoing }); return { success: true, provider, action: "upload" as const, version: 6 }; }, exitBlockedState: () => {}, notifyStateChange: () => {}, }; const conflicted = await syncAllProvidersImpl.call(manager, localPayload); assert.equal(conflicted.get("github")?.success, false); assert.equal(conflicted.get("github")?.conflictDetected, true); assert.equal(conflicted.get("github")?.error, undefined); assert.equal(uploaded.length, 0); const forced = await syncAllProvidersImpl.call(manager, localPayload, { conflictActionOverride: "upload-local", overrideShrink: true, }); assert.equal(forced.get("github")?.success, true); assert.equal(forced.get("github")?.action, "upload"); assert.equal(uploaded.length, 1); assert.equal(uploaded[0]?.payload.hosts[0]?.id, "local-after-reinstall"); // Keep-local under smartMerge must base on the conflicting remote version // (v5 → encrypted as v6), matching single-provider upload-local. assert.deepEqual(encryptBaseVersions, [5]); // Shrink-guard may attempt decrypt and ignore failure; merge path must not run. assert.ok(decryptCalls >= 1); } finally { EncryptionService.decryptPayload = originalDecryptPayload; EncryptionService.encryptPayload = originalEncryptPayload; } }); test("an initialized but paused v2 replica cannot fall through to legacy provider writes", async () => { const originalStorage = Object.getOwnPropertyDescriptor(globalThis, "localStorage"); const values = new Map(); Object.defineProperty(globalThis, "localStorage", { configurable: true, value: { getItem: (key: string) => values.get(key) ?? null, setItem: (key: string, value: string) => values.set(key, value), removeItem: (key: string) => values.delete(key), }, }); try { setConvergentSyncLocalConfig({ enabled: false, initialized: true }); let adapterRequested = false; const manager = { state: { providers: { github: { provider: "github", status: "connected" }, }, }, getConnectedAdapter: async () => { adapterRequested = true; throw new Error("legacy path must not run"); }, }; const all = await syncAllProvidersImpl.call(manager, payload("local")); const one = await syncToProviderImpl.call(manager, "github", payload("local")); assert.equal(all.get("github")?.success, false); assert.match(all.get("github")?.error ?? "", /paused/i); assert.equal(one.success, false); assert.match(one.error ?? "", /paused/i); assert.equal(adapterRequested, false); } finally { if (originalStorage) Object.defineProperty(globalThis, "localStorage", originalStorage); else Reflect.deleteProperty(globalThis, "localStorage"); } }); test("syncToProvider preserves a merged payload discovered by a non-target provider", () => { const mergedPayload = payload("remote-merged"); const results = new Map([ ["github", { success: false, provider: "github", action: "none", error: "github unavailable", }], ["google", { success: true, provider: "google", action: "merge", mergedPayload, }], ]); const selected = selectConvergentSyncToProviderResult("github", results); assert.equal(selected.success, false); assert.equal(selected.provider, "github"); assert.equal(selected.error, "github unavailable"); assert.equal(selected.mergedPayload, mergedPayload); assert.equal(selected.remoteFile, undefined); }); test("syncAllProviders smart-merge strips device-bound enc:v1 secrets before upload", async () => { const originalDecryptPayload = EncryptionService.decryptPayload; const originalEncryptPayload = EncryptionService.encryptPayload; const completeBlob = Buffer.alloc(31, 0); Buffer.from("v10", "utf8").copy(completeBlob, 0); const ENC = `enc:v1:${completeBlob.toString("base64")}`; const checkedRemote = remoteFile("github", 5, 500); const localPayload = { ...payload("shared"), hosts: [{ ...payload("shared").hosts[0]!, password: "kept-secret", }], }; const remotePoisoned: SyncPayload = { ...payload("shared"), hosts: [ { ...payload("shared").hosts[0]!, label: "remote-label", password: ENC, }, ], }; const uploaded: SyncPayload[] = []; EncryptionService.decryptPayload = async () => remotePoisoned; EncryptionService.encryptPayload = async (outgoing: SyncPayload) => ({ ...remoteFile("github", 6, 600), payload: JSON.stringify(outgoing), }); try { const manager = { masterPassword: "pw", adapters: new Map(), state: { securityState: "UNLOCKED", providers: { github: { enabled: true, connected: true, status: "connected" }, google: { enabled: false, connected: false, status: "disconnected" }, onedrive: { enabled: false, connected: false, status: "disconnected" }, webdav: { enabled: false, connected: false, status: "disconnected" }, s3: { enabled: false, connected: false, status: "disconnected" }, }, lastError: null, syncState: "IDLE", syncStrategy: "smartMerge", localVersion: 1, deviceId: "local-device", deviceName: "Local", }, getConnectedAdapter: async (provider: CloudProvider) => ({ provider }), updateProviderStatus: () => {}, emit: () => {}, checkProviderConflict: async () => ({ conflict: true, remoteFile: checkedRemote }), loadSyncBase: async () => ({ ...payload("shared"), hosts: [{ ...payload("shared").hosts[0]!, password: "kept-secret", }], }), uploadToProvider: async ( _provider: CloudProvider, _adapter: unknown, _file: SyncedFile, outgoing: SyncPayload, ) => { uploaded.push(outgoing); return { success: true, provider: "github" as const, action: "upload" as const, version: 6 }; }, exitBlockedState: () => {}, notifyStateChange: () => {}, }; const results = await syncAllProvidersImpl.call(manager, localPayload); assert.equal(results.get("github")?.success, true); assert.equal(uploaded.length, 1); const sharedHost = uploaded[0]?.hosts.find((host) => host.id === "shared"); assert.ok(sharedHost); // Local/base usable secret must survive; remote non-secret edits can still apply. assert.equal(sharedHost?.password, "kept-secret"); assert.equal(sharedHost?.label, "remote-label"); } finally { EncryptionService.decryptPayload = originalDecryptPayload; EncryptionService.encryptPayload = originalEncryptPayload; } });