From 9bd3f68bc427fc2f848af46031aeb2001e9e1704 Mon Sep 17 00:00:00 2001 From: Joshua Horton Date: Wed, 4 Mar 2026 15:57:40 -0600 Subject: [PATCH] refactor(web): improve clarity and organization of search-graph root + legacy-spur unit tests As the next work in line will introduce new, specialized spur types designed to replace the current 'legacy spurs', it is wise to clarify existing search-graph unit tests and which sections of the code they actually target. We'll eventually drop behaviors specific to 'legacy' spurs, but those that apply to the new incoming specialized spur types should be preserved. At the same time, it may be wise to improve the unit testing of each specific type by placing each within its own specialized unit-test suite, then adding new tests that test and clarify the role of each type. Build-bot: skip build:web Test-bot: skip --- .../main/correction/search-quotient-root.ts | 8 + .../legacy-quotient-spur.tests.ts | 385 ++++++++++++++++++ .../search-quotient-root.tests.ts | 86 ++++ .../search-quotient-spur.tests.ts | 233 ----------- 4 files changed, 479 insertions(+), 233 deletions(-) create mode 100644 web/src/test/auto/headless/engine/predictive-text/worker-thread/correction-search/legacy-quotient-spur.tests.ts create mode 100644 web/src/test/auto/headless/engine/predictive-text/worker-thread/correction-search/search-quotient-root.tests.ts diff --git a/web/src/engine/predictive-text/worker-thread/src/main/correction/search-quotient-root.ts b/web/src/engine/predictive-text/worker-thread/src/main/correction/search-quotient-root.ts index 90e8e83822..845b47c843 100644 --- a/web/src/engine/predictive-text/worker-thread/src/main/correction/search-quotient-root.ts +++ b/web/src/engine/predictive-text/worker-thread/src/main/correction/search-quotient-root.ts @@ -29,6 +29,10 @@ export class SearchQuotientRoot implements SearchQuotientNode { * @param model */ constructor(model: LexicalModel) { + if(!model.traverseFromRoot) { + throw new Error("The active lexical model does not support traversal-based searching."); + } + this.rootNode = new SearchNode(model.traverseFromRoot(), generateSpaceSeed(), t => model.toKey(t)); this.model = model; this.rootResult = new SearchResult(this.rootNode); @@ -122,6 +126,10 @@ export class SearchQuotientRoot implements SearchQuotientNode { } merge(space: SearchQuotientNode): SearchQuotientNode { + if(this.model != space.model) { + throw new Error("Cannot merge search graphs based on different LexicalModels"); + } + // Head node for the incoming path is empty, so skip it. if(space.parents.length == 0 || space instanceof SearchQuotientRoot) { return this; diff --git a/web/src/test/auto/headless/engine/predictive-text/worker-thread/correction-search/legacy-quotient-spur.tests.ts b/web/src/test/auto/headless/engine/predictive-text/worker-thread/correction-search/legacy-quotient-spur.tests.ts new file mode 100644 index 0000000000..fd5ad049a3 --- /dev/null +++ b/web/src/test/auto/headless/engine/predictive-text/worker-thread/correction-search/legacy-quotient-spur.tests.ts @@ -0,0 +1,385 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Created by jahorton on 2025-10-29 + * + * This file defines tests for the SearchQuotientSpur classes of the + * predictive-text correction-search engine. + */ + +import { assert } from 'chai'; + +import { jsonFixture } from '@keymanapp/common-test-resources/model-helpers.mjs'; +import { + generateSubsetId, + LegacyQuotientRoot, + LegacyQuotientSpur, + models, + PathResult, +} from '@keymanapp/lm-worker/test-index'; + +import { buildCantLinearFixture } from '../../helpers/buildCantLinearFixture.js'; + +import TrieModel = models.TrieModel; + +const testModel = new TrieModel(jsonFixture('models/tries/english-1000')); + +describe('LegacyQuotientSpur', () => { + describe('constructor', () => { + it('initializes from a lexical model', () => { + const path = new LegacyQuotientRoot(testModel); + assert.equal(path.inputCount, 0); + assert.equal(path.codepointLength, 0); + assert.isNumber(path.spaceId); + assert.deepEqual(path.bestExample, {text: '', p: 1}); + assert.deepEqual(path.parents, []); + }); + + it('may be extended from root path', () => { + const rootPath = new LegacyQuotientRoot(testModel); + + const leadEdgeDistribution = [ + {sample: {insert: 't', deleteLeft: 0, id: 13 }, p: 0.5}, + {sample: {insert: 'a', deleteLeft: 0, id: 13 }, p: 0.3}, + {sample: {insert: 'o', deleteLeft: 0, id: 13 }, p: 0.2} + ]; + + const extendedPath = new LegacyQuotientSpur(rootPath, leadEdgeDistribution, leadEdgeDistribution[0]); + + assert.equal(extendedPath.inputCount, 1); + assert.equal(extendedPath.codepointLength, 1); + assert.isNumber(extendedPath.spaceId); + assert.notEqual(extendedPath.spaceId, rootPath.spaceId); + assert.deepEqual(extendedPath.bestExample, {text: 't', p: 0.5}); + assert.deepEqual(extendedPath.parents, [rootPath]); + assert.deepEqual(extendedPath.inputs, leadEdgeDistribution); + assert.deepEqual(extendedPath.inputSegments, [ + { + transitionId: leadEdgeDistribution[0].sample.id, + start: 0 + } + ]); + + // Assert the root is unchanged. + assert.equal(rootPath.inputCount, 0); + // Should (still) have codepointLength == 0 once it's defined. + assert.deepEqual(rootPath.bestExample, {text: '', p: 1}); + assert.deepEqual(rootPath.parents, []); + }); + + it('may be built from arbitrary prior SearchQuotientSpur', () => { + const rootPath = new LegacyQuotientRoot(testModel); + + const leadEdgeDistribution = [ + {sample: {insert: 't', deleteLeft: 0, id: 13 }, p: 0.5}, + {sample: {insert: 'a', deleteLeft: 0, id: 13 }, p: 0.3}, + {sample: {insert: 'o', deleteLeft: 0, id: 13 }, p: 0.2} + ]; + const inputClone = leadEdgeDistribution.map(e => ({...e})); + + const length1Path = new LegacyQuotientSpur( + rootPath, + leadEdgeDistribution, + leadEdgeDistribution[0] + ); + + const tailEdgeDistribution = [ + {sample: {insert: 'r', deleteLeft: 0, id: 17 }, p: 0.6}, + {sample: {insert: 'e', deleteLeft: 0, id: 17 }, p: 0.25}, + {sample: {insert: 'h', deleteLeft: 0, id: 17 }, p: 0.15} + ]; + + const length2Path = new LegacyQuotientSpur( + length1Path, + tailEdgeDistribution, + tailEdgeDistribution[0] + ); + + // Verify that the prior distribution remains fully unaltered. + assert.deepEqual(leadEdgeDistribution, inputClone); + + assert.equal(length2Path.inputCount, 2); + assert.equal(length2Path.codepointLength, 2); + assert.isNumber(length2Path.spaceId); + assert.notEqual(length2Path.spaceId, length1Path.spaceId); + assert.deepEqual(length2Path.bestExample, {text: 'tr', p: leadEdgeDistribution[0].p * tailEdgeDistribution[0].p}); + assert.deepEqual(length2Path.parents, [length1Path]); + assert.deepEqual(length2Path.inputs, tailEdgeDistribution); + assert.deepEqual(length2Path.inputSegments, [ + { + transitionId: leadEdgeDistribution[0].sample.id, + start: 0 + }, { + transitionId: tailEdgeDistribution[0].sample.id, + start: 0 + } + ]); + + assert.equal(length1Path.inputCount, 1); + assert.equal(length1Path.codepointLength, 1); + assert.isNumber(length1Path.spaceId); + assert.notEqual(length1Path.spaceId, rootPath.spaceId); + assert.deepEqual(length1Path.bestExample, {text: 't', p: 0.5}); + assert.deepEqual(length1Path.parents, [rootPath]); + assert.deepEqual(length1Path.inputs, leadEdgeDistribution); + }); + + it('throws if input and input-source transition IDs mismatch', () => { + const rootPath = new LegacyQuotientRoot(testModel); + + const leadEdgeDistribution = [ + {sample: {insert: 't', deleteLeft: 0, id: 13 }, p: 0.5}, + {sample: {insert: 'a', deleteLeft: 0, id: 13 }, p: 0.3}, + {sample: {insert: 'o', deleteLeft: 0, id: 13 }, p: 0.2} + ]; + + assert.throws(() => new LegacyQuotientSpur(rootPath, leadEdgeDistribution, { + ...leadEdgeDistribution[0], + sample: {...leadEdgeDistribution[0].sample, id: 15} + })); + }); + + it('may extend with a Transform inserting multiple codepoints', () => { + const rootPath = new LegacyQuotientRoot(testModel); + + const leadEdgeDistribution = [ + {sample: {insert: 't', deleteLeft: 0, id: 13 }, p: 0.5}, + {sample: {insert: 'a', deleteLeft: 0, id: 13 }, p: 0.3}, + {sample: {insert: 'o', deleteLeft: 0, id: 13 }, p: 0.2} + ]; + const inputClone = leadEdgeDistribution.map(e => ({...e})); + + const length1Path = new LegacyQuotientSpur( + rootPath, + leadEdgeDistribution, + leadEdgeDistribution[0] + ); + + const tailEdgeDistribution = [ + {sample: {insert: 'ri', deleteLeft: 0, id: 17 }, p: 0.6}, + {sample: {insert: 'er', deleteLeft: 0, id: 17 }, p: 0.25}, + {sample: {insert: 'hi', deleteLeft: 0, id: 17 }, p: 0.15} + ]; + + const length2Path = new LegacyQuotientSpur( + length1Path, + tailEdgeDistribution, + tailEdgeDistribution[0] + ); + + // Verify that the prior distribution remains fully unaltered. + assert.deepEqual(leadEdgeDistribution, inputClone); + + assert.equal(length2Path.inputCount, 2); + assert.equal(length2Path.codepointLength, 3); + assert.isNumber(length2Path.spaceId); + assert.notEqual(length2Path.spaceId, length1Path.spaceId); + assert.deepEqual(length2Path.bestExample, {text: 'tri', p: leadEdgeDistribution[0].p * tailEdgeDistribution[0].p}); + assert.deepEqual(length2Path.parents, [length1Path]); + assert.deepEqual(length2Path.inputs, tailEdgeDistribution); + assert.deepEqual(length2Path.inputSegments, [ + { + transitionId: leadEdgeDistribution[0].sample.id, + start: 0 + }, { + transitionId: tailEdgeDistribution[0].sample.id, + start: 0 + } + ]); + + assert.equal(length1Path.inputCount, 1); + assert.equal(length1Path.codepointLength, 1); + assert.isNumber(length1Path.spaceId); + assert.notEqual(length1Path.spaceId, rootPath.spaceId); + assert.deepEqual(length1Path.bestExample, {text: 't', p: 0.5}); + assert.deepEqual(length1Path.parents, [rootPath]); + assert.deepEqual(length1Path.inputs, leadEdgeDistribution); + }); + }); + + describe('.edgeKey', () => { + it('changes when input source subset IDs differ', () => { + const root = new LegacyQuotientRoot(testModel); + + const {distributions} = buildCantLinearFixture(); + const inputSrc = { + segment: { + transitionId: distributions[0][0].sample.id, + start: 0 + }, + subsetId: generateSubsetId(), + bestProbFromSet: distributions[0][0].p + }; + + const spur1 = new LegacyQuotientSpur(root, distributions[0], { + ...inputSrc, + subsetId: generateSubsetId() + }); + const spur2 = new LegacyQuotientSpur(root, distributions[0], { + ...inputSrc, + subsetId: generateSubsetId() + }); + + assert.notEqual(spur1.edgeKey, spur2.edgeKey); + }); + + it('changes when different parts of the same input source are used', () => { + const root = new LegacyQuotientRoot(testModel); + + const {distributions} = buildCantLinearFixture(); + const inputSrc = { + segment: { + transitionId: distributions[0][0].sample.id, + start: 0 + }, + subsetId: generateSubsetId(), + bestProbFromSet: distributions[0][0].p + }; + + const spur1 = new LegacyQuotientSpur(root, distributions[0], inputSrc); + const spur2 = new LegacyQuotientSpur(root, distributions[0], { + ...inputSrc, + segment: { + ...inputSrc.segment, + end: 1 + } + }); + const spur3 = new LegacyQuotientSpur(root, distributions[0], { + ...inputSrc, + segment: { + ...inputSrc.segment, + start: inputSrc.segment.start + 1 + } + }); + + assert.notEqual(spur1.edgeKey, spur2.edgeKey); + assert.notEqual(spur2.edgeKey, spur3.edgeKey); + assert.notEqual(spur3.edgeKey, spur1.edgeKey); + }); + }); + + describe('handleNextNode()', () => { + it('outputs results that directly match inputs', () => { + const canPath = buildCantLinearFixture().paths[3]; + + const matchTargets = [ + 'can', + 'car', + 'cen', // 'cent' and 'center' are supported in this test model. + ]; + let matchCount = 0; + + let result: PathResult = canPath.handleNextNode(); + while(result.type != 'none') { + if(result.type == 'complete') { + const resultKey = result.finalNode.resultKey; + if(matchTargets.find((entry) => entry == resultKey)) { + matchCount++; + } + } + + if(matchCount == matchTargets.length) { + break; + } + + result = canPath.handleNextNode(); + } + + assert.notEqual(result.type, 'none'); + }); + + it('outputs results that substitute inputs', () => { + const canPath = buildCantLinearFixture().paths[3]; + + const matchTargets = [ + // Replacement of first char + 'man', + 'far', + // Replacement of second char + 'con', // 'consider' and variants thereof are also supported. + 'cor', // 'corner' + // Replacement of third char + 'cal', // 'call' + ]; + let matchCount = 0; + + let result: PathResult = canPath.handleNextNode(); + while(result.type != 'none') { + if(result.type == 'complete') { + const resultKey = result.finalNode.resultKey; + if(matchTargets.find((entry) => entry == resultKey)) { + matchCount++; + } + } + + if(matchCount == matchTargets.length) { + break; + } + + result = canPath.handleNextNode(); + } + + assert.notEqual(result.type, 'none'); + }); + + it('outputs results that insert characters as needed', () => { + const canPath = buildCantLinearFixture().paths[3]; + + const matchTargets = [ + 'char', // 'c' + (insert) 'h' + 'ar' + 'than', // 't' + (insert) 'h' + 'an' + ]; + let matchCount = 0; + + let result: PathResult = canPath.handleNextNode(); + while(result.type != 'none') { + if(result.type == 'complete') { + const resultKey = result.finalNode.resultKey; + if(matchTargets.find((entry) => entry == resultKey)) { + matchCount++; + } + } + + if(matchCount == matchTargets.length) { + break; + } + + result = canPath.handleNextNode(); + } + + assert.notEqual(result.type, 'none'); + }); + + it('outputs results that delete incoming keystrokes as needed', () => { + const canPath = buildCantLinearFixture().paths[3]; + + const matchTargets = [ + // Delete only first + 'an', // (delete) 'c'/'r'/'t' + 'an', for 'and' and 'any' + 'en', // (delete) 'c'/'r'/'t' + 'en', for 'end', + // Even delete second + 'n', // model possesses words starting with just 'n' + 't' // ... and 't'. + ]; + let matchCount = 0; + + let result: PathResult = canPath.handleNextNode(); + while(result.type != 'none') { + if(result.type == 'complete') { + const resultKey = result.finalNode.resultKey; + if(matchTargets.find((entry) => entry == resultKey)) { + matchCount++; + } + } + + if(matchCount == matchTargets.length) { + break; + } + + result = canPath.handleNextNode(); + } + + assert.notEqual(result.type, 'none'); + }); + }); +}); \ No newline at end of file diff --git a/web/src/test/auto/headless/engine/predictive-text/worker-thread/correction-search/search-quotient-root.tests.ts b/web/src/test/auto/headless/engine/predictive-text/worker-thread/correction-search/search-quotient-root.tests.ts new file mode 100644 index 0000000000..a140376c24 --- /dev/null +++ b/web/src/test/auto/headless/engine/predictive-text/worker-thread/correction-search/search-quotient-root.tests.ts @@ -0,0 +1,86 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Created by jahorton on 2025-03-04 + * + * This file defines tests for the SearchQuotientRoot class of the + * predictive-text correction-search engine. + */ + +import { assert } from 'chai'; + +import { jsonFixture } from '@keymanapp/common-test-resources/model-helpers.mjs'; +import { + models, + SearchQuotientRoot, +} from '@keymanapp/lm-worker/test-index'; + +import { constituentPaths } from '../../helpers/constituentPaths.js'; + +import DummyModel = models.DummyModel; +import TrieModel = models.TrieModel; + +const testModel = new TrieModel(jsonFixture('models/tries/english-1000')); +const altModel = new TrieModel(jsonFixture('models/tries/accented')); + +describe('SearchQuotientSpur', () => { + describe('constructor', () => { + it('initializes from a lexical model', () => { + const path = new SearchQuotientRoot(testModel); + assert.equal(path.inputCount, 0); + assert.equal(path.codepointLength, 0); + assert.isNumber(path.spaceId); + assert.deepEqual(path.bestExample, {text: '', p: 1}); + assert.deepEqual(path.parents, []); + }); + + it('throws an error for lexical models that do not support traversal-based searching', () => { + assert.throws(() => new SearchQuotientRoot(new DummyModel({}))); + }); + }); + + it('constituentPaths returns an empty array', () => { + const path = new SearchQuotientRoot(testModel); + assert.deepEqual(constituentPaths(path), []); + }); + + describe('.isSameNode returns true for separate instance on same model', () => { + const root1 = new SearchQuotientRoot(testModel); + const root2 = new SearchQuotientRoot(testModel); + + assert.isTrue(root1.isSameNode(root2)); + }); + + it('split() results in two separate root instances', () => { + const root = new SearchQuotientRoot(testModel); + + const splitResults = root.split(0); + + // Only one way to split it... + assert.equal(splitResults.length, 1); + + const [head, tail] = splitResults[0]; + assert.notStrictEqual(head, tail); + assert.isTrue(head.isSameNode(tail)); + }); + + describe('merge()', () => { + it('treats two copies of same-model root as same node', () => { + const head = new SearchQuotientRoot(testModel); + const tail = new SearchQuotientRoot(testModel); + + const merged = head.merge(tail); + + assert.isTrue(head.isSameNode(tail)); + assert.isTrue(merged.isSameNode(head)); + assert.isTrue(merged.isSameNode(tail)); + }); + + it('rejects roots from different models', () => { + const head = new SearchQuotientRoot(testModel); + const tail = new SearchQuotientRoot(altModel); + + assert.throws(() => head.merge(tail)); + }); + }); +}); \ No newline at end of file diff --git a/web/src/test/auto/headless/engine/predictive-text/worker-thread/correction-search/search-quotient-spur.tests.ts b/web/src/test/auto/headless/engine/predictive-text/worker-thread/correction-search/search-quotient-spur.tests.ts index 474ea09c76..70b428c5b5 100644 --- a/web/src/test/auto/headless/engine/predictive-text/worker-thread/correction-search/search-quotient-spur.tests.ts +++ b/web/src/test/auto/headless/engine/predictive-text/worker-thread/correction-search/search-quotient-spur.tests.ts @@ -63,239 +63,6 @@ function toMathematicalSMP(text: string) { } describe('SearchQuotientSpur', () => { - describe('constructor', () => { - it('initializes from a lexical model', () => { - const path = new LegacyQuotientRoot(testModel); - assert.equal(path.inputCount, 0); - assert.equal(path.codepointLength, 0); - assert.isNumber(path.spaceId); - assert.deepEqual(path.bestExample, {text: '', p: 1}); - assert.deepEqual(path.parents, []); - }); - - it('may be extended from root path', () => { - const rootPath = new LegacyQuotientRoot(testModel); - - const leadEdgeDistribution = [ - {sample: {insert: 't', deleteLeft: 0, id: 13 }, p: 0.5}, - {sample: {insert: 'a', deleteLeft: 0, id: 13 }, p: 0.3}, - {sample: {insert: 'o', deleteLeft: 0, id: 13 }, p: 0.2} - ]; - - const extendedPath = new LegacyQuotientSpur(rootPath, leadEdgeDistribution, leadEdgeDistribution[0]); - - assert.equal(extendedPath.inputCount, 1); - assert.equal(extendedPath.codepointLength, 1); - assert.isNumber(extendedPath.spaceId); - assert.notEqual(extendedPath.spaceId, rootPath.spaceId); - assert.deepEqual(extendedPath.bestExample, {text: 't', p: 0.5}); - assert.deepEqual(extendedPath.parents, [rootPath]); - assert.deepEqual(extendedPath.inputs, leadEdgeDistribution); - assert.deepEqual(extendedPath.inputSegments, [ - { - transitionId: leadEdgeDistribution[0].sample.id, - start: 0 - } - ]); - - // Assert the root is unchanged. - assert.equal(rootPath.inputCount, 0); - // Should (still) have codepointLength == 0 once it's defined. - assert.deepEqual(rootPath.bestExample, {text: '', p: 1}); - assert.deepEqual(rootPath.parents, []); - }); - - it('may be built from arbitrary prior SearchQuotientSpur', () => { - const rootPath = new LegacyQuotientRoot(testModel); - - const leadEdgeDistribution = [ - {sample: {insert: 't', deleteLeft: 0, id: 13 }, p: 0.5}, - {sample: {insert: 'a', deleteLeft: 0, id: 13 }, p: 0.3}, - {sample: {insert: 'o', deleteLeft: 0, id: 13 }, p: 0.2} - ]; - const inputClone = leadEdgeDistribution.map(e => ({...e})); - - const length1Path = new LegacyQuotientSpur( - rootPath, - leadEdgeDistribution, - leadEdgeDistribution[0] - ); - - const tailEdgeDistribution = [ - {sample: {insert: 'r', deleteLeft: 0, id: 17 }, p: 0.6}, - {sample: {insert: 'e', deleteLeft: 0, id: 17 }, p: 0.25}, - {sample: {insert: 'h', deleteLeft: 0, id: 17 }, p: 0.15} - ]; - - const length2Path = new LegacyQuotientSpur( - length1Path, - tailEdgeDistribution, - tailEdgeDistribution[0] - ); - - // Verify that the prior distribution remains fully unaltered. - assert.deepEqual(leadEdgeDistribution, inputClone); - - assert.equal(length2Path.inputCount, 2); - assert.equal(length2Path.codepointLength, 2); - assert.isNumber(length2Path.spaceId); - assert.notEqual(length2Path.spaceId, length1Path.spaceId); - assert.deepEqual(length2Path.bestExample, {text: 'tr', p: leadEdgeDistribution[0].p * tailEdgeDistribution[0].p}); - assert.deepEqual(length2Path.parents, [length1Path]); - assert.deepEqual(length2Path.inputs, tailEdgeDistribution); - assert.deepEqual(length2Path.inputSegments, [ - { - transitionId: leadEdgeDistribution[0].sample.id, - start: 0 - }, { - transitionId: tailEdgeDistribution[0].sample.id, - start: 0 - } - ]); - - assert.equal(length1Path.inputCount, 1); - assert.equal(length1Path.codepointLength, 1); - assert.isNumber(length1Path.spaceId); - assert.notEqual(length1Path.spaceId, rootPath.spaceId); - assert.deepEqual(length1Path.bestExample, {text: 't', p: 0.5}); - assert.deepEqual(length1Path.parents, [rootPath]); - assert.deepEqual(length1Path.inputs, leadEdgeDistribution); - }); - - it('throws if input and input-source transition IDs mismatch', () => { - const rootPath = new LegacyQuotientRoot(testModel); - - const leadEdgeDistribution = [ - {sample: {insert: 't', deleteLeft: 0, id: 13 }, p: 0.5}, - {sample: {insert: 'a', deleteLeft: 0, id: 13 }, p: 0.3}, - {sample: {insert: 'o', deleteLeft: 0, id: 13 }, p: 0.2} - ]; - - assert.throws(() => new LegacyQuotientSpur(rootPath, leadEdgeDistribution, { - ...leadEdgeDistribution[0], - sample: {...leadEdgeDistribution[0].sample, id: 15} - })); - }); - - it('may extend with a Transform inserting multiple codepoints', () => { - const rootPath = new LegacyQuotientRoot(testModel); - - const leadEdgeDistribution = [ - {sample: {insert: 't', deleteLeft: 0, id: 13 }, p: 0.5}, - {sample: {insert: 'a', deleteLeft: 0, id: 13 }, p: 0.3}, - {sample: {insert: 'o', deleteLeft: 0, id: 13 }, p: 0.2} - ]; - const inputClone = leadEdgeDistribution.map(e => ({...e})); - - const length1Path = new LegacyQuotientSpur( - rootPath, - leadEdgeDistribution, - leadEdgeDistribution[0] - ); - - const tailEdgeDistribution = [ - {sample: {insert: 'ri', deleteLeft: 0, id: 17 }, p: 0.6}, - {sample: {insert: 'er', deleteLeft: 0, id: 17 }, p: 0.25}, - {sample: {insert: 'hi', deleteLeft: 0, id: 17 }, p: 0.15} - ]; - - const length2Path = new LegacyQuotientSpur( - length1Path, - tailEdgeDistribution, - tailEdgeDistribution[0] - ); - - // Verify that the prior distribution remains fully unaltered. - assert.deepEqual(leadEdgeDistribution, inputClone); - - assert.equal(length2Path.inputCount, 2); - assert.equal(length2Path.codepointLength, 3); - assert.isNumber(length2Path.spaceId); - assert.notEqual(length2Path.spaceId, length1Path.spaceId); - assert.deepEqual(length2Path.bestExample, {text: 'tri', p: leadEdgeDistribution[0].p * tailEdgeDistribution[0].p}); - assert.deepEqual(length2Path.parents, [length1Path]); - assert.deepEqual(length2Path.inputs, tailEdgeDistribution); - assert.deepEqual(length2Path.inputSegments, [ - { - transitionId: leadEdgeDistribution[0].sample.id, - start: 0 - }, { - transitionId: tailEdgeDistribution[0].sample.id, - start: 0 - } - ]); - - assert.equal(length1Path.inputCount, 1); - assert.equal(length1Path.codepointLength, 1); - assert.isNumber(length1Path.spaceId); - assert.notEqual(length1Path.spaceId, rootPath.spaceId); - assert.deepEqual(length1Path.bestExample, {text: 't', p: 0.5}); - assert.deepEqual(length1Path.parents, [rootPath]); - assert.deepEqual(length1Path.inputs, leadEdgeDistribution); - }); - }); - - describe('.edgeKey', () => { - it('changes when input source subset IDs differ', () => { - const root = new LegacyQuotientRoot(testModel); - - const {distributions} = buildCantLinearFixture(); - const inputSrc = { - segment: { - transitionId: distributions[0][0].sample.id, - start: 0 - }, - subsetId: generateSubsetId(), - bestProbFromSet: distributions[0][0].p - }; - - const spur1 = new LegacyQuotientSpur(root, distributions[0], { - ...inputSrc, - subsetId: generateSubsetId() - }); - const spur2 = new LegacyQuotientSpur(root, distributions[0], { - ...inputSrc, - subsetId: generateSubsetId() - }); - - assert.notEqual(spur1.edgeKey, spur2.edgeKey); - }); - - it('changes when different parts of the same input source are used', () => { - const root = new LegacyQuotientRoot(testModel); - - const {distributions} = buildCantLinearFixture(); - const inputSrc = { - segment: { - transitionId: distributions[0][0].sample.id, - start: 0 - }, - subsetId: generateSubsetId(), - bestProbFromSet: distributions[0][0].p - }; - - const spur1 = new LegacyQuotientSpur(root, distributions[0], inputSrc); - const spur2 = new LegacyQuotientSpur(root, distributions[0], { - ...inputSrc, - segment: { - ...inputSrc.segment, - end: 1 - } - }); - const spur3 = new LegacyQuotientSpur(root, distributions[0], { - ...inputSrc, - segment: { - ...inputSrc.segment, - start: inputSrc.segment.start + 1 - } - }); - - assert.notEqual(spur1.edgeKey, spur2.edgeKey); - assert.notEqual(spur2.edgeKey, spur3.edgeKey); - assert.notEqual(spur3.edgeKey, spur1.edgeKey); - }); - }); - describe('split()', () => { describe(`on token comprised of single-char transforms: [crt][ae][nr][t]`, () => { const runSplit = (splitIndex: number) => {