Files
NetMesh/infrastructure/services/cloudSync/convergentSyncRuntimeMethods.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

1456 lines
54 KiB
TypeScript

import test from 'node:test';
import assert from 'node:assert/strict';
import type {
CloudProvider,
ConvergentProviderBaselineV2,
ConvergentSyncStateV2,
SyncPayload,
SyncedFile,
} from '../../../domain/sync.ts';
import { ONEDRIVE_REAUTH_REQUIRED_MARKER } from '../../../domain/sync.ts';
import {
applyLegacySyncPayload,
cloudSyncPayloadsEqual,
createConvergentSyncStateFromPayload,
materializeConvergentSyncState,
materializeSyncPayloadFromConvergentState,
mergeConvergentSyncStates,
validateConvergentSyncPayload,
withConvergentSyncEnvelope,
} from '../../../domain/convergentSync/index.ts';
import { EncryptionService } from '../EncryptionService.ts';
import type { CloudAdapter } from '../adapters/index.ts';
import {
downgradeConvergentSyncImpl,
previewConvergentRecoveryImpl,
resolveConvergentConflictAndSyncImpl,
syncAllProvidersConvergentlyImpl,
syncConvergentProvidersUnlockedImpl,
} from './convergentSyncRuntimeMethods.ts';
const NOW = 1_700_000_000_000;
function payload(label: string, username = 'root'): SyncPayload {
return {
hosts: [{
id: 'host-1',
label,
hostname: 'example.com',
port: 22,
username,
tags: [],
os: 'linux',
}],
keys: [],
identities: [],
proxyProfiles: [],
snippets: [],
customGroups: [],
snippetPackages: [],
notes: [],
noteGroups: [],
portForwardingRules: [],
groupConfigs: [],
syncedAt: NOW,
};
}
function payloadWithHostCount(count: number): SyncPayload {
return {
...payload('base'),
hosts: Array.from({ length: count }, (_, index) => ({
id: `host-${index}`,
label: `Host ${index}`,
hostname: `host-${index}.example.com`,
port: 22,
username: 'root',
tags: [],
os: 'linux' as const,
})),
};
}
function remoteState(
base: ConvergentSyncStateV2,
before: SyncPayload,
after: SyncPayload,
deviceId: string,
now: number,
): ConvergentSyncStateV2 {
return applyLegacySyncPayload(base, before, after, deviceId, now);
}
const applyAndCommit = async (
_payload: SyncPayload,
commitReplica: () => Promise<void>,
): Promise<void> => commitReplica();
interface MemoryAdapter extends CloudAdapter {
uploads: number;
remote: SyncedFile | null;
failUpload?: boolean;
failDownload?: boolean;
downloadError?: Error;
afterUpload?: (file: SyncedFile, adapter: MemoryAdapter) => void;
}
function adapter(initial: SyncedFile | null): MemoryAdapter {
const result: MemoryAdapter = {
isAuthenticated: true,
accountInfo: null,
resourceId: null,
uploads: 0,
remote: initial,
signOut: () => {},
initializeSync: async () => null,
upload: async (file) => {
if (result.failUpload) throw new Error('provider upload unavailable');
result.uploads += 1;
result.remote = file;
result.afterUpload?.(file, result);
return `resource-${result.uploads}`;
},
download: async () => {
if (result.failDownload) throw result.downloadError ?? new Error('provider unavailable');
return result.remote;
},
deleteSync: async () => {},
getTokens: () => null,
};
return result;
}
function manager(
replica: ConvergentSyncStateV2,
adapters: Partial<Record<CloudProvider, MemoryAdapter>>,
baselines: Partial<Record<CloudProvider, ConvergentProviderBaselineV2>> = {},
) {
const persisted: ConvergentSyncStateV2[] = [];
let currentReplica = replica;
const events: unknown[] = [];
const completedDowngrades: boolean[] = [];
const legacyCommits: Array<{
provider: CloudProvider;
remoteFile: SyncedFile;
payload: SyncPayload;
}> = [];
const providerConnections = Object.fromEntries(
(['github', 'google', 'onedrive', 'webdav', 's3'] as CloudProvider[]).map((provider) => [
provider,
adapters[provider]
? { provider, status: 'connected' as const }
: { provider, status: 'disconnected' as const },
]),
) as Record<CloudProvider, { provider: CloudProvider; status: 'connected' | 'disconnected'; resourceId?: string }>;
return {
masterPassword: 'pw',
state: {
securityState: 'UNLOCKED',
syncState: 'IDLE',
providers: providerConnections,
deviceId: 'local-device',
deviceName: 'Local device',
syncStrategy: 'smartMerge',
localVersion: 0,
localUpdatedAt: 0,
remoteVersion: 0,
remoteUpdatedAt: 0,
currentConflict: null,
lastError: null,
pendingLocalSync: false,
convergentConflicts: materializeConvergentSyncState(replica).conflicts,
syncHistory: [],
lastShrinkFinding: undefined,
},
persisted,
events,
completedDowngrades,
legacyCommits,
getConnectedAdapter: async (provider: CloudProvider) => adapters[provider],
loadConvergentReplica: async () => ({ schemaVersion: 2 as const, state: currentReplica, updatedAt: NOW }),
saveConvergentReplica: async (record: { state: ConvergentSyncStateV2 }) => {
currentReplica = record.state;
persisted.push(record.state);
},
loadConvergentProviderBaseline: async (provider: CloudProvider) => baselines[provider] ?? null,
saveConvergentProviderBaseline: async (baseline: ConvergentProviderBaselineV2) => {
baselines[baseline.provider] = baseline;
},
commitRemoteInspection: async (
provider: CloudProvider,
remoteFile: SyncedFile,
incoming: SyncPayload,
) => {
legacyCommits.push({ provider, remoteFile, payload: incoming });
},
completeConvergentSyncDowngrade: (confirmed: boolean) => {
completedDowngrades.push(confirmed);
},
updateProviderStatus(provider: CloudProvider, status: 'connected' | 'syncing' | 'error', error?: string) {
this.state.providers[provider] = { ...this.state.providers[provider], status, ...(error ? { error } : {}) };
},
emit: (event: unknown) => events.push(event),
notifyStateChange: () => {},
addSyncHistoryEntry: () => {},
saveProviderConnection: async () => {},
saveSyncConfig: () => {},
exitBlockedState: () => {},
};
}
function installEncryptionDouble() {
const originalEncrypt = EncryptionService.encryptPayload;
const originalDecrypt = EncryptionService.decryptPayload;
const payloads = new Map<string, SyncPayload>();
let sequence = 0;
const register = (value: SyncPayload, version = 1, deviceId = 'remote'): SyncedFile => {
const ciphertext = `cipher-${sequence += 1}`;
payloads.set(ciphertext, value);
return {
meta: {
version,
updatedAt: NOW + sequence,
deviceId,
deviceName: deviceId,
appVersion: 'test',
iv: '',
salt: '',
algorithm: 'AES-256-GCM',
kdf: 'PBKDF2',
kdfIterations: 1,
...(value.convergentSync ? { syncSchemaVersion: 2 as const } : {}),
},
payload: ciphertext,
};
};
EncryptionService.encryptPayload = async (value, _password, deviceId, _deviceName, _version, existingVersion) =>
register(value, (existingVersion ?? 0) + 1, deviceId);
EncryptionService.decryptPayload = async (file) => {
const value = payloads.get(file.payload);
if (!value) throw new Error('unknown test ciphertext');
return value;
};
return {
register,
payloads,
restore: () => {
EncryptionService.encryptPayload = originalEncrypt;
EncryptionService.decryptPayload = originalDecrypt;
},
};
}
test('unordered provider join preserves independent offline edits and persists before upload', async () => {
const encryption = installEncryptionDouble();
try {
const basePayload = payload('base');
const base = createConvergentSyncStateFromPayload(basePayload, 'seed', NOW);
const githubState = remoteState(base, basePayload, payload('github-label'), 'github-device', NOW + 1);
const googleState = remoteState(base, basePayload, payload('base', 'ubuntu'), 'google-device', NOW + 2);
const github = adapter(encryption.register(withConvergentSyncEnvelope(githubState, { syncedAt: NOW }), 2, 'github-device'));
const google = adapter(encryption.register(withConvergentSyncEnvelope(googleState, { syncedAt: NOW }), 2, 'google-device'));
const subject = manager(base, { github, google });
let persistedBeforeUpload = false;
const originalUpload = github.upload.bind(github);
github.upload = async (file) => {
persistedBeforeUpload = subject.persisted.length > 0;
return originalUpload(file);
};
const results = await syncConvergentProvidersUnlockedImpl.call(
subject,
basePayload,
{ jitter: async () => {}, now: () => NOW + 10, applyPayload: applyAndCommit },
);
assert.equal(persistedBeforeUpload, true);
assert.equal(results.get('github')?.success, true);
assert.equal(results.get('google')?.success, true);
assert.equal(results.get('github')?.mergedPayloadApplied, true);
const merged = results.get('github')?.mergedPayload;
assert.equal(merged?.hosts[0]?.label, 'github-label');
assert.equal(merged?.hosts[0]?.username, 'ubuntu');
assert.equal(subject.state.convergentConflicts.length, 0);
} finally {
encryption.restore();
}
});
test('a failed protected merged apply leaves the durable replica aligned with local data', async () => {
const encryption = installEncryptionDouble();
try {
const localPayload = payload('local');
const base = createConvergentSyncStateFromPayload(localPayload, 'seed', NOW);
const remote = remoteState(
base,
localPayload,
payload('local', 'remote-user'),
'remote-device',
NOW + 1,
);
const github = adapter(encryption.register(
withConvergentSyncEnvelope(remote, { syncedAt: NOW + 1 }),
2,
'remote-device',
));
const subject = manager(base, { github });
await assert.rejects(
() => syncConvergentProvidersUnlockedImpl.call(
subject,
localPayload,
{
jitter: async () => {},
now: () => NOW + 10,
applyPayload: async () => {
throw new Error('protective merged apply failed');
},
},
),
/protective merged apply failed/,
);
const durable = materializeSyncPayloadFromConvergentState(
subject.persisted.at(-1)!,
{ syncedAt: NOW },
);
assert.equal(durable.hosts[0]?.username, 'root');
assert.equal(github.uploads, 0);
assert.equal(subject.state.syncState, 'ERROR');
assert.equal(subject.state.pendingLocalSync, true);
} finally {
encryption.restore();
}
});
test('an unchanged convergent check verifies without uploading or returning a payload apply', async () => {
const encryption = installEncryptionDouble();
try {
const localPayload = payload('unchanged');
const base = createConvergentSyncStateFromPayload(localPayload, 'seed', NOW);
const github = adapter(encryption.register(
withConvergentSyncEnvelope(base, { syncedAt: NOW }),
3,
'github-device',
));
const subject = manager(base, { github });
const results = await syncConvergentProvidersUnlockedImpl.call(
subject,
localPayload,
{ jitter: async () => {}, now: () => NOW + 10 },
);
assert.equal(results.get('github')?.success, true);
assert.equal(results.get('github')?.mergedPayload, undefined);
assert.equal(github.uploads, 0);
assert.equal(github.remote?.meta.version, 3);
} finally {
encryption.restore();
}
});
test('recovery preview joins providers without persisting or uploading state', async () => {
const encryption = installEncryptionDouble();
const originalNavigator = Object.getOwnPropertyDescriptor(globalThis, 'navigator');
try {
Object.defineProperty(globalThis, 'navigator', {
configurable: true,
value: {
locks: {
request: async (_name: string, _options: unknown, callback: (lock: object) => unknown) => callback({}),
},
},
});
const basePayload = payload('base');
const base = createConvergentSyncStateFromPayload(basePayload, 'seed', NOW);
const githubState = remoteState(base, basePayload, payload('remote-label'), 'github-device', NOW + 1);
const googleState = remoteState(base, basePayload, payload('base', 'remote-user'), 'google-device', NOW + 2);
const github = adapter(encryption.register(withConvergentSyncEnvelope(githubState, { syncedAt: NOW }), 2));
const google = adapter(encryption.register(withConvergentSyncEnvelope(googleState, { syncedAt: NOW }), 2));
const subject = manager(base, { github, google });
const preview = await previewConvergentRecoveryImpl.call(subject);
assert.equal(preview?.hosts[0]?.label, 'remote-label');
assert.equal(preview?.hosts[0]?.username, 'remote-user');
assert.equal(subject.persisted.length, 0);
assert.equal(github.uploads, 0);
assert.equal(google.uploads, 0);
assert.equal(subject.events.length, 0);
} finally {
if (originalNavigator) Object.defineProperty(globalThis, 'navigator', originalNavigator);
else Reflect.deleteProperty(globalThis, 'navigator');
encryption.restore();
}
});
test('a concurrent provider write discovered during verification is rejoined and propagated', async () => {
const encryption = installEncryptionDouble();
try {
const basePayload = payload('base');
const base = createConvergentSyncStateFromPayload(basePayload, 'seed', NOW);
const github = adapter(encryption.register(withConvergentSyncEnvelope(base, { syncedAt: NOW }), 1, 'github-device'));
const google = adapter(encryption.register(withConvergentSyncEnvelope(base, { syncedAt: NOW }), 1, 'google-device'));
let injected = false;
github.afterUpload = (file, target) => {
if (injected) return;
injected = true;
const outgoing = encryption.payloads.get(file.payload)!;
const outgoingState = validateConvergentSyncPayload(file.meta, outgoing)!;
const outgoingMaterialized = materializeSyncPayloadFromConvergentState(outgoingState, { syncedAt: NOW });
const concurrent = remoteState(
outgoingState,
outgoingMaterialized,
payload('provider-race'),
'racing-device',
NOW + 20,
);
target.remote = encryption.register(
withConvergentSyncEnvelope(concurrent, { syncedAt: NOW + 20 }),
file.meta.version + 1,
'racing-device',
);
};
const subject = manager(base, { github, google });
const results = await syncConvergentProvidersUnlockedImpl.call(
subject,
payload('local-write'),
{ jitter: async () => {}, now: () => NOW + 30, applyPayload: applyAndCommit },
);
assert.equal(results.get('github')?.success, true);
assert.equal(results.get('google')?.success, true);
assert.equal(results.get('google')?.mergedPayload?.hosts[0]?.label, 'provider-race');
assert.ok(google.uploads >= 2, 'the concurrent write should trigger another propagation round');
const githubPayload = await EncryptionService.decryptPayload(github.remote!, 'pw');
const googlePayload = await EncryptionService.decryptPayload(google.remote!, 'pw');
const githubFinal = validateConvergentSyncPayload(github.remote!.meta, githubPayload)!;
const googleFinal = validateConvergentSyncPayload(google.remote!.meta, googlePayload)!;
assert.equal(
cloudSyncPayloadsEqual(
materializeSyncPayloadFromConvergentState(githubFinal, { syncedAt: NOW }),
materializeSyncPayloadFromConvergentState(googleFinal, { syncedAt: NOW }),
),
true,
);
} finally {
encryption.restore();
}
});
test('a failed provider does not roll back a provider that verified successfully', async () => {
const encryption = installEncryptionDouble();
try {
const localPayload = payload('local');
const base = createConvergentSyncStateFromPayload(localPayload, 'seed', NOW);
const github = adapter(encryption.register(withConvergentSyncEnvelope(base, { syncedAt: NOW }), 1));
const google = adapter(null);
google.failDownload = true;
const subject = manager(base, { github, google });
const results = await syncConvergentProvidersUnlockedImpl.call(
subject,
localPayload,
{ jitter: async () => {}, now: () => NOW + 10 },
);
assert.equal(results.get('github')?.success, true);
assert.equal(results.get('google')?.success, false);
assert.equal(subject.state.syncState, 'IDLE');
assert.equal(subject.state.pendingLocalSync, true);
} finally {
encryption.restore();
}
});
test('total upload failure keeps downloaded remote edits out of the durable local replica', async () => {
const encryption = installEncryptionDouble();
try {
const basePayload = payload('base');
const localPayload = payload('local-write');
const base = createConvergentSyncStateFromPayload(basePayload, 'seed', NOW);
const remote = remoteState(
base,
basePayload,
payload('base', 'remote-user'),
'remote-device',
NOW + 1,
);
const github = adapter(encryption.register(
withConvergentSyncEnvelope(remote, { syncedAt: NOW + 1 }),
2,
'remote-device',
));
github.failUpload = true;
const subject = manager(base, { github });
const failed = await syncConvergentProvidersUnlockedImpl.call(
subject,
localPayload,
{ maxRounds: 1, jitter: async () => {}, now: () => NOW + 10 },
);
assert.equal(failed.get('github')?.success, false);
assert.equal(
materializeSyncPayloadFromConvergentState(subject.persisted.at(-1)!, { syncedAt: NOW })
.hosts[0]?.username,
'root',
);
assert.equal(
materializeSyncPayloadFromConvergentState(subject.persisted.at(-1)!, { syncedAt: NOW })
.hosts[0]?.label,
'local-write',
);
github.failUpload = false;
subject.state.providers.github.status = 'connected';
const retried = await syncConvergentProvidersUnlockedImpl.call(
subject,
localPayload,
{
maxRounds: 1,
jitter: async () => {},
now: () => NOW + 20,
applyPayload: applyAndCommit,
},
);
assert.equal(retried.get('github')?.success, true);
assert.equal(retried.get('github')?.mergedPayload?.hosts[0]?.username, 'remote-user');
} finally {
encryption.restore();
}
});
test('convergent provider failures delegate expired OneDrive credentials to reauth handling', async () => {
const encryption = installEncryptionDouble();
try {
const localPayload = payload('local');
const base = createConvergentSyncStateFromPayload(localPayload, 'seed', NOW);
const onedrive = adapter(null);
onedrive.failDownload = true;
onedrive.downloadError = new Error(
`${ONEDRIVE_REAUTH_REQUIRED_MARKER}: OneDrive session expired, please reconnect.`,
);
const subject = manager(base, { onedrive });
subject.state.providers.onedrive = {
...subject.state.providers.onedrive,
tokens: {
accessToken: 'expired-access',
refreshToken: 'expired-refresh',
tokenType: 'Bearer',
},
} as typeof subject.state.providers.onedrive;
let reauthCalls = 0;
(subject as typeof subject & {
handleProviderReauthRequired: (provider: CloudProvider, error: unknown) => boolean;
}).handleProviderReauthRequired = (provider, error) => {
if (
provider !== 'onedrive'
|| !String(error).includes(ONEDRIVE_REAUTH_REQUIRED_MARKER)
) return false;
reauthCalls += 1;
subject.state.providers.onedrive = {
provider: 'onedrive',
status: 'error',
error: 'OneDrive session expired, please reconnect.',
};
return true;
};
const results = await syncConvergentProvidersUnlockedImpl.call(
subject,
localPayload,
{ maxRounds: 1, jitter: async () => {}, now: () => NOW + 10 },
);
assert.equal(results.get('onedrive')?.success, false);
assert.equal(reauthCalls, 1);
assert.equal('tokens' in subject.state.providers.onedrive, false);
assert.equal(subject.state.providers.onedrive.status, 'error');
assert.equal(
subject.state.providers.onedrive.error,
'OneDrive session expired, please reconnect.',
);
} finally {
encryption.restore();
}
});
test('legacy cloud writes without a trusted provider baseline fail closed', async () => {
const encryption = installEncryptionDouble();
try {
const localPayload = payload('local');
const base = createConvergentSyncStateFromPayload(localPayload, 'seed', NOW);
const github = adapter(encryption.register(payload('legacy'), 2, 'old-device'));
const subject = manager(base, { github });
const results = await syncConvergentProvidersUnlockedImpl.call(
subject,
localPayload,
{ maxRounds: 1, jitter: async () => {}, now: () => NOW + 10 },
);
assert.equal(results.get('github')?.success, false);
assert.match(results.get('github')?.error ?? '', /no trusted convergent baseline/i);
assert.equal(github.uploads, 0);
} finally {
encryption.restore();
}
});
test('a migration-seeded v1 baseline allows the first sync to publish v2', async () => {
const encryption = installEncryptionDouble();
try {
const legacyPayload = payload('legacy');
const replica = createConvergentSyncStateFromPayload(legacyPayload, 'local-device', NOW);
const github = adapter(encryption.register(legacyPayload, 7, 'legacy-device'));
const baseline: ConvergentProviderBaselineV2 = {
schemaVersion: 2,
provider: 'github',
remoteVersion: 7,
remoteUpdatedAt: NOW,
remoteDeviceId: 'legacy-device',
materializedPayload: legacyPayload,
state: replica,
};
const subject = manager(replica, { github }, { github: baseline });
const results = await syncConvergentProvidersUnlockedImpl.call(
subject,
legacyPayload,
{ jitter: async () => {}, now: () => NOW + 10 },
);
assert.equal(results.get('github')?.success, true);
assert.equal(github.uploads > 0, true);
assert.equal(github.remote?.meta.syncSchemaVersion, 2);
const uploaded = await EncryptionService.decryptPayload(github.remote!, 'pw');
assert.ok(validateConvergentSyncPayload(github.remote!.meta, uploaded));
} finally {
encryption.restore();
}
});
test('provider order cannot change the joined materialized result', async () => {
const encryption = installEncryptionDouble();
try {
const basePayload = payload('base');
const base = createConvergentSyncStateFromPayload(basePayload, 'seed', NOW);
const left = remoteState(base, basePayload, payload('left'), 'left', NOW + 1);
const right = remoteState(base, basePayload, payload('right'), 'right', NOW + 2);
const joinedA = mergeConvergentSyncStates(left, right);
const joinedB = mergeConvergentSyncStates(right, left);
assert.deepEqual(joinedA, joinedB);
} finally {
encryption.restore();
}
});
test('preferCloud adopts the joined remote replica without creating a local conflict', async () => {
const encryption = installEncryptionDouble();
try {
const basePayload = payload('base');
const base = createConvergentSyncStateFromPayload(basePayload, 'seed', NOW);
const remote = remoteState(base, basePayload, payload('cloud'), 'cloud-device', NOW + 1);
const github = adapter(encryption.register(withConvergentSyncEnvelope(remote, { syncedAt: NOW }), 2));
const subject = manager(base, { github });
subject.state.syncStrategy = 'preferCloud';
const results = await syncConvergentProvidersUnlockedImpl.call(
subject,
payload('unsaved-local'),
{ jitter: async () => {}, now: () => NOW + 10, applyPayload: applyAndCommit },
);
assert.equal(results.get('github')?.mergedPayload?.hosts[0]?.label, 'cloud');
assert.equal(results.get('github')?.convergentConflictCount, 0);
} finally {
encryption.restore();
}
});
test('preferLocal creates a causal write that dominates a remote edit', async () => {
const encryption = installEncryptionDouble();
try {
const basePayload = payload('base');
const base = createConvergentSyncStateFromPayload(basePayload, 'seed', NOW);
const remote = remoteState(base, basePayload, payload('cloud'), 'cloud-device', NOW + 1);
const github = adapter(encryption.register(withConvergentSyncEnvelope(remote, { syncedAt: NOW }), 2));
const subject = manager(base, { github });
subject.state.syncStrategy = 'preferLocal';
const results = await syncConvergentProvidersUnlockedImpl.call(
subject,
payload('local-wins'),
{ jitter: async () => {}, now: () => NOW + 10 },
);
assert.equal(results.get('github')?.mergedPayload, undefined);
assert.equal(results.get('github')?.convergentConflictCount, 0);
} finally {
encryption.restore();
}
});
test('a trusted baseline converts an old-client v1 write into deterministic causal writes', async () => {
const encryption = installEncryptionDouble();
try {
const basePayload = payload('base');
const base = createConvergentSyncStateFromPayload(basePayload, 'seed', NOW);
const github = adapter(encryption.register(payload('legacy-edit'), 2, 'old-device'));
const baseline: ConvergentProviderBaselineV2 = {
schemaVersion: 2,
provider: 'github',
remoteVersion: 1,
remoteUpdatedAt: NOW,
remoteDeviceId: 'old-device',
materializedPayload: basePayload,
state: base,
};
const subject = manager(base, { github }, { github: baseline });
const results = await syncConvergentProvidersUnlockedImpl.call(
subject,
basePayload,
{ jitter: async () => {}, now: () => NOW + 10, applyPayload: applyAndCommit },
);
assert.equal(results.get('github')?.success, true);
assert.equal(results.get('github')?.mergedPayload?.hosts[0]?.label, 'legacy-edit');
} finally {
encryption.restore();
}
});
test('the production entry fails closed when another window owns the Web Lock', async () => {
const encryption = installEncryptionDouble();
const originalNavigator = Object.getOwnPropertyDescriptor(globalThis, 'navigator');
try {
Object.defineProperty(globalThis, 'navigator', {
configurable: true,
value: {
locks: {
request: async (_name: string, _options: unknown, callback: (lock: null) => unknown) => callback(null),
},
},
});
const localPayload = payload('local');
const base = createConvergentSyncStateFromPayload(localPayload, 'seed', NOW);
const github = adapter(encryption.register(withConvergentSyncEnvelope(base, { syncedAt: NOW }), 1));
const subject = manager(base, { github });
const results = await syncAllProvidersConvergentlyImpl.call(subject, localPayload);
assert.equal(results.get('github')?.success, false);
assert.match(results.get('github')?.error ?? '', /already running in another window/i);
assert.equal(github.uploads, 0);
} finally {
if (originalNavigator) Object.defineProperty(globalThis, 'navigator', originalNavigator);
else Reflect.deleteProperty(globalThis, 'navigator');
encryption.restore();
}
});
test('explicit downgrade replaces v2 only after a verified legacy write', async () => {
const encryption = installEncryptionDouble();
const originalNavigator = Object.getOwnPropertyDescriptor(globalThis, 'navigator');
let lockHeld = false;
try {
Object.defineProperty(globalThis, 'navigator', {
configurable: true,
value: {
locks: {
request: async (
_name: string,
_options: unknown,
callback: (lock: object) => unknown,
) => {
lockHeld = true;
try {
return await callback({});
} finally {
lockHeld = false;
}
},
},
},
});
const localPayload = payload('local');
const base = createConvergentSyncStateFromPayload(localPayload, 'seed', NOW);
const github = adapter(encryption.register(withConvergentSyncEnvelope(base, { syncedAt: NOW }), 2));
const subject = manager(base, { github });
const completeDowngrade = subject.completeConvergentSyncDowngrade;
subject.completeConvergentSyncDowngrade = (confirmed: boolean) => {
assert.equal(lockHeld, true);
assert.equal(subject.legacyCommits.length, 1);
completeDowngrade(confirmed);
};
let appliedPayload: SyncPayload | null = null;
const results = await downgradeConvergentSyncImpl.call(
subject,
true,
async () => localPayload,
async (incoming: SyncPayload, commitReplica: () => Promise<void>) => {
appliedPayload = incoming;
await commitReplica();
},
);
assert.equal(results.get('github')?.success, true);
assert.equal(appliedPayload?.hosts[0]?.label, 'local');
assert.deepEqual(subject.completedDowngrades, [true]);
assert.equal(github.remote?.meta.syncSchemaVersion, undefined);
const verified = await EncryptionService.decryptPayload(github.remote!, 'pw');
assert.equal(verified.convergentSync, undefined);
assert.equal(verified.hosts[0]?.label, 'local');
assert.equal(subject.legacyCommits.length, 1);
assert.equal(subject.legacyCommits[0]?.provider, 'github');
assert.equal(subject.legacyCommits[0]?.remoteFile, github.remote);
assert.equal(subject.legacyCommits[0]?.payload.hosts[0]?.label, 'local');
} finally {
if (originalNavigator) Object.defineProperty(globalThis, 'navigator', originalNavigator);
else Reflect.deleteProperty(globalThis, 'navigator');
encryption.restore();
}
});
test('downgrade reports cleanup failure instead of releasing a successful result', async () => {
const encryption = installEncryptionDouble();
const originalNavigator = Object.getOwnPropertyDescriptor(globalThis, 'navigator');
try {
Object.defineProperty(globalThis, 'navigator', {
configurable: true,
value: {
locks: {
request: async (_name: string, _options: unknown, callback: (lock: object) => unknown) => callback({}),
},
},
});
const localPayload = payload('local');
const base = createConvergentSyncStateFromPayload(localPayload, 'seed', NOW);
const github = adapter(encryption.register(withConvergentSyncEnvelope(base, { syncedAt: NOW }), 2));
const subject = manager(base, { github });
subject.completeConvergentSyncDowngrade = () => {
throw new Error('local cleanup failed');
};
const results = await downgradeConvergentSyncImpl.call(
subject,
true,
async () => localPayload,
async (_incoming: SyncPayload, commitReplica: () => Promise<void>) => {
await commitReplica();
},
);
assert.equal(results.get('github')?.success, false);
assert.match(results.get('github')?.error ?? '', /local cleanup failed/);
assert.equal(github.uploads, 1);
assert.equal(subject.state.syncState, 'ERROR');
assert.equal(subject.state.pendingLocalSync, true);
} finally {
if (originalNavigator) Object.defineProperty(globalThis, 'navigator', originalNavigator);
else Reflect.deleteProperty(globalThis, 'navigator');
encryption.restore();
}
});
test('downgrade finalizes a local-only convergent replica without providers', async () => {
const originalNavigator = Object.getOwnPropertyDescriptor(globalThis, 'navigator');
try {
Object.defineProperty(globalThis, 'navigator', {
configurable: true,
value: {
locks: {
request: async (_name: string, _options: unknown, callback: (lock: object) => unknown) => callback({}),
},
},
});
const basePayload = payload('base');
const base = createConvergentSyncStateFromPayload(basePayload, 'seed', NOW);
const pausedLocalPayload = payload('paused-local');
const subject = manager(base, {});
let appliedPayload: SyncPayload | null = null;
const results = await downgradeConvergentSyncImpl.call(
subject,
true,
async () => pausedLocalPayload,
async (incoming: SyncPayload, commitReplica: () => Promise<void>) => {
appliedPayload = incoming;
await commitReplica();
},
);
assert.equal(results.size, 0);
assert.equal(appliedPayload?.hosts[0]?.label, 'paused-local');
assert.deepEqual(subject.completedDowngrades, [true]);
assert.equal(subject.state.syncState, 'IDLE');
assert.equal(subject.state.pendingLocalSync, false);
} finally {
if (originalNavigator) Object.defineProperty(globalThis, 'navigator', originalNavigator);
else Reflect.deleteProperty(globalThis, 'navigator');
}
});
test('local-only downgrade reports cleanup failures instead of returning an empty success', async () => {
const originalNavigator = Object.getOwnPropertyDescriptor(globalThis, 'navigator');
try {
Object.defineProperty(globalThis, 'navigator', {
configurable: true,
value: {
locks: {
request: async (_name: string, _options: unknown, callback: (lock: object) => unknown) => callback({}),
},
},
});
const localPayload = payload('local');
const base = createConvergentSyncStateFromPayload(localPayload, 'seed', NOW);
const subject = manager(base, {});
subject.completeConvergentSyncDowngrade = () => {
throw new Error('local cleanup failed');
};
await assert.rejects(
() => downgradeConvergentSyncImpl.call(
subject,
true,
async () => localPayload,
async (_incoming: SyncPayload, commitReplica: () => Promise<void>) => {
await commitReplica();
},
),
/local cleanup failed/,
);
assert.equal(subject.state.syncState, 'ERROR');
assert.equal(subject.state.pendingLocalSync, true);
assert.deepEqual(subject.completedDowngrades, []);
} finally {
if (originalNavigator) Object.defineProperty(globalThis, 'navigator', originalNavigator);
else Reflect.deleteProperty(globalThis, 'navigator');
}
});
test('local-only downgrade fails closed while the replica has unresolved conflicts', async () => {
const originalNavigator = Object.getOwnPropertyDescriptor(globalThis, 'navigator');
try {
Object.defineProperty(globalThis, 'navigator', {
configurable: true,
value: {
locks: {
request: async (_name: string, _options: unknown, callback: (lock: object) => unknown) => callback({}),
},
},
});
const basePayload = payload('base');
const base = createConvergentSyncStateFromPayload(basePayload, 'seed', NOW);
const left = remoteState(base, basePayload, payload('left'), 'left-device', NOW + 1);
const right = remoteState(base, basePayload, payload('right'), 'right-device', NOW + 2);
const conflicted = mergeConvergentSyncStates(left, right);
const subject = manager(conflicted, {});
await assert.rejects(
() => downgradeConvergentSyncImpl.call(
subject,
true,
async () => materializeSyncPayloadFromConvergentState(conflicted, { syncedAt: NOW }),
async (_incoming: SyncPayload, commitReplica: () => Promise<void>) => {
await commitReplica();
},
),
/Resolve 1 convergent conflict/,
);
assert.equal(subject.state.syncState, 'CONFLICT');
assert.deepEqual(subject.completedDowngrades, []);
} finally {
if (originalNavigator) Object.defineProperty(globalThis, 'navigator', originalNavigator);
else Reflect.deleteProperty(globalThis, 'navigator');
}
});
test('downgrade joins remote-only v2 edits before applying or writing v1', async () => {
const encryption = installEncryptionDouble();
const originalNavigator = Object.getOwnPropertyDescriptor(globalThis, 'navigator');
try {
Object.defineProperty(globalThis, 'navigator', {
configurable: true,
value: {
locks: {
request: async (_name: string, _options: unknown, callback: (lock: object) => unknown) => callback({}),
},
},
});
const basePayload = payload('base');
const base = createConvergentSyncStateFromPayload(basePayload, 'seed', NOW);
const remote = remoteState(base, basePayload, payload('remote-only'), 'remote-device', NOW + 1);
const github = adapter(encryption.register(
withConvergentSyncEnvelope(remote, { syncedAt: NOW + 1 }),
4,
'remote-device',
));
const baselines: Partial<Record<CloudProvider, ConvergentProviderBaselineV2>> = {};
const subject = manager(base, { github }, baselines);
let appliedPayload: SyncPayload | null = null;
const results = await downgradeConvergentSyncImpl.call(
subject,
true,
async () => basePayload,
async (incoming: SyncPayload, commitReplica: () => Promise<void>) => {
appliedPayload = incoming;
await commitReplica();
},
);
assert.equal(results.get('github')?.success, true);
assert.equal(appliedPayload?.hosts[0]?.label, 'remote-only');
assert.deepEqual(subject.completedDowngrades, [true]);
assert.equal(github.remote?.meta.syncSchemaVersion, undefined);
const verified = await EncryptionService.decryptPayload(github.remote!, 'pw');
assert.equal(verified.hosts[0]?.label, 'remote-only');
assert.equal(
materializeSyncPayloadFromConvergentState(subject.persisted.at(-1)!, { syncedAt: NOW }).hosts[0]?.label,
'remote-only',
);
assert.equal(baselines.github?.materializedPayload.hosts[0]?.label, 'remote-only');
} finally {
if (originalNavigator) Object.defineProperty(globalThis, 'navigator', originalNavigator);
else Reflect.deleteProperty(globalThis, 'navigator');
encryption.restore();
}
});
test('downgrade folds paused local edits into the joined provider state', async () => {
const encryption = installEncryptionDouble();
const originalNavigator = Object.getOwnPropertyDescriptor(globalThis, 'navigator');
try {
Object.defineProperty(globalThis, 'navigator', {
configurable: true,
value: {
locks: {
request: async (_name: string, _options: unknown, callback: (lock: object) => unknown) => callback({}),
},
},
});
const basePayload = payload('base');
const base = createConvergentSyncStateFromPayload(basePayload, 'seed', NOW);
const pausedLocalPayload = payload('paused-local');
const remote = remoteState(
base,
basePayload,
payload('base', 'remote-user'),
'remote-device',
NOW + 1,
);
const github = adapter(encryption.register(
withConvergentSyncEnvelope(remote, { syncedAt: NOW + 1 }),
4,
'remote-device',
));
const subject = manager(base, { github });
let appliedPayload: SyncPayload | null = null;
const results = await downgradeConvergentSyncImpl.call(
subject,
true,
async () => pausedLocalPayload,
async (incoming: SyncPayload, commitReplica: () => Promise<void>) => {
appliedPayload = incoming;
await commitReplica();
},
);
assert.equal(results.get('github')?.success, true);
assert.equal(appliedPayload?.hosts[0]?.label, 'paused-local');
assert.equal(appliedPayload?.hosts[0]?.username, 'remote-user');
const verified = await EncryptionService.decryptPayload(github.remote!, 'pw');
assert.equal(verified.hosts[0]?.label, 'paused-local');
assert.equal(verified.hosts[0]?.username, 'remote-user');
const persisted = materializeSyncPayloadFromConvergentState(
subject.persisted.at(-1)!,
{ syncedAt: NOW },
);
assert.equal(persisted.hosts[0]?.label, 'paused-local');
assert.equal(persisted.hosts[0]?.username, 'remote-user');
} finally {
if (originalNavigator) Object.defineProperty(globalThis, 'navigator', originalNavigator);
else Reflect.deleteProperty(globalThis, 'navigator');
encryption.restore();
}
});
test('downgrade preflight failure writes neither local state nor another provider', async () => {
const encryption = installEncryptionDouble();
const originalNavigator = Object.getOwnPropertyDescriptor(globalThis, 'navigator');
try {
Object.defineProperty(globalThis, 'navigator', {
configurable: true,
value: {
locks: {
request: async (_name: string, _options: unknown, callback: (lock: object) => unknown) => callback({}),
},
},
});
const localPayload = payload('local');
const base = createConvergentSyncStateFromPayload(localPayload, 'seed', NOW);
const github = adapter(encryption.register(withConvergentSyncEnvelope(base, { syncedAt: NOW }), 2));
const google = adapter(null);
google.failDownload = true;
const subject = manager(base, { github, google });
let applied = false;
const results = await downgradeConvergentSyncImpl.call(
subject,
true,
async () => localPayload,
async () => {
applied = true;
},
);
assert.equal([...results.values()].every((result) => !result.success), true);
assert.equal(applied, false);
assert.equal(subject.persisted.length, 0);
assert.equal(github.uploads, 0);
assert.equal(google.uploads, 0);
assert.deepEqual(subject.completedDowngrades, []);
} finally {
if (originalNavigator) Object.defineProperty(globalThis, 'navigator', originalNavigator);
else Reflect.deleteProperty(globalThis, 'navigator');
encryption.restore();
}
});
test('failed protected downgrade apply uploads nothing and exits syncing state', async () => {
const encryption = installEncryptionDouble();
const originalNavigator = Object.getOwnPropertyDescriptor(globalThis, 'navigator');
try {
Object.defineProperty(globalThis, 'navigator', {
configurable: true,
value: {
locks: {
request: async (_name: string, _options: unknown, callback: (lock: object) => unknown) => callback({}),
},
},
});
const localPayload = payload('local');
const base = createConvergentSyncStateFromPayload(localPayload, 'seed', NOW);
const github = adapter(encryption.register(withConvergentSyncEnvelope(base, { syncedAt: NOW }), 2));
const subject = manager(base, { github });
await assert.rejects(
() => downgradeConvergentSyncImpl.call(
subject,
true,
async () => localPayload,
async () => {
throw new Error('protective downgrade apply failed');
},
),
/protective downgrade apply failed/,
);
assert.equal(subject.persisted.length, 0);
assert.equal(github.uploads, 0);
assert.equal(subject.state.syncState, 'ERROR');
assert.equal(subject.state.pendingLocalSync, true);
assert.deepEqual(subject.completedDowngrades, []);
} finally {
if (originalNavigator) Object.defineProperty(globalThis, 'navigator', originalNavigator);
else Reflect.deleteProperty(globalThis, 'navigator');
encryption.restore();
}
});
test('downgrade preserves concurrent provider candidates and blocks legacy writes', async () => {
const encryption = installEncryptionDouble();
const originalNavigator = Object.getOwnPropertyDescriptor(globalThis, 'navigator');
try {
Object.defineProperty(globalThis, 'navigator', {
configurable: true,
value: {
locks: {
request: async (_name: string, _options: unknown, callback: (lock: object) => unknown) => callback({}),
},
},
});
const basePayload = payload('base');
const base = createConvergentSyncStateFromPayload(basePayload, 'seed', NOW);
const githubState = remoteState(base, basePayload, payload('github-edit'), 'github-device', NOW + 1);
const googleState = remoteState(base, basePayload, payload('google-edit'), 'google-device', NOW + 2);
const github = adapter(encryption.register(
withConvergentSyncEnvelope(githubState, { syncedAt: NOW + 1 }),
2,
'github-device',
));
const google = adapter(encryption.register(
withConvergentSyncEnvelope(googleState, { syncedAt: NOW + 2 }),
2,
'google-device',
));
const subject = manager(base, { github, google });
let appliedPayload: SyncPayload | null = null;
const results = await downgradeConvergentSyncImpl.call(
subject,
true,
async () => basePayload,
async (incoming: SyncPayload, commitReplica: () => Promise<void>) => {
appliedPayload = incoming;
await commitReplica();
},
);
assert.equal([...results.values()].every((result) => !result.success), true);
assert.equal(appliedPayload, null);
assert.equal(subject.state.convergentConflicts.length, 0);
assert.equal(subject.state.syncState, 'CONFLICT');
assert.equal(subject.persisted.length, 0);
assert.equal(github.uploads, 0);
assert.equal(google.uploads, 0);
assert.deepEqual(subject.completedDowngrades, []);
} finally {
if (originalNavigator) Object.defineProperty(globalThis, 'navigator', originalNavigator);
else Reflect.deleteProperty(globalThis, 'navigator');
encryption.restore();
}
});
test('conflict resolution and provider propagation share one Web Lock', async () => {
const encryption = installEncryptionDouble();
const originalNavigator = Object.getOwnPropertyDescriptor(globalThis, 'navigator');
try {
let lockCalls = 0;
let lockHeld = false;
Object.defineProperty(globalThis, 'navigator', {
configurable: true,
value: {
locks: {
request: async (_name: string, _options: unknown, callback: (lock: object) => unknown) => {
lockCalls += 1;
lockHeld = true;
try {
return await callback({});
} finally {
lockHeld = false;
}
},
},
},
});
const basePayload = payload('base');
const base = createConvergentSyncStateFromPayload(basePayload, 'seed', NOW);
const left = remoteState(base, basePayload, payload('left'), 'left-device', NOW + 1);
const right = remoteState(base, basePayload, payload('right'), 'right-device', NOW + 2);
const conflicted = mergeConvergentSyncStates(left, right);
const conflict = materializeConvergentSyncState(conflicted).conflicts.find(
(entry) => entry.address.kind === 'entity-field' && entry.address.field === 'label',
)!;
const selected = conflict.candidates.find((candidate) => candidate.value === 'left')!;
const github = adapter(encryption.register(withConvergentSyncEnvelope(conflicted, { syncedAt: NOW }), 2));
let injected = false;
github.afterUpload = (file, target) => {
if (injected) return;
injected = true;
const outgoingPayload = encryption.payloads.get(file.payload)!;
const outgoingState = validateConvergentSyncPayload(file.meta, outgoingPayload)!;
const outgoingMaterialized = materializeSyncPayloadFromConvergentState(
outgoingState,
{ syncedAt: NOW },
);
const concurrent = remoteState(
outgoingState,
outgoingMaterialized,
payload('left', 'remote-user'),
'racing-device',
NOW + 20,
);
target.remote = encryption.register(
withConvergentSyncEnvelope(concurrent, { syncedAt: NOW + 20 }),
file.meta.version + 1,
'racing-device',
);
};
const subject = manager(conflicted, { github });
const appliedPayloads: SyncPayload[] = [];
const result = await resolveConvergentConflictAndSyncImpl.call(
subject,
JSON.stringify(['entity-field', 'hosts', 'host-1', 'label']),
`${selected.dot.deviceId}:${selected.dot.counter}`,
async (incoming: SyncPayload, commitReplica: () => Promise<void>) => {
assert.equal(lockHeld, true);
appliedPayloads.push(incoming);
await commitReplica();
},
);
assert.equal(lockCalls, 1);
assert.equal(result.results.get('github')?.success, true);
assert.equal(result.payload.hosts[0]?.label, 'left');
assert.equal(result.payload.hosts[0]?.username, 'remote-user');
assert.equal(appliedPayloads.length, 2);
assert.equal(appliedPayloads[0]?.hosts[0]?.username, 'root');
assert.equal(appliedPayloads[1]?.hosts[0]?.username, 'remote-user');
assert.equal(subject.state.convergentConflicts.length, 0);
} finally {
if (originalNavigator) Object.defineProperty(globalThis, 'navigator', originalNavigator);
else Reflect.deleteProperty(globalThis, 'navigator');
encryption.restore();
}
});
test('a failed protected apply publishes neither the resolution replica nor a provider write', async () => {
const encryption = installEncryptionDouble();
const originalNavigator = Object.getOwnPropertyDescriptor(globalThis, 'navigator');
try {
Object.defineProperty(globalThis, 'navigator', {
configurable: true,
value: {
locks: {
request: async (_name: string, _options: unknown, callback: (lock: object) => unknown) => callback({}),
},
},
});
const basePayload = payload('base');
const base = createConvergentSyncStateFromPayload(basePayload, 'seed', NOW);
const left = remoteState(base, basePayload, payload('left'), 'left-device', NOW + 1);
const right = remoteState(base, basePayload, payload('right'), 'right-device', NOW + 2);
const conflicted = mergeConvergentSyncStates(left, right);
const conflict = materializeConvergentSyncState(conflicted).conflicts.find(
(entry) => entry.address.kind === 'entity-field' && entry.address.field === 'label',
)!;
const selected = conflict.candidates[0]!;
const github = adapter(encryption.register(withConvergentSyncEnvelope(conflicted, { syncedAt: NOW }), 2));
const subject = manager(conflicted, { github });
await assert.rejects(() => resolveConvergentConflictAndSyncImpl.call(
subject,
JSON.stringify(['entity-field', 'hosts', 'host-1', 'label']),
`${selected.dot.deviceId}:${selected.dot.counter}`,
async () => {
throw new Error('protective apply failed');
},
), /protective apply failed/);
assert.equal(subject.persisted.length, 0);
assert.equal(github.uploads, 0);
assert.equal(subject.state.convergentConflicts.length, 1);
} finally {
if (originalNavigator) Object.defineProperty(globalThis, 'navigator', originalNavigator);
else Reflect.deleteProperty(globalThis, 'navigator');
encryption.restore();
}
});
test('suspicious local shrink is blocked before replica persistence or provider upload', async () => {
const encryption = installEncryptionDouble();
try {
const basePayload = payloadWithHostCount(6);
const base = createConvergentSyncStateFromPayload(basePayload, 'seed', NOW);
const github = adapter(encryption.register(withConvergentSyncEnvelope(base, { syncedAt: NOW }), 2));
const subject = manager(base, { github });
const emptied = { ...basePayload, hosts: [] };
const results = await syncConvergentProvidersUnlockedImpl.call(
subject,
emptied,
{ jitter: async () => {}, now: () => NOW + 10 },
);
assert.equal(results.get('github')?.shrinkBlocked, true);
assert.equal(subject.persisted.length, 0);
assert.equal(github.uploads, 0);
assert.equal(subject.state.syncState, 'BLOCKED');
} finally {
encryption.restore();
}
});
test('one-shot shrink override produces causal deletions and verifies them', async () => {
const encryption = installEncryptionDouble();
try {
const basePayload = payloadWithHostCount(6);
const base = createConvergentSyncStateFromPayload(basePayload, 'seed', NOW);
const github = adapter(encryption.register(withConvergentSyncEnvelope(base, { syncedAt: NOW }), 2));
const subject = manager(base, { github });
subject.state.syncState = 'BLOCKED';
subject.state.lastShrinkFinding = {
suspicious: true,
reason: 'bulk-shrink',
entityType: 'hosts',
baseCount: 6,
outgoingCount: 0,
lost: 6,
};
const emptied = { ...basePayload, hosts: [] };
const results = await syncConvergentProvidersUnlockedImpl.call(
subject,
emptied,
{ overrideShrink: true, jitter: async () => {}, now: () => NOW + 10 },
);
assert.equal(results.get('github')?.success, true);
assert.equal(results.get('github')?.mergedPayload, undefined);
const verifiedPayload = await EncryptionService.decryptPayload(github.remote!, 'pw');
const verifiedState = validateConvergentSyncPayload(github.remote!.meta, verifiedPayload)!;
assert.equal(
materializeSyncPayloadFromConvergentState(verifiedState, { syncedAt: NOW }).hosts.length,
0,
);
assert.equal(subject.state.syncState, 'IDLE');
assert.equal(subject.state.lastShrinkFinding, undefined);
} finally {
encryption.restore();
}
});
test('all five provider adapters converge independent field edits into one replica', async () => {
const encryption = installEncryptionDouble();
try {
const basePayload = payload('base');
const base = createConvergentSyncStateFromPayload(basePayload, 'seed', NOW);
const edits: Array<[CloudProvider, SyncPayload]> = [
['github', payload('github-label')],
['google', payload('base', 'google-user')],
['onedrive', { ...basePayload, hosts: [{ ...basePayload.hosts[0]!, hostname: 'onedrive.example.com' }] }],
['webdav', { ...basePayload, hosts: [{ ...basePayload.hosts[0]!, port: 2200 }] }],
['s3', { ...basePayload, hosts: [{ ...basePayload.hosts[0]!, tags: ['s3-tag'] }] }],
];
const adapters = Object.fromEntries(edits.map(([provider, edited], index) => {
const state = remoteState(base, basePayload, edited, `${provider}-device`, NOW + index + 1);
return [provider, adapter(encryption.register(
withConvergentSyncEnvelope(state, { syncedAt: NOW }),
2,
`${provider}-device`,
))];
})) as Record<CloudProvider, MemoryAdapter>;
const subject = manager(base, adapters);
const results = await syncConvergentProvidersUnlockedImpl.call(
subject,
basePayload,
{ jitter: async () => {}, now: () => NOW + 20, applyPayload: applyAndCommit },
);
assert.equal([...results.values()].every((result) => result.success), true);
assert.equal(results.size, 5);
const host = results.get('github')?.mergedPayload?.hosts[0];
assert.equal(host?.label, 'github-label');
assert.equal(host?.username, 'google-user');
assert.equal(host?.hostname, 'onedrive.example.com');
assert.equal(host?.port, 2200);
assert.deepEqual(host?.tags, ['s3-tag']);
} finally {
encryption.restore();
}
});