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
229 lines
7.5 KiB
TypeScript
229 lines
7.5 KiB
TypeScript
/**
|
|
* @vitest-environment node
|
|
*
|
|
* Tests for the workflow-diff store's status modeling.
|
|
*
|
|
* Focus: the {@link WorkflowDiffStatus} enum is the single source of truth and
|
|
* the legacy `hasActiveDiff` / `isShowingDiff` / `isDiffReady` booleans are
|
|
* derived from it, so contradictory combinations are unrepresentable. We assert
|
|
* the exhaustive status → boolean mapping and the status transitions driven by
|
|
* the tractable actions (`toggleDiffView`, `clearDiff`, `_batchedStateUpdate`).
|
|
*
|
|
* @remarks
|
|
* The store transitively imports the diff engine, serializer, socket
|
|
* operations, and the workflow/registry stores, all of which drag in the block
|
|
* registry and emcn icon CSS. Every such dependency is mocked so the suite
|
|
* loads under the node environment and exercises only the store + its types.
|
|
*/
|
|
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
const { applyWorkflowStateToStores } = vi.hoisted(() => ({
|
|
applyWorkflowStateToStores: vi.fn(),
|
|
}))
|
|
|
|
vi.mock('@sim/logger', () => ({
|
|
createLogger: () => ({ info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() }),
|
|
}))
|
|
|
|
vi.mock('@/lib/workflows/diff', () => ({
|
|
WorkflowDiffEngine: class {
|
|
clearDiff = vi.fn()
|
|
createDiffFromWorkflowState = vi.fn()
|
|
},
|
|
stripWorkflowDiffMarkers: vi.fn((s) => s),
|
|
}))
|
|
|
|
vi.mock('@/lib/workflows/operations/socket-operations', () => ({
|
|
enqueueReplaceWorkflowState: vi.fn().mockResolvedValue(undefined),
|
|
}))
|
|
|
|
vi.mock('@/lib/workflows/sanitization/validation', () => ({
|
|
validateWorkflowState: vi.fn(() => ({ valid: true, errors: [], sanitizedState: null })),
|
|
}))
|
|
|
|
vi.mock('@/serializer', () => ({
|
|
Serializer: class {
|
|
serializeWorkflow = vi.fn()
|
|
deserializeWorkflow = vi.fn()
|
|
},
|
|
}))
|
|
|
|
vi.mock('@/stores/workflows/registry/store', () => ({
|
|
useWorkflowRegistry: { getState: vi.fn(() => ({ activeWorkflowId: null })) },
|
|
}))
|
|
|
|
vi.mock('@/stores/workflows/utils', () => ({
|
|
mergeSubblockState: vi.fn((blocks) => blocks),
|
|
}))
|
|
|
|
vi.mock('@/stores/workflows/workflow/store', () => ({
|
|
useWorkflowStore: {
|
|
getState: vi.fn(() => ({
|
|
getWorkflowState: vi.fn(() => ({ blocks: {}, edges: [], loops: {}, parallels: {} })),
|
|
blocks: {},
|
|
lastSaved: 0,
|
|
})),
|
|
setState: vi.fn(),
|
|
},
|
|
}))
|
|
|
|
vi.mock('@/stores/workflow-diff/utils', () => ({
|
|
applyWorkflowStateToStores,
|
|
captureBaselineSnapshot: vi.fn(),
|
|
cloneWorkflowState: vi.fn((s) => s),
|
|
createBatchedUpdater:
|
|
(set: (u: Record<string, unknown>) => void) => (updates: Record<string, unknown>) =>
|
|
set(updates),
|
|
getLatestUserMessageId: vi.fn().mockResolvedValue(null),
|
|
persistWorkflowStateToServer: vi.fn().mockResolvedValue(true),
|
|
WORKFLOW_DIFF_SETTLED_EVENT: 'workflow-diff-settled',
|
|
}))
|
|
|
|
import { RESET_DIFF_STATE, useWorkflowDiffStore } from '@/stores/workflow-diff/store'
|
|
import {
|
|
deriveDiffFlags,
|
|
type WorkflowDiffState,
|
|
type WorkflowDiffStatus,
|
|
} from '@/stores/workflow-diff/types'
|
|
|
|
function seedStatus(status: WorkflowDiffStatus): void {
|
|
useWorkflowDiffStore.setState(deriveDiffFlags(status))
|
|
}
|
|
|
|
describe('useWorkflowDiffStore status modeling', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
useWorkflowDiffStore.setState({
|
|
...RESET_DIFF_STATE,
|
|
pendingExternalUpdates: {},
|
|
remoteUpdateVersions: {},
|
|
reconcilingWorkflows: {},
|
|
reconciliationErrors: {},
|
|
} as Partial<WorkflowDiffState>)
|
|
})
|
|
|
|
describe('deriveDiffFlags', () => {
|
|
it('maps every status to the documented legacy booleans', () => {
|
|
expect(deriveDiffFlags('none')).toEqual({
|
|
status: 'none',
|
|
hasActiveDiff: false,
|
|
isShowingDiff: false,
|
|
isDiffReady: false,
|
|
})
|
|
expect(deriveDiffFlags('staged')).toEqual({
|
|
status: 'staged',
|
|
hasActiveDiff: true,
|
|
isShowingDiff: false,
|
|
isDiffReady: true,
|
|
})
|
|
expect(deriveDiffFlags('showing')).toEqual({
|
|
status: 'showing',
|
|
hasActiveDiff: true,
|
|
isShowingDiff: true,
|
|
isDiffReady: true,
|
|
})
|
|
})
|
|
|
|
it('keeps hasActiveDiff and isDiffReady in lockstep (legacy invariant)', () => {
|
|
for (const status of ['none', 'staged', 'showing'] as const) {
|
|
const flags = deriveDiffFlags(status)
|
|
expect(flags.hasActiveDiff).toBe(flags.isDiffReady)
|
|
}
|
|
})
|
|
})
|
|
|
|
describe('initial / reset state', () => {
|
|
it('starts in the none-derived state', () => {
|
|
const state = useWorkflowDiffStore.getState()
|
|
expect(state.status).toBe('none')
|
|
expect(state.hasActiveDiff).toBe(false)
|
|
expect(state.isShowingDiff).toBe(false)
|
|
expect(state.isDiffReady).toBe(false)
|
|
})
|
|
|
|
it('RESET_DIFF_STATE carries the none-derived flags and clears diff payload', () => {
|
|
expect(RESET_DIFF_STATE.status).toBe('none')
|
|
expect(RESET_DIFF_STATE.hasActiveDiff).toBe(false)
|
|
expect(RESET_DIFF_STATE.isShowingDiff).toBe(false)
|
|
expect(RESET_DIFF_STATE.isDiffReady).toBe(false)
|
|
expect(RESET_DIFF_STATE.baselineWorkflow).toBeNull()
|
|
expect(RESET_DIFF_STATE.diffAnalysis).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('toggleDiffView', () => {
|
|
it('is a guarded no-op when there is no active diff', () => {
|
|
seedStatus('none')
|
|
useWorkflowDiffStore.getState().toggleDiffView()
|
|
expect(useWorkflowDiffStore.getState().status).toBe('none')
|
|
})
|
|
|
|
it('toggles showing → staged (hides the proposed changes)', () => {
|
|
seedStatus('showing')
|
|
useWorkflowDiffStore.getState().toggleDiffView()
|
|
|
|
const state = useWorkflowDiffStore.getState()
|
|
expect(state.status).toBe('staged')
|
|
expect(state.hasActiveDiff).toBe(true)
|
|
expect(state.isDiffReady).toBe(true)
|
|
expect(state.isShowingDiff).toBe(false)
|
|
})
|
|
|
|
it('toggles staged → showing (reveals the proposed changes)', () => {
|
|
seedStatus('staged')
|
|
useWorkflowDiffStore.getState().toggleDiffView()
|
|
|
|
const state = useWorkflowDiffStore.getState()
|
|
expect(state.status).toBe('showing')
|
|
expect(state.isShowingDiff).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe('clearDiff', () => {
|
|
it('returns the store to the none status', () => {
|
|
seedStatus('showing')
|
|
useWorkflowDiffStore.getState().clearDiff({ restoreBaseline: false })
|
|
|
|
const state = useWorkflowDiffStore.getState()
|
|
expect(state.status).toBe('none')
|
|
expect(state.hasActiveDiff).toBe(false)
|
|
expect(state.isShowingDiff).toBe(false)
|
|
expect(state.isDiffReady).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('_batchedStateUpdate (undo/redo writer)', () => {
|
|
it('restores the showing status via deriveDiffFlags', () => {
|
|
seedStatus('none')
|
|
useWorkflowDiffStore.getState()._batchedStateUpdate({
|
|
...deriveDiffFlags('showing'),
|
|
baselineWorkflow: null,
|
|
baselineWorkflowId: 'wf-1',
|
|
})
|
|
|
|
const state = useWorkflowDiffStore.getState()
|
|
expect(state.status).toBe('showing')
|
|
expect(state.hasActiveDiff).toBe(true)
|
|
expect(state.isShowingDiff).toBe(true)
|
|
expect(state.isDiffReady).toBe(true)
|
|
})
|
|
|
|
it('the derived booleans always agree with the stored status', () => {
|
|
for (const status of ['none', 'staged', 'showing', 'none'] as const) {
|
|
seedStatus(status)
|
|
const state = useWorkflowDiffStore.getState()
|
|
expect({
|
|
hasActiveDiff: state.hasActiveDiff,
|
|
isShowingDiff: state.isShowingDiff,
|
|
isDiffReady: state.isDiffReady,
|
|
}).toEqual({
|
|
hasActiveDiff: deriveDiffFlags(status).hasActiveDiff,
|
|
isShowingDiff: deriveDiffFlags(status).isShowingDiff,
|
|
isDiffReady: deriveDiffFlags(status).isDiffReady,
|
|
})
|
|
}
|
|
})
|
|
})
|
|
})
|