import assert from 'node:assert/strict'; import test from 'node:test'; import { isSceneGraphEdgeRejected } from './sceneGraphEdgeRules'; import { canSetSideStoryStart, getNodeStoryRoot, isNodeInSideStoryline, isSideStoryEdge, listSideStoryStarts, } from './sceneGraphLineage'; import type { SceneGraphEdge, SceneGraphNode } from '../types'; import { asGraphNodeId, asSceneId } from '../types/ids'; function node(id: string, scene: string, opts?: Partial): SceneGraphNode { return { id: asGraphNodeId(id), sceneId: asSceneId(scene), x: 0, y: 0, isStartScene: false, isSideStoryStart: false, sideStoryLineTitle: '', ...opts, }; } function edge(id: string, src: string, tgt: string): SceneGraphEdge { return { id, sourceGraphNodeId: asGraphNodeId(src), targetGraphNodeId: asGraphNodeId(tgt) }; } void test('canSetSideStoryStart: false when main or side marker in component', () => { const nodes = [ node('main', 's1', { isStartScene: true }), node('a', 's2'), node('side', 's3', { isSideStoryStart: true }), node('b', 's4'), node('orphan', 's5'), ]; const edges = [edge('e1', 'main', 'a'), edge('e2', 'side', 'b')]; assert.equal(canSetSideStoryStart(nodes, edges, asGraphNodeId('a')), false); assert.equal(canSetSideStoryStart(nodes, edges, asGraphNodeId('b')), false); assert.equal(canSetSideStoryStart(nodes, edges, asGraphNodeId('orphan')), true); }); void test('getNodeStoryRoot classifies main, side, orphan', () => { const nodes = [ node('main', 's1', { isStartScene: true }), node('a', 's2'), node('side', 's3', { isSideStoryStart: true }), node('b', 's4'), node('orphan', 's5'), ]; const edges = [edge('e1', 'main', 'a'), edge('e2', 'side', 'b')]; assert.equal(getNodeStoryRoot(nodes, edges, asGraphNodeId('main')), 'main'); assert.equal(getNodeStoryRoot(nodes, edges, asGraphNodeId('a')), 'main'); assert.equal(getNodeStoryRoot(nodes, edges, asGraphNodeId('b')), asGraphNodeId('side')); assert.equal(getNodeStoryRoot(nodes, edges, asGraphNodeId('orphan')), null); assert.equal(isNodeInSideStoryline(nodes, edges, asGraphNodeId('main')), false); assert.equal(isNodeInSideStoryline(nodes, edges, asGraphNodeId('a')), false); assert.equal(isNodeInSideStoryline(nodes, edges, asGraphNodeId('b')), true); }); void test('isSceneGraphEdgeRejected: blocks main↔side and side↔side', () => { const nodes = [ node('main', 's1', { isStartScene: true }), node('side1', 's2', { isSideStoryStart: true }), node('side2', 's3', { isSideStoryStart: true }), node('a', 's4'), node('b', 's5'), ]; const edges = [edge('e1', 'side1', 'a'), edge('e2', 'side2', 'b')]; assert.equal(isSceneGraphEdgeRejected(nodes, edges, asGraphNodeId('main'), asGraphNodeId('side1')), true); assert.equal(isSceneGraphEdgeRejected(nodes, edges, asGraphNodeId('side1'), asGraphNodeId('main')), true); assert.equal(isSceneGraphEdgeRejected(nodes, edges, asGraphNodeId('main'), asGraphNodeId('a')), true); assert.equal(isSceneGraphEdgeRejected(nodes, edges, asGraphNodeId('a'), asGraphNodeId('main')), true); assert.equal(isSceneGraphEdgeRejected(nodes, edges, asGraphNodeId('a'), asGraphNodeId('b')), true); assert.equal(isSceneGraphEdgeRejected(nodes, edges, asGraphNodeId('a'), asGraphNodeId('side1')), false); }); void test('isSceneGraphEdgeRejected: blocks main to any node in side component without badge', () => { const nodes = [ node('main', 's1', { isStartScene: true }), node('side1', 's2', { isSideStoryStart: true }), node('a', 's3'), node('b', 's4'), ]; const edges = [edge('e1', 'side1', 'a'), edge('e2', 'a', 'b')]; assert.equal(isSceneGraphEdgeRejected(nodes, edges, asGraphNodeId('main'), asGraphNodeId('a')), true); assert.equal(isSceneGraphEdgeRejected(nodes, edges, asGraphNodeId('main'), asGraphNodeId('b')), true); assert.equal(isSceneGraphEdgeRejected(nodes, edges, asGraphNodeId('a'), asGraphNodeId('main')), true); }); void test('isSideStoryEdge: side chain edges are blue even without badge on endpoint', () => { const nodes = [ node('side', 's1', { isSideStoryStart: true }), node('a', 's2'), node('b', 's3'), ]; const edges = [edge('e1', 'side', 'a'), edge('e2', 'a', 'b')]; assert.equal(isSideStoryEdge(nodes, edges, edges[0]!), true); assert.equal(isSideStoryEdge(nodes, edges, edges[1]!), true); }); void test('isNodeInSideStoryline: follows current component after reconnect', () => { const nodes = [ node('main', 's1', { isStartScene: true }), node('side', 's2', { isSideStoryStart: true }), node('x', 's3'), ]; const mainEdge = [edge('e1', 'main', 'x')]; const sideEdge = [edge('e2', 'side', 'x')]; assert.equal(isNodeInSideStoryline(nodes, mainEdge, asGraphNodeId('x')), false); assert.equal(isNodeInSideStoryline(nodes, sideEdge, asGraphNodeId('x')), true); }); void test('isSideStoryEdge: main edges are not side edges', () => { const nodes = [ node('main', 's1', { isStartScene: true }), node('a', 's2'), node('side', 's3', { isSideStoryStart: true }), node('b', 's4'), ]; const edges = [edge('e1', 'main', 'a'), edge('e2', 'side', 'b')]; assert.equal(isSideStoryEdge(nodes, edges, edges[0]!), false); assert.equal(isSideStoryEdge(nodes, edges, edges[1]!), true); }); void test('isSceneGraphEdgeRejected: allows main-to-main edges', () => { const nodes = [node('main', 's1', { isStartScene: true }), node('a', 's2')]; const edges: SceneGraphEdge[] = []; assert.equal( isSceneGraphEdgeRejected(nodes, edges, asGraphNodeId('main'), asGraphNodeId('a')), false, ); }); void test('isSceneGraphEdgeRejected: allows main to free connected chain without markers', () => { const nodes = [ node('main', 's1', { isStartScene: true }), node('a', 's2'), node('b', 's3'), node('c', 's4'), ]; const edges = [edge('e1', 'main', 'a'), edge('e2', 'b', 'c')]; assert.equal(isSceneGraphEdgeRejected(nodes, edges, asGraphNodeId('main'), asGraphNodeId('b')), false); assert.equal(isSceneGraphEdgeRejected(nodes, edges, asGraphNodeId('a'), asGraphNodeId('b')), false); assert.equal(isSceneGraphEdgeRejected(nodes, edges, asGraphNodeId('a'), asGraphNodeId('c')), false); }); void test('isSceneGraphEdgeRejected: allows connections within main component without start badge on target', () => { const nodes = [ node('main', 's1', { isStartScene: true }), node('a', 's2'), node('b', 's3'), node('c', 's4'), ]; const edges = [edge('e1', 'main', 'a'), edge('e2', 'a', 'b')]; assert.equal(isSceneGraphEdgeRejected(nodes, edges, asGraphNodeId('main'), asGraphNodeId('b')), false); assert.equal(isSceneGraphEdgeRejected(nodes, edges, asGraphNodeId('a'), asGraphNodeId('c')), false); assert.equal(isSceneGraphEdgeRejected(nodes, edges, asGraphNodeId('b'), asGraphNodeId('c')), false); }); void test('listSideStoryStarts returns all side starters', () => { const nodes = [ node('side1', 's1', { isSideStoryStart: true, sideStoryLineTitle: 'Quest A' }), node('side2', 's2', { isSideStoryStart: true }), ]; assert.equal(listSideStoryStarts(nodes).length, 2); });