sotashimozono_obsidian-remo.../plugin/tests/AdapterManager.test.ts
sotashimozono 358bf9fc06
test(AdapterManager,ShadowStartupCoordinator): coverage for lifecycle and F2 startup paths (#267)
* 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>
2026-05-09 22:31:42 +09:00

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();
});
});