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) => {