diff --git a/packages/machine/src/classes/State.spec.ts b/packages/machine/src/classes/State.spec.ts index f32939c..3c16b0a 100644 --- a/packages/machine/src/classes/State.spec.ts +++ b/packages/machine/src/classes/State.spec.ts @@ -208,7 +208,7 @@ describe('State.withOverriddenHaltState', () => { }); describe('State.toGraph — unbound Reference', () => { - test('skips a transition whose nextState is an unbound Reference', () => { + test('skips a transition whose nextState is an unbound Reference (non-wrapper context)', () => { // An unbound Reference throws when its `.ref` getter is read. State.toGraph // catches that and skips the transition rather than failing the whole walk. const unboundRef = new Reference(); @@ -222,6 +222,30 @@ describe('State.toGraph — unbound Reference', () => { // Only the haltState-bound transition survives; the unbound one is dropped. expect(graph.nodes[state.id].transitions).toHaveLength(1); }); + + test('skips a transition whose nextState is an unbound Reference (wrapper context)', () => { + // toGraph has a separate try/catch in its wrapper-context branch — when the + // bare being walked is inside a `withOverriddenHaltState` wrapper. Same + // skip-and-continue semantic. + const unboundRef = new Reference(); + const bare = new State({ + [symbol(['0'])]: {nextState: unboundRef}, + [symbol(['1'])]: {nextState: haltState}, + }, 'bare'); + const override = new State({ + [symbol(['0'])]: {nextState: haltState}, + [symbol(['1'])]: {nextState: haltState}, + }, 'override'); + const wrapped = bare.withOverriddenHaltState(override); + + const graph = State.toGraph(wrapped, tapeBlock); + + // The collapsed wrapper node retains only the haltState-bound transition; + // the unbound-Ref one is dropped. + const collapsedNode = graph.nodes[wrapped.id]; + + expect(collapsedNode.transitions).toHaveLength(1); + }); }); describe('State.fromGraph — cyclic override-halt chain', () => { diff --git a/packages/machine/src/utilities/graph.spec.ts b/packages/machine/src/utilities/graph.spec.ts index e8592fd..f2efd9c 100644 --- a/packages/machine/src/utilities/graph.spec.ts +++ b/packages/machine/src/utilities/graph.spec.ts @@ -4,6 +4,7 @@ import { decodeWriteSymbol, parseMovementLabel, parsePatternString, + parseWriteSymbolLabel, splitUnescaped, } from './graph'; import {fromMermaid, toMermaid} from './graphFormats'; @@ -214,6 +215,34 @@ describe('parsePatternString', () => { test('per-cell `B` becomes the tape blank symbol', () => { expect(parsePatternString("B,'a'", [[' ', '0'], [' ', 'a']])).toEqual([[' ', 'a']]); }); + + test('fallback: cell that is not marker/blank/quoted is returned as-is', () => { + // Defensive — the parser doesn't throw on unexpected cells; it returns + // them as-is, so consumer code can decide whether to reject. + expect(parsePatternString('Q', [[' ', '0']])).toEqual([['Q']]); + }); + + test('blank-marker fallback when alphabet for the tape is missing', () => { + // Defensive: if alphabets[tapeIx] is undefined, returns the marker + // string itself rather than throwing. + expect(parsePatternString('B', [])).toEqual([['B']]); + }); +}); + +describe('parseWriteSymbolLabel', () => { + test('maps K/E to upstream symbolCommands', () => { + expect(parseWriteSymbolLabel('K')).toBe(symbolCommands.keep); + expect(parseWriteSymbolLabel('E')).toBe(symbolCommands.erase); + }); + + test('strips single quotes from a literal alphabet symbol', () => { + expect(parseWriteSymbolLabel("'X'")).toBe('X'); + }); + + test('fallback: label that is not K/E/quoted is returned as-is', () => { + // Defensive — same shape as parsePatternString's fallback. + expect(parseWriteSymbolLabel('Z')).toBe('Z'); + }); }); describe('parseMovementLabel', () => {