From 874d2c9f1aeb3a831216e445e7e4858a28f095d2 Mon Sep 17 00:00:00 2001 From: sudacode Date: Thu, 24 Sep 2026 14:02:27 -0700 Subject: [PATCH] fix(hachidori): preserve setup completion while host connects - Reopen setup only when a reachable host has no dictionaries --- src/main/runtime/first-run-setup-service.test.ts | 10 +++++++--- src/main/runtime/first-run-setup-service.ts | 8 ++++++-- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/main/runtime/first-run-setup-service.test.ts b/src/main/runtime/first-run-setup-service.test.ts index db84a203..1b503653 100644 --- a/src/main/runtime/first-run-setup-service.test.ts +++ b/src/main/runtime/first-run-setup-service.test.ts @@ -892,12 +892,13 @@ test('Hachidori setup gates on the linked host instead of local dictionaries', a assert.equal(snapshot.dictionaryCount, 2); assert.equal(snapshot.canFinish, true); assert.equal((await service.markSetupCompleted()).state.status, 'completed'); - host = { kind: 'disconnected', address: 'ws://127.0.0.1:8771/link', message: 'Host offline' }; + // The link is still connecting at startup; that must not undo a finished setup. + host = { kind: 'disconnected', address: 'ws://127.0.0.1:8771/link', message: 'Connecting' }; snapshot = await service.ensureSetupStateInitialized(); assert.equal(snapshot.canFinish, false); assert.equal(snapshot.dictionaryCount, 0); - assert.equal(snapshot.state.status, 'incomplete'); - assert.notEqual((await service.markSetupCompleted()).state.status, 'completed'); + assert.equal(snapshot.state.status, 'completed'); + assert.equal(service.isSetupCompleted(), true); host = { kind: 'connected', address: 'ws://127.0.0.1:8771/link', @@ -905,6 +906,9 @@ test('Hachidori setup gates on the linked host instead of local dictionaries', a dictionaryCount: 0, }; assert.equal((await service.getSetupStatus()).canFinish, false); + // A reachable host with no dictionaries does reopen setup. + assert.equal((await service.ensureSetupStateInitialized()).state.status, 'incomplete'); + assert.notEqual((await service.markSetupCompleted()).state.status, 'completed'); host = { kind: 'local' }; assert.equal((await service.getSetupStatus()).dictionaryCount, 7); assert.equal((await service.getSetupStatus()).canFinish, true); diff --git a/src/main/runtime/first-run-setup-service.ts b/src/main/runtime/first-run-setup-service.ts index 80fb9669..09bb8dfc 100644 --- a/src/main/runtime/first-run-setup-service.ts +++ b/src/main/runtime/first-run-setup-service.ts @@ -448,7 +448,7 @@ export function createFirstRunSetupService(deps: { getSetupStateDictionaryBackend(stored) === getDictionaryBackend() ? stored : writeState(projectState(stored)); - const { configReady, dictionaryCount, externalYomitanConfigured } = + const { configReady, dictionaryCount, externalYomitanConfigured, hachidoriHost } = await resolveYomitanSetupStatus({ configFilePaths, getYomitanDictionaryCount: deps.getYomitanDictionaryCount, @@ -461,7 +461,11 @@ export function createFirstRunSetupService(deps: { dictionaryCount, externalYomitanConfigured, }); - if (isSetupCompleted(state) && canFinish) { + // A linked host is usually still connecting at startup, so an unreachable host + // says nothing about its dictionaries; only a reachable empty host reopens setup. + const hostUnreachable = + hachidoriHost?.kind === 'disconnected' || hachidoriHost?.kind === 'unavailable'; + if (isSetupCompleted(state) && (canFinish || (configReady && hostUnreachable))) { completed = true; return refreshWithState(state); }