From c673b710530dd35eeae4423044b7a2d0d3d6ae06 Mon Sep 17 00:00:00 2001 From: sudacode Date: Tue, 22 Sep 2026 22:08:51 -0700 Subject: [PATCH] fix(dictionary): restore Anki proxy marker when Hachidori sync fails - Revert the proxy marker to its previous value when every settings write attempt fails, so it no longer claims a server that was never written - Add a test for the all-writes-fail path - Retry Windows managed runtime cleanup in the test while bun.exe stays locked after exit --- .../tokenizer/hachidori-anki-settings.test.ts | 10 +++++++++- .../tokenizer/hachidori-anki-settings.ts | 6 +++++- src/main/runtime/managed-launcher.test.ts | 19 +++++++++++++------ 3 files changed, 27 insertions(+), 8 deletions(-) diff --git a/src/core/services/tokenizer/hachidori-anki-settings.test.ts b/src/core/services/tokenizer/hachidori-anki-settings.test.ts index 2caccf46..236941b2 100644 --- a/src/core/services/tokenizer/hachidori-anki-settings.test.ts +++ b/src/core/services/tokenizer/hachidori-anki-settings.test.ts @@ -36,11 +36,12 @@ async function harness( vm.runInContext( ` let options = { ...HDReaderOptions.normaliseOptions({ anki: __initialAnki }), revision: 1 }; - let writes = 0, online = true, race = false, proxy = null; + let writes = 0, online = true, race = false, proxy = null, failWrites = false; const readOptions = async () => structuredClone(options); globalThis.__subminerSetAnkiProxyUrl = async value => { const old = proxy; proxy = value; return old; }; const send = async (type, request, target) => { if (type !== 'hd_options_write' || target !== 'hoshidicts-worker') throw Error('Unexpected request'); + if (failWrites) throw Error('offline'); if (race) { race = false; options.anki.templates[0].tags = options.anki.tags = ['User edit']; @@ -164,6 +165,13 @@ test('re-reads concurrent settings edits before retrying its revisioned write', assert.equal(await h.run('options.anki.model'), 'Japanese'); }); +test('restores the previous proxy marker when every settings write fails', async () => { + const h = await harness(); + await h.run(`proxy = 'http://127.0.0.1:9000'; failWrites = true`); + await assert.rejects(h.sync(), /offline/); + assert.equal(await h.run('proxy'), 'http://127.0.0.1:9000'); +}); + test('keeps sentence audio out of captured-audio settings and uses wordAudio first', () => { const hints = buildHachidoriAnkiHints({ fields: { audio: 'SentenceAudio', wordAudio: 'WordAudio' }, diff --git a/src/core/services/tokenizer/hachidori-anki-settings.ts b/src/core/services/tokenizer/hachidori-anki-settings.ts index b9e448c2..b579e7c4 100644 --- a/src/core/services/tokenizer/hachidori-anki-settings.ts +++ b/src/core/services/tokenizer/hachidori-anki-settings.ts @@ -112,7 +112,11 @@ export const HACHIDORI_ANKI_SETTINGS_SCRIPT = String.raw` }, 'hoshidicts-worker'); return { updated: changed, matched: !pending, pending }; } catch (error) { - if (attempt > 0) throw error; + if (attempt > 0) { + // The server was never written, so the marker must not claim it. + await globalThis.__subminerSetAnkiProxyUrl(previousProxy); + throw error; + } // Re-read after a concurrent settings save before filling anything. } } diff --git a/src/main/runtime/managed-launcher.test.ts b/src/main/runtime/managed-launcher.test.ts index fa1af02d..f11d9a67 100644 --- a/src/main/runtime/managed-launcher.test.ts +++ b/src/main/runtime/managed-launcher.test.ts @@ -372,12 +372,19 @@ test('Windows stages a new runtime version while the prior Bun executable is run await exited; } } - cleanupOldWindowsManagedRuntimes({ - platform: 'win32', - localAppData: root, - appVersion: '2.0.0', - }); - assert.equal(fs.existsSync(path.dirname(first.bunPath)), false); + // Windows can keep bun.exe locked briefly after the exit event; cleanup + // skips locked runtimes, so retry the way the next launch would. + const firstDirectory = path.dirname(first.bunPath); + for (let attempt = 0; attempt < 30; attempt += 1) { + cleanupOldWindowsManagedRuntimes({ + platform: 'win32', + localAppData: root, + appVersion: '2.0.0', + }); + if (!fs.existsSync(firstDirectory)) break; + await new Promise((resolve) => setTimeout(resolve, 100)); + } + assert.equal(fs.existsSync(firstDirectory), false); assert.ok(fs.existsSync(expectedSecond.bunPath)); });