mirror of
https://github.com/sotashimozono/obsidian-remote-ssh.git
synced 2026-07-22 17:10:32 +00:00
* test(AdapterManager,ShadowStartupCoordinator): coverage for lifecycle and startup paths AdapterManager.test.ts (17 tests): - PATCHED_METHODS constant: shape, read-side, write-side, bridge, basePath - isPatched() false on fresh instance - dataAdapter getter null on fresh instance - replayOfflineQueue(): early return (both labels) - showPendingEditsModal(): early return with no queue - restore(): idempotent, calls unsubscribe(), clears transferTracker ShadowStartupCoordinator.test.ts (5 tests): - prepareForAutoConnect(): resolves when suggestions absent/empty - does not call saveSettings when there is nothing to install - idempotent on repeated calls Also adds requestUrl stub to __mocks__/obsidian.ts so the coordinator import succeeds without a real Obsidian runtime. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test(shadow): expand ShadowStartupCoordinator coverage to F2 design intent Add 15 tests covering all pendingPluginSuggestions decision branches: - no suggestions (undefined/empty array): no modal, no side effects - decision later: snapshot preserved, no save, no install - decision skip: snapshot cleared, saveSettings called, no install - decision install: installMissing called with selected ids, snapshot cleared, settings saved; edge cases: empty selection, all-failed installs Fixes the previous test file which only covered no-suggestion paths and left the core F2 plugin-suggestion flow (arch §9) entirely untested. --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
139 lines
5.6 KiB
TypeScript
139 lines
5.6 KiB
TypeScript
import { describe, it, expect, vi } from 'vitest';
|
|
import { AdapterManager, PATCHED_METHODS } from '../src/adapter/AdapterManager';
|
|
import type { App, PluginManifest } from 'obsidian';
|
|
import type { ConnectionManager } from '../src/ConnectionManager';
|
|
import type { FsChangeListener } from '../src/vault/FsChangeListener';
|
|
import type { PendingEditsBar } from '../src/ui/PendingEditsBar';
|
|
import type { PluginSettings } from '../src/types';
|
|
|
|
/**
|
|
* Build a minimal AdapterManager with all constructor deps mocked at
|
|
* the typescript type boundary. The deps are only accessed when the
|
|
* adapter is _patched_ — the tests below never call patch(), so the
|
|
* mocks only need the methods that are called on a fresh (un-patched)
|
|
* instance: FsChangeListener.unsubscribe() and ConnectionManager.rpcConnection.
|
|
*/
|
|
function makeManager(opts: { transferTracker?: { clear: () => void } } = {}) {
|
|
const unsubscribeSpy = vi.fn();
|
|
const mgr = new AdapterManager(
|
|
{} as App,
|
|
{ id: 'remote-ssh' } as unknown as PluginManifest,
|
|
{ rpcConnection: null, activeRemoteBasePath: null } as unknown as ConnectionManager,
|
|
{ subscribe: vi.fn(), unsubscribe: unsubscribeSpy } as unknown as FsChangeListener,
|
|
{ startPolling: vi.fn() } as unknown as PendingEditsBar,
|
|
() => ({}) as unknown as PluginSettings,
|
|
opts.transferTracker ?? null,
|
|
);
|
|
return { mgr, unsubscribeSpy };
|
|
}
|
|
|
|
// ─── PATCHED_METHODS ─────────────────────────────────────────────────────────
|
|
|
|
describe('PATCHED_METHODS constant', () => {
|
|
it('is a non-empty array', () => {
|
|
expect(PATCHED_METHODS.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('includes core read-side methods', () => {
|
|
expect(PATCHED_METHODS).toContain('exists');
|
|
expect(PATCHED_METHODS).toContain('stat');
|
|
expect(PATCHED_METHODS).toContain('list');
|
|
expect(PATCHED_METHODS).toContain('read');
|
|
expect(PATCHED_METHODS).toContain('readBinary');
|
|
});
|
|
|
|
it('includes core write-side methods', () => {
|
|
expect(PATCHED_METHODS).toContain('write');
|
|
expect(PATCHED_METHODS).toContain('writeBinary');
|
|
expect(PATCHED_METHODS).toContain('mkdir');
|
|
expect(PATCHED_METHODS).toContain('remove');
|
|
expect(PATCHED_METHODS).toContain('rename');
|
|
expect(PATCHED_METHODS).toContain('copy');
|
|
});
|
|
|
|
it('includes getResourcePath for the binary bridge', () => {
|
|
expect(PATCHED_METHODS).toContain('getResourcePath');
|
|
});
|
|
|
|
it('includes basePath / getBasePath for shadow-vault compatibility', () => {
|
|
expect(PATCHED_METHODS).toContain('basePath');
|
|
expect(PATCHED_METHODS).toContain('getBasePath');
|
|
});
|
|
});
|
|
|
|
// ─── initial state ────────────────────────────────────────────────────────
|
|
|
|
describe('AdapterManager — initial state', () => {
|
|
it('isPatched() returns false before patch() is called', () => {
|
|
const { mgr } = makeManager();
|
|
expect(mgr.isPatched()).toBe(false);
|
|
});
|
|
|
|
it('dataAdapter getter returns null before patch() is called', () => {
|
|
const { mgr } = makeManager();
|
|
expect(mgr.dataAdapter).toBeNull();
|
|
});
|
|
});
|
|
|
|
// ─── replayOfflineQueue ────────────────────────────────────────────────────
|
|
|
|
describe('AdapterManager.replayOfflineQueue()', () => {
|
|
it('returns without throwing when offlineQueue is null', async () => {
|
|
const { mgr } = makeManager();
|
|
await expect(mgr.replayOfflineQueue('after-connect')).resolves.toBeUndefined();
|
|
});
|
|
|
|
it('returns without throwing for the after-reconnect label', async () => {
|
|
const { mgr } = makeManager();
|
|
await expect(mgr.replayOfflineQueue('after-reconnect')).resolves.toBeUndefined();
|
|
});
|
|
});
|
|
|
|
// ─── showPendingEditsModal ─────────────────────────────────────────────────
|
|
|
|
describe('AdapterManager.showPendingEditsModal()', () => {
|
|
it('returns without throwing when no offline queue is open', async () => {
|
|
const { mgr } = makeManager();
|
|
await expect(mgr.showPendingEditsModal()).resolves.toBeUndefined();
|
|
});
|
|
});
|
|
|
|
// ─── restore ──────────────────────────────────────────────────────────────
|
|
|
|
describe('AdapterManager.restore()', () => {
|
|
it('does not throw when called before patch()', () => {
|
|
const { mgr } = makeManager();
|
|
expect(() => mgr.restore()).not.toThrow();
|
|
});
|
|
|
|
it('calls fsChangeListener.unsubscribe() once', () => {
|
|
const { mgr, unsubscribeSpy } = makeManager();
|
|
mgr.restore();
|
|
expect(unsubscribeSpy).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it('leaves isPatched() false after restore()', () => {
|
|
const { mgr } = makeManager();
|
|
mgr.restore();
|
|
expect(mgr.isPatched()).toBe(false);
|
|
});
|
|
|
|
it('leaves dataAdapter null after restore()', () => {
|
|
const { mgr } = makeManager();
|
|
mgr.restore();
|
|
expect(mgr.dataAdapter).toBeNull();
|
|
});
|
|
|
|
it('is idempotent — two calls do not throw', () => {
|
|
const { mgr } = makeManager();
|
|
expect(() => mgr.restore()).not.toThrow();
|
|
expect(() => mgr.restore()).not.toThrow();
|
|
});
|
|
|
|
it('calls transferTracker.clear() when one is supplied', () => {
|
|
const clearSpy = vi.fn();
|
|
const { mgr } = makeManager({ transferTracker: { clear: clearSpy } });
|
|
mgr.restore();
|
|
expect(clearSpy).toHaveBeenCalledOnce();
|
|
});
|
|
});
|