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
74 lines
3.3 KiB
JavaScript
74 lines
3.3 KiB
JavaScript
/**
|
|
* syncAnchorDecision — pure "has the remote changed since we last saw it?"
|
|
* logic extracted from CloudSyncManager so it can be exercised by
|
|
* `node --test` without standing up the full manager harness.
|
|
*
|
|
* Called from CloudSyncManager.inspectProviderRemoteState after the
|
|
* remote has been downloaded and its signature computed. Given the
|
|
* previous anchor and the current state, decides whether the remote
|
|
* looks different enough to warrant re-merging.
|
|
*
|
|
* Four decisions matter for data integrity:
|
|
*
|
|
* 1. Anchor missing + remote empty → not changed (first sync, nothing
|
|
* to merge from). Callers MUST still guard against pushing an empty
|
|
* local vault (see useAutoSync `hasMeaningfulSyncData`) — that guard
|
|
* is orthogonal to this decision.
|
|
* 2. Anchor missing + remote non-empty → changed (first sync, remote
|
|
* has data we've never observed → three-way merge with empty base).
|
|
* 3. Anchor present + resourceId drift → changed (provider created a
|
|
* fresh file; reuse of the old anchor would be meaningless).
|
|
* 4. Anchor present + signature mismatch → changed (same resource, new
|
|
* ciphertext — standard drift).
|
|
*
|
|
* Any other state is "unchanged", and callers short-circuit the merge.
|
|
*
|
|
* @param {{
|
|
* currentSignature: string | null,
|
|
* currentResourceId: string | null,
|
|
* anchor: { signature?: string | null, resourceId?: string | null } | null,
|
|
* hasRemoteFile: boolean,
|
|
* }} input
|
|
* @returns {{ remoteChanged: boolean, reason: string }}
|
|
*/
|
|
export function decideRemoteChanged(input) {
|
|
const { currentSignature, currentResourceId, anchor, hasRemoteFile } = input;
|
|
|
|
if (!anchor) {
|
|
// No anchor means we've never observed this provider.
|
|
if (!hasRemoteFile) {
|
|
// Remote has no file at all → nothing to merge.
|
|
return { remoteChanged: false, reason: 'no-anchor-no-remote' };
|
|
}
|
|
if (currentSignature === null) {
|
|
// hasRemoteFile=true but the signature computed to null — the
|
|
// file exists but we can't hash its meta (malformed shape, newer
|
|
// schema, partial download). Treat as CHANGED so the caller
|
|
// routes through the three-way merge / decrypt path rather than
|
|
// silently short-circuiting and letting the next upload overwrite
|
|
// an unreadable-but-extant remote file. If the payload is
|
|
// decryptable the merge will succeed; if it isn't, the decrypt
|
|
// error surfaces to the user, which is strictly safer than a
|
|
// silent stomp.
|
|
return { remoteChanged: true, reason: 'unreadable-remote' };
|
|
}
|
|
return { remoteChanged: true, reason: 'no-anchor-remote-has-data' };
|
|
}
|
|
|
|
// Resource identity drift: provider returned a different resource
|
|
// (e.g. a freshly-created gist, or the user reconnected and the
|
|
// adapter picked a new file). The previous anchor's signature is
|
|
// meaningless once the resource id changes.
|
|
const anchorResourceId = anchor.resourceId ?? null;
|
|
if (anchorResourceId !== currentResourceId) {
|
|
return { remoteChanged: true, reason: 'resource-id-changed' };
|
|
}
|
|
|
|
// Same resource, different signature → new ciphertext/meta.
|
|
if ((anchor.signature ?? null) !== currentSignature) {
|
|
return { remoteChanged: true, reason: 'signature-mismatch' };
|
|
}
|
|
|
|
return { remoteChanged: false, reason: 'anchor-matches' };
|
|
}
|