From 04a75adef606f04c323937adbd8d738ef09942b9 Mon Sep 17 00:00:00 2001 From: Joshua Horton Date: Mon, 3 Aug 2026 12:54:52 -0500 Subject: [PATCH] feat(web): add new pred-text unit test cases Adds unit tests to cover context-reset patterns and to ensure that transition IDs are always set properly on generated suggestions, for all model types. Build-bot: skip build:web Test-bot: skip --- .../worker-model-compositor.tests.ts | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/web/src/test/auto/headless/engine/predictive-text/worker-thread/worker-model-compositor.tests.ts b/web/src/test/auto/headless/engine/predictive-text/worker-thread/worker-model-compositor.tests.ts index 4acc343f98..f01cafbac6 100644 --- a/web/src/test/auto/headless/engine/predictive-text/worker-thread/worker-model-compositor.tests.ts +++ b/web/src/test/auto/headless/engine/predictive-text/worker-thread/worker-model-compositor.tests.ts @@ -26,6 +26,23 @@ describe('ModelCompositor', function() { {wordBreaker: wordBreakers.default} ); + it('properly handles a context-reset sequence', async () => { + const compositor = new ModelCompositor(plainModel, true); + + const context = { + left: 'appl', + startOfBuffer: true, + endOfBuffer: true + }; + const resetTransitionID = 3; + compositor.resetContext(context, resetTransitionID); + + const results = await compositor.predict([{sample: {insert: '', deleteLeft: 0, id: resetTransitionID}, p: 1}], context); + + // returns a keep + "applied" + assert.isAbove(results.length, 0); + }); + it('does not correct when corrections are disabled', async function() { const compositor = new ModelCompositor(plainModel, true); @@ -533,6 +550,71 @@ describe('ModelCompositor', function() { }); describe('Prediction with legacy Models (12.0 / 13.0)', function() { + it('properly handles a context-reset sequence', async () => { + const dummiedModel = new models.DummyModel({ + futureSuggestions: [ + [{ + transform: { insert: 'e', deleteLeft: 0 }, + displayAs: "apple", + id: 1, + p: 0.7 + }, { + transform: { insert: 'y', deleteLeft: 0 }, + displayAs: "apply", + id: 2, + p: 0.3 + }] + ] + }); + + const compositor = new ModelCompositor(dummiedModel, true); + + const context = { + left: 'appl', + startOfBuffer: true, + endOfBuffer: true + }; + const resetTransitionID = 3; + compositor.resetContext(context, resetTransitionID); + + const results = await compositor.predict([{sample: {insert: '', deleteLeft: 0, id: resetTransitionID}, p: 1}], context); + + assert.isAbove(results.length, 0); + }); + + it('annotates suggestions with the original suggestion ID', async function () { + var model = new models.DummyModel({ + futureSuggestions: [ + [], [{ + transform: { insert: 'e', deleteLeft: 0 }, + displayAs: "the", + id: 1, + p: 0.7 + }], [{ + transform: { insert: 'ree', deleteLeft: 0}, + displayAs: "three", + id: 2, + p: 0.3 + }] + ] + }); + let compositor = new ModelCompositor(model, true); + + // Initialize context + const EXPECTED_TRANSITION_ID = 0; // Even 0 should be handled correctly. + let context = { + left: 'th', startOfBuffer: false, endOfBuffer: true, + }; + await compositor.predict({insert: '', deleteLeft: 0, id: EXPECTED_TRANSITION_ID-1}, context); + + // Pretend to fat finger "the" as "thr" + const the = { sample: { insert: 'r', deleteLeft: 0, id: EXPECTED_TRANSITION_ID }, p: 0.45 }; + const thr = { sample: { insert: 'e', deleteLeft: 0, id: EXPECTED_TRANSITION_ID }, p: 0.55 }; + const suggestions = await compositor.predict([thr, the], context); + + suggestions.forEach((s) => assert.equal(s.transform.id, EXPECTED_TRANSITION_ID)); + }); + it('should compose suggestions from a fat-fingered keypress (no keying needed)', async function () { var model = new models.DummyModel({ futureSuggestions: [