import type { SnippetWorkflow } from '@/types/snippet' import { renderHook, waitFor } from '@testing-library/react' import { beforeEach, describe, expect, it, vi } from 'vitest' import { useSnippetInit } from '../use-snippet-init' const mockWorkflowStoreSetState = vi.fn() const mockSetPublishedAt = vi.fn() const mockSetDraftUpdatedAt = vi.fn() const mockSetSyncWorkflowDraftHash = vi.fn() const mockUseSnippetApiDetail = vi.fn() const mockFetchSnippetDraftWorkflow = vi.fn() const mockUseSnippetDefaultBlockConfigs = vi.fn() const mockUseSnippetPublishedWorkflow = vi.fn() vi.mock('@/app/components/workflow/store', () => ({ useWorkflowStore: () => ({ setState: mockWorkflowStoreSetState, getState: () => ({ setDraftUpdatedAt: mockSetDraftUpdatedAt, setSyncWorkflowDraftHash: mockSetSyncWorkflowDraftHash, setPublishedAt: mockSetPublishedAt, }), }), })) vi.mock('@/service/use-snippets', async (importOriginal) => { const actual = await importOriginal() return { ...actual, useSnippetApiDetail: (snippetId: string) => mockUseSnippetApiDetail(snippetId), } }) vi.mock('@/service/use-snippet-workflows', () => ({ fetchSnippetDraftWorkflow: (snippetId: string) => mockFetchSnippetDraftWorkflow(snippetId), useSnippetDefaultBlockConfigs: (snippetId: string, onSuccess?: (data: unknown) => void) => mockUseSnippetDefaultBlockConfigs(snippetId, onSuccess), useSnippetPublishedWorkflow: ( snippetId: string, onSuccess?: (data: { created_at: number }) => void, ) => mockUseSnippetPublishedWorkflow(snippetId, onSuccess), })) const createDraftWorkflow = (overrides: Partial = {}): SnippetWorkflow => ({ id: 'draft-1', graph: {}, features: {}, input_fields: [], hash: 'draft-hash', created_at: 1_712_300_000, updated_at: 1_712_345_678, ...overrides, }) describe('useSnippetInit', () => { beforeEach(() => { vi.clearAllMocks() mockUseSnippetApiDetail.mockReturnValue({ data: { id: 'snippet-1', name: 'Tone Rewriter', description: 'A static snippet mock.', type: 'node', is_published: false, version: '1', use_count: 0, tags: [], input_fields: [], created_at: 1_712_300_000, created_by: 'user-1', updated_at: 1_712_300_000, updated_by: 'user-1', }, error: null, isLoading: false, }) mockFetchSnippetDraftWorkflow.mockResolvedValue(undefined) mockUseSnippetDefaultBlockConfigs.mockReturnValue({ data: undefined, }) mockUseSnippetPublishedWorkflow.mockReturnValue({ data: undefined, }) }) it('should return snippet detail query result', async () => { const { result } = renderHook(() => useSnippetInit('snippet-1')) await waitFor(() => { expect(result.current.isLoading).toBe(false) }) expect(mockUseSnippetApiDetail).toHaveBeenCalledWith('snippet-1') expect(mockFetchSnippetDraftWorkflow).toHaveBeenCalledWith('snippet-1') expect(result.current.data?.snippet.id).toBe('snippet-1') expect(result.current.data?.published.graph.viewport).toEqual({ x: 0, y: 0, zoom: 1 }) }) it('should use draft input_fields for snippet inputs', async () => { mockUseSnippetApiDetail.mockReturnValue({ data: { id: 'snippet-1', name: 'Tone Rewriter', description: 'A static snippet mock.', type: 'node', is_published: false, version: '1', use_count: 0, tags: [], input_fields: [ { label: 'Published field', variable: 'published_field', type: 'text-input', required: true, }, ], created_at: 1_712_300_000, created_by: 'user-1', updated_at: 1_712_300_000, updated_by: 'user-1', }, error: null, isLoading: false, }) mockFetchSnippetDraftWorkflow.mockResolvedValue( createDraftWorkflow({ input_fields: [ { label: 'Draft field', variable: 'draft_field', type: 'text-input', required: true, }, ], }), ) const { result } = renderHook(() => useSnippetInit('snippet-1')) await waitFor(() => { expect(result.current.isLoading).toBe(false) }) expect(result.current.data?.draft.inputFields).toEqual([ { label: 'Draft field', variable: 'draft_field', type: 'text-input', required: true, }, ]) }) it('should sync draft metadata before returning initialized data', async () => { mockFetchSnippetDraftWorkflow.mockResolvedValue( createDraftWorkflow({ hash: 'fetched-draft-hash', updated_at: 1_712_345_678, graph: { nodes: [{ id: 'node-1' }], edges: [], viewport: { x: 10, y: 20, zoom: 1.2 }, }, }), ) const { result } = renderHook(() => useSnippetInit('snippet-1')) await waitFor(() => { expect(result.current.isLoading).toBe(false) }) expect(mockSetDraftUpdatedAt).toHaveBeenCalledWith(1_712_345_678) expect(mockSetSyncWorkflowDraftHash).toHaveBeenCalledWith('fetched-draft-hash') expect(result.current.data?.draft.graph.viewport).toEqual({ x: 10, y: 20, zoom: 1.2 }) }) it('should not return stale draft data while the draft workflow request is pending', () => { mockFetchSnippetDraftWorkflow.mockReturnValue(new Promise(() => {})) const { result } = renderHook(() => useSnippetInit('snippet-1')) expect(result.current.data).toBeUndefined() expect(result.current.isLoading).toBe(true) }) it('should initialize with empty graph when the draft workflow does not exist', async () => { mockFetchSnippetDraftWorkflow.mockResolvedValue(undefined) const { result } = renderHook(() => useSnippetInit('snippet-1')) await waitFor(() => { expect(result.current.isLoading).toBe(false) }) expect(result.current.data?.published.graph).toEqual({ nodes: [], edges: [], viewport: { x: 0, y: 0, zoom: 1 }, }) }) it('should ignore outdated draft workflow response when snippet changes', async () => { let resolveFirstDraft: (workflow: SnippetWorkflow) => void = () => {} mockFetchSnippetDraftWorkflow.mockImplementation((snippetId: string) => { if (snippetId === 'snippet-1') { return new Promise((resolve) => { resolveFirstDraft = resolve }) } return Promise.resolve( createDraftWorkflow({ id: 'draft-2', hash: 'snippet-2-hash', graph: { nodes: [{ id: 'snippet-2-node' }], edges: [], viewport: { x: 2, y: 2, zoom: 1 }, }, }), ) }) const { result, rerender } = renderHook(({ snippetId }) => useSnippetInit(snippetId), { initialProps: { snippetId: 'snippet-1' }, }) rerender({ snippetId: 'snippet-2' }) await waitFor(() => { expect(result.current.isLoading).toBe(false) }) resolveFirstDraft( createDraftWorkflow({ hash: 'stale-snippet-1-hash', graph: { nodes: [{ id: 'stale-node' }], edges: [], viewport: { x: 1, y: 1, zoom: 1 }, }, }), ) await Promise.resolve() expect(result.current.data?.draft.graph.nodes).toEqual([{ id: 'snippet-2-node' }]) expect(mockSetSyncWorkflowDraftHash).toHaveBeenCalledWith('snippet-2-hash') expect(mockSetSyncWorkflowDraftHash).not.toHaveBeenCalledWith('stale-snippet-1-hash') }) it('should normalize array default block configs into workflow store state', () => { mockUseSnippetDefaultBlockConfigs.mockImplementation( (_snippetId: string, onSuccess?: (data: unknown) => void) => { onSuccess?.([ { type: 'llm', config: { model: 'gpt-4.1' } }, { type: 'code', config: { language: 'python3' } }, ]) return { data: undefined, isLoading: false } }, ) renderHook(() => useSnippetInit('snippet-1')) expect(mockWorkflowStoreSetState).toHaveBeenCalledWith({ nodesDefaultConfigs: { llm: { model: 'gpt-4.1' }, code: { language: 'python3' }, }, }) }) it('should keep object default block configs as-is', () => { mockUseSnippetDefaultBlockConfigs.mockImplementation( (_snippetId: string, onSuccess?: (data: unknown) => void) => { onSuccess?.({ llm: { model: 'gpt-4.1' }, }) return { data: undefined, isLoading: false } }, ) renderHook(() => useSnippetInit('snippet-1')) expect(mockWorkflowStoreSetState).toHaveBeenCalledWith({ nodesDefaultConfigs: { llm: { model: 'gpt-4.1' }, }, }) }) it('should sync published created_at into workflow store', () => { mockUseSnippetPublishedWorkflow.mockImplementation( (_snippetId: string, onSuccess?: (data: { created_at: number }) => void) => { onSuccess?.({ created_at: 1_712_345_678, }) return { data: { created_at: 1_712_345_678 }, isLoading: false } }, ) renderHook(() => useSnippetInit('snippet-1')) expect(mockSetPublishedAt).toHaveBeenCalledWith(1_712_345_678) }) it('should reset published metadata when the published workflow is unavailable', () => { mockUseSnippetPublishedWorkflow.mockReturnValue({ data: undefined, isLoading: false, }) renderHook(() => useSnippetInit('snippet-1')) expect(mockSetPublishedAt).toHaveBeenCalledWith(0) }) })