mirror of
https://github.com/sotashimozono/obsidian-remote-ssh.git
synced 2026-07-22 06:52:07 +00:00
Phase B/3. Until now the shadow window booted on its STALE local `.obsidian/` and only pulled the remote config (#342), plugin list (#434) and binaries (B/2) AFTER Obsidian had already read settings at startup — so a setting changed on another machine was a session late. openShadowVaultFor now runs a best-effort, time-boxed pre-spawn pull between bootstrap and spawn (via a new ShadowVaultManager onBootstrapped hook). preSpawnPull builds a STANDALONE SftpClient that does NOT patch the source window's vault adapter (so the user's real vault is never hijacked), pulls shared config + plugin list + binaries into the shadow dir, then disconnects. The window then boots on canonical config — no mid-session reload. Safety: - non-interactive by design: the kbd-interactive + host-key callbacks REJECT instead of prompting, so a 2FA / unknown-host profile falls through to the shadow window (which prompts exactly once) — no double prompt, no surprise modal in the source window - time-boxed (connectTimeoutMs + 8s) via new withTimeout util; a slow link falls back to spawn-and-catch-up - the manager swallows any hook throw, so the spawn NEVER blocks - disconnects in finally; first connect to an unknown host falls back and the next (host now trusted) gets the canonical boot Tests: withTimeout (4) + ShadowVaultManager hook order/fallback (2). The actual SSH connect + canonical-boot effect needs a manual Obsidian smoke (can't run headless) — the fallback path IS unit-tested. Refs #429 #342 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
31 lines
1.3 KiB
TypeScript
31 lines
1.3 KiB
TypeScript
import { describe, it, expect, vi, afterEach } from 'vitest';
|
|
import { withTimeout } from '../src/util/withTimeout';
|
|
|
|
describe('withTimeout', () => {
|
|
afterEach(() => { vi.useRealTimers(); });
|
|
|
|
it('resolves with the value when the promise settles before the timeout', async () => {
|
|
await expect(withTimeout(Promise.resolve('ok'), 1000, 'x')).resolves.toBe('ok');
|
|
});
|
|
|
|
it('propagates the promise rejection when it rejects before the timeout', async () => {
|
|
await expect(withTimeout(Promise.reject(new Error('boom')), 1000, 'x')).rejects.toThrow('boom');
|
|
});
|
|
|
|
it('rejects with a labelled timeout error when the promise is too slow', async () => {
|
|
vi.useFakeTimers();
|
|
const slow = new Promise<string>(() => { /* never settles */ });
|
|
const p = withTimeout(slow, 5000, 'pre-spawn pull');
|
|
const assertion = expect(p).rejects.toThrow('pre-spawn pull timed out after 5000ms');
|
|
await vi.advanceTimersByTimeAsync(5000);
|
|
await assertion;
|
|
});
|
|
|
|
it('clears the timer on a fast resolve so a later tick raises no timeout', async () => {
|
|
vi.useFakeTimers();
|
|
const p = withTimeout(Promise.resolve('fast'), 5000, 'x');
|
|
await expect(p).resolves.toBe('fast');
|
|
// Past the budget the cleared timer must not fire a stray rejection.
|
|
await vi.advanceTimersByTimeAsync(10_000);
|
|
});
|
|
});
|