feat(sync-ui): main-process runtime, window, tray entry, CLI wiring for sync window

This commit is contained in:
2026-07-11 18:07:48 -07:00
parent 0a3f76c0a8
commit 97aaf44b3c
37 changed files with 1351 additions and 7 deletions
@@ -0,0 +1,121 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { EventEmitter } from 'node:events';
import type { SyncProgressEvent } from '../../shared/sync/sync-events';
import { runSyncLauncher, type SyncLauncherSpawn } from './sync-launcher-client';
class FakeChild extends EventEmitter {
stdout = new EventEmitter();
stderr = new EventEmitter();
killed = false;
kill(): boolean {
this.killed = true;
this.emit('close', null, 'SIGTERM');
return true;
}
}
function makeSpawn(): { spawn: SyncLauncherSpawn; children: FakeChild[]; commands: string[][] } {
const children: FakeChild[] = [];
const commands: string[][] = [];
const spawn: SyncLauncherSpawn = (command, args) => {
commands.push([command, ...args]);
const child = new FakeChild();
children.push(child);
return child as never;
};
return { spawn, children, commands };
}
test('runSyncLauncher parses NDJSON events across chunk boundaries', async () => {
const { spawn, children, commands } = makeSpawn();
const events: SyncProgressEvent[] = [];
const handle = runSyncLauncher({
command: ['subminer'],
args: ['sync', 'media-box', '--json'],
onEvent: (event) => events.push(event),
spawn,
});
const child = children[0]!;
child.stdout.emit('data', Buffer.from('{"type":"stage","stage":"snapshot-'));
child.stdout.emit('data', Buffer.from('local","message":"Snapshotting"}\n{"type":"result",'));
child.stdout.emit('data', Buffer.from('"ok":true,"error":null}\n'));
child.emit('close', 0, null);
const result = await handle.done;
assert.deepEqual(commands[0], ['subminer', 'sync', 'media-box', '--json']);
assert.equal(events.length, 2);
assert.equal(events[0]!.type, 'stage');
assert.deepEqual(events[1], { type: 'result', ok: true, error: null });
assert.deepEqual(result, { ok: true, error: null });
});
test('runSyncLauncher reports failures with the result event error or stderr tail', async () => {
const { spawn, children } = makeSpawn();
const handle = runSyncLauncher({
command: ['subminer'],
args: ['sync', 'media-box', '--json'],
onEvent: () => {},
spawn,
});
const child = children[0]!;
child.stdout.emit(
'data',
Buffer.from('{"type":"result","ok":false,"error":"Remote merge failed"}\n'),
);
child.stderr.emit('data', Buffer.from('[ERROR] Remote merge failed\n'));
child.emit('close', 1, null);
const result = await handle.done;
assert.equal(result.ok, false);
assert.match(result.error ?? '', /Remote merge failed/);
});
test('runSyncLauncher falls back to stderr when no result event arrives', async () => {
const { spawn, children } = makeSpawn();
const handle = runSyncLauncher({
command: ['subminer'],
args: ['sync', '--snapshot', '/tmp/x.sqlite', '--json'],
onEvent: () => {},
spawn,
});
const child = children[0]!;
child.stderr.emit('data', Buffer.from('[ERROR] boom\n'));
child.emit('close', 1, null);
const result = await handle.done;
assert.equal(result.ok, false);
assert.match(result.error ?? '', /boom/);
});
test('runSyncLauncher cancel kills the child and resolves as cancelled', async () => {
const { spawn, children } = makeSpawn();
const handle = runSyncLauncher({
command: ['subminer'],
args: ['sync', 'media-box', '--json'],
onEvent: () => {},
spawn,
});
handle.cancel();
assert.equal(children[0]!.killed, true);
const result = await handle.done;
assert.equal(result.ok, false);
assert.match(result.error ?? '', /cancel/i);
});
test('runSyncLauncher surfaces spawn errors', async () => {
const { spawn, children } = makeSpawn();
const handle = runSyncLauncher({
command: ['subminer'],
args: ['sync', 'media-box', '--json'],
onEvent: () => {},
spawn,
});
children[0]!.emit('error', new Error('ENOENT: subminer not found'));
const result = await handle.done;
assert.equal(result.ok, false);
assert.match(result.error ?? '', /ENOENT/);
});