Files
SubMiner/src/shared/sync/sync-hosts-store.test.ts
T

183 lines
5.9 KiB
TypeScript

import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import {
createDefaultSyncHostsState,
getSyncHostsPath,
isValidSyncHost,
normalizeSyncHostsState,
readSyncHostsState,
recordSyncResult,
removeSyncHost,
upsertSyncHost,
writeSyncHostsState,
} from './sync-hosts-store';
function withTempDir(fn: (dir: string) => void): void {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-sync-hosts-test-'));
try {
fn(dir);
} finally {
fs.rmSync(dir, { recursive: true, force: true });
}
}
test('createDefaultSyncHostsState returns empty v1 state', () => {
assert.deepEqual(createDefaultSyncHostsState(), {
version: 1,
autoSyncIntervalMinutes: 30,
hosts: [],
});
});
test('isValidSyncHost accepts ssh destinations and rejects unsafe values', () => {
assert.equal(isValidSyncHost('desktop'), true);
assert.equal(isValidSyncHost('user@10.0.0.5'), true);
assert.equal(isValidSyncHost('my-alias.local'), true);
assert.equal(isValidSyncHost(''), false);
assert.equal(isValidSyncHost(' '), false);
assert.equal(isValidSyncHost('-oProxyCommand=evil'), false);
assert.equal(isValidSyncHost('host with spaces'), false);
});
test('upsertSyncHost adds a new host with defaults and preserves fields on update', () => {
const state = createDefaultSyncHostsState();
const added = upsertSyncHost(state, { host: 'user@laptop' }, 1000);
assert.deepEqual(added.hosts, [
{
host: 'user@laptop',
label: null,
direction: 'both',
autoSync: false,
addedAtMs: 1000,
lastSyncAtMs: null,
lastSyncStatus: null,
lastSyncDetail: null,
},
]);
// original state untouched
assert.equal(state.hosts.length, 0);
const synced = recordSyncResult(added, 'user@laptop', {
atMs: 2000,
status: 'success',
detail: 'Sessions merged: 3',
});
const updated = upsertSyncHost(synced, { host: 'user@laptop', direction: 'push' }, 3000);
assert.equal(updated.hosts.length, 1);
const entry = updated.hosts[0]!;
assert.equal(entry.direction, 'push');
assert.equal(entry.addedAtMs, 1000);
assert.equal(entry.lastSyncAtMs, 2000);
assert.equal(entry.lastSyncStatus, 'success');
assert.equal(entry.lastSyncDetail, 'Sessions merged: 3');
});
test('upsertSyncHost trims host and throws on invalid host', () => {
const state = upsertSyncHost(createDefaultSyncHostsState(), { host: ' desktop ' }, 1);
assert.equal(state.hosts[0]!.host, 'desktop');
assert.throws(() => upsertSyncHost(state, { host: '-bad' }, 2));
});
test('removeSyncHost removes only the matching host', () => {
let state = createDefaultSyncHostsState();
state = upsertSyncHost(state, { host: 'a' }, 1);
state = upsertSyncHost(state, { host: 'b' }, 2);
const removed = removeSyncHost(state, 'a');
assert.deepEqual(
removed.hosts.map((entry) => entry.host),
['b'],
);
assert.equal(removeSyncHost(removed, 'missing').hosts.length, 1);
});
test('recordSyncResult upserts unknown hosts so CLI syncs are remembered', () => {
const state = recordSyncResult(createDefaultSyncHostsState(), 'user@server', {
atMs: 5000,
status: 'error',
detail: 'Remote merge failed',
});
assert.equal(state.hosts.length, 1);
const entry = state.hosts[0]!;
assert.equal(entry.host, 'user@server');
assert.equal(entry.addedAtMs, 5000);
assert.equal(entry.lastSyncAtMs, 5000);
assert.equal(entry.lastSyncStatus, 'error');
assert.equal(entry.lastSyncDetail, 'Remote merge failed');
});
test('normalizeSyncHostsState drops invalid entries and falls back to defaults', () => {
assert.deepEqual(normalizeSyncHostsState(null), createDefaultSyncHostsState());
assert.deepEqual(normalizeSyncHostsState('junk'), createDefaultSyncHostsState());
assert.deepEqual(normalizeSyncHostsState({ version: 99 }), createDefaultSyncHostsState());
const normalized = normalizeSyncHostsState({
version: 1,
autoSyncIntervalMinutes: 15,
hosts: [
{
host: 'desktop',
label: 'Desktop PC',
direction: 'pull',
autoSync: true,
addedAtMs: 10,
lastSyncAtMs: 20,
lastSyncStatus: 'success',
lastSyncDetail: 'ok',
},
{ host: '-bad', direction: 'both', autoSync: false, addedAtMs: 1 },
{ host: 'dupe', direction: 'sideways', autoSync: false, addedAtMs: 1 },
'not-an-object',
],
});
assert.equal(normalized.autoSyncIntervalMinutes, 15);
assert.deepEqual(normalized.hosts, [
{
host: 'desktop',
label: 'Desktop PC',
direction: 'pull',
autoSync: true,
addedAtMs: 10,
lastSyncAtMs: 20,
lastSyncStatus: 'success',
lastSyncDetail: 'ok',
},
{
host: 'dupe',
label: null,
direction: 'both',
autoSync: false,
addedAtMs: 1,
lastSyncAtMs: null,
lastSyncStatus: null,
lastSyncDetail: null,
},
]);
});
test('read/write round-trips state and tolerates missing or corrupt files', () => {
withTempDir((root) => {
const filePath = getSyncHostsPath(root);
assert.equal(filePath, path.join(root, 'sync-hosts.json'));
assert.deepEqual(readSyncHostsState(filePath), createDefaultSyncHostsState());
fs.writeFileSync(filePath, '{corrupt');
assert.deepEqual(readSyncHostsState(filePath), createDefaultSyncHostsState());
let state = createDefaultSyncHostsState();
state = upsertSyncHost(state, { host: 'user@laptop', label: 'Laptop' }, 42);
writeSyncHostsState(filePath, state);
assert.deepEqual(readSyncHostsState(filePath), state);
});
});
test('writeSyncHostsState creates parent directories', () => {
withTempDir((root) => {
const filePath = path.join(root, 'nested', 'dir', 'sync-hosts.json');
writeSyncHostsState(filePath, createDefaultSyncHostsState());
assert.deepEqual(readSyncHostsState(filePath), createDefaultSyncHostsState());
});
});