Files
WeHub Mirror 6bf8bebf51
CI / Test and Build (push) Failing after 1s
CI / Migrate Dev DB (push) Has been skipped
CI / Migrate DB (push) Has been skipped
CodeQL / Analyze actions (push) Has been cancelled
CodeQL / Analyze javascript-typescript (push) Has been cancelled
CI / Detect Version (push) Has been cancelled
CI / Detect Desktop Changes (push) Has been cancelled
CI / Build AMD64 (blacksmith-2vcpu-ubuntu-2404, ./docker/cron.Dockerfile, ubuntu-latest, ghcr.io/simstudioai/cron) (push) Has been cancelled
CI / Build AMD64 (blacksmith-2vcpu-ubuntu-2404, ./docker/db.Dockerfile, ECR_MIGRATIONS, ubuntu-latest, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (blacksmith-4vcpu-ubuntu-2404, ./docker/pii.Dockerfile, ECR_PII, ubuntu-latest, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (blacksmith-4vcpu-ubuntu-2404, ./docker/realtime.Dockerfile, ECR_REALTIME, ubuntu-latest, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build AMD64 (blacksmith-8vcpu-ubuntu-2404, ./docker/app.Dockerfile, ECR_APP, linux-x64-8-core, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/cron.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/cron) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/db.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/pii.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/realtime.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-8vcpu-ubuntu-2404-arm, ./docker/app.Dockerfile, linux-arm64-8-core, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
Helm Chart / Lint, test, and validate chart (push) Has been cancelled
Helm Chart / Chart version bumped (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
CI / Build Dev ECR (blacksmith-8vcpu-ubuntu-2404, ./docker/app.Dockerfile, ECR_APP, linux-x64-8-core) (push) Has been cancelled
CI / Promote Images (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/cron) (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 / Build Dev ECR (blacksmith-2vcpu-ubuntu-2404, ./docker/db.Dockerfile, ECR_MIGRATIONS, ubuntu-latest) (push) Has been cancelled
CI / Build Dev ECR (blacksmith-4vcpu-ubuntu-2404, ./docker/pii.Dockerfile, ECR_PII, ubuntu-latest) (push) Has been cancelled
CI / Build Dev ECR (blacksmith-4vcpu-ubuntu-2404, ./docker/realtime.Dockerfile, ECR_REALTIME, ubuntu-latest) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Check Desktop Signing Secrets (push) Has been cancelled
CI / Desktop Release (push) Has been cancelled
CI / Create Desktop Prerelease (push) Has been cancelled
CI / Desktop Prerelease Build (push) Has been cancelled
CI / Publish Desktop Prerelease (push) Has been cancelled
CI / Prune Desktop Prereleases (push) Has been cancelled
Helm Chart / Install on kind and run helm test (push) Has been cancelled
WeHub snapshot of cb28d14c6f2c081de7a0d8729a8c816c9adef67a
2026-08-10 11:17:50 +08:00

269 lines
7.7 KiB
TypeScript

/**
* @vitest-environment node
*/
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { BlockState, WorkflowState } from '@/stores/workflows/workflow/types'
vi.mock('@/stores/workflows/workflow/store', () => ({
useWorkflowStore: {
getState: () => ({
getWorkflowState: () => ({ blocks: {}, edges: [], loops: {}, parallels: {} }),
}),
},
}))
vi.mock('@/stores/workflows/utils', () => ({
mergeSubblockState: (blocks: Record<string, BlockState>) => blocks,
}))
vi.mock('@/lib/workflows/sanitization/key-validation', () => ({
isValidKey: (key: string) => key !== 'undefined' && key !== 'null' && key !== '',
}))
vi.mock('@/lib/workflows/autolayout', () => ({
transferBlockHeights: vi.fn(),
applyTargetedLayout: (blocks: Record<string, BlockState>) => blocks,
getTargetedLayoutImpact: () => ({
layoutBlockIds: [],
shiftSourceBlockIds: [],
}),
}))
vi.mock('@/lib/workflows/autolayout/constants', () => ({
DEFAULT_HORIZONTAL_SPACING: 500,
DEFAULT_VERTICAL_SPACING: 400,
DEFAULT_LAYOUT_OPTIONS: {},
}))
vi.mock('@/stores/workflows/workflow/utils', () => ({
generateLoopBlocks: () => ({}),
generateParallelBlocks: () => ({}),
}))
vi.mock('@/blocks', () => ({
getBlock: () => null,
getAllBlocks: () => ({}),
getAllBlockTypes: () => [],
getBlockByToolName: () => null,
getBlocksByCategory: () => [],
isValidBlockType: () => false,
registry: {},
}))
vi.mock('@/tools/utils', () => ({
getTool: () => null,
}))
vi.mock('@/triggers', () => ({
getTrigger: () => null,
isTriggerValid: () => false,
}))
vi.mock('@/lib/workflows/blocks/block-outputs', () => ({
getEffectiveBlockOutputs: () => ({}),
}))
vi.mock('@/lib/workflows/subblocks/visibility', () => ({
buildDefaultCanonicalModes: () => ({}),
}))
vi.mock('@/lib/workflows/triggers/triggers', () => ({
TRIGGER_TYPES: {},
classifyStartBlockType: () => null,
StartBlockPath: {},
getTriggerOutputs: () => ({}),
}))
vi.mock('@/hooks/use-trigger-config-aggregation', () => ({
populateTriggerFieldsFromConfig: () => [],
}))
vi.mock('@/executor/constants', () => ({
isAnnotationOnlyBlock: () => false,
BLOCK_DIMENSIONS: { MIN_HEIGHT: 100 },
HANDLE_POSITIONS: {},
}))
vi.mock('@/stores/workflows/registry/store', () => ({
useWorkflowRegistry: {
getState: () => ({
activeWorkflowId: null,
}),
},
}))
vi.mock('@/stores/workflows/subblock/store', () => ({
useSubBlockStore: {
getState: () => ({
workflowValues: {},
getValue: () => null,
}),
},
}))
import { WorkflowDiffEngine } from './diff-engine'
function createMockBlock(overrides: Partial<BlockState> = {}): BlockState {
return {
id: 'block-1',
type: 'agent',
name: 'Test Block',
enabled: true,
position: { x: 0, y: 0 },
subBlocks: {},
outputs: {},
...overrides,
} as BlockState
}
function createMockWorkflowState(blocks: Record<string, BlockState>): WorkflowState {
return {
blocks,
edges: [],
loops: {},
parallels: {},
}
}
describe('WorkflowDiffEngine', () => {
let engine: WorkflowDiffEngine
beforeEach(() => {
engine = new WorkflowDiffEngine()
vi.clearAllMocks()
})
describe('hasBlockChanged detection', () => {
describe('locked state changes', () => {
it.concurrent(
'should NOT detect a diff when only the locked state changes (false -> true)',
async () => {
const freshEngine = new WorkflowDiffEngine()
const baseline = createMockWorkflowState({
'block-1': createMockBlock({ id: 'block-1', locked: false }),
})
const proposed = createMockWorkflowState({
'block-1': createMockBlock({ id: 'block-1', locked: true }),
})
const result = await freshEngine.createDiffFromWorkflowState(
proposed,
undefined,
baseline
)
expect(result.success).toBe(true)
expect(result.diff?.diffAnalysis?.edited_blocks ?? []).not.toContain('block-1')
expect(
result.diff?.diffAnalysis?.field_diffs?.['block-1']?.changed_fields ?? []
).not.toContain('locked')
}
)
it.concurrent('should not detect change when locked state is the same', async () => {
const freshEngine = new WorkflowDiffEngine()
const baseline = createMockWorkflowState({
'block-1': createMockBlock({ id: 'block-1', locked: true }),
})
const proposed = createMockWorkflowState({
'block-1': createMockBlock({ id: 'block-1', locked: true }),
})
const result = await freshEngine.createDiffFromWorkflowState(proposed, undefined, baseline)
expect(result.success).toBe(true)
expect(result.diff?.diffAnalysis?.edited_blocks ?? []).not.toContain('block-1')
})
it.concurrent(
'should NOT detect a diff when locked goes from undefined to true',
async () => {
const freshEngine = new WorkflowDiffEngine()
const baseline = createMockWorkflowState({
'block-1': createMockBlock({ id: 'block-1' }),
})
const proposed = createMockWorkflowState({
'block-1': createMockBlock({ id: 'block-1', locked: true }),
})
const result = await freshEngine.createDiffFromWorkflowState(
proposed,
undefined,
baseline
)
expect(result.success).toBe(true)
expect(result.diff?.diffAnalysis?.edited_blocks ?? []).not.toContain('block-1')
}
)
it.concurrent(
'should still detect real edits on a block whose locked state also changed',
async () => {
const freshEngine = new WorkflowDiffEngine()
const baseline = createMockWorkflowState({
'block-1': createMockBlock({ id: 'block-1', enabled: true, locked: false }),
})
const proposed = createMockWorkflowState({
'block-1': createMockBlock({ id: 'block-1', enabled: false, locked: true }),
})
const result = await freshEngine.createDiffFromWorkflowState(
proposed,
undefined,
baseline
)
expect(result.success).toBe(true)
expect(result.diff?.diffAnalysis?.edited_blocks).toContain('block-1')
const changed = result.diff?.diffAnalysis?.field_diffs?.['block-1']?.changed_fields ?? []
expect(changed).toContain('enabled')
expect(changed).not.toContain('locked')
}
)
})
describe('parent scope changes', () => {
it.concurrent('should detect when a block moves into a subflow', async () => {
const freshEngine = new WorkflowDiffEngine()
const baseline = createMockWorkflowState({
'block-1': createMockBlock({ id: 'block-1' }),
})
const proposed = createMockWorkflowState({
'block-1': createMockBlock({
id: 'block-1',
data: { parentId: 'loop-1', extent: 'parent' },
}),
})
const result = await freshEngine.createDiffFromWorkflowState(proposed, undefined, baseline)
expect(result.success).toBe(true)
expect(result.diff?.diffAnalysis?.edited_blocks).toContain('block-1')
expect(result.diff?.diffAnalysis?.field_diffs?.['block-1']?.changed_fields).toContain(
'parentId'
)
})
})
})
describe('diff lifecycle', () => {
it.concurrent('should start with no diff', () => {
const freshEngine = new WorkflowDiffEngine()
expect(freshEngine.hasDiff()).toBe(false)
expect(freshEngine.getCurrentDiff()).toBeUndefined()
})
it.concurrent('should clear diff', () => {
const freshEngine = new WorkflowDiffEngine()
freshEngine.clearDiff()
expect(freshEngine.hasDiff()).toBe(false)
})
})
})