diff --git a/esbuild.config.mjs b/esbuild.config.mjs index cf17845..6417393 100644 --- a/esbuild.config.mjs +++ b/esbuild.config.mjs @@ -1,6 +1,6 @@ import esbuild from "esbuild"; import process from "process"; -import builtins from "builtin-modules"; +import { builtinModules } from "module"; const banner = `/* @@ -22,7 +22,7 @@ const context = await esbuild.context({ 'electron', '@codemirror/*', 'lezer', - ...builtins], + ...builtinModules], format: 'cjs', target: 'es2018', logLevel: "info", diff --git a/manifest.json b/manifest.json index 29b1546..d1fdbd0 100644 --- a/manifest.json +++ b/manifest.json @@ -3,7 +3,7 @@ "name": "RustShare Vault Sync", "version": "0.1.0", "minAppVersion": "0.15.0", - "description": "Sync local Obsidian vaults to RustShare. Obsidian is a trademark of Dynalist Inc. RustShare is not affiliated with, endorsed by, or sponsored by Obsidian.", + "description": "Sync local vaults to RustShare. RustShare is not affiliated with, endorsed by, or sponsored by Dynalist Inc.", "author": "RustShare", "authorUrl": "https://rustshare.io", "fundingUrl": "", diff --git a/package-lock.json b/package-lock.json index 0a285de..fbb9eed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,6 @@ "devDependencies": { "@types/node": "^20.0.0", "@vitest/coverage-v8": "^4.1.8", - "builtin-modules": "4.0.0", "esbuild": "0.24.0", "obsidian": "1.7.2", "tslib": "2.8.0", @@ -1123,19 +1122,6 @@ "js-tokens": "^10.0.0" } }, - "node_modules/builtin-modules": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-4.0.0.tgz", - "integrity": "sha512-p1n8zyCkt1BVrKNFymOHjcDSAl7oq/gUvfgULv2EblgpPVQlQr9yHnWjg9IJ2MhfwPqiYqMMrr01OY7yQoK2yA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/chai": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", diff --git a/package.json b/package.json index 3c2de74..48e9a96 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "rustshare-vault-sync", "version": "0.1.0", - "description": "RustShare Vault Sync for Obsidian. Obsidian is a trademark of Dynalist Inc. RustShare is not affiliated with, endorsed by, or sponsored by Obsidian.", + "description": "Sync local vaults to RustShare. RustShare is not affiliated with, endorsed by, or sponsored by Dynalist Inc.", "main": "main.js", "scripts": { "dev": "node esbuild.config.mjs", @@ -20,7 +20,6 @@ "devDependencies": { "@types/node": "^20.0.0", "@vitest/coverage-v8": "^4.1.8", - "builtin-modules": "4.0.0", "esbuild": "0.24.0", "obsidian": "1.7.2", "tslib": "2.8.0", diff --git a/src/api.ts b/src/api.ts index e57f9a0..7b1e076 100644 --- a/src/api.ts +++ b/src/api.ts @@ -1,5 +1,15 @@ -// Disclaimer: Obsidian is a trademark of Dynalist Inc. RustShare is not affiliated with, endorsed by, or sponsored by Obsidian. -// RustShare Vault Sync — API client for the RustShare vault sync backend. +import { requestUrl } from 'obsidian'; + +export interface APIError extends Error { + status?: number; + retry_after?: number; + error?: string; + path?: string; + client_rev?: number; + current_rev?: number; + server_sha256?: string; + resolution?: string; +} export interface Vault { id: string; @@ -130,14 +140,6 @@ export class RustShareAPI { } } - private fetchWithTimeout(url: string, init: RequestInit, timeoutMs = 30000): Promise { - const controller = new AbortController(); - const timeout = setTimeout(() => controller.abort(), timeoutMs); - return fetch(url, { ...init, signal: controller.signal }).finally(() => { - clearTimeout(timeout); - }); - } - private async request(method: string, endpoint: string, body?: unknown, extraHeaders?: Record): Promise; private async request(method: string, endpoint: string, body?: unknown, extraHeaders?: Record): Promise { const headers: Record = {}; @@ -149,18 +151,27 @@ export class RustShareAPI { } Object.assign(headers, extraHeaders); - if (body !== undefined && !(body instanceof ArrayBuffer) && !headers['Content-Type']) { + const contentType = headers['Content-Type']; + if (body !== undefined && !(body instanceof ArrayBuffer) && !contentType) { headers['Content-Type'] = 'application/json'; } - const response = await this.fetchWithTimeout(this.buildUrl(endpoint), { + const response = await requestUrl({ + url: this.buildUrl(endpoint), method, headers, body: body instanceof ArrayBuffer ? body : body !== undefined ? JSON.stringify(body) : undefined, + contentType: contentType || (body !== undefined && !(body instanceof ArrayBuffer) ? 'application/json' : undefined), + throw: false, }); + const headersLower = Object.keys(response.headers).reduce((acc, key) => { + acc[key.toLowerCase()] = response.headers[key]; + return acc; + }, {} as Record); + if (response.status === 409) { - const conflict = await response.json().catch(() => ({})) as Partial; + const conflict = (response.json || {}) as Partial; const error: ConflictError = { error: 'conflict', message: conflict.message ?? 'Conflict detected', @@ -170,30 +181,30 @@ export class RustShareAPI { server_sha256: conflict.server_sha256, resolution: conflict.resolution ?? 'create_conflict_copy', }; - const err = new Error(error.message || 'Conflict'); + const err = new Error(error.message || 'Conflict') as APIError; Object.assign(err, error); throw err; } if (response.status === 429) { - const retryAfter = response.headers.get('retry-after') || response.headers.get('Retry-After'); + const retryAfter = headersLower['retry-after']; let retryAfterSeconds: number | undefined; if (retryAfter) { const trimmed = retryAfter.trim(); - // Only parse if it's a pure integer (delta-seconds). Ignore HTTP-date strings. if (/^\d+$/.test(trimmed)) { retryAfterSeconds = parseInt(trimmed, 10); } } - const text = await response.text(); - const err = new Error(`HTTP 429: ${text}`); - (err as any).status = 429; - (err as any).retry_after = retryAfterSeconds; + const text = response.text || ''; + const err = new Error(`HTTP 429: ${text}`) as APIError; + err.status = 429; + err.retry_after = retryAfterSeconds; throw err; } - if (!response.ok) { - const text = await response.text().catch(() => 'Unknown error'); + const isOk = response.status >= 200 && response.status < 300; + if (!isOk) { + const text = response.text || 'Unknown error'; throw new Error(`HTTP ${response.status}: ${text}`); } @@ -201,12 +212,12 @@ export class RustShareAPI { return undefined; } - const contentType = response.headers.get('Content-Type') ?? ''; - if (contentType.includes('application/json')) { - return response.json(); + const contentTypeHeader = headersLower['content-type'] ?? ''; + if (contentTypeHeader.includes('application/json')) { + return response.json as unknown; } - return response.arrayBuffer(); + return response.arrayBuffer as unknown; } // Device pairing methods diff --git a/src/main.ts b/src/main.ts index e0bccaa..e054a1f 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,8 +1,8 @@ // Obsidian is a trademark of Dynalist Inc. RustShare is not affiliated with, endorsed by, or sponsored by Obsidian. // This file is part of RustShare Vault Sync. -import { Plugin, Notice, TFile } from 'obsidian'; -import { RustShareVaultSyncSettings, DEFAULT_SETTINGS, validateSettings } from './settings'; +import { Plugin, Notice, TFile, Platform, FileSystemAdapter } from 'obsidian'; +import { RustShareVaultSyncSettings, DEFAULT_SETTINGS } from './settings'; import { RustShareVaultSyncSettingTab } from './ui/settings-tab'; import { StatusBar } from './ui/status-bar'; import { RustShareAPI } from './api'; @@ -10,7 +10,7 @@ import { SyncEngine } from './sync'; import { SyncState, createEmptySyncState, migrateSyncState, pruneTombstones } from './state'; import { SyncQueue, SyncOperation } from './sync-queue'; import { syncLog } from './sync-log'; -import { generateDeviceId, detectCloudSyncFolder, shouldIgnorePath } from './utils'; +import { detectCloudSyncFolder, shouldIgnorePath } from './utils'; export default class RustShareVaultSyncPlugin extends Plugin { declare settings: RustShareVaultSyncSettings; @@ -25,7 +25,19 @@ export default class RustShareVaultSyncPlugin extends Plugin { // Generate device name if not set if (!this.settings.deviceName) { - this.settings.deviceName = `${this.app.vault.getName()} - ${navigator.platform}`; + let osName = 'Unknown OS'; + if (Platform.isMacOS) { + osName = 'macOS'; + } else if (Platform.isWin) { + osName = 'Windows'; + } else if (Platform.isLinux) { + osName = 'Linux'; + } else if (Platform.isIosApp) { + osName = 'iOS'; + } else if (Platform.isAndroidApp) { + osName = 'Android'; + } + this.settings.deviceName = `${this.app.vault.getName()} - ${osName}`; await this.saveSettings(); } @@ -56,7 +68,7 @@ export default class RustShareVaultSyncPlugin extends Plugin { // Ribbon icon this.addRibbonIcon('cloud', 'RustShare Vault Sync', (evt: MouseEvent) => { - this.runManualSync(); + void this.runManualSync(); }); // Commands @@ -120,7 +132,10 @@ export default class RustShareVaultSyncPlugin extends Plugin { }); // Check for double-sync warning - const vaultPath = (this.app.vault.adapter as any).getBasePath?.() || ''; + let vaultPath = ''; + if (this.app.vault.adapter instanceof FileSystemAdapter) { + vaultPath = this.app.vault.adapter.getBasePath(); + } const cloudFolder = detectCloudSyncFolder(vaultPath); if (cloudFolder) { new Notice( @@ -136,10 +151,10 @@ export default class RustShareVaultSyncPlugin extends Plugin { } async loadSettings() { - const data = await this.loadData(); + const data = (await this.loadData()) as Record | null; this.settings = Object.assign({}, DEFAULT_SETTINGS, data); - if (data?.syncState) { - this.syncState = migrateSyncState(data.syncState); + if (data && data.syncState && typeof data.syncState === 'object') { + this.syncState = migrateSyncState(data.syncState as Record); } } @@ -213,9 +228,10 @@ export default class RustShareVaultSyncPlugin extends Plugin { deviceId = resp.id; this.settings.deviceId = deviceId; await this.saveSettings(); - } catch (e: any) { + } catch (e) { + const message = e instanceof Error ? e.message : String(e); const isNetworkError = e instanceof TypeError || - /fetch|network|Failed to fetch/i.test(e?.message || ''); + /fetch|network|Failed to fetch/i.test(message); if (this.settings.deviceId) { deviceId = this.settings.deviceId; @@ -225,7 +241,7 @@ export default class RustShareVaultSyncPlugin extends Plugin { console.warn('Using cached device ID, registration failed:', e); } } else { - throw new Error(`Failed to register device: ${e.message || e}`); + throw new Error(`Failed to register device: ${message}`); } } @@ -282,7 +298,7 @@ export default class RustShareVaultSyncPlugin extends Plugin { } const api = new RustShareAPI(this.settings.rustshareUrl, this.settings.authToken); - const engine = new SyncEngine(this.app.vault, api, this.syncState, this.settings.deviceName); + const engine = new SyncEngine(this.app, api, this.syncState, this.settings.deviceName); try { this.statusBar.updateStatus('syncing', 'Syncing...'); @@ -321,7 +337,7 @@ export default class RustShareVaultSyncPlugin extends Plugin { this.isSyncing = true; try { const api = new RustShareAPI(this.settings.rustshareUrl, this.settings.authToken); - const engine = new SyncEngine(this.app.vault, api, this.syncState, this.settings.deviceName); + const engine = new SyncEngine(this.app, api, this.syncState, this.settings.deviceName); this.statusBar.updateStatus('syncing', 'Syncing...'); const result = await engine.syncIncremental(ops); @@ -355,7 +371,9 @@ export default class RustShareVaultSyncPlugin extends Plugin { private startAutoSync(): void { if (this.syncInterval) return; const ms = this.settings.autoSyncIntervalMinutes * 60 * 1000; - this.syncInterval = window.setInterval(() => this.runManualSync(), ms); + this.syncInterval = window.setInterval(() => { + void this.runManualSync(); + }, ms); } private stopAutoSync(): void { diff --git a/src/state.ts b/src/state.ts index 922ebdf..9a52b3d 100644 --- a/src/state.ts +++ b/src/state.ts @@ -34,22 +34,26 @@ export function createEmptySyncState(vaultId: string, deviceId: string, deviceNa }; } -export function migrateSyncState(state: Partial & Record): SyncState { - const version = state.version ?? 0; +export function migrateSyncState(state: Record): SyncState { + const version = typeof state.version === 'number' ? state.version : 0; if (version === 1) { // Current version, return as-is with defaults return { version: 1, - vault_id: state.vault_id || '', - device_id: state.device_id || '', - device_name: state.device_name || '', - last_server_rev: state.last_server_rev ?? 0, - files: state.files || {}, - tombstones: state.tombstones || {}, + vault_id: typeof state.vault_id === 'string' ? state.vault_id : '', + device_id: typeof state.device_id === 'string' ? state.device_id : '', + device_name: typeof state.device_name === 'string' ? state.device_name : '', + last_server_rev: typeof state.last_server_rev === 'number' ? state.last_server_rev : 0, + files: (state.files && typeof state.files === 'object') ? (state.files as Record) : {}, + tombstones: (state.tombstones && typeof state.tombstones === 'object') ? (state.tombstones as Record) : {}, }; } // Future migrations go here - return createEmptySyncState(state.vault_id || '', state.device_id || '', state.device_name || ''); + return createEmptySyncState( + typeof state.vault_id === 'string' ? state.vault_id : '', + typeof state.device_id === 'string' ? state.device_id : '', + typeof state.device_name === 'string' ? state.device_name : '' + ); } const TOMBSTONE_RETENTION_DAYS = 30; diff --git a/src/sync-queue.ts b/src/sync-queue.ts index 584287c..a5339f6 100644 --- a/src/sync-queue.ts +++ b/src/sync-queue.ts @@ -113,12 +113,13 @@ export class SyncQueue { try { await this.onSync(ops); this.retryCount = 0; - } catch (error: any) { - if (error?.status === 409) { + } catch (error) { + const apiError = error as { status?: number; retry_after?: number } | null; + if (apiError?.status === 409) { throw error; } - if (error?.status === 429 && typeof error?.retry_after === 'number' && !Number.isNaN(error.retry_after)) { - this.lastRetryAfterMs = error.retry_after * 1000; + if (apiError?.status === 429 && typeof apiError.retry_after === 'number' && !Number.isNaN(apiError.retry_after)) { + this.lastRetryAfterMs = apiError.retry_after * 1000; } if (this.isRetryableError(error)) { this.online = false; @@ -192,7 +193,9 @@ export class SyncQueue { return true; } // 429 Too Many Requests is a retryable network condition - if ((error as any).status === 429) { + // 429 Too Many Requests is a retryable network condition + const status = (error as { status?: number }).status; + if (status === 429) { return true; } } @@ -201,11 +204,12 @@ export class SyncQueue { private isRetryableError(error: unknown): boolean { if (this.isNetworkError(error)) return true; - const status = (error as any)?.status; + const apiError = error as { status?: number; message?: string } | null; + const status = apiError?.status; if (status === 429) return true; if (typeof status === 'number' && status >= 500 && status < 600) return true; // Also check message prefix for HTTP status codes - const msg = (error as any)?.message || ''; + const msg = apiError?.message || ''; if (/^HTTP 429:/.test(msg)) return true; if (/^HTTP 5\d\d:/.test(msg)) return true; return false; diff --git a/src/sync.ts b/src/sync.ts index d77573e..e13626e 100644 --- a/src/sync.ts +++ b/src/sync.ts @@ -1,11 +1,11 @@ // Obsidian is a trademark of Dynalist Inc. RustShare is not affiliated with, endorsed by, or sponsored by Obsidian. // This file is part of RustShare Vault Sync. -import { Vault, TFile, normalizePath } from 'obsidian'; +import { Vault, TFile, normalizePath, App } from 'obsidian'; import { RustShareAPI, VaultManifestEntry } from './api'; import { SyncState } from './state'; import { sha256ArrayBuffer, formatConflictFileName, shouldIgnorePath } from './utils'; -import { SyncOperation, SyncOperationType } from './sync-queue'; +import { SyncOperation } from './sync-queue'; import { syncLog } from './sync-log'; export interface SyncResult { @@ -26,13 +26,15 @@ class UploadConflictError extends Error { } } -export class SyncEngine { + private vault: Vault; constructor( - private vault: Vault, + private app: App, private api: RustShareAPI, private state: SyncState, private deviceName: string, - ) {} + ) { + this.vault = app.vault; + } async sync(): Promise { // 1. Scan local files @@ -94,8 +96,7 @@ export class SyncEngine { if (!remote) { // New file — upload try { - const resp = await this.uploadFile(path, 0); - // resp.server_rev is authoritative + await this.uploadFile(path, 0); result.uploaded++; } catch (e) { if (e instanceof UploadConflictError) { @@ -153,8 +154,7 @@ export class SyncEngine { } else if (remote.sha256 === localState.sha256) { // Local changed, remote unchanged — upload try { - const resp = await this.uploadFile(path, remote.server_rev); - // resp.server_rev is authoritative + await this.uploadFile(path, remote.server_rev); result.uploaded++; } catch (e) { if (e instanceof UploadConflictError) { @@ -331,11 +331,12 @@ export class SyncEngine { last_synced_at: new Date().toISOString(), }; result.uploaded++; - } catch (e: any) { - if (e.error === 'conflict') { - const serverSha256 = e.server_sha256; + } catch (e) { + const apiError = e as { error?: string; server_sha256?: string; current_rev?: number } | null; + if (apiError && apiError.error === 'conflict') { + const serverSha256 = apiError.server_sha256; try { - await this.handleUploadConflict(op.path, e.current_rev ?? remote.server_rev, serverSha256); + await this.handleUploadConflict(op.path, apiError.current_rev ?? remote.server_rev, serverSha256); result.conflicts++; } catch (conflictErr) { result.errors.push(`Conflict resolution failed: ${op.path}: ${conflictErr}`); @@ -383,7 +384,7 @@ export class SyncEngine { const batch = allFiles.slice(i, i + batchSize); await Promise.all(batch.map(async (file) => { const path = file.path; - if (shouldIgnorePath(path)) return; + if (shouldIgnorePath(path, this.vault.configDir)) return; if (file instanceof TFile) { const buffer = await this.vault.readBinary(file); const hash = await sha256ArrayBuffer(buffer); @@ -391,7 +392,7 @@ export class SyncEngine { } })); // Yield to event loop - await new Promise(resolve => setTimeout(resolve, 0)); + await new Promise(resolve => window.setTimeout(resolve, 0)); } return files; } @@ -406,9 +407,10 @@ export class SyncEngine { let resp: { server_rev: number }; try { resp = await this.api.uploadFile(this.state.vault_id, path, buffer, hash, baseServerRev, this.state.device_id); - } catch (e: any) { - if (e.error === 'conflict') { - throw new UploadConflictError(path, e.server_sha256, e.current_rev); + } catch (e) { + const apiError = e as { error?: string; server_sha256?: string; current_rev?: number } | null; + if (apiError && apiError.error === 'conflict') { + throw new UploadConflictError(path, apiError.server_sha256, apiError.current_rev); } throw e; } @@ -449,7 +451,7 @@ export class SyncEngine { private async deleteLocalFile(path: string, remoteServerRev?: number): Promise { const file = this.vault.getAbstractFileByPath(normalizePath(path)); if (!(file instanceof TFile)) return; - await this.vault.delete(file); + await this.app.fileManager.trashFile(file); delete this.state.files[path]; if (remoteServerRev !== undefined) { this.state.tombstones[path] = { deleted_at: new Date().toISOString(), server_rev: remoteServerRev }; diff --git a/src/ui/settings-tab.ts b/src/ui/settings-tab.ts index 0cf16f3..f960255 100644 --- a/src/ui/settings-tab.ts +++ b/src/ui/settings-tab.ts @@ -13,7 +13,9 @@ export class RustShareVaultSyncSettingTab extends PluginSettingTab { const { containerEl } = this; containerEl.empty(); - containerEl.createEl('h2', { text: 'RustShare Vault Sync Settings' }); + new Setting(containerEl) + .setName('RustShare Vault Sync Settings') + .setHeading(); // Disclaimer containerEl.createEl('p', { @@ -71,7 +73,9 @@ export class RustShareVaultSyncSettingTab extends PluginSettingTab { await this.plugin.saveSettings(); })); - containerEl.createEl('h3', { text: 'Advanced Sync Settings' }); + new Setting(containerEl) + .setName('Advanced Sync Settings') + .setHeading(); new Setting(containerEl) .setName('Save debounce (ms)') diff --git a/src/ui/status-bar.ts b/src/ui/status-bar.ts index 104127a..ff9d3ce 100644 --- a/src/ui/status-bar.ts +++ b/src/ui/status-bar.ts @@ -1,7 +1,7 @@ // Obsidian is a trademark of Dynalist Inc. RustShare is not affiliated with, endorsed by, or sponsored by Obsidian. // This file is part of RustShare Vault Sync. -import { Plugin, setIcon } from 'obsidian'; +import { Plugin } from 'obsidian'; type SyncStatus = 'disconnected' | 'connected' | 'syncing' | 'synced' | 'conflict' | 'error' | 'offline'; @@ -19,8 +19,8 @@ export class StatusBar { this.currentStatus = status; this.statusBarItemEl.empty(); - const iconEl = this.statusBarItemEl.createEl('span', { cls: 'rustshare-sync-status-icon' }); - const textEl = this.statusBarItemEl.createEl('span', { text: message || this.statusText(status) }); + this.statusBarItemEl.createEl('span', { cls: 'rustshare-sync-status-icon' }); + this.statusBarItemEl.createEl('span', { text: message || this.statusText(status) }); this.statusBarItemEl.removeClass( 'rustshare-sync-status--disconnected', diff --git a/src/utils.ts b/src/utils.ts index 105e604..7a5a997 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -32,17 +32,17 @@ export function formatConflictFileName(originalPath: string, deviceName: string, return `${dir}${name} (RustShare conflicted copy ${safeDeviceName} ${timestamp})${ext}`; } -export const DEFAULT_IGNORED_PATHS = [ - '.obsidian/', - '.git/', - '.DS_Store', - 'node_modules/', - 'Thumbs.db', - '.rustshare-sync-state.json', -]; - -export function shouldIgnorePath(path: string): boolean { - for (const ignored of DEFAULT_IGNORED_PATHS) { +export function shouldIgnorePath(path: string, configDir = '.obsidian'): boolean { + const normalizedConfigDir = configDir.endsWith('/') ? configDir : configDir + '/'; + const ignoredPaths = [ + normalizedConfigDir, + '.git/', + '.DS_Store', + 'node_modules/', + 'Thumbs.db', + '.rustshare-sync-state.json', + ]; + for (const ignored of ignoredPaths) { if (ignored.endsWith('/')) { // Directory patterns: match at start or after a slash if (path.startsWith(ignored) || path.includes('/' + ignored)) return true; diff --git a/tests/mocks/obsidian.ts b/tests/mocks/obsidian.ts index 3f39944..c0b9067 100644 --- a/tests/mocks/obsidian.ts +++ b/tests/mocks/obsidian.ts @@ -26,6 +26,7 @@ export function normalizePath(path: string): string { } export class Vault { + configDir = '.obsidian'; private files = new Map(); private contents = new Map(); @@ -90,8 +91,23 @@ export class Vault { } } +export class FileManager { + constructor(private vault: Vault) {} + async trashFile(file: TAbstractFile): Promise { + await this.vault.delete(file); + } +} + +export class App { + vault: Vault; + fileManager: FileManager; + constructor(vault: Vault) { + this.vault = vault; + this.fileManager = new FileManager(vault); + } +} + export class Plugin {} export class Notice {} -export class App {} export class PluginSettingTab {} export class Setting {} diff --git a/tests/sync-engine.test.ts b/tests/sync-engine.test.ts index 2ef84db..29c17c8 100644 --- a/tests/sync-engine.test.ts +++ b/tests/sync-engine.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect, beforeEach } from 'vitest'; -import { Vault, TFile } from './mocks/obsidian'; +import { Vault, TFile, App } from './mocks/obsidian'; import { SyncEngine } from '../src/sync'; import { createEmptySyncState, SyncState } from '../src/state'; import { VaultManifest, VaultManifestEntry } from '../src/api'; @@ -105,15 +105,17 @@ class MockAPI { describe('SyncEngine', () => { let vault: Vault; + let app: App; let api: MockAPI; let state: SyncState; let engine: SyncEngine; beforeEach(() => { vault = new Vault(); + app = new App(vault); api = new MockAPI(); state = createEmptySyncState('test-vault', 'device-123', 'test-device'); - engine = new SyncEngine(vault, api as any, state, 'test-device'); + engine = new SyncEngine(app as any, api as any, state, 'test-device'); }); describe('full sync', () => {