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
This commit is contained in:
2026-09-22 22:08:51 -07:00
parent d31a28c54f
commit c673b71053
3 changed files with 27 additions and 8 deletions
@@ -36,11 +36,12 @@ async function harness(
vm.runInContext( vm.runInContext(
` `
let options = { ...HDReaderOptions.normaliseOptions({ anki: __initialAnki }), revision: 1 }; 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); const readOptions = async () => structuredClone(options);
globalThis.__subminerSetAnkiProxyUrl = async value => { const old = proxy; proxy = value; return old; }; globalThis.__subminerSetAnkiProxyUrl = async value => { const old = proxy; proxy = value; return old; };
const send = async (type, request, target) => { const send = async (type, request, target) => {
if (type !== 'hd_options_write' || target !== 'hoshidicts-worker') throw Error('Unexpected request'); if (type !== 'hd_options_write' || target !== 'hoshidicts-worker') throw Error('Unexpected request');
if (failWrites) throw Error('offline');
if (race) { if (race) {
race = false; race = false;
options.anki.templates[0].tags = options.anki.tags = ['User edit']; 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'); 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', () => { test('keeps sentence audio out of captured-audio settings and uses wordAudio first', () => {
const hints = buildHachidoriAnkiHints({ const hints = buildHachidoriAnkiHints({
fields: { audio: 'SentenceAudio', wordAudio: 'WordAudio' }, fields: { audio: 'SentenceAudio', wordAudio: 'WordAudio' },
@@ -112,7 +112,11 @@ export const HACHIDORI_ANKI_SETTINGS_SCRIPT = String.raw`
}, 'hoshidicts-worker'); }, 'hoshidicts-worker');
return { updated: changed, matched: !pending, pending }; return { updated: changed, matched: !pending, pending };
} catch (error) { } 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. // Re-read after a concurrent settings save before filling anything.
} }
} }
+8 -1
View File
@@ -372,12 +372,19 @@ test('Windows stages a new runtime version while the prior Bun executable is run
await exited; await exited;
} }
} }
// 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({ cleanupOldWindowsManagedRuntimes({
platform: 'win32', platform: 'win32',
localAppData: root, localAppData: root,
appVersion: '2.0.0', appVersion: '2.0.0',
}); });
assert.equal(fs.existsSync(path.dirname(first.bunPath)), false); if (!fs.existsSync(firstDirectory)) break;
await new Promise((resolve) => setTimeout(resolve, 100));
}
assert.equal(fs.existsSync(firstDirectory), false);
assert.ok(fs.existsSync(expectedSecond.bunPath)); assert.ok(fs.existsSync(expectedSecond.bunPath));
}); });