diff --git a/.github/media/command-palette.png b/.github/media/command-palette.png index 09f3c08d..b144704c 100644 Binary files a/.github/media/command-palette.png and b/.github/media/command-palette.png differ diff --git a/src/plugins/runtime.exportState.test.ts b/src/plugins/runtime.exportState.test.ts new file mode 100644 index 00000000..fc772126 --- /dev/null +++ b/src/plugins/runtime.exportState.test.ts @@ -0,0 +1,57 @@ +import { describe, test, expect, vi, beforeEach } from "vitest"; +import type { PluginAPI, PluginManifest } from "@/plugins/api"; + +// Spy on the Tauri `backup_export` bridge so we can inspect the args the plugin +// runtime forwards to it. +const invokeMock = vi.fn(); +vi.mock("@tauri-apps/api/core", () => ({ + invoke: (...args: unknown[]) => invokeMock(...args), +})); + +// Stub the sync service: `getExcludedObjectIds()` returns a known set so the test +// can assert it is threaded through to `backup_export` (issue #47). The other +// three named imports are the surface runtime.ts pulls from this module. +vi.mock("@/services/sync", () => ({ + getExcludedObjectIds: () => ["excluded-host", "excluded-key"], + getSyncState: () => ({ status: "idle" }), + onSyncStateChange: () => () => {}, + ENTITY_FILES: [], +})); + +import { loadPlugin, unloadPlugin } from "@/plugins/runtime"; + +function captureApi(manifest: PluginManifest): PluginAPI { + let captured: PluginAPI | undefined; + loadPlugin(manifest, (api) => { + captured = api; + }, true); + if (!captured) throw new Error("register() did not receive an api"); + return captured; +} + +describe("plugin sync.exportState honours sync exclusions", () => { + beforeEach(() => { + invokeMock.mockReset(); + invokeMock.mockResolvedValue([1, 2, 3]); + }); + + test("forwards getExcludedObjectIds() into backup_export", async () => { + const manifest: PluginManifest = { + id: "gist-sync-test", + name: "Gist Sync", + version: "1.0.0", + permissions: ["sync:write"], + }; + const api = captureApi(manifest); + try { + await api.sync.exportState("aabb", "device-1"); + } finally { + unloadPlugin("gist-sync-test"); + } + + expect(invokeMock).toHaveBeenCalledWith( + "backup_export", + expect.objectContaining({ excludedIds: ["excluded-host", "excluded-key"] }), + ); + }); +}); diff --git a/src/plugins/runtime.ts b/src/plugins/runtime.ts index f842fd5f..564e372f 100644 --- a/src/plugins/runtime.ts +++ b/src/plugins/runtime.ts @@ -9,7 +9,7 @@ import { useNotificationStore } from "@/stores/notificationStore"; import { useSessionStore } from "@/stores/sessionStore"; import { useSnippetStore } from "@/stores/snippetStore"; import { useFolderStore } from "@/stores/folderStore"; -import { getSyncState, onSyncStateChange, ENTITY_FILES, type BlobPayload } from "@/services/sync"; +import { getSyncState, onSyncStateChange, ENTITY_FILES, getExcludedObjectIds, type BlobPayload } from "@/services/sync"; import { useThemeStore } from "@/stores/themeStore"; import { mergeEntities, mergeSecrets } from "@/services/crdt"; import type { @@ -812,6 +812,9 @@ function createPluginAPI(manifest: PluginManifest): PluginAPI { encKey: encKeyBytes, accountId: "gist-sync", deviceId, + // Strip cloud-off objects (and their secrets) from third-party sync + // destinations too, mirroring the built-in server push (issue #47). + excludedIds: getExcludedObjectIds(), }); const CHUNK = 8192; let binary = ""; diff --git a/src/services/sync.ts b/src/services/sync.ts index 7ef7899d..b85f2ec5 100644 --- a/src/services/sync.ts +++ b/src/services/sync.ts @@ -335,8 +335,11 @@ function base64ToBytes(b64: string): number[] { * Ids of every entity object that must not participate in sync — individually * excluded, or belonging to a sync-disabled type. Used to filter both the * outbound blob (`backup_export`) and inbound remote payloads (pull merge). + * + * Exported so non-server sync destinations (e.g. the gist-sync plugin export + * path, issue #47) apply the same exclusion filter as the built-in server sync. */ -function getExcludedObjectIds(): string[] { +export function getExcludedObjectIds(): string[] { const prefs = useSyncPrefsStore.getState(); return collectExcludedIds( [