Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified .github/media/command-palette.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
57 changes: 57 additions & 0 deletions src/plugins/runtime.exportState.test.ts
Original file line number Diff line number Diff line change
@@ -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"] }),
);
});
});
5 changes: 4 additions & 1 deletion src/plugins/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 = "";
Expand Down
5 changes: 4 additions & 1 deletion src/services/sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
[
Expand Down
Loading