diff --git a/chrome/content/zotero/xpcom/embeddings.js b/chrome/content/zotero/xpcom/embeddings.js index 8555796a0f..1e764fe9a2 100644 --- a/chrome/content/zotero/xpcom/embeddings.js +++ b/chrome/content/zotero/xpcom/embeddings.js @@ -144,6 +144,20 @@ Zotero.Embeddings = new function () { return Object.prototype.hasOwnProperty.call(MODELS, this.getModelName()); }; + /** + * Disable semantic search if the selected model isn't one this build has: + * a model removed from MODELS after a user chose it leaves the pref naming + * something nothing can run. Clearing the pref switches to Disabled, which + * drops the stored index and the downloaded files with it. + */ + this.ensureModelAvailable = function () { + let name = this.getModelName(); + if (name && !this.isEnabled()) { + Zotero.debug(`Embeddings: model '${name}' is no longer available -- disabling semantic search`); + Zotero.Prefs.clear('embeddings.model'); + } + }; + /** * Available models, in display order, for the preferences UI. * @@ -1792,6 +1806,9 @@ Zotero.Embeddings.Indexing = new function () { Zotero.Prefs.registerObserver('embeddings.model', () => { _switchModel(); }); + // Through the observer above, a model this build no longer has is + // switched off, and its index and files dropped + Zotero.Embeddings.ensureModelAvailable(); // Toggling fulltext indexing changes what's eligible: on, the newly // eligible attachments get indexed; off, their stored chunks are diff --git a/test/tests/collectionViewItemTreeTest.js b/test/tests/collectionViewItemTreeTest.js index 91c88f5cc8..34fe5e1c1e 100644 --- a/test/tests/collectionViewItemTreeTest.js +++ b/test/tests/collectionViewItemTreeTest.js @@ -219,37 +219,45 @@ describe("CollectionViewItemTree", function () { }); it("should rank items lexically", async function () { - let col = await createDataObject('collection'); - // Unrelated items, so the query words the matches share still - // separate documents in this corpus -- in a corpus of nothing - // but matches, FTS5 floors their idf as separating nothing - // and no match earns a score - for (let i = 0; i < 6; i++) { - await createDataObject('item', { title: `unrelated filler number ${i}` }); + // Ranking is under test, not membership: keep the two-word + // match that the result margin would otherwise cut + Zotero.Prefs.set('search.bestMatchMargin', 100); + try { + let col = await createDataObject('collection'); + // Unrelated items, so the query words the matches share still + // separate documents in this corpus -- in a corpus of nothing + // but matches, FTS5 floors their idf as separating nothing + // and no match earns a score + for (let i = 0; i < 6; i++) { + await createDataObject('item', { title: `unrelated filler number ${i}` }); + } + let full = await createDataObject('item', + { title: 'Lexint owl migration patterns', collections: [col.id] }); + // Three of the query's four terms, ranked below the full match + let partial = await createDataObject('item', + { title: 'Lexint owl migration handbook', collections: [col.id] }); + // Two of four, ranked below both + let sparse = await createDataObject('item', + { title: 'Lexint owl guidebook', collections: [col.id] }); + + await select(win, col); + let itemsView = zp.itemsView; + await itemsView.setFilter('search', 'lexint owl migration patterns'); + + // Scored items only, ranked by coverage, most relevant first + assert.deepEqual(itemsView._rows.map(row => row.id), + [full.id, partial.id, sparse.id]); + assert.equal(itemsView.getSortField(), 'relevance'); + // The bars carry the lexical scores directly + let fractions = itemsView.rowProvider.getBestMatchBarFractions(); + assert.isAbove(fractions.get(full.id), fractions.get(partial.id)); + assert.isAbove(fractions.get(partial.id), fractions.get(sparse.id)); + assert.isAtMost(fractions.get(full.id), 1); + assert.isAbove(fractions.get(sparse.id), 0); + } + finally { + Zotero.Prefs.clear('search.bestMatchMargin'); } - let full = await createDataObject('item', - { title: 'Lexint owl migration patterns', collections: [col.id] }); - // Three of the query's four terms, ranked below the full match - let partial = await createDataObject('item', - { title: 'Lexint owl migration handbook', collections: [col.id] }); - // Two of four, ranked below both - let sparse = await createDataObject('item', - { title: 'Lexint owl guidebook', collections: [col.id] }); - - await select(win, col); - let itemsView = zp.itemsView; - await itemsView.setFilter('search', 'lexint owl migration patterns'); - - // Scored items only, ranked by coverage, most relevant first - assert.deepEqual(itemsView._rows.map(row => row.id), - [full.id, partial.id, sparse.id]); - assert.equal(itemsView.getSortField(), 'relevance'); - // The bars carry the lexical scores directly - let fractions = itemsView.rowProvider.getBestMatchBarFractions(); - assert.isAbove(fractions.get(full.id), fractions.get(partial.id)); - assert.isAbove(fractions.get(partial.id), fractions.get(sparse.id)); - assert.isAtMost(fractions.get(full.id), 1); - assert.isAbove(fractions.get(sparse.id), 0); }); }); diff --git a/test/tests/embeddingsTest.js b/test/tests/embeddingsTest.js index d95c505dff..ae0997dc58 100644 --- a/test/tests/embeddingsTest.js +++ b/test/tests/embeddingsTest.js @@ -572,8 +572,9 @@ describe("Zotero.Embeddings", function () { (x, i) => `${tag} sentence number ${i} with several words in it.`).join(' '); var stubs = []; + // A model with a 512-token window, which the fixtures are sized to beforeEach(function () { - stubs.push(sinon.stub(Zotero.Embeddings, 'getModelName').returns('bge-small-en-v1.5')); + stubs.push(sinon.stub(Zotero.Embeddings, 'getModelName').returns('bge-small-zh-v1.5')); }); afterEach(function () { @@ -728,8 +729,9 @@ describe("Zotero.Embeddings", function () { describe("#chunkSections()", function () { var stubs = []; + // A model with a 512-token window, which the fixtures are sized to beforeEach(function () { - stubs.push(sinon.stub(Zotero.Embeddings, 'getModelName').returns('bge-small-en-v1.5')); + stubs.push(sinon.stub(Zotero.Embeddings, 'getModelName').returns('bge-small-zh-v1.5')); }); afterEach(function () { @@ -1007,7 +1009,7 @@ describe("Zotero.Embeddings", function () { sinon.stub(Zotero.Embeddings, 'pruneModels').resolves(), embedStub ]; - Zotero.Prefs.set('embeddings.model', 'bge-small-en-v1.5'); + Zotero.Prefs.set('embeddings.model', 'bekko-embedding-v1-a8m'); try { await Zotero.Embeddings.Indexing.waitForPendingModelSwitch(); assert.ok(await getPromiseError(Zotero.Embeddings.embedQuery('retry query'))); @@ -1031,7 +1033,7 @@ describe("Zotero.Embeddings", function () { sinon.stub(Zotero.Embeddings, 'pruneModels').resolves(), embedStub ]; - Zotero.Prefs.set('embeddings.model', 'bge-small-en-v1.5'); + Zotero.Prefs.set('embeddings.model', 'bekko-embedding-v1-a8m'); try { await Zotero.Embeddings.Indexing.waitForPendingModelSwitch(); // Whitespace around the quotes doesn't defeat the stripping @@ -1058,7 +1060,7 @@ describe("Zotero.Embeddings", function () { ]; // Select a model so the query prefix and model version resolve; the // switch's indexing side effects are stubbed out above - Zotero.Prefs.set('embeddings.model', 'bge-small-en-v1.5'); + Zotero.Prefs.set('embeddings.model', 'bekko-embedding-v1-a8m'); try { await Zotero.Embeddings.Indexing.waitForPendingModelSwitch(); let promise1 = Zotero.Embeddings.embedQuery('concurrent query'); @@ -1095,7 +1097,7 @@ describe("Zotero.Embeddings", function () { createEngine, sinon.stub(Zotero.ML, 'shutdown').resolves(), sinon.stub(Zotero.ML, 'getOptimalConcurrency').returns(2), - sinon.stub(Zotero.Embeddings, 'getModelName').returns('bge-small-en-v1.5'), + sinon.stub(Zotero.Embeddings, 'getModelName').returns('bekko-embedding-v1-a8m'), sinon.stub(Zotero.Embeddings, 'getModelVersion').returns('test-model/1') ]; } @@ -1237,7 +1239,7 @@ describe("Zotero.Embeddings", function () { sinon.stub(Zotero.Embeddings, 'getModelVersion').returns('kept-model/1'), sinon.stub(Zotero.Embeddings.Indexing, 'startIndexing').resolves() ]; - Zotero.Prefs.set('embeddings.model', 'bge-small-en-v1.5'); + Zotero.Prefs.set('embeddings.model', 'bekko-embedding-v1-a8m'); try { await Zotero.Embeddings.Indexing.waitForPendingModelSwitch(); await Zotero.Embeddings.pruneModels(); @@ -1264,6 +1266,48 @@ describe("Zotero.Embeddings", function () { }); }); + describe("#ensureModelAvailable()", function () { + it("should disable semantic search and drop the index when the selected model no longer exists", async function () { + let stubs = [ + sinon.stub(Zotero.ML, 'listModels').resolves([]), + sinon.stub(Zotero.Embeddings.Indexing, 'startIndexing').resolves() + ]; + let item = await createDataObject('item'); + try { + // As if a build removed the model a user had selected + Zotero.Prefs.set('embeddings.model', 'removed-model'); + await Zotero.Embeddings.Indexing.waitForPendingModelSwitch(); + await Zotero.Embeddings.initDB(); + await Zotero.DB.queryAsync( + "REPLACE INTO embeddings.itemEmbeddings " + + "(itemID, chunkIndex, embedding, sourceHash) VALUES (?, 0, ?, ?)", + [item.id, new Uint8Array([0, 0, 0, 0]), 'hash'] + ); + assert.isFalse(Zotero.Embeddings.isEnabled()); + + Zotero.Embeddings.ensureModelAvailable(); + await Zotero.Embeddings.Indexing.waitForPendingModelSwitch(); + assert.equal(Zotero.Embeddings.getModelName(), ''); + assert.equal( + await Zotero.DB.valueQueryAsync("SELECT COUNT(*) FROM embeddings.itemEmbeddings"), + 0 + ); + // Nothing to do for a model that exists, or none + Zotero.Embeddings.ensureModelAvailable(); + assert.equal(Zotero.Embeddings.getModelName(), ''); + } + finally { + Zotero.Prefs.clear('embeddings.model'); + await Zotero.Embeddings.Indexing.waitForPendingModelSwitch(); + stubs.forEach(stub => stub.restore()); + Zotero.Prefs.clear('embeddings.indexingPaused'); + // The disabled-model prune cleared the row the rest of the + // file scores against + testMean = await calibrateTestModel(); + } + }); + }); + describe("Diagnostics", function () { it("should estimate remaining time once the window spans long enough", function () { let diagnostics = Zotero.Embeddings.Diagnostics; @@ -1610,7 +1654,7 @@ describe("Zotero.Embeddings", function () { [item.id, 'hash'] ); // The model switch clears the old vectors - Zotero.Prefs.set('embeddings.model', 'bge-small-en-v1.5'); + Zotero.Prefs.set('embeddings.model', 'bekko-embedding-v1-a8m'); await Zotero.Embeddings.Indexing.waitForPendingModelSwitch(); assert.equal( await Zotero.DB.valueQueryAsync( @@ -1678,7 +1722,7 @@ describe("Zotero.Embeddings", function () { sinon.stub(Zotero.Embeddings, 'isDownloaded').resolves(true), sinon.stub(Zotero.Embeddings, 'download').resolves(), sinon.stub(Zotero.Embeddings, 'ensureCalibration').resolves(), - sinon.stub(Zotero.Embeddings, 'getModelName').returns('bge-small-en-v1.5'), + sinon.stub(Zotero.Embeddings, 'getModelName').returns('bekko-embedding-v1-a8m'), ]; try { await Zotero.Embeddings.Indexing.startIndexing(); @@ -1722,7 +1766,7 @@ describe("Zotero.Embeddings", function () { // These fake an active model rather than selecting one (which // would kick off a model switch), so name one to keep the // window and passage prefix chunking reads consistent with it - sinon.stub(Zotero.Embeddings, 'getModelName').returns('bge-small-en-v1.5'), + sinon.stub(Zotero.Embeddings, 'getModelName').returns('bekko-embedding-v1-a8m'), ]; try { await Zotero.Embeddings.Indexing.startIndexing(); @@ -1776,7 +1820,7 @@ describe("Zotero.Embeddings", function () { sinon.stub(Zotero.Embeddings, 'isDownloaded').resolves(true), sinon.stub(Zotero.Embeddings, 'download').resolves(), sinon.stub(Zotero.Embeddings, 'ensureCalibration').resolves(), - sinon.stub(Zotero.Embeddings, 'getModelName').returns('bge-small-en-v1.5'), + sinon.stub(Zotero.Embeddings, 'getModelName').returns('bekko-embedding-v1-a8m'), ]; try { await Zotero.Embeddings.Indexing.startIndexing(); @@ -1831,7 +1875,7 @@ describe("Zotero.Embeddings", function () { sinon.stub(Zotero.Embeddings, 'isDownloaded').resolves(true), sinon.stub(Zotero.Embeddings, 'download').resolves(), sinon.stub(Zotero.Embeddings, 'ensureCalibration').resolves(), - sinon.stub(Zotero.Embeddings, 'getModelName').returns('bge-small-en-v1.5'), + sinon.stub(Zotero.Embeddings, 'getModelName').returns('bekko-embedding-v1-a8m'), ]; try { await Zotero.Embeddings.Indexing.startIndexing(); @@ -1883,7 +1927,7 @@ describe("Zotero.Embeddings", function () { sinon.stub(Zotero.Embeddings, 'isDownloaded').resolves(true), sinon.stub(Zotero.Embeddings, 'download').resolves(), sinon.stub(Zotero.Embeddings, 'ensureCalibration').resolves(), - sinon.stub(Zotero.Embeddings, 'getModelName').returns('bge-small-en-v1.5'), + sinon.stub(Zotero.Embeddings, 'getModelName').returns('bekko-embedding-v1-a8m'), ]; try { await Zotero.Embeddings.Indexing.startIndexing(); @@ -1925,7 +1969,7 @@ describe("Zotero.Embeddings", function () { sinon.stub(Zotero.Embeddings, 'isDownloaded').resolves(true), sinon.stub(Zotero.Embeddings, 'download').resolves(), sinon.stub(Zotero.Embeddings, 'ensureCalibration').resolves(), - sinon.stub(Zotero.Embeddings, 'getModelName').returns('bge-small-en-v1.5'), + sinon.stub(Zotero.Embeddings, 'getModelName').returns('bekko-embedding-v1-a8m'), // The extraction itself is sdt.js's concern (see sdtTest.js); // what's under test is what indexing does with the sections sinon.stub(Zotero.SDT, 'ensure').resolves(true), @@ -2044,7 +2088,7 @@ describe("Zotero.Embeddings", function () { sinon.stub(Zotero.Embeddings, 'isDownloaded').resolves(true), sinon.stub(Zotero.Embeddings, 'download').resolves(), sinon.stub(Zotero.Embeddings, 'ensureCalibration').resolves(), - sinon.stub(Zotero.Embeddings, 'getModelName').returns('bge-small-en-v1.5'), + sinon.stub(Zotero.Embeddings, 'getModelName').returns('bekko-embedding-v1-a8m'), sinon.stub(Zotero.SDT, 'ensure').resolves(true), sinon.stub(Zotero.SDT, 'getSections').resolves({ ok: true, @@ -2112,7 +2156,7 @@ describe("Zotero.Embeddings", function () { sinon.stub(Zotero.Embeddings, 'isDownloaded').resolves(true), sinon.stub(Zotero.Embeddings, 'download').resolves(), sinon.stub(Zotero.Embeddings, 'ensureCalibration').resolves(), - sinon.stub(Zotero.Embeddings, 'getModelName').returns('bge-small-en-v1.5'), + sinon.stub(Zotero.Embeddings, 'getModelName').returns('bekko-embedding-v1-a8m'), sinon.stub(Zotero.SDT, 'ensure').resolves(true), sinon.stub(Zotero.SDT, 'getSections').resolves({ ok: true, sections }) ]; @@ -2212,7 +2256,7 @@ describe("Zotero.Embeddings", function () { sinon.stub(Zotero.Embeddings, 'isDownloaded').resolves(true), sinon.stub(Zotero.Embeddings, 'download').resolves(), sinon.stub(Zotero.Embeddings, 'ensureCalibration').resolves(), - sinon.stub(Zotero.Embeddings, 'getModelName').returns('bge-small-en-v1.5'), + sinon.stub(Zotero.Embeddings, 'getModelName').returns('bekko-embedding-v1-a8m'), sinon.stub(Zotero.SDT, 'ensure').resolves(true), sinon.stub(Zotero.SDT, 'getSections').resolves({ ok: true, sections }) ]; @@ -2269,7 +2313,7 @@ describe("Zotero.Embeddings", function () { sinon.stub(Zotero.Embeddings, 'isDownloaded').resolves(true), sinon.stub(Zotero.Embeddings, 'download').resolves(), sinon.stub(Zotero.Embeddings, 'ensureCalibration').resolves(), - sinon.stub(Zotero.Embeddings, 'getModelName').returns('bge-small-en-v1.5'), + sinon.stub(Zotero.Embeddings, 'getModelName').returns('bekko-embedding-v1-a8m'), sinon.stub(Zotero.SDT, 'ensure').resolves(true), sinon.stub(Zotero.SDT, 'getSections').resolves({ ok: true, @@ -2340,7 +2384,7 @@ describe("Zotero.Embeddings", function () { sinon.stub(Zotero.Embeddings, 'isDownloaded').resolves(true), sinon.stub(Zotero.Embeddings, 'download').resolves(), sinon.stub(Zotero.Embeddings, 'ensureCalibration').resolves(), - sinon.stub(Zotero.Embeddings, 'getModelName').returns('bge-small-en-v1.5'), + sinon.stub(Zotero.Embeddings, 'getModelName').returns('bekko-embedding-v1-a8m'), sinon.stub(Zotero.SDT, 'ensure').resolves(true), sinon.stub(Zotero.SDT, 'getSections').resolves({ ok: true, @@ -2401,7 +2445,7 @@ describe("Zotero.Embeddings", function () { sinon.stub(Zotero.Embeddings, 'isDownloaded').resolves(true), sinon.stub(Zotero.Embeddings, 'download').resolves(), sinon.stub(Zotero.Embeddings, 'ensureCalibration').resolves(), - sinon.stub(Zotero.Embeddings, 'getModelName').returns('bge-small-en-v1.5'), + sinon.stub(Zotero.Embeddings, 'getModelName').returns('bekko-embedding-v1-a8m'), sinon.stub(Zotero.SDT, 'ensure').resolves(true), sinon.stub(Zotero.SDT, 'getSections').resolves({ ok: true, @@ -2454,7 +2498,7 @@ describe("Zotero.Embeddings", function () { let item = await createDataObject('item', { title: 'Parent of derived attachment' }); let attachment = await importPDFAttachment(item); let stubs = [ - sinon.stub(Zotero.Embeddings, 'getModelName').returns('bge-small-en-v1.5'), + sinon.stub(Zotero.Embeddings, 'getModelName').returns('bekko-embedding-v1-a8m'), sinon.stub(Zotero.SDT, 'getSections').resolves({ ok: true, sections: [ @@ -2525,7 +2569,7 @@ describe("Zotero.Embeddings", function () { sinon.stub(Zotero.Embeddings, 'isDownloaded').resolves(true), sinon.stub(Zotero.Embeddings, 'download').resolves(), sinon.stub(Zotero.Embeddings, 'ensureCalibration').resolves(), - sinon.stub(Zotero.Embeddings, 'getModelName').returns('bge-small-en-v1.5'), + sinon.stub(Zotero.Embeddings, 'getModelName').returns('bekko-embedding-v1-a8m'), sinon.stub(Zotero.SDT, 'ensure').resolves(true), sinon.stub(Zotero.SDT, 'getSections').callsFake(async (itemID) => { // Sections are read once to count chunks and again to @@ -2586,7 +2630,7 @@ describe("Zotero.Embeddings", function () { sinon.stub(Zotero.Embeddings, 'isDownloaded').resolves(true), sinon.stub(Zotero.Embeddings, 'download').resolves(), sinon.stub(Zotero.Embeddings, 'ensureCalibration').resolves(), - sinon.stub(Zotero.Embeddings, 'getModelName').returns('bge-small-en-v1.5'), + sinon.stub(Zotero.Embeddings, 'getModelName').returns('bekko-embedding-v1-a8m'), ensureStub, getSectionsStub ]; @@ -2637,7 +2681,7 @@ describe("Zotero.Embeddings", function () { sinon.stub(Zotero.Embeddings, 'isDownloaded').resolves(true), sinon.stub(Zotero.Embeddings, 'download').resolves(), sinon.stub(Zotero.Embeddings, 'ensureCalibration').resolves(), - sinon.stub(Zotero.Embeddings, 'getModelName').returns('bge-small-en-v1.5'), + sinon.stub(Zotero.Embeddings, 'getModelName').returns('bekko-embedding-v1-a8m'), ensureStub, sinon.stub(Zotero.SDT, 'getSections').resolves({ ok: true, @@ -2679,7 +2723,7 @@ describe("Zotero.Embeddings", function () { sinon.stub(Zotero.Embeddings, 'isDownloaded').resolves(true), sinon.stub(Zotero.Embeddings, 'download').resolves(), sinon.stub(Zotero.Embeddings, 'ensureCalibration').resolves(), - sinon.stub(Zotero.Embeddings, 'getModelName').returns('bge-small-en-v1.5'), + sinon.stub(Zotero.Embeddings, 'getModelName').returns('bekko-embedding-v1-a8m'), sinon.stub(Zotero.SDT, 'ensure').resolves(true), sinon.stub(Zotero.SDT, 'getSections').resolves({ ok: false, reason: 'failed' }) ]; @@ -2747,7 +2791,7 @@ describe("Zotero.Embeddings", function () { sinon.stub(Zotero.Embeddings, 'isDownloaded').resolves(true), sinon.stub(Zotero.Embeddings, 'download').resolves(), sinon.stub(Zotero.Embeddings, 'ensureCalibration').resolves(), - sinon.stub(Zotero.Embeddings, 'getModelName').returns('bge-small-en-v1.5'), + sinon.stub(Zotero.Embeddings, 'getModelName').returns('bekko-embedding-v1-a8m'), sinon.stub(Zotero.Embeddings, 'embedQuery').resolves(Float32Array.from(testMean)), sinon.stub(Zotero.SDT, 'ensure').resolves(true), getSectionsStub @@ -2844,7 +2888,7 @@ describe("Zotero.Embeddings", function () { // The runtime's model cache resolves navigator.storage via the most // recent browser window await loadZoteroPane(); - Zotero.Prefs.set('embeddings.model', 'bge-small-en-v1.5'); + Zotero.Prefs.set('embeddings.model', 'bekko-embedding-v1-a8m'); await Zotero.Embeddings.preloadModel(); let [related, unrelated] = await Zotero.Embeddings.embedPassages([ @@ -2863,7 +2907,7 @@ describe("Zotero.Embeddings", function () { it("should report a cached model as downloaded and keep it when pruning", async function () { this.timeout(1800000); await loadZoteroPane(); - Zotero.Prefs.set('embeddings.model', 'bge-small-en-v1.5'); + Zotero.Prefs.set('embeddings.model', 'bekko-embedding-v1-a8m'); await Zotero.Embeddings.preloadModel(); assert.isTrue(await Zotero.Embeddings.isDownloaded()); @@ -2875,7 +2919,7 @@ describe("Zotero.Embeddings", function () { it("should chunk within the model's real window", async function () { this.timeout(1800000); await loadZoteroPane(); - Zotero.Prefs.set('embeddings.model', 'bge-small-en-v1.5'); + Zotero.Prefs.set('embeddings.model', 'bekko-embedding-v1-a8m'); await Zotero.Embeddings.download(); // The model's real tokenizer, built from the same files the diff --git a/test/tests/preferences_advancedTest.js b/test/tests/preferences_advancedTest.js index 7c1464e8dd..205fa063b2 100644 --- a/test/tests/preferences_advancedTest.js +++ b/test/tests/preferences_advancedTest.js @@ -166,17 +166,17 @@ describe("Advanced Preferences", function () { try { // Enabling from Disabled prompts nothing (the test would hang on // an unexpected modal prompt) - menu.value = 'bge-small-en-v1.5'; + menu.value = 'bekko-embedding-v1-a8m'; await win.Zotero_Preferences.Advanced.handleSemanticSearchModeChange(); - assert.equal(Zotero.Prefs.get('embeddings.model'), 'bge-small-en-v1.5'); + assert.equal(Zotero.Prefs.get('embeddings.model'), 'bekko-embedding-v1-a8m'); // Cancelling a switch restores the menu and leaves the pref alone var promise = waitForDialog(null, 'cancel'); - menu.value = 'multilingual-e5-small'; + menu.value = 'bekko-embedding-v1-a25m'; await win.Zotero_Preferences.Advanced.handleSemanticSearchModeChange(); await promise; - assert.equal(Zotero.Prefs.get('embeddings.model'), 'bge-small-en-v1.5'); - assert.equal(menu.value, 'bge-small-en-v1.5'); + assert.equal(Zotero.Prefs.get('embeddings.model'), 'bekko-embedding-v1-a8m'); + assert.equal(menu.value, 'bekko-embedding-v1-a8m'); // Confirming applies the change promise = waitForDialog();