diff --git a/web/src/test/auto/headless/engine/predictive-text/helpers/constituentPaths.tests.ts b/web/src/test/auto/headless/engine/predictive-text/helpers/constituentPaths.tests.ts index 8ea47e9029f..d3df0742d50 100644 --- a/web/src/test/auto/headless/engine/predictive-text/helpers/constituentPaths.tests.ts +++ b/web/src/test/auto/headless/engine/predictive-text/helpers/constituentPaths.tests.ts @@ -9,9 +9,13 @@ import { assert } from 'chai'; +import { DeletionQuotientSpur, InsertionQuotientSpur } from '@keymanapp/lm-worker/test-index'; + import { constituentPaths } from "./constituentPaths.js"; +import { toSpurTypeSequence } from './toSpurTypeSequence.js'; import { buildCantLinearFixture } from './buildCantLinearFixture.js'; import { buildAlphabeticClusterFixtures } from './buildAlphabeticClusteredFixture.js'; +import { buildQuotientDocFixture } from './buildQuotientDocFixture.js'; describe('constituentPaths', () => { it('includes a single entry array when all parents are SearchQuotientSpurs', () => { @@ -42,4 +46,50 @@ describe('constituentPaths', () => { return p; })); }); + + describe('for the final quotient-graph doc example', () => { + it('handles insertion-only quotient-graph paths', () => { + const { sc2 } = buildQuotientDocFixture().nodes; + + const sc2Constituents = constituentPaths(sc2); + assert.equal(sc2Constituents.length, 1); + sc2Constituents.forEach(s => s.forEach(p => assert.isTrue(p instanceof InsertionQuotientSpur))); + }); + + it('handles deletion-only quotient-graph paths', () => { + const { k2c0 } = buildQuotientDocFixture().nodes; + + const k2c0Constituents = constituentPaths(k2c0); + assert.equal(k2c0Constituents.length, 1); + k2c0Constituents.forEach(s => s.forEach(p => assert.isTrue(p instanceof DeletionQuotientSpur))); + }); + + it('does not emit sequences with inserts immediately following deletes', () => { + const { k2c3 } = buildQuotientDocFixture().nodes; + + const k2c3Constituents = constituentPaths(k2c3); + + const shouldNotOccur = k2c3Constituents.find((seq) => { + const typeSeq = toSpurTypeSequence(seq); + return typeSeq.find((type, index) => { + return type == 'delete' && typeSeq[index+1] == 'insert'; + }); + }); + assert.isNotOk(shouldNotOccur); + }); + + it('does emit sequences with deletes immediately following inserts', () => { + const { k2c3 } = buildQuotientDocFixture().nodes; + + const k2c3Constituents = constituentPaths(k2c3); + + const shouldOccur = k2c3Constituents.find((seq) => { + const typeSeq = toSpurTypeSequence(seq); + return typeSeq.find((type, index) => { + return type == 'insert' && typeSeq[index+1] == 'delete'; + }); + }); + assert.isOk(shouldOccur); + }); + }); }); \ No newline at end of file diff --git a/web/src/test/auto/headless/engine/predictive-text/helpers/constituentPaths.ts b/web/src/test/auto/headless/engine/predictive-text/helpers/constituentPaths.ts index 9c01297f0ce..38325078719 100644 --- a/web/src/test/auto/headless/engine/predictive-text/helpers/constituentPaths.ts +++ b/web/src/test/auto/headless/engine/predictive-text/helpers/constituentPaths.ts @@ -8,6 +8,8 @@ */ import { + DeletionQuotientSpur, + InsertionQuotientSpur, SearchQuotientCluster, SearchQuotientNode, SearchQuotientRoot, @@ -29,6 +31,28 @@ export function constituentPaths(node: SearchQuotientNode): SearchQuotientSpur[] const parentPaths = constituentPaths(node.parents[0]); let pathsToExtend = parentPaths; + if(node instanceof InsertionQuotientSpur) { + pathsToExtend = pathsToExtend.filter(s => { + const tail = s[s.length - 1]; + + // Deletion nodes and modules should always be ordered after those for + // insertion in order to avoid duplicating search paths. (Insertions may + // stick to the right of a root, while deletions always process inputs; insertions + // may thus precede deletions.) + // + // Also, internally, insertion edges are not built after deletion (or empty) edges. + if(tail instanceof DeletionQuotientSpur) { + return false; + } else if(tail.insertLength == 0 && tail.leftDeleteLength == 0) { + // Insertions should also not appear after empty nodes; there's no net + // difference between inserting before and inserting after. + return false; + } + + return true; + }); + } + if(parentPaths.length > 0) { return pathsToExtend.map(p => { p.push(node); diff --git a/web/src/test/auto/headless/engine/predictive-text/helpers/toSpurTypeSequence.ts b/web/src/test/auto/headless/engine/predictive-text/helpers/toSpurTypeSequence.ts new file mode 100644 index 00000000000..74c9ea182cb --- /dev/null +++ b/web/src/test/auto/headless/engine/predictive-text/helpers/toSpurTypeSequence.ts @@ -0,0 +1,20 @@ +import { + DeletionQuotientSpur, + InsertionQuotientSpur, + SearchQuotientNode, + SubstitutionQuotientSpur +} from "@keymanapp/lm-worker/test-index"; + +export function toSpurTypeSequence(spurs: SearchQuotientNode[]): ('insert' | 'delete' | 'substitute' | 'legacy')[] { + return spurs.map(s => { + if(s instanceof InsertionQuotientSpur) { + return 'insert'; + } else if(s instanceof DeletionQuotientSpur) { + return 'delete'; + } else if(s instanceof SubstitutionQuotientSpur) { + return 'substitute'; + } else { + return 'legacy'; + } + }) +} \ No newline at end of file