524 lines
21 KiB
JavaScript
524 lines
21 KiB
JavaScript
import { describe, expect, test } from "bun:test";
|
|
|
|
const {
|
|
classifyDesktopSyncFailure,
|
|
discardDesktopConflicts,
|
|
createDesktopSyncIssueDetails,
|
|
createDesktopSyncDiagnosticText,
|
|
createDesktopSyncSummary,
|
|
isStagedResourceReferenced,
|
|
stagedIdsReferencedByUnsavedContent,
|
|
hasDesktopSyncStateReset,
|
|
mergeMemoIdMappings,
|
|
mergeSyncedMemos,
|
|
normalizeDesktopMemoPayload,
|
|
pauseDesktopSyncForImport,
|
|
orderBootstrapNotebooks,
|
|
orderDesktopSyncChanges,
|
|
resolveDesktopMemoSyncBase,
|
|
resolveDesktopStaleMemoUpdate,
|
|
desktopLocalRevisionWitnessesRemote,
|
|
rewriteStagedResource,
|
|
syncDesktopData,
|
|
shouldAttemptDesktopRecoveryPull,
|
|
shouldPullDesktopChanges,
|
|
} = await import("./desktop-sync.ts");
|
|
const { api, ApiRequestError } = await import("./api.ts");
|
|
|
|
test("desktop notebook create announces its durable id after sidecar acknowledgment", async () => {
|
|
const previousWindow = globalThis.window;
|
|
const previousNavigator = globalThis.navigator;
|
|
const originalCreateNotebook = api.createNotebook;
|
|
const events = [];
|
|
const calls = [];
|
|
let outboxReads = 0;
|
|
const item = { id: 4, version: 1, kind: "notebook.create", entityId: "nb_local_1", payload: { name: "测试" } };
|
|
globalThis.window = {
|
|
dispatchEvent: (event) => { events.push(event); return true; },
|
|
edgeeverDesktop: {
|
|
isAvailable: true,
|
|
listStagedResources: async () => [],
|
|
listStagedResourceAliases: async () => [],
|
|
sidecarRequest: async (method, params) => {
|
|
calls.push(method);
|
|
if (method === "sync.outbox.list") {
|
|
if (params.limit === 200) return { items: [] };
|
|
outboxReads += 1;
|
|
return { items: outboxReads === 1 ? [] : [item] };
|
|
}
|
|
if (method === "sync.status") return { pending: 1, syncing: 0 };
|
|
return { ok: true };
|
|
},
|
|
},
|
|
};
|
|
globalThis.navigator = { onLine: true };
|
|
api.createNotebook = async () => ({ notebook: { id: "nb_remote_1", name: "测试", parentId: null } });
|
|
try {
|
|
const result = await syncDesktopData();
|
|
expect(result.failed).toBe(0);
|
|
expect(calls).toContain("sync.outbox.ack");
|
|
expect(events.find((event) => event.type === "edgeever:notebook-id-remapped")?.detail).toEqual({
|
|
temporaryId: "nb_local_1",
|
|
remoteId: "nb_remote_1",
|
|
});
|
|
expect(calls.indexOf("sync.outbox.ack")).toBeLessThan(calls.indexOf("sync.status"));
|
|
} finally {
|
|
api.createNotebook = originalCreateNotebook;
|
|
globalThis.navigator = previousNavigator;
|
|
if (previousWindow === undefined) delete globalThis.window;
|
|
else globalThis.window = previousWindow;
|
|
}
|
|
});
|
|
|
|
test("desktop creates notebooks before notes and rereads remapped queue versions", async () => {
|
|
const previousWindow = globalThis.window;
|
|
const previousNavigator = globalThis.navigator;
|
|
const originalCreateNotebook = api.createNotebook;
|
|
const originalCreateMemo = api.createMemo;
|
|
const calls = [];
|
|
let queue = [
|
|
{ id: 1, version: 1, kind: "notebook.create", entityId: "nb_local_parent", payload: { name: "对对对" } },
|
|
...[2, 3].map((id) => ({ id, version: 1, kind: "memo.create", entityId: `memo_local_${id}`, payload: { notebookId: "nb_local_parent", contentMarkdown: `body ${id}` } })),
|
|
];
|
|
globalThis.window = {
|
|
dispatchEvent: () => true,
|
|
edgeeverDesktop: {
|
|
isAvailable: true,
|
|
listStagedResources: async () => [],
|
|
listStagedResourceAliases: async () => [],
|
|
sidecarRequest: async (method, params) => {
|
|
if (method === "sync.outbox.list") return { items: queue.map((item) => ({ ...item, payload: { ...item.payload } })) };
|
|
if (method === "sync.status") return { pending: 1, syncing: 0 };
|
|
if (method === "sync.outbox.ack") {
|
|
const current = queue.find((item) => item.id === params.id);
|
|
expect(params.version).toBe(current.version);
|
|
queue = queue.filter((item) => item.id !== params.id);
|
|
if (params.remoteNotebook) {
|
|
queue = queue.map((item) => ({ ...item, version: item.version + 1, payload: { ...item.payload, notebookId: "nb_cloud" } }));
|
|
}
|
|
}
|
|
if (method === "sync.outbox.fail") throw new Error("No dependency should fail");
|
|
return { ok: true };
|
|
},
|
|
},
|
|
};
|
|
globalThis.navigator = { onLine: true };
|
|
api.createNotebook = async () => {
|
|
calls.push("notebook");
|
|
return { notebook: { id: "nb_cloud", name: "对对对", parentId: null } };
|
|
};
|
|
api.createMemo = async (payload) => {
|
|
expect(payload.notebookId).toBe("nb_cloud");
|
|
calls.push(payload.contentMarkdown);
|
|
return { memo: { id: `memo_cloud_${calls.length}`, contentMarkdown: payload.contentMarkdown, contentJson: { type: "doc", content: [] } } };
|
|
};
|
|
try {
|
|
const result = await syncDesktopData();
|
|
expect(result.failed).toBe(0);
|
|
expect(result.synced).toBe(3);
|
|
expect(calls).toEqual(["notebook", "body 2", "body 3"]);
|
|
expect(queue).toEqual([]);
|
|
} finally {
|
|
api.createNotebook = originalCreateNotebook;
|
|
api.createMemo = originalCreateMemo;
|
|
globalThis.navigator = previousNavigator;
|
|
if (previousWindow === undefined) delete globalThis.window;
|
|
else globalThis.window = previousWindow;
|
|
}
|
|
});
|
|
|
|
test("desktop leaves dependent notes pending when their notebook upload fails", async () => {
|
|
const previousWindow = globalThis.window;
|
|
const previousNavigator = globalThis.navigator;
|
|
const originalCreateNotebook = api.createNotebook;
|
|
const originalCreateMemo = api.createMemo;
|
|
const failures = [];
|
|
let notebookEligible = true;
|
|
const notebook = { id: 1, version: 1, attemptCount: 0, kind: "notebook.create", entityId: "nb_local_failed", payload: { name: "Pending" } };
|
|
const note = { id: 2, version: 1, kind: "memo.create", entityId: "memo_local_waiting", payload: { notebookId: notebook.entityId } };
|
|
globalThis.window = {
|
|
dispatchEvent: () => true,
|
|
edgeeverDesktop: {
|
|
isAvailable: true,
|
|
listStagedResources: async () => [],
|
|
listStagedResourceAliases: async () => [],
|
|
sidecarRequest: async (method, params) => {
|
|
if (method === "sync.outbox.list") return { items: notebookEligible ? [notebook, note] : [note] };
|
|
if (method === "sync.status") return { pending: 1, syncing: 0, error: 1 };
|
|
if (method === "sync.outbox.fail") { failures.push(params.id); notebookEligible = false; }
|
|
return { ok: true };
|
|
},
|
|
},
|
|
};
|
|
globalThis.navigator = { onLine: true };
|
|
api.createNotebook = async () => { throw new TypeError("Network unavailable"); };
|
|
api.createMemo = async () => { throw new Error("Dependent notes must wait"); };
|
|
try {
|
|
const result = await syncDesktopData();
|
|
expect(result.failed).toBe(1);
|
|
expect(result.attempted).toBe(1);
|
|
expect(failures).toEqual([notebook.id]);
|
|
} finally {
|
|
api.createNotebook = originalCreateNotebook;
|
|
api.createMemo = originalCreateMemo;
|
|
globalThis.navigator = previousNavigator;
|
|
if (previousWindow === undefined) delete globalThis.window;
|
|
else globalThis.window = previousWindow;
|
|
}
|
|
});
|
|
|
|
test("desktop explicit discard clears a conflict whose cloud note is gone", async () => {
|
|
const previousWindow = globalThis.window;
|
|
const originalGetMemo = api.getMemo;
|
|
const calls = [];
|
|
globalThis.window = {
|
|
edgeeverDesktop: {
|
|
isAvailable: true,
|
|
sidecarRequest: async (method, params) => {
|
|
calls.push({ method, params });
|
|
if (method === "sync.outbox.list") return { items: [{ id: 7, version: 2, kind: "memo.update", entityId: "memo-gone", status: "conflict" }] };
|
|
return { ok: true };
|
|
},
|
|
},
|
|
};
|
|
api.getMemo = async () => { throw new ApiRequestError("Not found", 404, "not_found"); };
|
|
try {
|
|
expect(await discardDesktopConflicts()).toBe(1);
|
|
expect(calls.map((call) => call.method)).toEqual(["sync.outbox.list", "sync.outbox.discard"]);
|
|
expect(calls[1].params).toEqual({ id: 7, version: 2 });
|
|
} finally {
|
|
api.getMemo = originalGetMemo;
|
|
if (previousWindow === undefined) delete globalThis.window;
|
|
else globalThis.window = previousWindow;
|
|
}
|
|
});
|
|
|
|
describe("desktop staged resource sync", () => {
|
|
test("holds cloud sync until an imported screenshot is saved", async () => {
|
|
const originalWindow = globalThis.window;
|
|
const events = [];
|
|
globalThis.window = { dispatchEvent: (event) => { events.push(event.type); return true; } };
|
|
try {
|
|
const release = await pauseDesktopSyncForImport();
|
|
const result = await syncDesktopData();
|
|
expect(result.attempted).toBe(0);
|
|
expect(events).not.toContain("edgeever:sync-queue-changed");
|
|
release();
|
|
release();
|
|
expect(events.filter((event) => event === "edgeever:sync-queue-changed")).toHaveLength(1);
|
|
} finally {
|
|
if (originalWindow !== undefined) globalThis.window = originalWindow;
|
|
}
|
|
});
|
|
|
|
test("rewrites placeholders in memo JSON and markdown", () => {
|
|
const rewrites = [{ memoId: "memo-1", placeholder: "edgeever-staged://stage-1", url: "/api/v1/resources/resource-1/blob" }];
|
|
const value = {
|
|
contentJson: { type: "doc", content: [{ type: "image", attrs: { src: "edgeever-staged://stage-1" } }] },
|
|
contentMarkdown: "",
|
|
};
|
|
|
|
expect(rewriteStagedResource(value, rewrites)).toEqual({
|
|
contentJson: { type: "doc", content: [{ type: "image", attrs: { src: "/api/v1/resources/resource-1/blob" } }] },
|
|
contentMarkdown: "",
|
|
});
|
|
});
|
|
|
|
test("never sends desktop-only PDF URLs to the cloud", () => {
|
|
expect(normalizeDesktopMemoPayload({
|
|
contentJson: {
|
|
type: "doc",
|
|
content: [{ type: "pdfAttachment", attrs: { url: "edgeever-resource://resource/res_pdf" } }],
|
|
},
|
|
contentMarkdown: "[Attachment: report.pdf](edgeever-resource://resource/res_pdf)",
|
|
})).toMatchObject({
|
|
contentJson: {
|
|
content: [{ attrs: { url: "/api/v1/resources/res_pdf/blob" } }],
|
|
},
|
|
contentMarkdown: "[Attachment: report.pdf](/api/v1/resources/res_pdf/blob)",
|
|
});
|
|
});
|
|
|
|
test("does not consume a staged image before a saved memo update references it", () => {
|
|
const stagedId = "stage-1";
|
|
|
|
expect(isStagedResourceReferenced([], stagedId)).toBe(false);
|
|
expect(isStagedResourceReferenced([
|
|
{ contentJson: { type: "doc", content: [{ type: "image", attrs: { src: `edgeever-staged://${stagedId}` } }] } },
|
|
], stagedId)).toBe(true);
|
|
});
|
|
|
|
test("does not confuse one staged image id with a longer id that shares its prefix", () => {
|
|
expect(isStagedResourceReferenced([
|
|
{ contentMarkdown: "" },
|
|
], "stage-1")).toBe(false);
|
|
expect(isStagedResourceReferenced([
|
|
{ contentMarkdown: "" },
|
|
], "stage-1")).toBe(true);
|
|
});
|
|
|
|
test("keeps staged bytes while an editor draft or visible image still uses the old URL", () => {
|
|
const rewrites = [{ memoId: "memo-1", placeholder: "edgeever-staged://stage-1", url: "/api/v1/resources/res-1/blob" }];
|
|
expect(stagedIdsReferencedByUnsavedContent(rewrites, [
|
|
{ contentJson: { type: "doc", content: [{ type: "image", attrs: { src: "edgeever-staged://stage-1" } }] } },
|
|
])).toEqual(new Set(["stage-1"]));
|
|
expect(stagedIdsReferencedByUnsavedContent(rewrites, ["edgeever-staged://stage-1"])).toEqual(new Set(["stage-1"]));
|
|
expect(stagedIdsReferencedByUnsavedContent(rewrites, ["edgeever-staged://stage-10"])).toEqual(new Set());
|
|
});
|
|
|
|
test("retains a temporary id mapping when a later sync phase fails", () => {
|
|
const retained = mergeMemoIdMappings(new Map(), new Map([["memo_local_1", "memo_remote_1"]]));
|
|
|
|
expect(retained.get("memo_local_1")).toBe("memo_remote_1");
|
|
});
|
|
|
|
test("keeps the latest acknowledged memo base across sync phases", () => {
|
|
const created = { id: "memo_remote_1", revision: 0 };
|
|
const updated = { id: "memo_remote_1", revision: 1 };
|
|
const retained = mergeSyncedMemos(
|
|
new Map([[created.id, created]]),
|
|
new Map([[updated.id, updated]]),
|
|
);
|
|
|
|
expect(retained.get("memo_remote_1")).toEqual(updated);
|
|
});
|
|
});
|
|
|
|
describe("desktop bootstrap sync", () => {
|
|
test("rebuilds when the server cursor rewinds or its identity changes", () => {
|
|
const local = { cursor: 42, syncIdentity: "workspace-a" };
|
|
expect(hasDesktopSyncStateReset(local, { serverCursor: 7, syncIdentity: "workspace-a" })).toBe(true);
|
|
expect(hasDesktopSyncStateReset(local, { serverCursor: 42, syncIdentity: "workspace-b" })).toBe(true);
|
|
expect(hasDesktopSyncStateReset(local, { serverCursor: 64, syncIdentity: "workspace-a" })).toBe(false);
|
|
});
|
|
|
|
test("orders parent notebooks before their children", () => {
|
|
const child = { id: "child", parentId: "parent", name: "Child" };
|
|
const parent = { id: "parent", parentId: null, name: "Parent" };
|
|
const grandchild = { id: "grandchild", parentId: "child", name: "Grandchild" };
|
|
|
|
expect(orderBootstrapNotebooks([grandchild, child, parent]).map((notebook) => notebook.id)).toEqual([
|
|
"parent",
|
|
"child",
|
|
"grandchild",
|
|
]);
|
|
});
|
|
|
|
test("applies notebook upserts before memos in an incremental change page", () => {
|
|
const memo = { entityType: "memo", entityId: "memo-1", operation: "upsert", notebook: null };
|
|
const child = {
|
|
entityType: "notebook",
|
|
entityId: "child",
|
|
operation: "upsert",
|
|
notebook: { id: "child", parentId: "parent" },
|
|
};
|
|
const parent = {
|
|
entityType: "notebook",
|
|
entityId: "parent",
|
|
operation: "upsert",
|
|
notebook: { id: "parent", parentId: null },
|
|
};
|
|
const deleted = { entityType: "notebook", entityId: "gone", operation: "delete", notebook: null };
|
|
|
|
expect(orderDesktopSyncChanges([memo, child, deleted, parent]).map((change) => change.entityId)).toEqual([
|
|
"parent",
|
|
"child",
|
|
"gone",
|
|
"memo-1",
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe("desktop memo sync base", () => {
|
|
test("repairs a legacy local autosave revision that is ahead of the cloud", () => {
|
|
expect(resolveDesktopMemoSyncBase(
|
|
{ revision: 3, contentHash: "cloud-3" },
|
|
{ expectedRevision: 9, expectedContentHash: "local-autosave-9" },
|
|
)).toEqual({ expectedRevision: 3, expectedContentHash: "cloud-3" });
|
|
});
|
|
|
|
test("keeps a genuinely stale base so the server update remains protected", () => {
|
|
expect(resolveDesktopMemoSyncBase(
|
|
{ revision: 9, contentHash: "cloud-9" },
|
|
{ expectedRevision: 3, expectedContentHash: "cloud-3" },
|
|
)).toEqual({ expectedRevision: 3, expectedContentHash: "cloud-3" });
|
|
});
|
|
|
|
test("acks a lost save when the cloud already has the queued payload", () => {
|
|
expect(resolveDesktopStaleMemoUpdate({
|
|
current: { revision: 10, contentHash: "hash-a" },
|
|
expected: { expectedRevision: 9, expectedContentHash: "hash-before" },
|
|
payload: { title: "", tags: [], contentMarkdown: "一期验收通过。", contentJson: { type: "doc" } },
|
|
remote: { title: "无标题笔记", tags: [], contentMarkdown: "一期验收通过。", contentHash: "hash-a", contentJson: { type: "doc" } },
|
|
localRevisions: [],
|
|
})).toBe("ack");
|
|
});
|
|
|
|
test("rebases a later local draft when a sidecar snapshot already contains the cloud body", () => {
|
|
const remote = {
|
|
title: "无标题笔记",
|
|
tags: [],
|
|
contentMarkdown: "一期验收通过。",
|
|
contentHash: "hash-a",
|
|
contentJson: { type: "doc", content: [{ type: "paragraph" }] },
|
|
};
|
|
|
|
expect(desktopLocalRevisionWitnessesRemote([
|
|
{
|
|
id: "revision_local_1",
|
|
revision: 9,
|
|
contentHash: "sidecar-a",
|
|
contentMarkdown: "一期验收通过。",
|
|
contentJson: remote.contentJson,
|
|
},
|
|
], remote, 9)).toBe(true);
|
|
|
|
expect(resolveDesktopStaleMemoUpdate({
|
|
current: { revision: 10, contentHash: "hash-a" },
|
|
expected: { expectedRevision: 9, expectedContentHash: "hash-before" },
|
|
payload: { title: "", tags: [], contentMarkdown: "一期验收通过。然后继续写。", contentJson: { type: "doc" } },
|
|
remote,
|
|
localRevisions: [{
|
|
id: "revision_local_1",
|
|
revision: 9,
|
|
contentHash: "sidecar-a",
|
|
contentMarkdown: "一期验收通过。",
|
|
contentJson: remote.contentJson,
|
|
}],
|
|
})).toBe("rebase");
|
|
});
|
|
|
|
test("does not treat an older local snapshot or a remote-cached revision as proof", () => {
|
|
const remote = {
|
|
title: "无标题笔记",
|
|
tags: [],
|
|
contentMarkdown: "别人改过的正文",
|
|
contentHash: "hash-other",
|
|
contentJson: { type: "doc" },
|
|
};
|
|
|
|
expect(desktopLocalRevisionWitnessesRemote([
|
|
{
|
|
id: "revision_local_old",
|
|
revision: 3,
|
|
contentHash: "hash-other",
|
|
contentMarkdown: "别人改过的正文",
|
|
contentJson: remote.contentJson,
|
|
},
|
|
{
|
|
id: "rev_remote_cached",
|
|
revision: 10,
|
|
contentHash: "hash-other",
|
|
contentMarkdown: "别人改过的正文",
|
|
contentJson: remote.contentJson,
|
|
},
|
|
], remote, 9)).toBe(false);
|
|
|
|
expect(resolveDesktopStaleMemoUpdate({
|
|
current: { revision: 10, contentHash: "hash-other" },
|
|
expected: { expectedRevision: 9, expectedContentHash: "hash-before" },
|
|
payload: { title: "", tags: [], contentMarkdown: "本地还在写的草稿", contentJson: { type: "doc" } },
|
|
remote,
|
|
localRevisions: [{
|
|
id: "revision_local_9",
|
|
revision: 9,
|
|
contentHash: "sidecar-local",
|
|
contentMarkdown: "本地还在写的草稿",
|
|
contentJson: { type: "doc" },
|
|
}],
|
|
})).toBe("conflict");
|
|
});
|
|
});
|
|
|
|
describe("desktop sync failure handling", () => {
|
|
test("stops retrying a missing memo update and keeps transient failures retryable", () => {
|
|
expect(classifyDesktopSyncFailure(
|
|
{ kind: "memo.update" },
|
|
new ApiRequestError("Memo not found", 404, "not_found"),
|
|
)).toEqual({ conflict: false, retryable: false, errorCode: "memo_not_found" });
|
|
|
|
expect(classifyDesktopSyncFailure(
|
|
{ kind: "memo.update" },
|
|
new ApiRequestError("Temporarily unavailable", 503),
|
|
)).toEqual({ conflict: false, retryable: true, errorCode: "http_503" });
|
|
});
|
|
|
|
test("copies diagnostics without exposing entity ids or payload content", () => {
|
|
const diagnostic = createDesktopSyncDiagnosticText([{
|
|
id: 7,
|
|
kind: "memo.update",
|
|
entityId: "private-memo-id",
|
|
payload: { contentMarkdown: "private note body" },
|
|
attemptCount: 3,
|
|
version: 1,
|
|
status: "error",
|
|
lastError: "Memo memo_private123 not found at https://private.example.test/api",
|
|
lastErrorCode: "memo_not_found",
|
|
retryable: false,
|
|
}]);
|
|
|
|
expect(diagnostic).toContain("memo_not_found");
|
|
expect(diagnostic).toContain('"totalItemCount": 1');
|
|
expect(diagnostic).not.toContain('"id": 7');
|
|
expect(diagnostic).not.toContain("private-memo-id");
|
|
expect(diagnostic).not.toContain("private note body");
|
|
expect(diagnostic).not.toContain("memo_private123");
|
|
expect(diagnostic).not.toContain("private.example.test");
|
|
});
|
|
|
|
test("keeps a global failure visible in both the summary and details", () => {
|
|
const globalIssue = {
|
|
phase: "pull_remote_changes",
|
|
message: "Request failed at https://private.example.test/api/v1/sync/changes",
|
|
errorCode: "http_500",
|
|
occurredAt: "2026-09-05T00:00:00.000Z",
|
|
};
|
|
|
|
expect(createDesktopSyncSummary({ pending: 0, syncing: 0, conflict: 0, error: 0 }, globalIssue)).toEqual({
|
|
total: 1,
|
|
pending: 0,
|
|
syncing: 0,
|
|
conflict: 0,
|
|
error: 1,
|
|
});
|
|
expect(createDesktopSyncIssueDetails([], globalIssue)).toEqual({ globalIssue, items: [] });
|
|
expect(createDesktopSyncDiagnosticText([], globalIssue)).toContain("pull_remote_changes");
|
|
expect(createDesktopSyncDiagnosticText([], globalIssue)).not.toContain("private.example.test");
|
|
});
|
|
|
|
test("bounds diagnostics for a prefilled GitHub Issue URL", () => {
|
|
const items = Array.from({ length: 200 }, (_, id) => ({
|
|
id,
|
|
kind: "memo.update",
|
|
entityId: `memo_private_${id}`,
|
|
payload: { contentMarkdown: "private note body" },
|
|
attemptCount: 99_999,
|
|
version: 1,
|
|
status: "error",
|
|
lastError: "Temporary failure ".repeat(40),
|
|
lastErrorCode: "network_error",
|
|
retryable: true,
|
|
}));
|
|
const diagnostic = createDesktopSyncDiagnosticText(items);
|
|
|
|
expect(diagnostic).toContain('"totalItemCount": 200');
|
|
expect(diagnostic).toContain('"includedItemCount": 5');
|
|
expect(encodeURIComponent(diagnostic).length).toBeLessThan(6_000);
|
|
});
|
|
|
|
test("allows remote pulls while durable errors are handled separately", () => {
|
|
expect(shouldPullDesktopChanges({ pending: 0, syncing: 0, error: 1, conflict: 1 }, true)).toBe(true);
|
|
expect(shouldPullDesktopChanges({ pending: 1, syncing: 0, error: 0, conflict: 0 }, true)).toBe(false);
|
|
expect(shouldPullDesktopChanges({ pending: 0, syncing: 0, error: 0, conflict: 0 }, false)).toBe(false);
|
|
});
|
|
|
|
test("attempts a recovery pull only when no local upload can be overwritten", () => {
|
|
expect(shouldAttemptDesktopRecoveryPull("sync_staged_resources", { pending: 0, syncing: 0 }, true)).toBe(true);
|
|
expect(shouldAttemptDesktopRecoveryPull("sync_staged_resources", { pending: 1, syncing: 0 }, true)).toBe(false);
|
|
expect(shouldAttemptDesktopRecoveryPull("sync_staged_resources", { pending: 0, syncing: 1 }, true)).toBe(false);
|
|
expect(shouldAttemptDesktopRecoveryPull("pull_remote_changes", { pending: 0, syncing: 0 }, true)).toBe(false);
|
|
expect(shouldAttemptDesktopRecoveryPull("finalize_resource_remap", { pending: 0, syncing: 0 }, true)).toBe(false);
|
|
expect(shouldAttemptDesktopRecoveryPull("sync_staged_resources", { pending: 0, syncing: 0 }, false)).toBe(false);
|
|
});
|
|
});
|