Files
simstudioai--sim/apps/sim/executor/utils/subflow-node-id-codec.test.ts
wehub-resource-sync d25d482dc2
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:20:55 +08:00

176 lines
7.5 KiB
TypeScript

/**
* @vitest-environment node
*/
import { describe, expect, it } from 'vitest'
import { SubflowNodeIdCodec } from '@/executor/utils/subflow-node-id-codec'
describe('SubflowNodeIdCodec', () => {
describe('branch subscripts', () => {
it('builds and round-trips branch node IDs', () => {
const id = SubflowNodeIdCodec.buildBranchNodeId('block-1', 2)
expect(id).toBe('block-1₍2₎')
expect(SubflowNodeIdCodec.isBranchNodeId(id)).toBe(true)
expect(SubflowNodeIdCodec.extractBaseBlockId(id)).toBe('block-1')
expect(SubflowNodeIdCodec.extractBranchIndex(id)).toBe(2)
})
it('returns null index and false predicate for non-branch IDs', () => {
expect(SubflowNodeIdCodec.isBranchNodeId('block-1')).toBe(false)
expect(SubflowNodeIdCodec.extractBranchIndex('block-1')).toBeNull()
expect(SubflowNodeIdCodec.extractBaseBlockId('block-1')).toBe('block-1')
})
it('only strips a trailing branch subscript', () => {
expect(SubflowNodeIdCodec.extractBaseBlockId('a₍1₎b')).toBe('a₍1₎b')
expect(SubflowNodeIdCodec.extractBaseBlockId('a₍1₎b₍2₎')).toBe('a₍1₎b')
})
})
describe('loop sentinels', () => {
it('builds and parses loop sentinel IDs', () => {
const start = SubflowNodeIdCodec.buildLoopSentinelStartId('loop-1')
const end = SubflowNodeIdCodec.buildLoopSentinelEndId('loop-1')
expect(start).toBe('loop-loop-1-sentinel-start')
expect(end).toBe('loop-loop-1-sentinel-end')
expect(SubflowNodeIdCodec.isLoopSentinelNodeId(start)).toBe(true)
expect(SubflowNodeIdCodec.isLoopSentinelNodeId(end)).toBe(true)
expect(SubflowNodeIdCodec.extractLoopIdFromSentinel(start)).toBe('loop-1')
expect(SubflowNodeIdCodec.extractLoopIdFromSentinel(end)).toBe('loop-1')
})
it('returns null when not a loop sentinel', () => {
expect(SubflowNodeIdCodec.isLoopSentinelNodeId('block-1')).toBe(false)
expect(SubflowNodeIdCodec.extractLoopIdFromSentinel('block-1')).toBeNull()
})
})
describe('parallel sentinels', () => {
it('builds and parses parallel sentinel IDs', () => {
const start = SubflowNodeIdCodec.buildParallelSentinelStartId('p-1')
const end = SubflowNodeIdCodec.buildParallelSentinelEndId('p-1')
expect(start).toBe('parallel-p-1-sentinel-start')
expect(end).toBe('parallel-p-1-sentinel-end')
expect(SubflowNodeIdCodec.isParallelSentinelNodeId(start)).toBe(true)
expect(SubflowNodeIdCodec.isParallelSentinelNodeId(end)).toBe(true)
expect(SubflowNodeIdCodec.extractParallelIdFromSentinel(start)).toBe('p-1')
expect(SubflowNodeIdCodec.extractParallelIdFromSentinel(end)).toBe('p-1')
})
it('returns null when not a parallel sentinel', () => {
expect(SubflowNodeIdCodec.isParallelSentinelNodeId('block-1')).toBe(false)
expect(SubflowNodeIdCodec.extractParallelIdFromSentinel('block-1')).toBeNull()
})
})
describe('outer-branch clone scoping', () => {
it('builds and extracts outer branch index', () => {
const id = SubflowNodeIdCodec.buildOuterBranchScopedId('loop-1', 3)
expect(id).toBe('loop-1__obranch-3')
expect(SubflowNodeIdCodec.extractOuterBranchIndex(id)).toBe(3)
})
it('extracts the innermost outer branch index for nested clones', () => {
const id = 'loop-1__obranch-2__obranch-5'
expect(SubflowNodeIdCodec.extractOuterBranchIndex(id)).toBe(2)
expect(SubflowNodeIdCodec.extractInnermostOuterBranchIndex(id)).toBe(5)
})
it('returns undefined when no outer branch suffix is present', () => {
expect(SubflowNodeIdCodec.extractOuterBranchIndex('loop-1')).toBeUndefined()
expect(SubflowNodeIdCodec.extractInnermostOuterBranchIndex('loop-1')).toBeUndefined()
})
it('strips outer-branch and clone-digest suffixes', () => {
expect(SubflowNodeIdCodec.stripOuterBranchSuffix('loop-1__obranch-2')).toBe('loop-1')
expect(SubflowNodeIdCodec.stripOuterBranchSuffix('loop-1__cloneABCDEF__obranch-2')).toBe(
'loop-1'
)
})
it('strips all clone suffixes and branch subscripts to the base block ID', () => {
expect(SubflowNodeIdCodec.stripCloneSuffixes('block-1__obranch-2₍3₎')).toBe('block-1')
expect(SubflowNodeIdCodec.stripCloneSuffixes('block-1__clone0a1f__obranch-2₍0₎')).toBe(
'block-1'
)
})
})
describe('normalizeNodeId', () => {
it('normalizes branch, loop sentinel, and parallel sentinel IDs', () => {
expect(SubflowNodeIdCodec.normalizeNodeId('block-1₍2₎')).toBe('block-1')
expect(SubflowNodeIdCodec.normalizeNodeId('loop-loop-1-sentinel-start')).toBe('loop-1')
expect(SubflowNodeIdCodec.normalizeNodeId('parallel-p-1-sentinel-end')).toBe('p-1')
expect(SubflowNodeIdCodec.normalizeNodeId('block-1')).toBe('block-1')
})
})
describe('loop digest lookup helpers', () => {
it('strips branch subscripts and loop digests for lookup keys', () => {
expect(SubflowNodeIdCodec.normalizeLookupId('block-1₍2₎_loop3')).toBe('block-1')
expect(SubflowNodeIdCodec.normalizeLookupId('block-1')).toBe('block-1')
})
it('extracts the leading branch suffix and loop digest segments', () => {
expect(SubflowNodeIdCodec.extractBranchSuffix('block-1₍2₎_loop3')).toBe('₍2₎')
expect(SubflowNodeIdCodec.extractBranchSuffix('block-1')).toBe('')
expect(SubflowNodeIdCodec.extractLoopSuffix('block-1₍2₎_loop3')).toBe('_loop3')
expect(SubflowNodeIdCodec.extractLoopSuffix('block-1')).toBe('')
})
})
describe('findEffectiveContainerId', () => {
it('returns the original ID for branch 0 / missing scope', () => {
const map = new Map<string, unknown>([['loop-1', {}]])
expect(SubflowNodeIdCodec.findEffectiveContainerId('loop-1', 'block-1', map)).toBe('loop-1')
})
it('prefers the mapped cloned scope when present', () => {
const map = new Map<string, unknown>([
['loop-1', {}],
['loop-1__obranch-2', {}],
])
expect(SubflowNodeIdCodec.findEffectiveContainerId('loop-1', 'block-1', map, 2)).toBe(
'loop-1__obranch-2'
)
})
it('resolves the cloned scope from the current node ID suffix', () => {
const map = new Map<string, unknown>([
['loop-1', {}],
['loop-1__obranch-3', {}],
])
expect(SubflowNodeIdCodec.findEffectiveContainerId('loop-1', 'block-1__obranch-3', map)).toBe(
'loop-1__obranch-3'
)
})
it('prefers __clone scopes when the current node carries a clone marker', () => {
const map = new Map<string, unknown>([
['loop-1__obranch-2', {}],
['loop-1__cloneabc__obranch-2', {}],
])
expect(
SubflowNodeIdCodec.findEffectiveContainerId('loop-1', 'block-1__cloneabc__obranch-2', map)
).toBe('loop-1__cloneabc__obranch-2')
})
})
describe('round-trip parse ∘ build', () => {
it('builds then parses branch IDs symmetrically', () => {
for (const index of [0, 1, 7, 20]) {
const id = SubflowNodeIdCodec.buildBranchNodeId('base-id', index)
expect(SubflowNodeIdCodec.extractBranchIndex(id)).toBe(index)
expect(SubflowNodeIdCodec.extractBaseBlockId(id)).toBe('base-id')
}
})
it('builds then parses outer-branch scoped IDs symmetrically', () => {
for (const index of [1, 4, 19]) {
const id = SubflowNodeIdCodec.buildOuterBranchScopedId('base-id', index)
expect(SubflowNodeIdCodec.extractOuterBranchIndex(id)).toBe(index)
expect(SubflowNodeIdCodec.stripOuterBranchSuffix(id)).toBe('base-id')
}
})
})
})