From 7ac3d2624fabed7ac29cfac77a860e96296d8d51 Mon Sep 17 00:00:00 2001 From: yyh Date: Thu, 15 Oct 2026 22:40:41 +0800 Subject: [PATCH] test(workflow): verify editor identity and request isolation Keep all test and fixture migrations in this commit. Cover scoped session reset versus Query refresh, App/Pipeline/Snippet variable contracts and cache isolation, deletion failures and reset 204, Pipeline environment persistence, and distinct LLM model modes. Replace obsolete hook mocks with real editor scopes and retain browser interaction coverage. --- .../[appId]/__tests__/layout-main.spec.tsx | 29 +-- .../__tests__/app-detail-section.spec.tsx | 13 +- .../__tests__/app-info-modals.spec.tsx | 3 +- .../__tests__/app-info-trigger.spec.tsx | 7 +- .../__tests__/app-mode-labels.spec.ts | 11 +- .../__tests__/use-app-info-actions.spec.ts | 13 +- .../access-config/__tests__/index.spec.tsx | 3 +- .../__tests__/built-in-access-points.spec.tsx | 4 +- .../environment-access-point-cards.spec.tsx | 8 +- .../app/access-point/__tests__/index.spec.tsx | 8 +- .../access-point/__tests__/mcp-card.spec.tsx | 5 +- .../__tests__/service-api-card.spec.tsx | 33 ++- .../__tests__/trigger-card.spec.tsx | 3 +- .../__tests__/web-app-card.spec.tsx | 53 ++-- .../app/annotation/__tests__/index.spec.tsx | 3 +- .../app-publisher/__tests__/index.spec.tsx | 33 ++- .../__tests__/publisher-panel.spec.tsx | 3 +- .../app-publisher/__tests__/sections.spec.tsx | 21 +- .../app/app-publisher/__tests__/utils.spec.ts | 7 +- .../__tests__/configuration-view.spec.tsx | 8 +- .../app/configuration/__tests__/utils.spec.ts | 10 +- .../__tests__/advanced-prompt-input.spec.tsx | 3 +- .../config-prompt/__tests__/index.spec.tsx | 6 +- ...simple-prompt-input-accessibility.spec.tsx | 7 +- .../__tests__/simple-prompt-input.spec.tsx | 18 +- .../config-var/__tests__/index.spec.tsx | 5 +- .../__tests__/index-logic.spec.tsx | 5 +- .../config-modal/__tests__/index.spec.tsx | 4 +- .../config/__tests__/index.spec.tsx | 9 +- .../generator-reflow.browser.spec.tsx | 3 +- .../__tests__/get-automatic-res.spec.tsx | 18 +- .../__tests__/get-code-generator-res.spec.tsx | 18 +- .../dataset-config/__tests__/index.spec.tsx | 18 +- .../debug/__tests__/index.spec.tsx | 32 +-- .../__tests__/debug-item.spec.tsx | 19 +- .../__tests__/index.spec.tsx | 13 +- .../__tests__/index.spec.tsx | 4 +- .../use-advanced-prompt-config.spec.tsx | 18 +- .../__tests__/use-configuration-utils.spec.ts | 27 +- .../__tests__/use-configuration.spec.tsx | 10 +- .../__tests__/index.spec.tsx | 10 +- .../app-card/__tests__/index.spec.tsx | 3 +- .../create-app-modal/__tests__/index.spec.tsx | 27 +- .../__tests__/index.spec.tsx | 43 ++-- .../app/deploy/__tests__/index.spec.tsx | 3 +- .../log-annotation/__tests__/index.spec.tsx | 11 +- .../app/log/__tests__/empty-element.spec.tsx | 23 +- .../app/log/__tests__/index.spec.tsx | 11 +- .../app/log/__tests__/list.spec.tsx | 72 +++--- .../settings/__tests__/index.spec.tsx | 3 +- .../switch-app-modal/__tests__/index.spec.tsx | 3 +- .../type-selector/__tests__/index.spec.tsx | 30 +-- .../app/workflow-log/__tests__/index.spec.tsx | 5 +- .../app/workflow-log/__tests__/list.spec.tsx | 9 +- .../apps/__tests__/app-card.spec.tsx | 33 ++- .../components/apps/__tests__/list.spec.tsx | 29 +-- .../moderation-setting-modal.spec.tsx | 6 +- .../__tests__/index.spec.tsx | 7 +- .../create-app-modal/__tests__/index.spec.tsx | 11 +- .../installed-app/__tests__/index.spec.tsx | 2 +- .../learn-dify/__tests__/item.spec.tsx | 3 +- .../try-app/app/__tests__/index.spec.tsx | 4 +- .../try-app/preview/__tests__/index.spec.tsx | 2 +- .../__tests__/tool-provider-list.spec.tsx | 2 +- .../main-nav/__tests__/index.spec.tsx | 3 +- .../__tests__/app-picker.spec.tsx | 3 +- .../app-selector/__tests__/index.spec.tsx | 7 +- .../use-app-inputs-form-schema.spec.ts | 18 +- .../__tests__/rag-pipeline-main.spec.tsx | 12 +- .../__tests__/update-dsl-modal.spec.tsx | 147 ++++++++--- .../components/panel/__tests__/index.spec.tsx | 7 +- .../preview/__tests__/data-source.spec.tsx | 7 +- .../preview/__tests__/index.spec.tsx | 8 +- .../__tests__/process-documents.spec.tsx | 7 +- .../__tests__/hooks.spec.ts | 6 +- .../__tests__/index.spec.tsx | 119 ++++++--- .../publisher/__tests__/index.spec.tsx | 36 +-- .../publisher/__tests__/popup.spec.tsx | 7 +- .../hooks/__tests__/use-DSL.spec.ts | 80 +++--- .../hooks/__tests__/use-configs-map.spec.ts | 69 ----- .../hooks/__tests__/use-file-settings.spec.ts | 53 ++++ .../use-get-run-and-trace-url.spec.ts | 14 +- .../__tests__/use-inspect-vars-crud.spec.ts | 98 ------- .../__tests__/use-nodes-sync-draft.spec.ts | 120 +++++---- .../__tests__/use-pipeline-config.spec.ts | 18 +- .../hooks/__tests__/use-pipeline-init.spec.ts | 74 ++++-- .../use-pipeline-refresh-draft.spec.ts | 47 +++- .../hooks/__tests__/use-pipeline-run.spec.ts | 154 +++++++---- .../__tests__/use-update-dsl-modal.spec.ts | 19 +- .../snippets/__tests__/index.spec.tsx | 17 +- .../__tests__/snippet-main.spec.tsx | 18 +- .../hooks/__tests__/use-configs-map.spec.ts | 51 ---- .../hooks/__tests__/use-file-settings.spec.ts | 28 ++ .../__tests__/use-inspect-vars-crud.spec.ts | 95 ------- .../hooks/__tests__/use-snippet-run.spec.ts | 17 +- .../workflow-app/__tests__/index.spec.tsx | 5 +- .../__tests__/provider-contract.spec.tsx | 13 +- .../__tests__/workflow-children.spec.tsx | 34 +-- .../__tests__/workflow-main.spec.tsx | 35 ++- .../__tests__/workflow-panel.spec.tsx | 23 +- .../__tests__/chat-variable-trigger.spec.tsx | 24 +- .../__tests__/features-trigger.spec.tsx | 16 +- .../hooks/__tests__/use-DSL.spec.ts | 74 ++++-- .../hooks/__tests__/use-configs-map.spec.ts | 44 ---- .../use-get-run-and-trace-url.spec.ts | 6 +- .../hooks/__tests__/use-is-chat-mode.spec.ts | 33 --- .../__tests__/use-nodes-sync-draft.spec.ts | 29 +-- ...se-workflow-draft-graph-for-canvas.spec.ts | 13 +- .../hooks/__tests__/use-workflow-init.spec.ts | 4 +- .../use-workflow-refresh-draft.spec.ts | 15 +- .../__tests__/use-workflow-run-utils.spec.ts | 19 +- .../hooks/__tests__/use-workflow-run.spec.ts | 26 +- .../__tests__/use-workflow-start-run.spec.tsx | 61 +++-- .../__tests__/use-workflow-template.spec.ts | 26 +- .../edge-insert-focus.browser.spec.tsx | 31 ++- .../workflow/__tests__/features.spec.tsx | 9 +- .../__tests__/node-catalog.spec.ts} | 74 ++++-- .../__tests__/panel-contextmenu.spec.tsx | 17 +- .../update-dsl-modal.helpers.spec.ts | 21 +- .../__tests__/workflow-edge-events.spec.tsx | 17 +- .../__tests__/workflow-test-env.spec.tsx | 14 +- .../workflow/__tests__/workflow-test-env.tsx | 50 +++- .../__tests__/agent-selector.spec.tsx | 66 +++-- .../__tests__/all-start-blocks.spec.tsx | 12 +- .../block-selector/__tests__/blocks.spec.tsx | 47 ++-- .../block-selector/__tests__/index.spec.tsx | 9 +- .../__tests__/start-blocks.spec.tsx | 12 +- .../__tests__/comment-close-focus.spec.tsx | 4 - .../__tests__/comment-thread-focus.spec.tsx | 12 +- .../workflow/comment/mention-input.spec.tsx | 13 +- .../comment/placement-preview.spec.tsx | 2 - .../comment/reply-deletion.browser.spec.tsx | 28 +- .../workflow/comment/thread.spec.tsx | 14 +- .../editor-resource/__tests__/render.ts | 66 +++++ .../editor-resource/__tests__/scope.spec.tsx | 146 +++++++++++ .../__tests__/header-in-restoring.spec.tsx | 38 +-- .../header/__tests__/header-layouts.spec.tsx | 31 +-- .../header/__tests__/running-title.spec.tsx | 11 +- .../header/__tests__/view-history.spec.tsx | 11 +- .../header/checklist/__tests__/index.spec.tsx | 31 ++- .../use-auto-generate-webhook-url.spec.ts | 64 +++-- .../hooks/__tests__/use-checklist.spec.ts | 19 +- .../hooks/__tests__/use-config-vision.spec.ts | 11 +- .../use-fetch-workflow-inspect-vars.spec.ts | 37 +-- .../use-inspect-vars-crud-common.spec.ts | 166 +++++++----- .../__tests__/use-inspect-vars-crud.spec.ts | 53 ++-- .../use-nodes-available-var-list.spec.ts | 84 +++--- .../__tests__/use-nodes-interactions.spec.ts | 24 +- .../__tests__/use-workflow-comment.spec.ts | 4 - .../__tests__/use-workflow-variables.spec.ts | 21 +- .../hooks/__tests__/use-workflow.spec.ts | 77 +----- .../__tests__/details.spec.tsx | 16 +- .../add-variable-popup-with-position.spec.tsx | 8 +- .../__tests__/agent-strategy.spec.tsx | 2 +- .../__tests__/code-generator-button.spec.tsx | 7 +- .../form-input-item.branches.spec.tsx | 9 +- .../__tests__/form-input-item.spec.tsx | 9 +- .../__tests__/node-focus.browser.spec.tsx | 23 +- .../__tests__/node-handle.browser.spec.tsx | 23 +- .../components/__tests__/node-handle.spec.tsx | 28 +- .../variable/__tests__/utils.spec.ts | 15 +- .../var-reference-picker.branches.spec.tsx | 9 +- .../__tests__/var-reference-picker.spec.tsx | 9 +- .../workflow-panel/__tests__/helpers.spec.tsx | 4 +- .../workflow-panel/__tests__/index.spec.tsx | 4 +- .../last-run/__tests__/index.spec.tsx | 27 +- .../last-run/__tests__/use-last-run.spec.ts | 7 +- .../snippet-input-field-vars.spec.ts | 6 +- .../__tests__/use-available-var-list.spec.ts | 55 ++-- .../hooks/__tests__/use-one-step-run.spec.ts | 16 +- .../nodes/agent-v2/__tests__/hooks.spec.tsx | 101 +++----- .../nodes/agent-v2/__tests__/panel.spec.tsx | 14 +- .../agent-orchestrate-panel-content.spec.tsx | 23 +- ...ave-inline-agent-to-roster-dialog.spec.tsx | 19 +- .../agent/__tests__/integration.spec.tsx | 2 +- .../nodes/agent/__tests__/node.spec.tsx | 2 +- .../nodes/agent/__tests__/panel.spec.tsx | 4 +- .../nodes/agent/__tests__/use-config.spec.ts | 8 +- .../assigner/__tests__/use-config.spec.tsx | 9 +- .../var-list/__tests__/index.spec.tsx | 9 +- .../nodes/code/__tests__/use-config.spec.ts | 119 +++++---- .../__tests__/before-run-form.spec.tsx | 5 +- .../use-before-run-form.branches.spec.tsx | 11 +- .../__tests__/use-before-run-form.spec.tsx | 38 +-- .../__tests__/use-config.spec.ts | 13 +- .../nodes/end/__tests__/node.spec.tsx | 12 +- .../__tests__/human-input.spec.tsx | 8 +- .../__tests__/test-email-sender.spec.tsx | 10 +- .../use-single-run-form-params.spec.ts | 33 +-- .../if-else/__tests__/integration.spec.tsx | 18 +- .../iteration-start/__tests__/index.spec.tsx | 12 +- .../iteration/__tests__/use-config.spec.ts | 9 +- .../__tests__/use-config.spec.ts | 25 +- .../use-knowledge-metadata-config.spec.ts | 11 +- .../use-knowledge-model-config.spec.ts | 14 +- .../__tests__/use-config.spec.tsx | 55 ++-- .../nodes/llm/__tests__/default.spec.ts | 14 +- .../nodes/llm/__tests__/node.spec.tsx | 7 +- .../nodes/llm/__tests__/panel.spec.tsx | 26 +- .../nodes/llm/__tests__/use-config.spec.ts | 44 ++-- .../use-single-run-form-params.spec.ts | 72 +++--- .../nodes/llm/__tests__/utils.spec.ts | 17 +- .../__tests__/config-prompt.spec.tsx | 2 +- .../__tests__/panel-memory-section.spec.tsx | 12 +- .../__tests__/panel-output-section.spec.tsx | 3 +- .../__tests__/prompt-generator-btn.spec.tsx | 7 +- .../__tests__/use-llm-input-manager.spec.ts | 9 +- .../__tests__/use-llm-prompt-config.spec.ts | 19 +- .../use-llm-structured-output-config.spec.ts | 3 +- .../nodes/loop-start/__tests__/index.spec.tsx | 12 +- .../nodes/loop/__tests__/use-config.spec.tsx | 9 +- .../__tests__/integration.spec.tsx | 11 +- .../__tests__/node.spec.tsx | 5 +- .../__tests__/panel.spec.tsx | 9 +- .../use-single-run-form-params.spec.ts | 4 +- .../__tests__/import-from-tool.spec.tsx | 5 +- .../__tests__/integration.spec.tsx | 2 +- .../__tests__/panel.spec.tsx | 2 +- .../__tests__/use-config.spec.ts | 54 ++-- .../nodes/start/__tests__/panel.spec.tsx | 6 +- .../nodes/start/__tests__/use-config.spec.ts | 10 +- .../use-single-run-form-params.spec.ts | 54 ++-- .../nodes/tool/__tests__/panel.spec.tsx | 7 +- .../__tests__/use-config.spec.tsx | 19 +- .../variable-assigner/__tests__/hooks.spec.ts | 42 ++- .../operator/__tests__/add-block.spec.tsx | 16 +- .../operator/__tests__/export-image.spec.tsx | 59 ++++- .../panel/__tests__/inputs-panel.spec.tsx | 23 +- .../chat-record/__tests__/index.spec.tsx | 17 +- .../__tests__/index.spec.tsx | 11 +- .../__tests__/chat-wrapper.spec.tsx | 7 - .../__tests__/components.spec.tsx | 4 +- .../conversation-variable-modal.spec.tsx | 4 +- .../debug-and-preview/__tests__/hooks.spec.ts | 19 +- .../__tests__/hooks/handle-resume.spec.ts | 52 ++-- .../__tests__/hooks/handle-send.spec.ts | 33 ++- .../hooks/handle-stop-restart.spec.ts | 38 ++- .../__tests__/hooks/misc.spec.ts | 49 +++- .../__tests__/hooks/opening-statement.spec.ts | 31 ++- .../__tests__/hooks/sse-callbacks.spec.ts | 19 +- .../panel/env-panel/__tests__/index.spec.tsx | 123 ++++++++- .../__tests__/index.spec.tsx | 11 +- .../__tests__/index.spec.tsx | 14 +- .../__tests__/version-history-item.spec.tsx | 2 +- .../action-menu/__tests__/index.spec.tsx | 4 +- .../__tests__/resource-requests.spec.tsx | 243 ++++++++++++++++++ .../__tests__/value-content.helpers.spec.ts | 22 +- .../__tests__/apply.spec.ts | 31 ++- .../home/__tests__/template-card.spec.tsx | 25 +- .../__tests__/home-content.spec.tsx | 59 +++-- web/hooks/use-import-dsl.spec.tsx | 9 +- web/service/__tests__/use-workflow.spec.tsx | 5 +- web/service/workflow.spec.ts | 30 +-- .../__tests__/create-app-tracking.spec.ts | 31 ++- web/utils/app-redirection.spec.ts | 46 ++-- web/utils/imported-app-redirection.spec.ts | 7 +- 256 files changed, 3709 insertions(+), 2754 deletions(-) delete mode 100644 web/app/components/rag-pipeline/hooks/__tests__/use-configs-map.spec.ts create mode 100644 web/app/components/rag-pipeline/hooks/__tests__/use-file-settings.spec.ts delete mode 100644 web/app/components/rag-pipeline/hooks/__tests__/use-inspect-vars-crud.spec.ts delete mode 100644 web/app/components/snippets/hooks/__tests__/use-configs-map.spec.ts create mode 100644 web/app/components/snippets/hooks/__tests__/use-file-settings.spec.ts delete mode 100644 web/app/components/snippets/hooks/__tests__/use-inspect-vars-crud.spec.ts delete mode 100644 web/app/components/workflow-app/hooks/__tests__/use-configs-map.spec.ts delete mode 100644 web/app/components/workflow-app/hooks/__tests__/use-is-chat-mode.spec.ts rename web/app/components/{workflow-app/hooks/__tests__/use-available-nodes-meta-data.spec.ts => workflow/__tests__/node-catalog.spec.ts} (74%) create mode 100644 web/app/components/workflow/editor-resource/__tests__/render.ts create mode 100644 web/app/components/workflow/editor-resource/__tests__/scope.spec.tsx create mode 100644 web/app/components/workflow/variable-inspect/__tests__/resource-requests.spec.tsx diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/__tests__/layout-main.spec.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/__tests__/layout-main.spec.tsx index 12f6dc08fa0..6f626b14ee8 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/__tests__/layout-main.spec.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/__tests__/layout-main.spec.tsx @@ -9,7 +9,6 @@ import { seedAppDetail, } from '@/test/console/query-data' import { createAppDetailFixture } from '@/test/fixtures/app' -import { AppModeEnum } from '@/types/app' import { AppACLPermission } from '@/utils/permission' import AppDetailLayout from '../layout-main' @@ -63,7 +62,7 @@ const createAppDetail = (overrides: Partial = {}) => createAppDetailFixture({ id: 'app-1', name: 'Demo App', - mode: AppModeEnum.WORKFLOW, + mode: 'workflow' as const, permission_keys: [AppACLPermission.ViewLayout, AppACLPermission.Monitor], ...overrides, }) @@ -148,15 +147,15 @@ describe('AppDetailLayout', () => { describe('Document title', () => { it.each([ - ['/app/app-1/workflow', 'common.appMenus.promptEng', AppModeEnum.WORKFLOW], - ['/app/app-1/configuration', 'common.appMenus.promptEng', AppModeEnum.CHAT], - ['/app/app-1/access-point', 'common.appMenus.accessPoint', AppModeEnum.WORKFLOW], - ['/app/app-1/deploy', 'common.appMenus.deploy', AppModeEnum.WORKFLOW], - ['/app/app-1/logs', 'common.appMenus.logs', AppModeEnum.WORKFLOW], - ['/app/app-1/annotations', 'common.appMenus.annotations', AppModeEnum.CHAT], - ['/app/app-1/overview', 'common.appMenus.overview', AppModeEnum.WORKFLOW], - ['/app/app-1/access-config', 'navigation.settings.resourceAccess', AppModeEnum.WORKFLOW], - ])('identifies the current detail page for %s', async (pathname, pageTitle, mode) => { + ['/app/app-1/workflow', 'common.appMenus.promptEng', 'workflow'], + ['/app/app-1/configuration', 'common.appMenus.promptEng', 'chat'], + ['/app/app-1/access-point', 'common.appMenus.accessPoint', 'workflow'], + ['/app/app-1/deploy', 'common.appMenus.deploy', 'workflow'], + ['/app/app-1/logs', 'common.appMenus.logs', 'workflow'], + ['/app/app-1/annotations', 'common.appMenus.annotations', 'chat'], + ['/app/app-1/overview', 'common.appMenus.overview', 'workflow'], + ['/app/app-1/access-config', 'navigation.settings.resourceAccess', 'workflow'], + ] as const)('identifies the current detail page for %s', async (pathname, pageTitle, mode) => { mockPathname = pathname mockAppResponse.mockResolvedValue( createAppDetail({ @@ -543,7 +542,7 @@ describe('AppDetailLayout', () => { mockPathname = '/app/app-1/access-config' mockAppResponse.mockResolvedValue( createAppDetail({ - mode: AppModeEnum.AGENT, + mode: 'agent' as const, bound_agent_id: 'agent-1', permission_keys: [AppACLPermission.AccessConfig], }), @@ -570,7 +569,7 @@ describe('AppDetailLayout', () => { , { cachedAppDetail: createAppDetail({ - mode: AppModeEnum.AGENT, + mode: 'agent' as const, bound_agent_id: 'agent-1', permission_keys: [AppACLPermission.AccessConfig], }), @@ -608,7 +607,7 @@ describe('AppDetailLayout', () => { mockPathname = '/app/app-1/annotations' mockAppResponse.mockResolvedValue( createAppDetail({ - mode: AppModeEnum.CHAT, + mode: 'chat' as const, permission_keys: [AppACLPermission.Monitor], }), ) @@ -629,7 +628,7 @@ describe('AppDetailLayout', () => { mockPathname = '/app/app-1/annotations' mockAppResponse.mockResolvedValue( createAppDetail({ - mode: AppModeEnum.CHAT, + mode: 'chat' as const, permission_keys: [AppACLPermission.LogAndAnnotation], }), ) diff --git a/web/app/components/app-sidebar/__tests__/app-detail-section.spec.tsx b/web/app/components/app-sidebar/__tests__/app-detail-section.spec.tsx index c2197c8c4d0..1e669cdaadb 100644 --- a/web/app/components/app-sidebar/__tests__/app-detail-section.spec.tsx +++ b/web/app/components/app-sidebar/__tests__/app-detail-section.spec.tsx @@ -3,7 +3,6 @@ import { screen } from '@testing-library/react' import { consoleQuery } from '@/service/console' import { createConsoleQueryClient, renderWithConsoleQuery } from '@/test/console/query-data' import { createAppDetailFixture } from '@/test/fixtures/app' -import { AppModeEnum } from '@/types/app' import { AppACLPermission } from '@/utils/permission' import AppDetailSection from '../app-detail-section' @@ -221,18 +220,18 @@ describe('AppDetailSection', () => { it.each([ { label: 'the app is not a workflow app', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, permissionKeys: [AppACLPermission.Deploy], }, { label: 'app deploy ACL permission is missing', - mode: AppModeEnum.WORKFLOW, + mode: 'workflow' as const, permissionKeys: [AppACLPermission.Monitor], }, - ])('should hide deploy navigation when $label', ({ mode, permissionKeys }) => { + ] as const)('should hide deploy navigation when $label', ({ mode, permissionKeys }) => { // Arrange mockAppMode = mode - mockAppPermissionKeys = permissionKeys + mockAppPermissionKeys = [...permissionKeys] // Act render() @@ -241,7 +240,7 @@ describe('AppDetailSection', () => { expect(screen.queryByRole('link', { name: 'common.appMenus.deploy' })).not.toBeInTheDocument() }) - it.each([AppModeEnum.CHAT, AppModeEnum.AGENT_CHAT])( + it.each(['chat', 'agent-chat'] as const)( 'should render resource access navigation for %s apps when app access config permission is granted', (mode) => { // Arrange @@ -263,7 +262,7 @@ describe('AppDetailSection', () => { it('should hide resource access navigation for Agent apps', () => { // Arrange - mockAppMode = AppModeEnum.AGENT + mockAppMode = 'agent' mockAppPermissionKeys = [AppACLPermission.AccessConfig] // Act diff --git a/web/app/components/app-sidebar/app-info/__tests__/app-info-modals.spec.tsx b/web/app/components/app-sidebar/app-info/__tests__/app-info-modals.spec.tsx index 898248457f0..4b79b467b23 100644 --- a/web/app/components/app-sidebar/app-info/__tests__/app-info-modals.spec.tsx +++ b/web/app/components/app-sidebar/app-info/__tests__/app-info-modals.spec.tsx @@ -3,7 +3,6 @@ import { act, fireEvent, render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import * as React from 'react' import { createAppDetailFixture } from '@/test/fixtures/app' -import { AppModeEnum } from '@/types/app' import AppInfoModals from '../app-info-modals' vi.mock('next/dynamic', () => ({ @@ -112,7 +111,7 @@ const createAppDetail = (overrides: Partial = {}) => createAppDetailFixture({ id: 'app-1', name: 'Test App', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, icon: '🤖', icon_type: 'emoji', icon_background: '#FFEAD5', diff --git a/web/app/components/app-sidebar/app-info/__tests__/app-info-trigger.spec.tsx b/web/app/components/app-sidebar/app-info/__tests__/app-info-trigger.spec.tsx index ba63d3a1973..c60c4c63fc2 100644 --- a/web/app/components/app-sidebar/app-info/__tests__/app-info-trigger.spec.tsx +++ b/web/app/components/app-sidebar/app-info/__tests__/app-info-trigger.spec.tsx @@ -5,7 +5,6 @@ import * as React from 'react' import { createAccountProfileQueryWrapper } from '@/test/console/account-profile' import { render as renderWithConsoleState } from '@/test/console/render' import { createAppDetailFixture } from '@/test/fixtures/app' -import { AppModeEnum } from '@/types/app' import { AppACLPermission } from '@/utils/permission' import AppInfoTrigger from '../app-info-trigger' @@ -41,7 +40,7 @@ const createAppDetail = (overrides: Partial = {}) => createAppDetailFixture({ id: 'app-1', name: 'Test App', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, icon: '🤖', icon_type: 'emoji', icon_background: '#FFEAD5', @@ -74,7 +73,7 @@ describe('AppInfoTrigger', () => { it('renders expanded app metadata without making the app info clickable', async () => { const user = userEvent.setup() const props = createProps({ - appDetail: createAppDetail({ name: 'My Chatbot', mode: AppModeEnum.ADVANCED_CHAT }), + appDetail: createAppDetail({ name: 'My Chatbot', mode: 'advanced-chat' as const }), }) render() @@ -119,7 +118,7 @@ describe('AppInfoTrigger', () => { it('shows import DSL for workflow apps without a workflow conversion operation', async () => { const user = userEvent.setup() const props = createProps({ - appDetail: createAppDetail({ mode: AppModeEnum.WORKFLOW }), + appDetail: createAppDetail({ mode: 'workflow' as const }), }) render() diff --git a/web/app/components/app-sidebar/app-info/__tests__/app-mode-labels.spec.ts b/web/app/components/app-sidebar/app-info/__tests__/app-mode-labels.spec.ts index e954621d001..a31b6fa7d03 100644 --- a/web/app/components/app-sidebar/app-info/__tests__/app-mode-labels.spec.ts +++ b/web/app/components/app-sidebar/app-info/__tests__/app-mode-labels.spec.ts @@ -1,6 +1,5 @@ import type { TFunction } from 'i18next' import { withSelectorKey } from '@/test/i18n-mock' -import { AppModeEnum } from '@/types/app' import { getAppModeLabel } from '../app-mode-labels' describe('getAppModeLabel', () => { @@ -10,19 +9,19 @@ describe('getAppModeLabel', () => { }, 'app') as unknown as TFunction<['app']> it('should return advanced chat label', () => { - expect(getAppModeLabel(AppModeEnum.ADVANCED_CHAT, t)).toBe('app.types.advanced') + expect(getAppModeLabel('advanced-chat', t)).toBe('app.types.advanced') }) it('should return agent chat label', () => { - expect(getAppModeLabel(AppModeEnum.AGENT_CHAT, t)).toBe('app.types.agent') + expect(getAppModeLabel('agent-chat', t)).toBe('app.types.agent') }) it('should return chatbot label', () => { - expect(getAppModeLabel(AppModeEnum.CHAT, t)).toBe('app.types.chatbot') + expect(getAppModeLabel('chat', t)).toBe('app.types.chatbot') }) it('should return completion label', () => { - expect(getAppModeLabel(AppModeEnum.COMPLETION, t)).toBe('app.types.completion') + expect(getAppModeLabel('completion', t)).toBe('app.types.completion') }) it('should return workflow label for unknown mode', () => { @@ -30,6 +29,6 @@ describe('getAppModeLabel', () => { }) it('should return workflow label for workflow mode', () => { - expect(getAppModeLabel(AppModeEnum.WORKFLOW, t)).toBe('app.types.workflow') + expect(getAppModeLabel('workflow', t)).toBe('app.types.workflow') }) }) diff --git a/web/app/components/app-sidebar/app-info/__tests__/use-app-info-actions.spec.ts b/web/app/components/app-sidebar/app-info/__tests__/use-app-info-actions.spec.ts index 8f7c63d1afd..013ba7ae1a2 100644 --- a/web/app/components/app-sidebar/app-info/__tests__/use-app-info-actions.spec.ts +++ b/web/app/components/app-sidebar/app-info/__tests__/use-app-info-actions.spec.ts @@ -3,7 +3,6 @@ import { act, waitFor } from '@testing-library/react' import { consoleQuery } from '@/service/console' import { createConsoleQueryClient, renderHookWithConsoleQuery } from '@/test/console/query-data' import { createAppDetailFixture } from '@/test/fixtures/app' -import { AppModeEnum } from '@/types/app' import { getRedirection } from '@/utils/app-redirection' import { useAppInfoActions } from '../use-app-info-actions' @@ -140,7 +139,7 @@ describe('useAppInfoActions', () => { mockAppDetail = createAppDetailFixture({ id: 'app-1', name: 'Test App', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, icon: '🤖', icon_type: 'emoji', icon_background: '#FFEAD5', @@ -370,7 +369,7 @@ describe('useAppInfoActions', () => { }) it('should open export warning modal for workflow mode', async () => { - mockAppDetail = createAppDetailFixture({ ...mockAppDetail, mode: AppModeEnum.WORKFLOW }) + mockAppDetail = createAppDetailFixture({ ...mockAppDetail, mode: 'workflow' as const }) const { result } = renderActions() @@ -382,7 +381,7 @@ describe('useAppInfoActions', () => { }) it('should open export warning modal for advanced_chat mode', async () => { - mockAppDetail = createAppDetailFixture({ ...mockAppDetail, mode: AppModeEnum.ADVANCED_CHAT }) + mockAppDetail = createAppDetailFixture({ ...mockAppDetail, mode: 'advanced-chat' as const }) const { result } = renderActions() @@ -396,7 +395,7 @@ describe('useAppInfoActions', () => { describe('handleConfirmExport', () => { it('keeps the warning open after failure so the user can retry', async () => { - mockAppDetail = createAppDetailFixture({ ...mockAppDetail, mode: AppModeEnum.WORKFLOW }) + mockAppDetail = createAppDetailFixture({ ...mockAppDetail, mode: 'workflow' as const }) mockExportWorkflowAppDsl .mockResolvedValueOnce({ status: 'failed' }) .mockResolvedValueOnce({ status: 'downloaded' }) @@ -415,7 +414,7 @@ describe('useAppInfoActions', () => { }) it('should export directly when no secret env variables', async () => { - mockAppDetail = createAppDetailFixture({ ...mockAppDetail, mode: AppModeEnum.WORKFLOW }) + mockAppDetail = createAppDetailFixture({ ...mockAppDetail, mode: 'workflow' as const }) const { result } = renderActions() await act(async () => { @@ -429,7 +428,7 @@ describe('useAppInfoActions', () => { }) it('should set secret env list when secret variables exist', async () => { - mockAppDetail = createAppDetailFixture({ ...mockAppDetail, mode: AppModeEnum.WORKFLOW }) + mockAppDetail = createAppDetailFixture({ ...mockAppDetail, mode: 'workflow' as const }) const secretVars = [{ value_type: 'secret', name: 'API_KEY', value: 'secret' }] mockExportWorkflowAppDsl.mockResolvedValue({ status: 'confirmation-required', diff --git a/web/app/components/app/access-config/__tests__/index.spec.tsx b/web/app/components/app/access-config/__tests__/index.spec.tsx index f77c906df52..957f347bffa 100644 --- a/web/app/components/app/access-config/__tests__/index.spec.tsx +++ b/web/app/components/app/access-config/__tests__/index.spec.tsx @@ -10,7 +10,6 @@ import { import { consoleQuery } from '@/service/console' import { createConsoleQueryClient, renderWithConsoleQuery } from '@/test/console/query-data' import { createAppDetailFixture } from '@/test/fixtures/app' -import { AppModeEnum } from '@/types/app' import { AppACLPermission } from '@/utils/permission' import AppAccessConfigPage from '../index' @@ -375,7 +374,7 @@ describe('AppAccessConfigPage', () => { it('should not mount access config data hooks for Agent apps', () => { appDetail = createAppDetailFixture({ id: 'app-1', - mode: AppModeEnum.AGENT, + mode: 'agent' as const, maintainer: 'account-1', permission_keys: [AppACLPermission.AccessConfig], }) diff --git a/web/app/components/app/access-point/__tests__/built-in-access-points.spec.tsx b/web/app/components/app/access-point/__tests__/built-in-access-points.spec.tsx index 1f2b648bebf..74bf32fe3ef 100644 --- a/web/app/components/app/access-point/__tests__/built-in-access-points.spec.tsx +++ b/web/app/components/app/access-point/__tests__/built-in-access-points.spec.tsx @@ -1,5 +1,5 @@ +import type { AppMode } from '@dify/contracts/api/console/apps/types.gen' import type { ReactElement } from 'react' -import type { AppModeEnum } from '@/types/app' import { screen } from '@testing-library/react' import { consoleQuery } from '@/service/console' import { createHttpError } from '@/service/http-error' @@ -43,7 +43,7 @@ function render(ui: ReactElement) { queryClient.setQueryData(['system-features'], { webapp_auth: { enabled: true } }) queryClient.setQueryData( consoleQuery.apps.byAppId.get.queryKey({ input: { params: { app_id: 'app-1' } } }), - createAppDetailFixture({ ...mocks.appInfo, mode: mocks.appInfo.mode as AppModeEnum }), + createAppDetailFixture({ ...mocks.appInfo, mode: mocks.appInfo.mode as AppMode }), ) return renderWithConsole(ui, { wrapper: createQueryClientWrapper(queryClient) }) } diff --git a/web/app/components/app/access-point/__tests__/environment-access-point-cards.spec.tsx b/web/app/components/app/access-point/__tests__/environment-access-point-cards.spec.tsx index e47bb6b609e..c2d8588b87b 100644 --- a/web/app/components/app/access-point/__tests__/environment-access-point-cards.spec.tsx +++ b/web/app/components/app/access-point/__tests__/environment-access-point-cards.spec.tsx @@ -1,3 +1,4 @@ +import type { AppMode } from '@dify/contracts/api/console/apps/types.gen' import type { ReactElement } from 'react' import { screen, waitFor, within } from '@testing-library/react' import userEvent from '@testing-library/user-event' @@ -9,7 +10,6 @@ import { QueryClientTestProvider } from '@/test/console/query-provider' import { render } from '@/test/console/render' import { createAppDetailFixture, createAppSiteFixture } from '@/test/fixtures/app' import { createTestQueryClient } from '@/test/query-client' -import { AppModeEnum } from '@/types/app' import { EnvironmentServiceApiCard } from '../deployed-environment-access-points/environment-service-api-card' import { EnvironmentWebAppCard } from '../deployed-environment-access-points/environment-web-app-card' @@ -32,7 +32,7 @@ vi.mock('@/features/system-features/client', () => ({ }), })) -let mockAppMode: AppModeEnum = AppModeEnum.WORKFLOW +let mockAppMode: AppMode = 'workflow' vi.mock('@/context/i18n', () => ({ useDocLink: () => (path: string) => `https://docs.example.test/en${path}`, @@ -149,7 +149,7 @@ describe('environment access point cards', () => { beforeEach(() => { vi.clearAllMocks() mocks.saveSiteConfig.mockResolvedValue(true) - mockAppMode = AppModeEnum.WORKFLOW + mockAppMode = 'workflow' vi.stubGlobal('fetch', async (input: RequestInfo | URL, init?: RequestInit) => { const request = new Request(input, init) const path = new URL(request.url).pathname @@ -247,7 +247,7 @@ describe('environment access point cards', () => { ) it('sends a chatflow app to the chat web app shell', async () => { - mockAppMode = AppModeEnum.ADVANCED_CHAT + mockAppMode = 'advanced-chat' renderCard() diff --git a/web/app/components/app/access-point/__tests__/index.spec.tsx b/web/app/components/app/access-point/__tests__/index.spec.tsx index bdc3b28e15b..5ea1a003da9 100644 --- a/web/app/components/app/access-point/__tests__/index.spec.tsx +++ b/web/app/components/app/access-point/__tests__/index.spec.tsx @@ -1,3 +1,4 @@ +import type { AppMode } from '@dify/contracts/api/console/apps/types.gen' import type { AppEnvironment } from '@dify/contracts/enterprise-app-deploy/types.gen' import type { ReactNode } from 'react' import type { AccessPoint as AccessPointType } from '@/app/components/app/deploy/utils/access-point' @@ -11,11 +12,10 @@ import { QueryClientTestProvider } from '@/test/console/query-provider' import { render } from '@/test/console/render' import { createAppDetailFixture } from '@/test/fixtures/app' import { createTestQueryClient } from '@/test/query-client' -import { AppModeEnum } from '@/types/app' import { AppACLPermission } from '@/utils/permission' import AccessPoint from '..' -let appMode: AppModeEnum = AppModeEnum.WORKFLOW +let appMode: AppMode = 'workflow' let appPermissionKeys: string[] = [AppACLPermission.AccessPointView] const accessPointMocks = vi.hoisted(() => ({ builtIn: vi.fn(), @@ -133,7 +133,7 @@ const renderAccessPoint = ({ describe('AccessPoint', () => { beforeEach(() => { vi.clearAllMocks() - appMode = AppModeEnum.WORKFLOW + appMode = 'workflow' appPermissionKeys = [AppACLPermission.AccessPointView] }) @@ -243,7 +243,7 @@ describe('AccessPoint', () => { }) it('hides environment tabs for app types without multi-environment support', () => { - appMode = AppModeEnum.CHAT + appMode = 'chat' renderAccessPoint() diff --git a/web/app/components/app/access-point/__tests__/mcp-card.spec.tsx b/web/app/components/app/access-point/__tests__/mcp-card.spec.tsx index 4fd5ec63630..c5582800c69 100644 --- a/web/app/components/app/access-point/__tests__/mcp-card.spec.tsx +++ b/web/app/components/app/access-point/__tests__/mcp-card.spec.tsx @@ -8,7 +8,6 @@ import { toast } from '@/app/notifications' import { render } from '@/test/console/render' import { createAppDetailFixture, createAppModelConfigFixture } from '@/test/fixtures/app' import { createTestQueryClient } from '@/test/query-client' -import { AppModeEnum } from '@/types/app' import { MCPAccessPointCard } from '../built-in-access-points/mcp-card' const mocks = vi.hoisted(() => ({ @@ -65,7 +64,7 @@ vi.mock('@/app/components/tools/mcp/mcp-server-modal', () => ({ const appInfo = createAppDetailFixture({ api_base_url: 'https://api.example.test/v1', id: 'app-1', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, model_config: createAppModelConfigFixture({ updated_at: 1_710_000_000, user_input_form: [ @@ -82,7 +81,7 @@ const appInfo = createAppDetailFixture({ const workflowAppInfo = createAppDetailFixture({ ...appInfo, - mode: AppModeEnum.WORKFLOW, + mode: 'workflow' as const, model_config: null, }) diff --git a/web/app/components/app/access-point/__tests__/service-api-card.spec.tsx b/web/app/components/app/access-point/__tests__/service-api-card.spec.tsx index 48624195066..e5b6805ee09 100644 --- a/web/app/components/app/access-point/__tests__/service-api-card.spec.tsx +++ b/web/app/components/app/access-point/__tests__/service-api-card.spec.tsx @@ -1,4 +1,4 @@ -import type { AppDetailWithSite } from '@dify/contracts/api/console/apps/types.gen' +import type { AppDetailWithSite, AppMode } from '@dify/contracts/api/console/apps/types.gen' import { QueryClientProvider, useSuspenseQuery } from '@tanstack/react-query' import { screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' @@ -7,7 +7,6 @@ import { consoleQuery } from '@/service/console' import { render } from '@/test/console/render' import { createAppDetailFixture } from '@/test/fixtures/app' import { createTestQueryClient } from '@/test/query-client' -import { AppModeEnum } from '@/types/app' import { ServiceApiAccessPointCard } from '../built-in-access-points/service-api-card' const mocks = vi.hoisted(() => ({ @@ -53,7 +52,7 @@ vi.mock('../built-in-access-points/api-secret-key-button', () => ({ })) function createAppInfo( - mode: AppModeEnum, + mode: AppMode, overrides: Partial = {}, ): AppDetailWithSite { return createAppDetailFixture({ @@ -66,7 +65,7 @@ function createAppInfo( } function renderCard( - mode: AppModeEnum, + mode: AppMode, availability: 'available' | 'loading' | 'unavailable' = 'available', canManage = true, overrides: Partial = {}, @@ -124,12 +123,12 @@ describe('ServiceApiAccessPointCard', () => { }) it.each([ - [AppModeEnum.ADVANCED_CHAT, '/api-reference/guides/chatflow'], - [AppModeEnum.WORKFLOW, '/api-reference/guides/workflow'], - [AppModeEnum.CHAT, '/api-reference/guides/chat'], - [AppModeEnum.AGENT_CHAT, '/api-reference/guides/chat'], - [AppModeEnum.COMPLETION, '/api-reference/guides/completion'], - ])('links %s apps to the matching external API reference', (mode, path) => { + ['advanced-chat', '/api-reference/guides/chatflow'], + ['workflow', '/api-reference/guides/workflow'], + ['chat', '/api-reference/guides/chat'], + ['agent-chat', '/api-reference/guides/chat'], + ['completion', '/api-reference/guides/completion'], + ] as const)('links %s apps to the matching external API reference', (mode, path) => { renderCard(mode) const apiReferenceLink = screen.getByRole('link', { name: /apiInfo\.doc/ }) @@ -141,7 +140,7 @@ describe('ServiceApiAccessPointCard', () => { it('updates API status through the generated contract', async () => { const user = userEvent.setup() - renderCard(AppModeEnum.WORKFLOW) + renderCard('workflow') await user.click(screen.getByRole('switch')) @@ -154,7 +153,7 @@ describe('ServiceApiAccessPointCard', () => { }) it('shows loading without reporting an environment failure', () => { - renderCard(AppModeEnum.WORKFLOW, 'loading') + renderCard('workflow', 'loading') const card = screen.getByRole('region', { name: /serviceApi\.title/ }) expect(card).toHaveAttribute('aria-busy', 'true') @@ -165,7 +164,7 @@ describe('ServiceApiAccessPointCard', () => { }) it('keeps API keys and external documentation available when the API is stopped', () => { - renderCard(AppModeEnum.WORKFLOW, 'available', true, { enable_api: false }) + renderCard('workflow', 'available', true, { enable_api: false }) expect(screen.getByRole('button', { name: 'api-secret-keys' })).toBeEnabled() const apiReferenceLink = screen.getByRole('link', { name: /apiInfo\.doc/ }) @@ -176,14 +175,14 @@ describe('ServiceApiAccessPointCard', () => { }) it('disables API management without Access Point management permission', () => { - renderCard(AppModeEnum.WORKFLOW, 'available', false) + renderCard('workflow', 'available', false) expect(screen.getByRole('button', { name: 'api-secret-keys' })).toBeDisabled() expect(screen.getByRole('switch')).toHaveAttribute('aria-disabled', 'true') }) it('disables API keys and external documentation when the access point is unavailable', () => { - renderCard(AppModeEnum.WORKFLOW, 'unavailable') + renderCard('workflow', 'unavailable') expect(screen.getByRole('button', { name: 'api-secret-keys' })).toBeDisabled() expect(screen.getByRole('button', { name: /apiInfo\.doc/ })).toBeDisabled() @@ -193,7 +192,7 @@ describe('ServiceApiAccessPointCard', () => { const user = userEvent.setup() const toggle = createDeferredPromise<{ enable_api: boolean }>() mocks.apiEnable.mockReturnValueOnce(toggle.promise) - renderCard(AppModeEnum.WORKFLOW) + renderCard('workflow') const accessSwitch = screen.getByRole('switch') await user.click(accessSwitch) @@ -212,7 +211,7 @@ describe('ServiceApiAccessPointCard', () => { it('keeps successful status changes silent', async () => { const user = userEvent.setup() mocks.apiEnable.mockResolvedValueOnce(createAppDetailFixture({ enable_api: false })) - renderCard(AppModeEnum.WORKFLOW) + renderCard('workflow') const accessSwitch = screen.getByRole('switch') await user.click(accessSwitch) diff --git a/web/app/components/app/access-point/__tests__/trigger-card.spec.tsx b/web/app/components/app/access-point/__tests__/trigger-card.spec.tsx index a0d9de65e82..d478942b42e 100644 --- a/web/app/components/app/access-point/__tests__/trigger-card.spec.tsx +++ b/web/app/components/app/access-point/__tests__/trigger-card.spec.tsx @@ -6,7 +6,6 @@ import { toast } from '@/app/notifications' import { render } from '@/test/console/render' import { createAppDetailFixture } from '@/test/fixtures/app' import { createTestQueryClient } from '@/test/query-client' -import { AppModeEnum } from '@/types/app' import { TriggerAccessPointCard } from '../built-in-access-points/trigger-card' const mocks = vi.hoisted(() => ({ @@ -71,7 +70,7 @@ vi.mock('@/app/components/workflow/block-icon', () => ({ const appInfo = createAppDetailFixture({ id: 'app-1', - mode: AppModeEnum.WORKFLOW, + mode: 'workflow' as const, }) function createTrigger(id: string, status: AppTrigger['status']): AppTrigger { diff --git a/web/app/components/app/access-point/__tests__/web-app-card.spec.tsx b/web/app/components/app/access-point/__tests__/web-app-card.spec.tsx index 622a800cb8b..911c054d2f4 100644 --- a/web/app/components/app/access-point/__tests__/web-app-card.spec.tsx +++ b/web/app/components/app/access-point/__tests__/web-app-card.spec.tsx @@ -1,4 +1,8 @@ -import type { AppDetailWithSite, AppSiteResponse } from '@dify/contracts/api/console/apps/types.gen' +import type { + AppDetailWithSite, + AppMode, + AppSiteResponse, +} from '@dify/contracts/api/console/apps/types.gen' import type { PublishedWorkflow } from '../shared/utils' import type { InputVar, Node } from '@/app/components/workflow/types' import { QueryClientProvider, useSuspenseQuery } from '@tanstack/react-query' @@ -12,7 +16,6 @@ import { seedAccountProfileQuery } from '@/test/console/account-profile' import { render } from '@/test/console/render' import { createAppDetailFixture, createAppSiteFixture } from '@/test/fixtures/app' import { createTestQueryClient } from '@/test/query-client' -import { AppModeEnum } from '@/types/app' import { basePath } from '@/utils/var' import { WebAppAccessPointCard } from '../built-in-access-points/web-app-card' @@ -92,7 +95,7 @@ vi.mock('@/context/i18n', () => ({ useDocLink: () => (path: string) => `https://docs.example.test/en${path}`, })) -function createAppInfo(mode: AppModeEnum): AppDetailWithSite { +function createAppInfo(mode: AppMode): AppDetailWithSite { return createAppDetailFixture({ access_mode: AccessMode.PUBLIC, api_base_url: 'https://api.example.test/v1', @@ -111,7 +114,7 @@ function createAppInfo(mode: AppModeEnum): AppDetailWithSite { } function renderCard( - mode: AppModeEnum, + mode: AppMode, availability: 'available' | 'loading' | 'unavailable' = 'available', workflow?: PublishedWorkflow, { @@ -259,7 +262,7 @@ describe('WebAppAccessPointCard', () => { }) it('disables site actions when the app has no site', () => { - renderCard(AppModeEnum.CHAT, 'available', undefined, { appOverrides: { site: null } }) + renderCard('chat', 'available', undefined, { appOverrides: { site: null } }) expect(screen.getByRole('button', { name: /settings\.settings/ })).toBeDisabled() expect( @@ -272,7 +275,7 @@ describe('WebAppAccessPointCard', () => { }) it('does not present an unknown access mode as public or allow launching', () => { - renderCard(AppModeEnum.CHAT, 'available', undefined, { accessMode: null }) + renderCard('chat', 'available', undefined, { accessMode: null }) expect( screen.queryByRole('button', { name: /accessControlDialog\.accessItems\.anyone/ }), @@ -287,7 +290,7 @@ describe('WebAppAccessPointCard', () => { }) it('shows the current access mode without a redundant section label', () => { - renderCard(AppModeEnum.CHAT) + renderCard('chat') expect(screen.queryByText(/publishApp\.title/)).not.toBeInTheDocument() expect( @@ -298,7 +301,7 @@ describe('WebAppAccessPointCard', () => { it('disables Open and explains when the user cannot access the Web App', async () => { const user = userEvent.setup() mocks.getUserCanAccess.mockResolvedValue({ result: false }) - renderCard(AppModeEnum.WORKFLOW, 'available', undefined, { + renderCard('workflow', 'available', undefined, { accessMode: AccessMode.SPECIFIC_GROUPS_MEMBERS, }) @@ -319,7 +322,7 @@ describe('WebAppAccessPointCard', () => { it('keeps Open disabled until access is granted', async () => { const permission = createDeferredPromise<{ result: boolean }>() mocks.getUserCanAccess.mockReturnValueOnce(permission.promise) - renderCard(AppModeEnum.WORKFLOW) + renderCard('workflow') expect(screen.getByRole('button', { name: /studio\.accessPoint\.open/ })).toHaveAttribute( 'aria-disabled', @@ -336,7 +339,7 @@ describe('WebAppAccessPointCard', () => { it('allows opening external-member Web Apps independently of platform access', () => { mocks.getUserCanAccess.mockResolvedValue({ result: false }) - renderCard(AppModeEnum.WORKFLOW, 'available', undefined, { + renderCard('workflow', 'available', undefined, { accessMode: AccessMode.EXTERNAL_MEMBERS, }) @@ -347,7 +350,7 @@ describe('WebAppAccessPointCard', () => { }) it('allows opening Web Apps without querying permissions when access control is disabled', () => { - renderCard(AppModeEnum.CHAT, 'available', undefined, { showAccessControl: false }) + renderCard('chat', 'available', undefined, { showAccessControl: false }) expect(screen.getByRole('link', { name: /studio\.accessPoint\.open/ })).toHaveAttribute( 'href', @@ -359,7 +362,7 @@ describe('WebAppAccessPointCard', () => { it('updates Open after saving Web App access permissions', async () => { const user = userEvent.setup() mocks.getUserCanAccess.mockResolvedValue({ result: false }) - renderCard(AppModeEnum.CHAT, 'available', undefined, { + renderCard('chat', 'available', undefined, { accessMode: AccessMode.SPECIFIC_GROUPS_MEMBERS, }) @@ -380,7 +383,7 @@ describe('WebAppAccessPointCard', () => { ) }) - it.each([AppModeEnum.WORKFLOW, AppModeEnum.COMPLETION])( + it.each(['workflow', 'completion'] as const)( 'does not offer Embed into site for %s apps', (mode) => { renderCard(mode) @@ -393,7 +396,7 @@ describe('WebAppAccessPointCard', () => { it('keeps Embed into site for non-workflow Web apps', async () => { const user = userEvent.setup() - renderCard(AppModeEnum.CHAT) + renderCard('chat') await user.click(screen.getByRole('button', { name: /embedIntoSite/ })) @@ -404,7 +407,7 @@ describe('WebAppAccessPointCard', () => { it('updates site status through the generated contract', async () => { const user = userEvent.setup() - renderCard(AppModeEnum.CHAT) + renderCard('chat') await user.click(screen.getByRole('switch')) @@ -418,7 +421,7 @@ describe('WebAppAccessPointCard', () => { it('keeps site status changes behind Access Point management permission', async () => { const user = userEvent.setup() - renderCard(AppModeEnum.CHAT, 'available', undefined, { canManageAccessPoint: false }) + renderCard('chat', 'available', undefined, { canManageAccessPoint: false }) await user.click(screen.getByRole('switch')) @@ -429,7 +432,7 @@ describe('WebAppAccessPointCard', () => { const user = userEvent.setup() const refresh = createDeferredPromise() mocks.getAppDetail.mockReturnValueOnce(refresh.promise) - renderCard(AppModeEnum.CHAT) + renderCard('chat') await user.click(screen.getByRole('button', { name: /overview\.appInfo\.regenerate/ })) await user.click(screen.getByRole('button', { name: /operation\.confirm/ })) @@ -456,7 +459,7 @@ describe('WebAppAccessPointCard', () => { const user = userEvent.setup() const error = new Error('request failed') mocks.siteEnable.mockRejectedValueOnce(error) - renderCard(AppModeEnum.CHAT) + renderCard('chat') await user.click(screen.getByRole('switch')) @@ -467,7 +470,7 @@ describe('WebAppAccessPointCard', () => { it('passes hidden Chatflow inputs to the embed dialog', async () => { const user = userEvent.setup() - renderCard(AppModeEnum.ADVANCED_CHAT, 'available', workflowWithHiddenInput) + renderCard('advanced-chat', 'available', workflowWithHiddenInput) await user.click(screen.getByRole('button', { name: /embedIntoSite/ })) @@ -478,7 +481,7 @@ describe('WebAppAccessPointCard', () => { it('configures hidden workflow inputs before opening the Web App', async () => { const user = userEvent.setup() const openSpy = vi.spyOn(window, 'open').mockImplementation(() => null) - renderCard(AppModeEnum.WORKFLOW, 'available', workflowWithHiddenInput) + renderCard('workflow', 'available', workflowWithHiddenInput) await user.click(screen.getByRole('button', { name: /operation\.config/ })) await user.type(screen.getByLabelText('Secret'), 'top-secret') @@ -496,7 +499,7 @@ describe('WebAppAccessPointCard', () => { const user = userEvent.setup() const openSpy = vi.spyOn(window, 'open').mockImplementation(() => null) mocks.getUserCanAccess.mockResolvedValue({ result: false }) - renderCard(AppModeEnum.WORKFLOW, 'available', workflowWithHiddenInput, { + renderCard('workflow', 'available', workflowWithHiddenInput, { accessMode: AccessMode.SPECIFIC_GROUPS_MEMBERS, }) @@ -514,7 +517,7 @@ describe('WebAppAccessPointCard', () => { }) it('shows loading without reporting an environment failure', () => { - renderCard(AppModeEnum.WORKFLOW, 'loading') + renderCard('workflow', 'loading') const card = screen.getByRole('region', { name: /webApp\.title/ }) expect(card).toHaveAttribute('aria-busy', 'true') @@ -525,7 +528,7 @@ describe('WebAppAccessPointCard', () => { }) it('does not announce an unavailable access control entry as loading', () => { - renderCard(AppModeEnum.WORKFLOW, 'unavailable') + renderCard('workflow', 'unavailable') const card = screen.getByRole('region', { name: /webApp\.title/ }) expect(within(card).queryByRole('status', { name: 'common.loading' })).not.toBeInTheDocument() @@ -538,7 +541,7 @@ describe('WebAppAccessPointCard', () => { mocks.siteEnable .mockReturnValueOnce(firstToggle.promise) .mockReturnValueOnce(secondToggle.promise) - renderCard(AppModeEnum.CHAT) + renderCard('chat') const accessSwitch = screen.getByRole('switch') await user.click(accessSwitch) @@ -573,7 +576,7 @@ describe('WebAppAccessPointCard', () => { it('disables Web App management actions without Access Point management', async () => { const user = userEvent.setup() - renderCard(AppModeEnum.CHAT, 'available', undefined, { canManageAccessPoint: false }) + renderCard('chat', 'available', undefined, { canManageAccessPoint: false }) expect(screen.getByRole('switch')).toHaveAttribute('aria-disabled', 'true') expect(screen.getByRole('button', { name: /embedIntoSite/ })).toBeDisabled() diff --git a/web/app/components/app/annotation/__tests__/index.spec.tsx b/web/app/components/app/annotation/__tests__/index.spec.tsx index b919d82bd65..6a10430d40c 100644 --- a/web/app/components/app/annotation/__tests__/index.spec.tsx +++ b/web/app/components/app/annotation/__tests__/index.spec.tsx @@ -17,7 +17,6 @@ import { } from '@/service/annotation' import { renderWithConsoleQuery } from '@/test/console/query-data' import { createAppDetailFixture } from '@/test/fixtures/app' -import { AppModeEnum } from '@/types/app' import Annotation from '../index' import { AnnotationEnableStatus, JobStatus } from '../type' @@ -181,7 +180,7 @@ const updateAnnotationStatusMock = updateAnnotationStatus as Mock const appDetail = createAppDetailFixture({ id: 'app-id', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, }) const createAnnotation = (overrides: Partial = {}): AnnotationItem => ({ diff --git a/web/app/components/app/app-publisher/__tests__/index.spec.tsx b/web/app/components/app/app-publisher/__tests__/index.spec.tsx index dcb0abf3421..b2c76e2b488 100644 --- a/web/app/components/app/app-publisher/__tests__/index.spec.tsx +++ b/web/app/components/app/app-publisher/__tests__/index.spec.tsx @@ -11,7 +11,6 @@ import { renderWithConsoleQuery as renderWithQuery, } from '@/test/console/query-data' import { createAppDetailFixture } from '@/test/fixtures/app' -import { AppModeEnum } from '@/types/app' import { AppACLPermission } from '@/utils/permission' import { basePath } from '@/utils/var' import { AppPublisher } from '../index' @@ -269,7 +268,7 @@ describe('AppPublisher', () => { mockAppDetail = { id: 'app-1', name: 'Demo App', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, maintainer: 'user-2', permission_keys: [], access_mode: AccessMode.SPECIFIC_GROUPS_MEMBERS, @@ -346,7 +345,7 @@ describe('AppPublisher', () => { const invalidateQueries = vi.spyOn(queryClient, 'invalidateQueries') mockAppDetail = { ...mockAppDetail, - mode: AppModeEnum.WORKFLOW, + mode: 'workflow' as const, permission_keys: [AppACLPermission.Deploy], } mockOnPublish.mockResolvedValue(undefined) @@ -398,7 +397,7 @@ describe('AppPublisher', () => { const invalidateQueries = vi.spyOn(queryClient, 'invalidateQueries') mockAppDetail = { ...mockAppDetail, - mode: AppModeEnum.WORKFLOW, + mode: 'workflow' as const, permission_keys: [], } mockOnPublish.mockResolvedValue(undefined) @@ -423,7 +422,7 @@ describe('AppPublisher', () => { const user = userEvent.setup() mockAppDetail = { ...mockAppDetail, - mode: AppModeEnum.WORKFLOW, + mode: 'workflow' as const, } mockPublishedWorkflow = { created_at: Math.floor(Date.now() / 1000), @@ -458,7 +457,7 @@ describe('AppPublisher', () => { expect(mockUpdateWorkflow).toHaveBeenCalledWith( { appId: 'app-1', - appMode: AppModeEnum.WORKFLOW, + appMode: 'workflow' as const, url: '/apps/app-1/workflows/workflow-version-5', title: 'Release 6', releaseNotes: 'Updated notes', @@ -480,7 +479,7 @@ describe('AppPublisher', () => { const queryClient = createConsoleQueryClient() mockAppDetail = { ...mockAppDetail, - mode: AppModeEnum.WORKFLOW, + mode: 'workflow' as const, permission_keys: [AppACLPermission.Deploy], } const environmentsQuery = @@ -513,7 +512,7 @@ describe('AppPublisher', () => { it('should keep the workflow publisher single-environment without app deploy ACL', () => { mockAppDetail = { ...mockAppDetail, - mode: AppModeEnum.WORKFLOW, + mode: 'workflow' as const, permission_keys: [], } @@ -581,7 +580,7 @@ describe('AppPublisher', () => { } mockAppDetail = { ...mockAppDetail, - mode: AppModeEnum.WORKFLOW, + mode: 'workflow' as const, permission_keys: [AppACLPermission.Deploy], } const environmentsQuery = @@ -684,7 +683,7 @@ describe('AppPublisher', () => { const detailRequests: Request[] = [] mockAppDetail = { ...mockAppDetail, - mode: AppModeEnum.WORKFLOW, + mode: 'workflow' as const, permission_keys: [AppACLPermission.Deploy], } const environmentsQuery = @@ -832,7 +831,7 @@ describe('AppPublisher', () => { it('should keep workflow tool drawer mounted after closing the publish popover', () => { mockAppDetail = { ...mockAppDetail, - mode: AppModeEnum.WORKFLOW, + mode: 'workflow' as const, } render() @@ -853,7 +852,7 @@ describe('AppPublisher', () => { it('should show one success toast when automatically publishing a workflow tool', async () => { mockAppDetail = { ...mockAppDetail, - mode: AppModeEnum.WORKFLOW, + mode: 'workflow' as const, } mockOnPublish.mockImplementation(async (_params, options?: { showSuccessToast?: boolean }) => { if (options?.showSuccessToast !== false) mockToastSuccess('common.api.actionSuccess') @@ -876,7 +875,7 @@ describe('AppPublisher', () => { it('should not show a success toast when workflow tool creation fails after publishing', async () => { mockAppDetail = { ...mockAppDetail, - mode: AppModeEnum.WORKFLOW, + mode: 'workflow' as const, } mockOnPublish.mockImplementation(async (_params, options?: { showSuccessToast?: boolean }) => { if (options?.showSuccessToast !== false) mockToastSuccess('common.api.actionSuccess') @@ -900,7 +899,7 @@ describe('AppPublisher', () => { const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined) mockAppDetail = { ...mockAppDetail, - mode: AppModeEnum.WORKFLOW, + mode: 'workflow' as const, } mockOnPublish.mockRejectedValueOnce(new Error('publish failed')) @@ -922,7 +921,7 @@ describe('AppPublisher', () => { mockWorkspacePermissionKeys = [] mockAppDetail = { ...mockAppDetail, - mode: AppModeEnum.WORKFLOW, + mode: 'workflow' as const, } render() @@ -1066,7 +1065,7 @@ describe('AppPublisher', () => { const user = userEvent.setup() mockAppDetail = { ...mockAppDetail, - mode: AppModeEnum.WORKFLOW, + mode: 'workflow' as const, } mockPublishedWorkflow = { created_at: 1_710_000_100, @@ -1102,7 +1101,7 @@ describe('AppPublisher', () => { } mockAppDetail = { ...mockAppDetail, - mode: AppModeEnum.WORKFLOW, + mode: 'workflow' as const, } mockFetchPublishedWorkflow.mockResolvedValue({ created_at: 1_710_000_300, diff --git a/web/app/components/app/app-publisher/__tests__/publisher-panel.spec.tsx b/web/app/components/app/app-publisher/__tests__/publisher-panel.spec.tsx index b9196155f7e..157daebc275 100644 --- a/web/app/components/app/app-publisher/__tests__/publisher-panel.spec.tsx +++ b/web/app/components/app/app-publisher/__tests__/publisher-panel.spec.tsx @@ -2,7 +2,6 @@ import { detectPlatform } from '@tanstack/react-hotkeys' import { act, render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { useState } from 'react' -import { AppModeEnum } from '@/types/app' import { PublisherPanel } from '../publisher-content/publisher-panel' vi.mock('../environment-deployment-flow', () => ({ @@ -38,7 +37,7 @@ function PublisherPanelHarness({ builtInPublisher={{ actions: { appId: 'app-1', - appMode: AppModeEnum.CHAT, + appMode: 'chat' as const, appURL: 'https://example.com/app', canViewAccessPoint: false, disabledFunctionButton: false, diff --git a/web/app/components/app/app-publisher/__tests__/sections.spec.tsx b/web/app/components/app/app-publisher/__tests__/sections.spec.tsx index 7acbe20d41c..080cb1a10d2 100644 --- a/web/app/components/app/app-publisher/__tests__/sections.spec.tsx +++ b/web/app/components/app/app-publisher/__tests__/sections.spec.tsx @@ -8,7 +8,6 @@ import { useRef } from 'react' import { expectLoadingButton } from '@/test/button' import { createConsoleQueryWrapper } from '@/test/console/query-data' import { render as renderWithConsoleState } from '@/test/console/render' -import { AppModeEnum } from '@/types/app' import { PublisherActionsSection } from '../built-in-publisher/actions-section' import { PublisherSummarySection } from '../built-in-publisher/summary-section' import { usePublishController } from '../publisher-content/use-publish-controller' @@ -82,7 +81,7 @@ function PublisherPopup({ function PublishingPopup({ onPublish }: { onPublish: () => Promise }) { const publish = usePublishController({ - appMode: AppModeEnum.CHAT, + appMode: 'chat' as const, supportsMultiEnvironment: false, onClose: () => {}, onPublish, @@ -535,7 +534,7 @@ describe('app-publisher sections', () => { render( { render( { it('should show the disabled reason below setup and configured workflow tool actions', () => { const commonProps = { appId: 'workflow-app', - appMode: AppModeEnum.WORKFLOW, + appMode: 'workflow' as const, appURL: 'https://example.com/app', canViewAccessPoint: true, disabledFunctionButton: false, @@ -666,7 +665,7 @@ describe('app-publisher sections', () => { const onConfigureWorkflowTool = vi.fn() const commonProps = { appId: 'workflow-app', - appMode: AppModeEnum.WORKFLOW, + appMode: 'workflow' as const, appURL: 'https://example.com/app', canViewAccessPoint: true, disabledFunctionButton: false, @@ -710,7 +709,7 @@ describe('app-publisher sections', () => { render( { render( { render( { render( { render( { describe('getPublisherAppMode', () => { it('should normalize chat-like apps to chat mode', () => { - expect(getPublisherAppMode(AppModeEnum.AGENT_CHAT)).toBe(AppModeEnum.CHAT) + expect(getPublisherAppMode('agent-chat')).toBe('chat') }) it('should keep completion mode unchanged', () => { - expect(getPublisherAppMode(AppModeEnum.COMPLETION)).toBe(AppModeEnum.COMPLETION) + expect(getPublisherAppMode('completion')).toBe('completion') }) }) @@ -21,7 +20,7 @@ describe('app-publisher utils', () => { getPublisherAppUrl({ appBaseUrl: 'https://example.com', accessToken: 'token-1', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, }), ).toBe(`https://example.com${basePath}/chat/token-1`) }) diff --git a/web/app/components/app/configuration/__tests__/configuration-view.spec.tsx b/web/app/components/app/configuration/__tests__/configuration-view.spec.tsx index 72793efaaf6..a8bf13af7bc 100644 --- a/web/app/components/app/configuration/__tests__/configuration-view.spec.tsx +++ b/web/app/components/app/configuration/__tests__/configuration-view.spec.tsx @@ -9,7 +9,7 @@ import { fireEvent, render, screen } from '@testing-library/react' import * as React from 'react' import { PluginCategoryEnum } from '@/app/components/plugins/types' import { CollectionType } from '@/app/components/tools/types' -import { AppModeEnum, ModelModeType } from '@/types/app' +import { ModelModeType } from '@/types/app' import ConfigurationView from '../configuration-view' const mockIsAgentV2Enabled = vi.hoisted(() => vi.fn(() => false)) @@ -96,7 +96,7 @@ const createContextValue = (): ComponentProps['va appId: 'app-1', onOpenFeatures: vi.fn(), isTrailFinished: false, - mode: AppModeEnum.CHAT, + mode: 'chat' as const, modelModeType: ModelModeType.chat, promptMode: 'simple' as never, setPromptMode: vi.fn(), @@ -351,7 +351,7 @@ describe('ConfigurationView', () => { it('should show the legacy Agent badge for legacy Agent apps when Agent v2 is enabled', async () => { mockIsAgentV2Enabled.mockReturnValue(true) const contextValue = createContextValue() - contextValue.mode = AppModeEnum.AGENT_CHAT + contextValue.mode = 'agent-chat' render() @@ -375,7 +375,7 @@ describe('ConfigurationView', () => { it('should not show the legacy Agent badge when Agent v2 is disabled', () => { const contextValue = createContextValue() - contextValue.mode = AppModeEnum.AGENT_CHAT + contextValue.mode = 'agent-chat' render() diff --git a/web/app/components/app/configuration/__tests__/utils.spec.ts b/web/app/components/app/configuration/__tests__/utils.spec.ts index 5ee8fe2e37c..0d68301899c 100644 --- a/web/app/components/app/configuration/__tests__/utils.spec.ts +++ b/web/app/components/app/configuration/__tests__/utils.spec.ts @@ -1,5 +1,5 @@ import type { ModelConfig } from '@/models/debug' -import { AppModeEnum, ModelModeType, Resolution, TransferMethod } from '@/types/app' +import { ModelModeType, Resolution, TransferMethod } from '@/types/app' import { buildConfigurationFeaturesData, getConfigurationPublishingState, @@ -129,7 +129,7 @@ describe('configuration utils', () => { hasSetContextVar: false, hasSelectedDataSets: false, isAdvancedMode: true, - mode: AppModeEnum.CHAT, + mode: 'chat' as const, modelModeType: ModelModeType.completion, promptTemplate: 'ignored', }) @@ -158,7 +158,7 @@ describe('configuration utils', () => { hasSetContextVar: false, hasSelectedDataSets: true, isAdvancedMode: false, - mode: AppModeEnum.COMPLETION, + mode: 'completion' as const, modelModeType: ModelModeType.completion, promptTemplate: 'Prompt', }) @@ -188,7 +188,7 @@ describe('configuration utils', () => { hasSetContextVar: true, hasSelectedDataSets: false, isAdvancedMode: true, - mode: AppModeEnum.COMPLETION, + mode: 'completion' as const, modelModeType: ModelModeType.chat, promptTemplate: 'ignored', }) @@ -217,7 +217,7 @@ describe('configuration utils', () => { hasSetContextVar: true, hasSelectedDataSets: false, isAdvancedMode: true, - mode: AppModeEnum.COMPLETION, + mode: 'completion' as const, modelModeType: ModelModeType.completion, promptTemplate: 'ignored', }) diff --git a/web/app/components/app/configuration/config-prompt/__tests__/advanced-prompt-input.spec.tsx b/web/app/components/app/configuration/config-prompt/__tests__/advanced-prompt-input.spec.tsx index 1f3b2bddab8..45f512d058d 100644 --- a/web/app/components/app/configuration/config-prompt/__tests__/advanced-prompt-input.spec.tsx +++ b/web/app/components/app/configuration/config-prompt/__tests__/advanced-prompt-input.spec.tsx @@ -4,7 +4,6 @@ import { fireEvent, render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { INSERT_VARIABLE_VALUE_BLOCK_COMMAND } from '@/app/components/base/prompt-editor/plugins/variable-block' import ConfigContext from '@/context/debug-configuration' -import { AppModeEnum } from '@/types/app' import AdvancedPromptInput from '../advanced-prompt-input' const mockEmit = vi.fn() @@ -82,7 +81,7 @@ vi.mock('../prompt-editor-height-resize-wrap', () => ({ const createContextValue = () => ({ - mode: AppModeEnum.CHAT, + mode: 'chat' as const, hasSetBlockStatus: { context: false, history: false, diff --git a/web/app/components/app/configuration/config-prompt/__tests__/index.spec.tsx b/web/app/components/app/configuration/config-prompt/__tests__/index.spec.tsx index 06ec5c116c6..bd0518b638b 100644 --- a/web/app/components/app/configuration/config-prompt/__tests__/index.spec.tsx +++ b/web/app/components/app/configuration/config-prompt/__tests__/index.spec.tsx @@ -5,7 +5,7 @@ import * as React from 'react' import { MAX_PROMPT_MESSAGE_LENGTH } from '@/config' import ConfigContext from '@/context/debug-configuration' import { PromptRole } from '@/models/debug' -import { AppModeEnum, ModelModeType } from '@/types/app' +import { ModelModeType } from '@/types/app' import Prompt from '../index' type DebugConfiguration = { @@ -113,7 +113,7 @@ const renderComponent = ( contextOverrides: Partial = {}, ) => { const mergedProps: IPromptProps = { - mode: AppModeEnum.CHAT, + mode: 'chat' as const, promptTemplate: 'initial template', promptVariables: defaultPromptVariables, onChange: vi.fn(), @@ -144,7 +144,7 @@ describe('Prompt config component', () => { const simplePrompt = screen.getByTestId('simple-prompt-input') expect(simplePrompt)!.toBeInTheDocument() - expect(simplePrompt)!.toHaveAttribute('data-mode', AppModeEnum.CHAT) + expect(simplePrompt)!.toHaveAttribute('data-mode', 'chat') expect(mockSimplePromptInputProps?.promptTemplate).toBe('initial template') fireEvent.click(simplePrompt) expect(onChange).toHaveBeenCalledWith('mocked prompt', defaultPromptVariables) diff --git a/web/app/components/app/configuration/config-prompt/__tests__/simple-prompt-input-accessibility.spec.tsx b/web/app/components/app/configuration/config-prompt/__tests__/simple-prompt-input-accessibility.spec.tsx index ae4bc5aa971..7aeac6de217 100644 --- a/web/app/components/app/configuration/config-prompt/__tests__/simple-prompt-input-accessibility.spec.tsx +++ b/web/app/components/app/configuration/config-prompt/__tests__/simple-prompt-input-accessibility.spec.tsx @@ -1,5 +1,4 @@ import { render, screen } from '@testing-library/react' -import { AppModeEnum } from '@/types/app' import Prompt from '../simple-prompt-input' vi.mock('@/app/components/base/features/hooks', () => ({ @@ -9,7 +8,7 @@ vi.mock('@/app/components/base/features/hooks', () => ({ })) describe('SimplePromptInput accessibility', () => { - it.each([AppModeEnum.CHAT, AppModeEnum.COMPLETION])( + it.each(['chat', 'completion'] as const)( 'names the real editor from the visible section heading in %s mode', (mode) => { render() @@ -22,9 +21,7 @@ describe('SimplePromptInput accessibility', () => { ) it('keeps an accessible editor name when embedded without a section heading', () => { - render( - , - ) + render() expect(screen.getByRole('textbox', { name: 'appDebug.chatSubTitle' })).toBeInTheDocument() expect(screen.queryByRole('heading')).not.toBeInTheDocument() }) diff --git a/web/app/components/app/configuration/config-prompt/__tests__/simple-prompt-input.spec.tsx b/web/app/components/app/configuration/config-prompt/__tests__/simple-prompt-input.spec.tsx index d1c8359a2d4..640888f1815 100644 --- a/web/app/components/app/configuration/config-prompt/__tests__/simple-prompt-input.spec.tsx +++ b/web/app/components/app/configuration/config-prompt/__tests__/simple-prompt-input.spec.tsx @@ -2,7 +2,6 @@ import type { ReactNode } from 'react' import { fireEvent, render, screen } from '@testing-library/react' import { INSERT_VARIABLE_VALUE_BLOCK_COMMAND } from '@/app/components/base/prompt-editor/plugins/variable-block' import ConfigContext from '@/context/debug-configuration' -import { AppModeEnum } from '@/types/app' import Prompt from '../simple-prompt-input' const mockEmit = vi.fn() @@ -144,7 +143,7 @@ describe('SimplePromptInput', () => { render( { render( { it('should apply automatic generation results to prompt and opening statement', () => { render( - + , ) @@ -275,7 +269,7 @@ describe('SimplePromptInput', () => { })} > { })} > { render( ['value'] const defaultDebugConfigValue = { - mode: AppModeEnum.CHAT, + mode: 'chat' as const, dataSets: [], modelConfig: { model_id: 'test-model', @@ -399,7 +398,7 @@ describe('ConfigVar', () => { onPromptVariablesChange, }, { - mode: AppModeEnum.COMPLETION, + mode: 'completion' as const, dataSets: [{ id: 'dataset-1' } as DebugConfigurationState['dataSets'][number]], }, ) diff --git a/web/app/components/app/configuration/config-var/config-modal/__tests__/index-logic.spec.tsx b/web/app/components/app/configuration/config-var/config-modal/__tests__/index-logic.spec.tsx index b56a1cf9504..84baf7d5a23 100644 --- a/web/app/components/app/configuration/config-var/config-modal/__tests__/index-logic.spec.tsx +++ b/web/app/components/app/configuration/config-var/config-modal/__tests__/index-logic.spec.tsx @@ -10,7 +10,6 @@ import { seedAppDetail, } from '@/test/console/query-data' import { createAppDetailFixture } from '@/test/fixtures/app' -import { AppModeEnum } from '@/types/app' import ConfigModal from '../index' vi.mock('@/next/navigation', async (importOriginal) => ({ @@ -116,12 +115,12 @@ const createPayload = (overrides: Partial = {}): InputVar => ({ const renderConfigModal = (payload: InputVar = createPayload()) => { const queryClient = createConsoleQueryClient() - seedAppDetail(queryClient, createAppDetailFixture({ id: 'app-1', mode: AppModeEnum.CHAT })) + seedAppDetail(queryClient, createAppDetailFixture({ id: 'app-1', mode: 'chat' as const })) return renderWithConsoleQuery( ({ @@ -37,7 +37,7 @@ vi.mock('@monaco-editor/react', async () => ({ const render = (ui: React.ReactElement) => { const queryClient = createConsoleQueryClient() - seedAppDetail(queryClient, createAppDetailFixture({ id: 'app-1', mode: AppModeEnum.CHAT })) + seedAppDetail(queryClient, createAppDetailFixture({ id: 'app-1', mode: 'chat' as const })) return renderWithConsoleQuery(ui, { queryClient }) } diff --git a/web/app/components/app/configuration/config/__tests__/index.spec.tsx b/web/app/components/app/configuration/config/__tests__/index.spec.tsx index f98d912247d..08bec277a9d 100644 --- a/web/app/components/app/configuration/config/__tests__/index.spec.tsx +++ b/web/app/components/app/configuration/config/__tests__/index.spec.tsx @@ -1,9 +1,10 @@ +import type { AppMode } from '@dify/contracts/api/console/apps/types.gen' import type { ModelConfig, PromptVariable } from '@/models/debug' import type { ToolItem } from '@/types/app' import { render, screen } from '@testing-library/react' import * as React from 'react' import * as useContextSelector from 'use-context-selector' -import { AgentStrategy, AppModeEnum, ModelModeType } from '@/types/app' +import { AgentStrategy, ModelModeType } from '@/types/app' import Config from '../index' vi.mock('use-context-selector', async (importOriginal) => { @@ -64,7 +65,7 @@ vi.mock('../../config-prompt/conversation-history/history-panel', () => ({ })) type MockContext = { - mode: AppModeEnum + mode: AppMode isAdvancedMode: boolean modelModeType: ModelModeType isAgent: boolean @@ -118,7 +119,7 @@ const createModelConfig = (overrides: Partial = {}): ModelConfig => }) const createContextValue = (overrides: Partial = {}): MockContext => ({ - mode: AppModeEnum.CHAT, + mode: 'chat' as const, isAdvancedMode: false, modelModeType: ModelModeType.chat, isAgent: false, @@ -177,7 +178,7 @@ describe('Config - Rendering', () => { const showHistoryModal = vi.fn() renderConfig({ isAdvancedMode: true, - mode: AppModeEnum.ADVANCED_CHAT, + mode: 'advanced-chat' as const, modelModeType: ModelModeType.completion, hasSetBlockStatus: { context: false, diff --git a/web/app/components/app/configuration/config/automatic/__tests__/generator-reflow.browser.spec.tsx b/web/app/components/app/configuration/config/automatic/__tests__/generator-reflow.browser.spec.tsx index 3d9df3eaaf3..d0a9e193531 100644 --- a/web/app/components/app/configuration/config/automatic/__tests__/generator-reflow.browser.spec.tsx +++ b/web/app/components/app/configuration/config/automatic/__tests__/generator-reflow.browser.spec.tsx @@ -1,7 +1,6 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { page } from 'vite-plus/test/browser' import { render } from 'vitest-browser-react' -import { AppModeEnum } from '@/types/app' import GetAutomaticRes from '../get-automatic-res' vi.mock('react-i18next', async () => { @@ -71,7 +70,7 @@ describe('Prompt generator reflow', () => { const screen = await render( (
@@ -121,7 +122,7 @@ describe('GetAutomaticRes', () => { it('should apply a basic suggestion and persist model updates', async () => { render( { render( { it('should block generation when instruction is empty', () => { render( { render( { render( { render( { current: 'current prompt', instruction: 'workflow instruction', ideal_output: 'ideal output', + model_config: expect.objectContaining({ mode: 'chat' }), }), ) }) diff --git a/web/app/components/app/configuration/config/code-generator/__tests__/get-code-generator-res.spec.tsx b/web/app/components/app/configuration/config/code-generator/__tests__/get-code-generator-res.spec.tsx index 80c5ae8f66d..9ab9941338f 100644 --- a/web/app/components/app/configuration/config/code-generator/__tests__/get-code-generator-res.spec.tsx +++ b/web/app/components/app/configuration/config/code-generator/__tests__/get-code-generator-res.spec.tsx @@ -1,6 +1,5 @@ import { fireEvent, render, screen, waitFor, within } from '@testing-library/react' import { CodeLanguage } from '@/app/components/workflow/nodes/code/types' -import { AppModeEnum } from '@/types/app' import GetCodeGeneratorResModal from '../get-code-generator-res' const mockGenerateRule = vi.fn() @@ -52,7 +51,9 @@ vi.mock( }) => (
@@ -119,7 +120,7 @@ describe('GetCodeGeneratorResModal', () => { flowId="flow-1" nodeId="node-1" currentCode="print(1)" - mode={AppModeEnum.CHAT} + modelMode="chat" isShow codeLanguages={CodeLanguage.python3} onClose={mockOnClose} @@ -146,7 +147,7 @@ describe('GetCodeGeneratorResModal', () => { flowId="flow-1" nodeId="node-1" currentCode="print(1)" - mode={AppModeEnum.CHAT} + modelMode="chat" isShow codeLanguages={CodeLanguage.python3} onClose={mockOnClose} @@ -173,7 +174,7 @@ describe('GetCodeGeneratorResModal', () => { flowId="flow-1" nodeId="node-1" currentCode="print(1)" - mode={AppModeEnum.CHAT} + modelMode="chat" isShow codeLanguages={CodeLanguage.python3} onClose={mockOnClose} @@ -200,7 +201,7 @@ describe('GetCodeGeneratorResModal', () => { flowId="flow-1" nodeId="node-1" currentCode="print(1)" - mode={AppModeEnum.CHAT} + modelMode="completion" isShow codeLanguages={CodeLanguage.python3} onClose={mockOnClose} @@ -221,6 +222,7 @@ describe('GetCodeGeneratorResModal', () => { instruction: 'code instruction', ideal_output: 'code output', language: 'python', + model_config: expect.objectContaining({ mode: 'completion' }), }), ) }) @@ -255,7 +257,7 @@ describe('GetCodeGeneratorResModal', () => { flowId="flow-1" nodeId="node-1" currentCode="print(1)" - mode={AppModeEnum.CHAT} + modelMode="chat" isShow codeLanguages={CodeLanguage.python3} onClose={mockOnClose} @@ -295,7 +297,7 @@ describe('GetCodeGeneratorResModal', () => { flowId="flow-1" nodeId="node-1" currentCode="print(1)" - mode={AppModeEnum.CHAT} + modelMode="chat" isShow codeLanguages={CodeLanguage.javascript} onClose={mockOnClose} diff --git a/web/app/components/app/configuration/dataset-config/__tests__/index.spec.tsx b/web/app/components/app/configuration/dataset-config/__tests__/index.spec.tsx index 4ec1f23c33f..75f3ac0ae2a 100644 --- a/web/app/components/app/configuration/dataset-config/__tests__/index.spec.tsx +++ b/web/app/components/app/configuration/dataset-config/__tests__/index.spec.tsx @@ -11,7 +11,7 @@ import { getSelectedDatasetsMode } from '@/app/components/workflow/nodes/knowled import { DatasetPermission, DataSourceType } from '@/models/datasets' import { createConsoleQueryWrapper } from '@/test/console/query-data' import { render as renderWithConsoleState } from '@/test/console/render' -import { AppModeEnum, ModelModeType, RETRIEVE_TYPE } from '@/types/app' +import { ModelModeType, RETRIEVE_TYPE } from '@/types/app' import { DatasetACLPermission, getDatasetACLCapabilities } from '@/utils/permission' import DatasetConfig from '../index' @@ -185,7 +185,7 @@ vi.mock( // Mock context const mockConfigContext: any = { - mode: AppModeEnum.CHAT, + mode: 'chat' as const, modelModeType: ModelModeType.chat, isAgent: false, dataSets: [], @@ -516,7 +516,7 @@ describe('DatasetConfig', () => { it('should show context variable selector in completion mode with datasets', () => { const dataset = createMockDataset() renderDatasetConfig({ - mode: AppModeEnum.COMPLETION, + mode: 'completion' as const, dataSets: [dataset], modelConfig: { configs: { @@ -537,7 +537,7 @@ describe('DatasetConfig', () => { it('should not show context variable selector in chat mode', () => { const dataset = createMockDataset() renderDatasetConfig({ - mode: AppModeEnum.CHAT, + mode: 'chat' as const, dataSets: [dataset], modelConfig: { configs: { @@ -555,7 +555,7 @@ describe('DatasetConfig', () => { const user = userEvent.setup() const dataset = createMockDataset() renderDatasetConfig({ - mode: AppModeEnum.COMPLETION, + mode: 'completion' as const, dataSets: [dataset], modelConfig: { configs: { @@ -877,7 +877,7 @@ describe('DatasetConfig', () => { // Context var is only shown when datasets exist AND there are prompt_variables // Test with no datasets to ensure context var is not shown renderDatasetConfig({ - mode: AppModeEnum.COMPLETION, + mode: 'completion' as const, dataSets: [], modelConfig: { configs: { @@ -960,7 +960,7 @@ describe('DatasetConfig', () => { metadata_model_config: { provider: 'openai', name: 'gpt-3.5-turbo', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, completion_params: { temperature: 0.7 }, }, }, @@ -976,7 +976,7 @@ describe('DatasetConfig', () => { metadata_model_config: { provider: 'openai', name: 'gpt-4o-mini', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, completion_params: { temperature: 0.7 }, }, }), @@ -993,7 +993,7 @@ describe('DatasetConfig', () => { metadata_model_config: { provider: 'openai', name: 'gpt-3.5-turbo', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, completion_params: { temperature: 0.5, max_tokens: 100 }, }, }, diff --git a/web/app/components/app/configuration/debug/__tests__/index.spec.tsx b/web/app/components/app/configuration/debug/__tests__/index.spec.tsx index 6c5dfa75401..6a884273e9f 100644 --- a/web/app/components/app/configuration/debug/__tests__/index.spec.tsx +++ b/web/app/components/app/configuration/debug/__tests__/index.spec.tsx @@ -6,7 +6,7 @@ import * as React from 'react' import Log from '@/app/components/base/chat/chat/log' import { ModelFeatureEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' import ConfigContext from '@/context/debug-configuration' -import { AppModeEnum, ModelModeType, TransferMethod } from '@/types/app' +import { ModelModeType, TransferMethod } from '@/types/app' import Debug from '../index' import { APP_CHAT_WITH_MULTIPLE_MODEL, APP_CHAT_WITH_MULTIPLE_MODEL_RESTART } from '../types' @@ -345,7 +345,7 @@ const createContextValue = (overrides: Partial = {}): DebugCo appId: 'app-id', onOpenFeatures: vi.fn(), isTrailFinished: false, - mode: AppModeEnum.CHAT, + mode: 'chat', modelModeType: ModelModeType.chat, promptMode: 'simple' as DebugContextValue['promptMode'], setPromptMode: vi.fn(), @@ -676,7 +676,7 @@ describe('Debug', () => { const { notify } = renderDebug({ contextValue: { isAdvancedMode: true, - mode: AppModeEnum.CHAT, + mode: 'chat', modelModeType: ModelModeType.completion, hasSetBlockStatus: { context: false, history: false, query: true }, }, @@ -693,7 +693,7 @@ describe('Debug', () => { const { notify } = renderDebug({ contextValue: { isAdvancedMode: true, - mode: AppModeEnum.CHAT, + mode: 'chat', modelModeType: ModelModeType.completion, hasSetBlockStatus: { context: false, history: true, query: false }, }, @@ -711,7 +711,7 @@ describe('Debug', () => { it('should render prompt value panel and no-result placeholder', () => { renderDebug({ contextValue: { - mode: AppModeEnum.COMPLETION, + mode: 'completion' as const, }, }) @@ -722,7 +722,7 @@ describe('Debug', () => { it('should notify when required input is missing', () => { const { notify } = renderDebug({ contextValue: { - mode: AppModeEnum.COMPLETION, + mode: 'completion' as const, inputs: {}, modelConfig: { ...createContextValue().modelConfig, @@ -752,7 +752,7 @@ describe('Debug', () => { it('should notify when local file upload is still pending', () => { const { notify } = renderDebug({ contextValue: { - mode: AppModeEnum.COMPLETION, + mode: 'completion' as const, modelConfig: { ...createContextValue().modelConfig, configs: { @@ -776,7 +776,7 @@ describe('Debug', () => { it('should show cannot-query-dataset warning when dataset context variable is missing', () => { renderDebug({ contextValue: { - mode: AppModeEnum.COMPLETION, + mode: 'completion' as const, dataSets: [{ id: 'dataset-1' }] as DebugContextValue['dataSets'], hasSetContextVar: false, modelConfig: { @@ -800,7 +800,7 @@ describe('Debug', () => { const user = userEvent.setup() const { notify } = renderDebug({ contextValue: { - mode: AppModeEnum.COMPLETION, + mode: 'completion' as const, isAdvancedMode: true, chatPromptConfig: { prompt: [{ text: 'Missing role' }] }, }, @@ -817,7 +817,7 @@ describe('Debug', () => { const user = userEvent.setup() const file = { ...mockState.mockFeaturesState.file, enabled: true, number_limits: 1.5 } mockState.mockFeaturesState.file = file - const { notify } = renderDebug({ contextValue: { mode: AppModeEnum.COMPLETION } }) + const { notify } = renderDebug({ contextValue: { mode: 'completion' as const } }) await user.click(screen.getByRole('button', { name: 'Send' })) @@ -870,7 +870,7 @@ describe('Debug', () => { renderDebug({ contextValue: { - mode: AppModeEnum.COMPLETION, + mode: 'completion' as const, promptMode: 'simple' as DebugContextValue['promptMode'], textToSpeechConfig: { enabled: true, voice: 'alloy', language: 'en' }, modelConfig: { @@ -924,7 +924,7 @@ describe('Debug', () => { mockState.mockSendCompletionMessage.mockImplementation(() => undefined) const { notify } = renderDebug({ contextValue: { - mode: AppModeEnum.COMPLETION, + mode: 'completion' as const, modelConfig: { ...createContextValue().modelConfig, configs: { @@ -975,7 +975,7 @@ describe('Debug', () => { renderDebug({ contextValue: { - mode: AppModeEnum.COMPLETION, + mode: 'completion' as const, modelConfig: { ...createContextValue().modelConfig, configs: { @@ -999,7 +999,7 @@ describe('Debug', () => { handlers.onData('Completion answer', true, { messageId: 'completion-1' }) handlers.onCompleted() }) - renderDebug({ contextValue: { mode: AppModeEnum.COMPLETION } }) + renderDebug({ contextValue: { mode: 'completion' as const } }) await user.click(screen.getByRole('button', { name: 'Send' })) await user.click( within(screen.getByRole('region', { name: 'First answer' })).getByRole('button', { @@ -1080,7 +1080,7 @@ describe('Debug', () => { it('should emit completion event in multiple-model completion mode', () => { renderDebug({ contextValue: { - mode: AppModeEnum.COMPLETION, + mode: 'completion' as const, modelConfig: { ...createContextValue().modelConfig, configs: { @@ -1166,7 +1166,7 @@ describe('Debug', () => { it('should update feature store according to multiple-model vision support', () => { renderDebug({ contextValue: { - mode: AppModeEnum.CHAT, + mode: 'chat', }, props: { debugWithMultipleModel: true, diff --git a/web/app/components/app/configuration/debug/debug-with-multiple-model/__tests__/debug-item.spec.tsx b/web/app/components/app/configuration/debug/debug-with-multiple-model/__tests__/debug-item.spec.tsx index 85f4a8a9d4b..6fb5602c583 100644 --- a/web/app/components/app/configuration/debug/debug-with-multiple-model/__tests__/debug-item.spec.tsx +++ b/web/app/components/app/configuration/debug/debug-with-multiple-model/__tests__/debug-item.spec.tsx @@ -3,7 +3,6 @@ import type { ModelAndParameter } from '../../types' import { render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { ModelStatusEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' -import { AppModeEnum } from '@/types/app' import DebugItem from '../debug-item' const mockUseDebugConfigurationContext = vi.fn() @@ -98,7 +97,7 @@ describe('DebugItem', () => { capturedModelParameterTriggerProps = null mockUseDebugConfigurationContext.mockReturnValue({ - mode: AppModeEnum.CHAT, + mode: 'chat' as const, }) mockUseDebugWithMultipleModelContext.mockReturnValue({ @@ -156,7 +155,7 @@ describe('DebugItem', () => { describe('ChatItem rendering', () => { it('should render ChatItem in CHAT mode with active model', () => { - mockUseDebugConfigurationContext.mockReturnValue({ mode: AppModeEnum.CHAT }) + mockUseDebugConfigurationContext.mockReturnValue({ mode: 'chat' as const }) mockModelListQuery.mockReturnValue({ data: createTextGenerationModelList([ { provider: 'openai', model: 'gpt-3.5-turbo', status: ModelStatusEnum.active }, @@ -170,7 +169,7 @@ describe('DebugItem', () => { }) it('should render ChatItem in AGENT_CHAT mode with active model', () => { - mockUseDebugConfigurationContext.mockReturnValue({ mode: AppModeEnum.AGENT_CHAT }) + mockUseDebugConfigurationContext.mockReturnValue({ mode: 'agent-chat' as const }) mockModelListQuery.mockReturnValue({ data: createTextGenerationModelList([ { provider: 'openai', model: 'gpt-3.5-turbo', status: ModelStatusEnum.active }, @@ -183,7 +182,7 @@ describe('DebugItem', () => { }) it('should not render ChatItem when model is not active', () => { - mockUseDebugConfigurationContext.mockReturnValue({ mode: AppModeEnum.CHAT }) + mockUseDebugConfigurationContext.mockReturnValue({ mode: 'chat' as const }) mockModelListQuery.mockReturnValue({ data: createTextGenerationModelList([ { provider: 'openai', model: 'gpt-3.5-turbo', status: ModelStatusEnum.disabled }, @@ -196,7 +195,7 @@ describe('DebugItem', () => { }) it('should not render ChatItem when provider not found', () => { - mockUseDebugConfigurationContext.mockReturnValue({ mode: AppModeEnum.CHAT }) + mockUseDebugConfigurationContext.mockReturnValue({ mode: 'chat' as const }) mockModelListQuery.mockReturnValue({ data: createTextGenerationModelList([ { provider: 'anthropic', model: 'claude-3', status: ModelStatusEnum.active }, @@ -209,7 +208,7 @@ describe('DebugItem', () => { }) it('should not render ChatItem when model not found', () => { - mockUseDebugConfigurationContext.mockReturnValue({ mode: AppModeEnum.CHAT }) + mockUseDebugConfigurationContext.mockReturnValue({ mode: 'chat' as const }) mockModelListQuery.mockReturnValue({ data: createTextGenerationModelList([ { provider: 'openai', model: 'gpt-4', status: ModelStatusEnum.active }, @@ -224,7 +223,7 @@ describe('DebugItem', () => { describe('TextGenerationItem rendering', () => { it('should render TextGenerationItem in COMPLETION mode with active model', () => { - mockUseDebugConfigurationContext.mockReturnValue({ mode: AppModeEnum.COMPLETION }) + mockUseDebugConfigurationContext.mockReturnValue({ mode: 'completion' as const }) mockModelListQuery.mockReturnValue({ data: createTextGenerationModelList([ { provider: 'openai', model: 'gpt-3.5-turbo', status: ModelStatusEnum.active }, @@ -238,7 +237,7 @@ describe('DebugItem', () => { }) it('should not render TextGenerationItem when provider is not found', () => { - mockUseDebugConfigurationContext.mockReturnValue({ mode: AppModeEnum.COMPLETION }) + mockUseDebugConfigurationContext.mockReturnValue({ mode: 'completion' as const }) mockModelListQuery.mockReturnValue({ data: createTextGenerationModelList([ { provider: 'anthropic', model: 'claude-3', status: ModelStatusEnum.active }, @@ -503,7 +502,7 @@ describe('DebugItem', () => { }) it('should handle model with quotaExceeded status', () => { - mockUseDebugConfigurationContext.mockReturnValue({ mode: AppModeEnum.CHAT }) + mockUseDebugConfigurationContext.mockReturnValue({ mode: 'chat' as const }) mockModelListQuery.mockReturnValue({ data: createTextGenerationModelList([ { provider: 'anthropic', model: 'not-matching', status: ModelStatusEnum.quotaExceeded }, diff --git a/web/app/components/app/configuration/debug/debug-with-multiple-model/__tests__/index.spec.tsx b/web/app/components/app/configuration/debug/debug-with-multiple-model/__tests__/index.spec.tsx index f3c03c11375..e0be61c5725 100644 --- a/web/app/components/app/configuration/debug/debug-with-multiple-model/__tests__/index.spec.tsx +++ b/web/app/components/app/configuration/debug/debug-with-multiple-model/__tests__/index.spec.tsx @@ -1,3 +1,4 @@ +import type { AppMode } from '@dify/contracts/api/console/apps/types.gen' import type { CSSProperties } from 'react' import type { ModelAndParameter } from '../../types' import type { DebugWithMultipleModelContextType } from '../context' @@ -12,7 +13,7 @@ import { DEFAULT_CHAT_PROMPT_CONFIG, DEFAULT_COMPLETION_PROMPT_CONFIG, } from '@/config' -import { AppModeEnum, ModelModeType, Resolution, TransferMethod } from '@/types/app' +import { ModelModeType, Resolution, TransferMethod } from '@/types/app' import { APP_CHAT_WITH_MULTIPLE_MODEL } from '../../types' import DebugWithMultipleModel from '../index' @@ -161,7 +162,7 @@ const createModelConfig = (promptVariables: PromptVariableWithMeta[] = []): Mode type DebugConfiguration = { onOpenFeatures: () => void - mode: AppModeEnum + mode: AppMode inputs: Inputs modelConfig: ModelConfig readonly: boolean @@ -172,7 +173,7 @@ const createDebugConfiguration = ( overrides: Partial = {}, ): DebugConfiguration => ({ onOpenFeatures: vi.fn(), - mode: AppModeEnum.CHAT, + mode: 'chat' as const, inputs: {}, modelConfig: createModelConfig(), readonly: false, @@ -536,7 +537,7 @@ describe('DebugWithMultipleModel', () => { // Arrange mockUseDebugConfigurationContext.mockReturnValue( createDebugConfiguration({ - mode: AppModeEnum.AGENT_CHAT, + mode: 'agent-chat' as const, }), ) @@ -552,7 +553,7 @@ describe('DebugWithMultipleModel', () => { // Arrange mockUseDebugConfigurationContext.mockReturnValue( createDebugConfiguration({ - mode: AppModeEnum.COMPLETION, + mode: 'completion' as const, }), ) const multipleModelConfigs = [createModelAndParameter()] @@ -842,7 +843,7 @@ describe('DebugWithMultipleModel', () => { it('should set full height when chat input is hidden', () => { mockUseDebugConfigurationContext.mockReturnValue( createDebugConfiguration({ - mode: AppModeEnum.COMPLETION, + mode: 'completion' as const, }), ) diff --git a/web/app/components/app/configuration/debug/debug-with-single-model/__tests__/index.spec.tsx b/web/app/components/app/configuration/debug/debug-with-single-model/__tests__/index.spec.tsx index 43041781fa3..d6049b27283 100644 --- a/web/app/components/app/configuration/debug/debug-with-single-model/__tests__/index.spec.tsx +++ b/web/app/components/app/configuration/debug/debug-with-single-model/__tests__/index.spec.tsx @@ -10,7 +10,7 @@ import { CollectionType } from '@/app/components/tools/types' import { SupportUploadFileTypes } from '@/app/components/workflow/types' import { PromptMode } from '@/models/debug' import { renderWithAccountProfile as render } from '@/test/console/account-profile' -import { AgentStrategy, AppModeEnum, ModelModeType, Resolution, TransferMethod } from '@/types/app' +import { AgentStrategy, ModelModeType, Resolution, TransferMethod } from '@/types/app' import DebugWithSingleModel from '../index' // ============================================================================ @@ -135,7 +135,7 @@ const mockDebugConfigContext = { appId: 'test-app-id', onOpenFeatures: vi.fn(), isTrailFinished: false, - mode: AppModeEnum.CHAT, + mode: 'chat' as const, modelModeType: ModelModeType.chat, promptMode: PromptMode.simple, setPromptMode: vi.fn(), diff --git a/web/app/components/app/configuration/hooks/__tests__/use-advanced-prompt-config.spec.tsx b/web/app/components/app/configuration/hooks/__tests__/use-advanced-prompt-config.spec.tsx index 36d3d7984d3..47088ba26d1 100644 --- a/web/app/components/app/configuration/hooks/__tests__/use-advanced-prompt-config.spec.tsx +++ b/web/app/components/app/configuration/hooks/__tests__/use-advanced-prompt-config.spec.tsx @@ -7,7 +7,7 @@ import { } from '@/app/components/base/prompt-editor/constants' import { PromptMode, PromptRole } from '@/models/debug' import { fetchPromptTemplate } from '@/service/debug' -import { AppModeEnum, ModelModeType } from '@/types/app' +import { ModelModeType } from '@/types/app' import useAdvancedPromptConfig from '../use-advanced-prompt-config' vi.mock('@/service/debug', () => ({ @@ -25,7 +25,7 @@ describe('useAdvancedPromptConfig', () => { const handleUserChangedPrompt = vi.fn() const { result } = renderHook(() => useAdvancedPromptConfig({ - appMode: AppModeEnum.CHAT, + appMode: 'chat' as const, modelModeType: ModelModeType.chat, modelName: 'gpt-4o', promptMode: PromptMode.advanced, @@ -55,7 +55,7 @@ describe('useAdvancedPromptConfig', () => { it('should derive simple prompt block status from the pre-prompt', () => { const { result } = renderHook(() => useAdvancedPromptConfig({ - appMode: AppModeEnum.COMPLETION, + appMode: 'completion' as const, modelModeType: ModelModeType.completion, modelName: 'gpt-4o', promptMode: PromptMode.simple, @@ -79,7 +79,7 @@ describe('useAdvancedPromptConfig', () => { const handleUserChangedPrompt = vi.fn() const { result } = renderHook(() => useAdvancedPromptConfig({ - appMode: AppModeEnum.CHAT, + appMode: 'chat' as const, modelModeType: ModelModeType.chat, modelName: 'gpt-4o', promptMode: PromptMode.simple, @@ -118,7 +118,7 @@ describe('useAdvancedPromptConfig', () => { const { result } = renderHook(() => useAdvancedPromptConfig({ - appMode: AppModeEnum.COMPLETION, + appMode: 'completion' as const, modelModeType: ModelModeType.completion, modelName: 'gpt-4o', promptMode: PromptMode.simple, @@ -137,7 +137,7 @@ describe('useAdvancedPromptConfig', () => { await waitFor(() => { expect(mockFetchPromptTemplate).toHaveBeenCalledWith({ - appMode: AppModeEnum.COMPLETION, + appMode: 'completion' as const, mode: ModelModeType.completion, modelName: 'gpt-4o', hasSetDataSet: true, @@ -168,7 +168,7 @@ describe('useAdvancedPromptConfig', () => { const { result } = renderHook(() => useAdvancedPromptConfig({ - appMode: AppModeEnum.CHAT, + appMode: 'chat' as const, modelModeType: ModelModeType.chat, modelName: 'gpt-4o', promptMode: PromptMode.simple, @@ -211,7 +211,7 @@ describe('useAdvancedPromptConfig', () => { const { result } = renderHook(() => useAdvancedPromptConfig({ - appMode: AppModeEnum.CHAT, + appMode: 'chat' as const, modelModeType: ModelModeType.completion, modelName: 'gpt-4o', promptMode: PromptMode.advanced, @@ -268,7 +268,7 @@ describe('useAdvancedPromptConfig', () => { const { result } = renderHook(() => useAdvancedPromptConfig({ - appMode: AppModeEnum.COMPLETION, + appMode: 'completion' as const, modelModeType: ModelModeType.completion, modelName: 'gpt-4o', promptMode: PromptMode.advanced, diff --git a/web/app/components/app/configuration/hooks/__tests__/use-configuration-utils.spec.ts b/web/app/components/app/configuration/hooks/__tests__/use-configuration-utils.spec.ts index 5772f408eb6..7beac405124 100644 --- a/web/app/components/app/configuration/hooks/__tests__/use-configuration-utils.spec.ts +++ b/web/app/components/app/configuration/hooks/__tests__/use-configuration-utils.spec.ts @@ -6,7 +6,6 @@ import { createAppDetailFixture, createAppModelConfigFixture } from '@/test/fixt import { withSelectorKey } from '@/test/i18n-mock' import { AgentStrategy, - AppModeEnum, ModelModeType, Resolution, RETRIEVE_TYPE, @@ -133,7 +132,7 @@ describe('useConfiguration utils', () => { mockFetchCollectionList.mockResolvedValue([]) mockFetchAppDetailDirect.mockResolvedValue( createAppDetailFixture({ - mode: AppModeEnum.AGENT_CHAT, + mode: 'agent-chat' as const, model_config: createAppModelConfigFixture({ model: { provider: 'langgenius/openai/openai', @@ -277,7 +276,7 @@ describe('useConfiguration utils', () => { top_k: 4, } as any, deletedTools: [{ type: 'tool', provider_id: 'tool-1', tool_name: 'search' }], - mode: AppModeEnum.AGENT_CHAT, + mode: 'agent-chat' as const, nextDataSets: [{ id: 'dataset-1' }] as any, }) @@ -336,7 +335,7 @@ describe('useConfiguration utils', () => { datasets: { datasets: [] }, retrieval_model: RETRIEVE_TYPE.multiWay, } as any, - mode: AppModeEnum.COMPLETION, + mode: 'completion' as const, nextDataSets: [], }) @@ -369,7 +368,7 @@ describe('useConfiguration utils', () => { datasets: { datasets: [] }, retrieval_model: RETRIEVE_TYPE.multiWay, } as any, - mode: AppModeEnum.CHAT, + mode: 'chat', nextDataSets: [], }) @@ -496,7 +495,7 @@ describe('useConfiguration utils', () => { ]) mockFetchAppDetailDirect.mockResolvedValue({ deleted_tools: [], - mode: AppModeEnum.CHAT, + mode: 'chat', model_config: createAppModelConfigFixture({ prompt_type: 'advanced', chat_prompt_config: { @@ -581,7 +580,7 @@ describe('useConfiguration utils', () => { }) mockFetchAppDetailDirect.mockResolvedValue({ deleted_tools: [], - mode: AppModeEnum.AGENT_CHAT, + mode: 'agent-chat' as const, model_config: createAppModelConfigFixture({ prompt_type: 'simple', chat_prompt_config: { prompt: [] }, @@ -642,7 +641,7 @@ describe('useConfiguration utils', () => { mockFetchCollectionList.mockResolvedValue([]) mockFetchAppDetailDirect.mockResolvedValue({ deleted_tools: [], - mode: AppModeEnum.CHAT, + mode: 'chat', model_config: createAppModelConfigFixture({ prompt_type: 'simple', chat_prompt_config: { prompt: [] }, @@ -839,7 +838,7 @@ describe('useConfiguration utils', () => { backendModelConfig, collectionList: [], datasetConfigs: buildConfigurationDatasetConfigs({ backendModelConfig, nextDataSets: [] }), - mode: AppModeEnum.CHAT, + mode: 'chat', nextDataSets: [], }) const setPublishedConfig = vi.fn() @@ -862,7 +861,7 @@ describe('useConfiguration utils', () => { hasSetBlockStatus: { history: true, query: true }, isAdvancedMode: true, isFunctionCall: false, - mode: AppModeEnum.CHAT, + mode: 'chat', modelConfig: config.modelConfig, promptEmpty: false, promptMode: 'advanced', @@ -911,7 +910,7 @@ describe('useConfiguration utils', () => { }, isAdvancedMode: true, isFunctionCall: true, - mode: AppModeEnum.CHAT, + mode: 'chat', modelConfig: { agentConfig: { enabled: true, @@ -1019,7 +1018,7 @@ describe('useConfiguration utils', () => { }, isAdvancedMode: true, isFunctionCall: false, - mode: AppModeEnum.CHAT, + mode: 'chat', modelConfig: { configs: { prompt_template: 'hello', @@ -1080,7 +1079,7 @@ describe('useConfiguration utils', () => { handleSetVisionConfig, isAdvancedMode: true, migrateToDefaultPrompt, - mode: AppModeEnum.CHAT, + mode: 'chat', modelConfig: { model_id: 'gpt-4o-mini', provider: 'langgenius/openai/openai', @@ -1137,7 +1136,7 @@ describe('useConfiguration utils', () => { handleSetVisionConfig, isAdvancedMode: true, migrateToDefaultPrompt, - mode: AppModeEnum.COMPLETION, + mode: 'completion' as const, modelConfig: { model_id: 'gpt-4o-mini', provider: 'langgenius/openai/openai', diff --git a/web/app/components/app/configuration/hooks/__tests__/use-configuration.spec.tsx b/web/app/components/app/configuration/hooks/__tests__/use-configuration.spec.tsx index 4067aaece94..11ebc94ba1c 100644 --- a/web/app/components/app/configuration/hooks/__tests__/use-configuration.spec.tsx +++ b/web/app/components/app/configuration/hooks/__tests__/use-configuration.spec.tsx @@ -12,7 +12,7 @@ import { seedAppDetail } from '@/test/console/query-data' import { renderHook as renderHookWithConsoleState } from '@/test/console/render' import { createAppDetailFixture, createAppModelConfigFixture } from '@/test/fixtures/app' import { createTestQueryClient } from '@/test/query-client' -import { AppModeEnum, ModelModeType } from '@/types/app' +import { ModelModeType } from '@/types/app' import { AppACLPermission } from '@/utils/permission' import { useConfiguration } from '../use-configuration' @@ -24,7 +24,7 @@ const createRouteAppDetail = (overrides: Partial = {}) => model_config: createAppModelConfigFixture({ updated_at: 1710000000, }), - mode: AppModeEnum.CHAT, + mode: 'chat' as const, permission_keys: mockAppPermissionKeys, ...overrides, }) @@ -252,7 +252,7 @@ describe('useConfiguration', () => { vi.mocked(consoleClient.apps.byAppId.modelConfig.post).mockResolvedValue(undefined as never) mockFetchAppDetailDirect.mockResolvedValue({ deleted_tools: [], - mode: AppModeEnum.CHAT, + mode: 'chat' as const, model_config: createAppModelConfigFixture({ prompt_type: 'advanced', chat_prompt_config: { @@ -675,7 +675,7 @@ describe('useConfiguration', () => { mockCurrentModelFeatures = ['vision', 'document', 'audio', 'video'] mockFetchAppDetailDirect.mockResolvedValueOnce({ deleted_tools: [], - mode: AppModeEnum.CHAT, + mode: 'chat' as const, model_config: createAppModelConfigFixture({ prompt_type: 'simple', chat_prompt_config: { prompt: [] }, @@ -788,7 +788,7 @@ describe('useConfiguration', () => { }) mockFetchAppDetailDirect.mockResolvedValueOnce({ deleted_tools: [], - mode: AppModeEnum.CHAT, + mode: 'chat' as const, model_config: createAppModelConfigFixture({ prompt_type: 'simple', chat_prompt_config: { prompt: [] }, diff --git a/web/app/components/app/configuration/prompt-value-panel/__tests__/index.spec.tsx b/web/app/components/app/configuration/prompt-value-panel/__tests__/index.spec.tsx index d1ad96783d0..86b3794fac5 100644 --- a/web/app/components/app/configuration/prompt-value-panel/__tests__/index.spec.tsx +++ b/web/app/components/app/configuration/prompt-value-panel/__tests__/index.spec.tsx @@ -4,7 +4,7 @@ import userEvent from '@testing-library/user-event' import * as React from 'react' import { beforeEach, describe, expect, it, vi } from 'vite-plus/test' import ConfigContext from '@/context/debug-configuration' -import { AppModeEnum, ModelModeType, Resolution } from '@/types/app' +import { ModelModeType, Resolution } from '@/types/app' import PromptValuePanel from '../index' const mockOnOpenFeatures = vi.fn() @@ -78,7 +78,7 @@ const baseContextValue: any = { }, setInputs: mockSetInputs, canTestAndRun: true, - mode: AppModeEnum.COMPLETION, + mode: 'completion' as const, isAdvancedMode: false, completionPromptConfig: { prompt: { text: 'completion' }, @@ -88,7 +88,7 @@ const baseContextValue: any = { } as any const defaultProps: IPromptValuePanelProps = { - appType: AppModeEnum.COMPLETION, + appType: 'completion', onSend: mockOnSend, inputs: { textVar: 'initial', boolVar: false }, visionConfig: { enabled: false, number_limits: 0, detail: Resolution.low, transfer_methods: [] }, @@ -142,10 +142,10 @@ describe('PromptValuePanel', () => { it('disables run when mode is not completion', () => { renderPanel({ context: { - mode: AppModeEnum.CHAT, + mode: 'chat' as const, }, props: { - appType: AppModeEnum.CHAT, + appType: 'chat', }, }) diff --git a/web/app/components/app/create-app-dialog/app-card/__tests__/index.spec.tsx b/web/app/components/app/create-app-dialog/app-card/__tests__/index.spec.tsx index cc50aefb762..139d83a3730 100644 --- a/web/app/components/app/create-app-dialog/app-card/__tests__/index.spec.tsx +++ b/web/app/components/app/create-app-dialog/app-card/__tests__/index.spec.tsx @@ -4,7 +4,6 @@ import { screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { trackEvent } from '@/app/components/base/amplitude' import { renderWithConsoleQuery } from '@/test/console/query-data' -import { AppModeEnum } from '@/types/app' import AppCard from '../index' vi.mock('@/app/components/base/amplitude', () => ({ trackEvent: vi.fn() })) @@ -16,7 +15,7 @@ const app = { can_trial: true, app: { id: 'app-1', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, icon_type: 'emoji', icon: '🤖', icon_background: '#FFEAD5', diff --git a/web/app/components/app/create-app-modal/__tests__/index.spec.tsx b/web/app/components/app/create-app-modal/__tests__/index.spec.tsx index c70fae8cbf5..a68faac6e49 100644 --- a/web/app/components/app/create-app-modal/__tests__/index.spec.tsx +++ b/web/app/components/app/create-app-modal/__tests__/index.spec.tsx @@ -8,7 +8,6 @@ import { useRouter } from '@/next/navigation' import { renderWithConsoleQuery } from '@/test/console/query-data' import { mockEmojiData } from '@/test/emoji-picker' import { createAppDetailFixture } from '@/test/fixtures/app' -import { AppModeEnum } from '@/types/app' import { getRedirection } from '@/utils/app-redirection' import { trackCreateApp } from '@/utils/create-app-tracking' import CreateAppModal from '../index' @@ -109,7 +108,7 @@ const renderModal = () => { show onClose={onClose} onCreateFromTemplate={onCreateFromTemplate} - defaultAppMode={AppModeEnum.ADVANCED_CHAT} + defaultAppMode={'advanced-chat'} />, ) return { onClose, onCreateFromTemplate } @@ -138,7 +137,7 @@ describe('CreateAppModal', () => { it('creates an app and fires callbacks', async () => { const mockApp = createAppDetailFixture({ - mode: AppModeEnum.ADVANCED_CHAT, + mode: 'advanced-chat' as const, maintainer: 'user-1', }) mockCreateApp.mockResolvedValue(mockApp) @@ -155,13 +154,13 @@ describe('CreateAppModal', () => { icon_type: 'emoji', icon: '🤖', icon_background: '#FFEAD5', - mode: AppModeEnum.ADVANCED_CHAT, + mode: 'advanced-chat' as const, }), ) expect(mockTrackCreateApp).toHaveBeenCalledWith({ source: 'studio_blank', - appMode: AppModeEnum.ADVANCED_CHAT, + appMode: 'advanced-chat' as const, }) expect(onClose).toHaveBeenCalledTimes(1) await waitFor(() => @@ -176,7 +175,7 @@ describe('CreateAppModal', () => { it('waits for create_app tracking before redirecting after blank app creation', async () => { const mockApp = createAppDetailFixture({ - mode: AppModeEnum.ADVANCED_CHAT, + mode: 'advanced-chat' as const, maintainer: 'user-1', }) let resolveTracking: (() => void) | undefined @@ -196,7 +195,7 @@ describe('CreateAppModal', () => { await waitFor(() => { expect(mockTrackCreateApp).toHaveBeenCalledWith({ source: 'studio_blank', - appMode: AppModeEnum.ADVANCED_CHAT, + appMode: 'advanced-chat' as const, }) }) const createButton = screen.getByRole('button', { name: /app\.newApp\.Create/ }) @@ -251,7 +250,7 @@ describe('CreateAppModal', () => { async ({ platform, modifier }) => { vi.spyOn(navigator, 'platform', 'get').mockReturnValue(platform) mockCreateApp.mockResolvedValue( - createAppDetailFixture({ id: 'chat-app', mode: AppModeEnum.CHAT }), + createAppDetailFixture({ id: 'chat-app', mode: 'chat' as const }), ) renderModal() @@ -288,7 +287,7 @@ describe('CreateAppModal', () => { icon_type: 'emoji', icon: '🤖', icon_background: '#F3FEE7', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, }) }) }, @@ -302,7 +301,7 @@ describe('CreateAppModal', () => { async ({ platform, modifier }) => { vi.spyOn(navigator, 'platform', 'get').mockReturnValue(platform) mockCreateApp.mockResolvedValue( - createAppDetailFixture({ id: 'reopened-app', mode: AppModeEnum.ADVANCED_CHAT }), + createAppDetailFixture({ id: 'reopened-app', mode: 'advanced-chat' as const }), ) const onClose = vi.fn() const modal = (show: boolean) => ( @@ -310,7 +309,7 @@ describe('CreateAppModal', () => { show={show} onClose={onClose} onCreateFromTemplate={vi.fn()} - defaultAppMode={AppModeEnum.ADVANCED_CHAT} + defaultAppMode={'advanced-chat'} /> ) const { rerender } = render(modal(true)) @@ -366,7 +365,7 @@ describe('CreateAppModal', () => { it('should switch between app types before creating a completion app', async () => { mockCreateApp.mockResolvedValue( - createAppDetailFixture({ id: 'completion-app', mode: AppModeEnum.COMPLETION }), + createAppDetailFixture({ id: 'completion-app', mode: 'completion' as const }), ) renderModal() @@ -385,7 +384,7 @@ describe('CreateAppModal', () => { expect(mockCreateApp).toHaveBeenCalledWith( expect.objectContaining({ name: 'Completion App', - mode: AppModeEnum.COMPLETION, + mode: 'completion' as const, }), ) }) @@ -416,7 +415,7 @@ describe('CreateAppModal', () => { expect(mockCreateApp).toHaveBeenCalledTimes(1) - resolveCreate?.(createAppDetailFixture({ id: 'slow-app', mode: AppModeEnum.ADVANCED_CHAT })) + resolveCreate?.(createAppDetailFixture({ id: 'slow-app', mode: 'advanced-chat' as const })) await waitFor(() => { expect(mockGetRedirection).toHaveBeenCalledTimes(1) }) diff --git a/web/app/components/app/create-from-dsl-modal/__tests__/index.spec.tsx b/web/app/components/app/create-from-dsl-modal/__tests__/index.spec.tsx index f8b0513da20..0a394b0b6fd 100644 --- a/web/app/components/app/create-from-dsl-modal/__tests__/index.spec.tsx +++ b/web/app/components/app/create-from-dsl-modal/__tests__/index.spec.tsx @@ -4,7 +4,6 @@ import userEvent from '@testing-library/user-event' import { DSLImportMode, DSLImportStatus } from '@/models/app' import { httpErrorFromResponse } from '@/service/http-error' import { renderWithConsoleQuery } from '@/test/console/query-data' -import { AppModeEnum } from '@/types/app' import CreateFromDSLModal from '../index' import { CreateFromDSLModalTab } from '../types' @@ -163,7 +162,7 @@ describe('CreateFromDSLModal', () => { id: 'package-import', status: 'completed-with-warnings', app_id: 'agent-app', - app_mode: AppModeEnum.AGENT, + app_mode: 'agent', warnings: [ { code: 'agent_skill_missing', path: 'skills.s_000001', message: 'Missing Skill' }, ], @@ -180,7 +179,7 @@ describe('CreateFromDSLModal', () => { }) expect(mockResolveImportedAppRedirectionTarget).toHaveBeenCalledWith({ id: 'agent-app', - mode: AppModeEnum.AGENT, + mode: 'agent' as const, permission_keys: undefined, }) }, @@ -193,7 +192,7 @@ describe('CreateFromDSLModal', () => { mockImportDSL.mockResolvedValue({ id: 'import', status: 'completed', - app_mode: AppModeEnum.AGENT, + app_mode: 'agent', }) render() const input = document.querySelector('input[type="file"]') @@ -309,7 +308,7 @@ describe('CreateFromDSLModal', () => { id: 'import-1', status: DSLImportStatus.COMPLETED, app_id: 'app-1', - app_mode: AppModeEnum.CHAT, + app_mode: 'chat', permission_keys: ['app.acl.view_layout'], }) @@ -336,7 +335,7 @@ describe('CreateFromDSLModal', () => { }) expect(mockTrackCreateApp).toHaveBeenCalledWith({ source: 'studio_upload', - appMode: AppModeEnum.CHAT, + appMode: 'chat' as const, }) expect(handleSuccess).toHaveBeenCalledTimes(1) expect(handleClose).toHaveBeenCalledTimes(1) @@ -358,7 +357,7 @@ describe('CreateFromDSLModal', () => { id: 'import-no-permissions', status: DSLImportStatus.COMPLETED, app_id: 'app-no-permissions', - app_mode: AppModeEnum.WORKFLOW, + app_mode: 'workflow', }) render( @@ -375,7 +374,7 @@ describe('CreateFromDSLModal', () => { }) expect(mockGetRedirection).toHaveBeenCalledWith( - { id: 'app-no-permissions', mode: AppModeEnum.WORKFLOW, permission_keys: undefined }, + { id: 'app-no-permissions', mode: 'workflow' as const, permission_keys: undefined }, mockPush, { currentUserId: 'user-1', @@ -391,7 +390,7 @@ describe('CreateFromDSLModal', () => { id: 'import-2', status: DSLImportStatus.COMPLETED_WITH_WARNINGS, app_id: 'app-2', - app_mode: AppModeEnum.AGENT, + app_mode: 'agent', permission_keys: ['app.acl.view_layout'], warnings: [ { @@ -446,7 +445,7 @@ describe('CreateFromDSLModal', () => { id: 'import-file-in-flight', status: DSLImportStatus.COMPLETED, app_id: 'app-1', - app_mode: AppModeEnum.CHAT, + app_mode: 'chat', permission_keys: ['app.acl.view_layout'], }) @@ -517,7 +516,7 @@ describe('CreateFromDSLModal', () => { mockImportDSLConfirm.mockResolvedValue({ status: DSLImportStatus.COMPLETED, app_id: 'app-3', - app_mode: AppModeEnum.WORKFLOW, + app_mode: 'workflow', permission_keys: ['app.acl.view_layout'], }) @@ -548,10 +547,10 @@ describe('CreateFromDSLModal', () => { }) expect(mockTrackCreateApp).toHaveBeenCalledWith({ source: 'studio_upload', - appMode: AppModeEnum.WORKFLOW, + appMode: 'workflow' as const, }) expect(mockGetRedirection).toHaveBeenCalledWith( - { id: 'app-3', mode: AppModeEnum.WORKFLOW, permission_keys: ['app.acl.view_layout'] }, + { id: 'app-3', mode: 'workflow' as const, permission_keys: ['app.acl.view_layout'] }, mockPush, { currentUserId: 'user-1', @@ -569,7 +568,7 @@ describe('CreateFromDSLModal', () => { const completed = { status: DSLImportStatus.COMPLETED, app_id: 'created-app', - app_mode: AppModeEnum.WORKFLOW, + app_mode: 'workflow', } mockImportDSL.mockResolvedValue( confirmed ? { id: 'pending-import', status: DSLImportStatus.PENDING } : completed, @@ -606,7 +605,7 @@ describe('CreateFromDSLModal', () => { mockImportDSLConfirm.mockResolvedValue({ status: DSLImportStatus.COMPLETED_WITH_WARNINGS, app_id: 'agent-app-1', - app_mode: AppModeEnum.AGENT, + app_mode: 'agent', warnings: [ { code: 'agent_tool_authorization_required', @@ -618,7 +617,7 @@ describe('CreateFromDSLModal', () => { }) mockResolveImportedAppRedirectionTarget.mockResolvedValueOnce({ id: 'agent-app-1', - mode: AppModeEnum.AGENT, + mode: 'agent' as const, permission_keys: undefined, bound_agent_id: 'agent-1', }) @@ -645,13 +644,13 @@ describe('CreateFromDSLModal', () => { }) expect(mockResolveImportedAppRedirectionTarget).toHaveBeenCalledWith({ id: 'agent-app-1', - mode: AppModeEnum.AGENT, + mode: 'agent' as const, permission_keys: undefined, }) expect(mockGetRedirection).toHaveBeenCalledWith( expect.objectContaining({ id: 'agent-app-1', - mode: AppModeEnum.AGENT, + mode: 'agent' as const, bound_agent_id: 'agent-1', }), mockPush, @@ -771,7 +770,7 @@ describe('CreateFromDSLModal', () => { id: 'import-in-flight', status: DSLImportStatus.COMPLETED, app_id: 'app-1', - app_mode: AppModeEnum.CHAT, + app_mode: 'chat', permission_keys: ['app.acl.view_layout'], }) }) @@ -834,7 +833,7 @@ describe('CreateFromDSLModal', () => { id: 'import-confirming', status: DSLImportStatus.COMPLETED, app_id: 'app-confirming', - app_mode: AppModeEnum.WORKFLOW, + app_mode: 'workflow', }) }) }) @@ -882,7 +881,7 @@ describe('CreateFromDSLModal', () => { mockImportDSL.mockResolvedValue({ id: 'package-import', status: 'completed', - app_mode: AppModeEnum.AGENT, + app_mode: 'agent', }) render() expect(screen.getByText('apps-full')).toBeInTheDocument() @@ -918,7 +917,7 @@ describe('CreateFromDSLModal', () => { mockImportDSL.mockResolvedValue({ id: 'package-import', status: 'completed', - app_mode: AppModeEnum.AGENT, + app_mode: 'agent', }) render() expect( diff --git a/web/app/components/app/deploy/__tests__/index.spec.tsx b/web/app/components/app/deploy/__tests__/index.spec.tsx index 37b925cfc41..df8515bc0cd 100644 --- a/web/app/components/app/deploy/__tests__/index.spec.tsx +++ b/web/app/components/app/deploy/__tests__/index.spec.tsx @@ -33,7 +33,6 @@ import { seedAppDetail, } from '@/test/console/query-data' import { createAppDetailFixture } from '@/test/fixtures/app' -import { AppModeEnum } from '@/types/app' import { AppACLPermission } from '@/utils/permission' import AppDeploy from '..' import { EnvironmentTable } from '../environment-table' @@ -745,7 +744,7 @@ function render( queryClient, createAppDetailFixture({ ...mockBuiltInEnvironment.appDetail, - mode: AppModeEnum.WORKFLOW, + mode: 'workflow', permission_keys: appPermissionKeys, }), ) diff --git a/web/app/components/app/log-annotation/__tests__/index.spec.tsx b/web/app/components/app/log-annotation/__tests__/index.spec.tsx index 939c69e9086..49fac6e718c 100644 --- a/web/app/components/app/log-annotation/__tests__/index.spec.tsx +++ b/web/app/components/app/log-annotation/__tests__/index.spec.tsx @@ -6,7 +6,6 @@ import { PageType } from '@/app/components/base/features/new-feature-panel/annot import { consoleQuery } from '@/service/console' import { createConsoleQueryClient, renderWithConsoleQuery } from '@/test/console/query-data' import { createAppDetailFixture, createAppSiteFixture } from '@/test/fixtures/app' -import { AppModeEnum } from '@/types/app' import LogAnnotation from '../index' vi.mock('@/app/components/app/annotation', () => ({ @@ -75,7 +74,7 @@ describe('LogAnnotation', () => { it('should render log content without the old page tabs', () => { // Arrange - appDetail = createMockApp({ mode: AppModeEnum.CHAT }) + appDetail = createMockApp({ mode: 'chat' }) // Act render() @@ -88,7 +87,7 @@ describe('LogAnnotation', () => { it('should render completion logs without the old page tabs', () => { // Arrange - appDetail = createMockApp({ mode: AppModeEnum.COMPLETION }) + appDetail = createMockApp({ mode: 'completion' as const }) // Act render() @@ -101,7 +100,7 @@ describe('LogAnnotation', () => { it('should hide tabs and render workflow log in workflow mode', () => { // Arrange - appDetail = createMockApp({ mode: AppModeEnum.WORKFLOW }) + appDetail = createMockApp({ mode: 'workflow' as const }) // Act render() @@ -116,7 +115,7 @@ describe('LogAnnotation', () => { describe('Props', () => { it('should render log content when page type is log', () => { // Arrange - appDetail = createMockApp({ mode: AppModeEnum.CHAT }) + appDetail = createMockApp({ mode: 'chat' }) // Act render() @@ -128,7 +127,7 @@ describe('LogAnnotation', () => { it('should render annotation content when page type is annotation', () => { // Arrange - appDetail = createMockApp({ mode: AppModeEnum.CHAT }) + appDetail = createMockApp({ mode: 'chat' }) // Act render() diff --git a/web/app/components/app/log/__tests__/empty-element.spec.tsx b/web/app/components/app/log/__tests__/empty-element.spec.tsx index 6f48de616c5..ddff1dd6759 100644 --- a/web/app/components/app/log/__tests__/empty-element.spec.tsx +++ b/web/app/components/app/log/__tests__/empty-element.spec.tsx @@ -1,8 +1,7 @@ -import type { AppDetailWithSite } from '@dify/contracts/api/console/apps/types.gen' +import type { AppDetailWithSite, AppMode } from '@dify/contracts/api/console/apps/types.gen' import { screen } from '@testing-library/react' import { renderWithConsoleQuery as render } from '@/test/console/query-data' import { createAppDetailFixture, createAppSiteFixture } from '@/test/fixtures/app' -import { AppModeEnum } from '@/types/app' import EmptyElement from '../empty-element' vi.mock('react-i18next', async () => { @@ -39,7 +38,7 @@ vi.mock('@/utils/var', () => ({ })) describe('EmptyElement', () => { - const createMockAppDetail = (mode: AppModeEnum) => + const createMockAppDetail = (mode: AppMode) => createAppDetailFixture({ id: 'test-app-id', name: 'Test App', @@ -59,14 +58,14 @@ describe('EmptyElement', () => { describe('Rendering', () => { it('should render empty element with title', () => { - const appDetail = createMockAppDetail(AppModeEnum.CHAT) + const appDetail = createMockAppDetail('chat') render() expect(screen.getByText('table.empty.element.title')).toBeInTheDocument() }) it('should render Trans component with i18n key', () => { - const appDetail = createMockAppDetail(AppModeEnum.CHAT) + const appDetail = createMockAppDetail('chat') render() expect(screen.getByText('table.empty.element.content', { exact: false })).toBeInTheDocument() @@ -75,7 +74,7 @@ describe('EmptyElement', () => { describe('App Mode Handling', () => { it('should use CHAT mode for chat apps', () => { - const appDetail = createMockAppDetail(AppModeEnum.CHAT) + const appDetail = createMockAppDetail('chat') render() const link = screen.getAllByRole('link')[0] @@ -83,7 +82,7 @@ describe('EmptyElement', () => { }) it('should use COMPLETION mode for completion apps', () => { - const appDetail = createMockAppDetail(AppModeEnum.COMPLETION) + const appDetail = createMockAppDetail('completion') render() const link = screen.getAllByRole('link')[0] @@ -91,7 +90,7 @@ describe('EmptyElement', () => { }) it('should use WORKFLOW mode for workflow apps', () => { - const appDetail = createMockAppDetail(AppModeEnum.WORKFLOW) + const appDetail = createMockAppDetail('workflow') render() const link = screen.getAllByRole('link')[0] @@ -99,7 +98,7 @@ describe('EmptyElement', () => { }) it('should use CHAT mode for advanced-chat apps', () => { - const appDetail = createMockAppDetail(AppModeEnum.ADVANCED_CHAT) + const appDetail = createMockAppDetail('advanced-chat') render() const link = screen.getAllByRole('link')[0] @@ -107,7 +106,7 @@ describe('EmptyElement', () => { }) it('should use CHAT mode for agent-chat apps', () => { - const appDetail = createMockAppDetail(AppModeEnum.AGENT_CHAT) + const appDetail = createMockAppDetail('agent-chat') render() const link = screen.getAllByRole('link')[0] @@ -128,7 +127,7 @@ describe('EmptyElement', () => { describe('Links', () => { it('should render share link with correct attributes', () => { - const appDetail = createMockAppDetail(AppModeEnum.CHAT) + const appDetail = createMockAppDetail('chat') render() const links = screen.getAllByRole('link') @@ -139,7 +138,7 @@ describe('EmptyElement', () => { }) it('should render test link with redirection path', () => { - const appDetail = createMockAppDetail(AppModeEnum.CHAT) + const appDetail = createMockAppDetail('chat') render() const links = screen.getAllByRole('link') diff --git a/web/app/components/app/log/__tests__/index.spec.tsx b/web/app/components/app/log/__tests__/index.spec.tsx index 99f9894307c..035b692d01f 100644 --- a/web/app/components/app/log/__tests__/index.spec.tsx +++ b/web/app/components/app/log/__tests__/index.spec.tsx @@ -3,7 +3,6 @@ import { fireEvent, render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' import dayjs from 'dayjs' import { APP_PAGE_LIMIT } from '@/config' -import { AppModeEnum } from '@/types/app' import Logs from '../index' vi.mock('@/context/i18n', () => ({ @@ -94,7 +93,7 @@ describe('Logs', () => { appDetail={ { id: 'app-1', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, } as any } />, @@ -125,7 +124,7 @@ describe('Logs', () => { appDetail={ { id: 'app-2', - mode: AppModeEnum.COMPLETION, + mode: 'completion' as const, } as any } />, @@ -150,7 +149,7 @@ describe('Logs', () => { appDetail={ { id: 'app-3', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, } as any } />, @@ -174,7 +173,7 @@ describe('Logs', () => { appDetail={ { id: 'app-sandbox-last-30-days', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, } as any } />, @@ -200,7 +199,7 @@ describe('Logs', () => { const user = userEvent.setup() const appDetail = { id: 'app-period-transition', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, } as any mockUseChatConversations.mockReturnValue({ data: { total: 0 }, diff --git a/web/app/components/app/log/__tests__/list.spec.tsx b/web/app/components/app/log/__tests__/list.spec.tsx index 58fa91eb8ef..25d55db8ed0 100644 --- a/web/app/components/app/log/__tests__/list.spec.tsx +++ b/web/app/components/app/log/__tests__/list.spec.tsx @@ -8,7 +8,6 @@ import { QueryClientTestProvider } from '@/test/console/query-provider' import { render } from '@/test/console/render' import { createAppDetailFixture } from '@/test/fixtures/app' import { renderWithNuqs } from '@/test/nuqs-testing' -import { AppModeEnum } from '@/types/app' import ConversationList from '../list' const mockFetchChatMessages = vi.fn() @@ -304,7 +303,7 @@ const createChatMessage = (id: string, overrides: Record = {}) }) const renderConversationList = ({ - appDetail = { id: 'app-1', mode: AppModeEnum.CHAT } as any, + appDetail = { id: 'app-1', mode: 'chat' as const } as any, logs = createLogs() as any, searchParams = '?page=2', }: { @@ -518,36 +517,39 @@ describe('ConversationList', () => { ) it.each([ - ['chatbot', AppModeEnum.CHAT, 'false'], - ['agent', AppModeEnum.AGENT_CHAT, 'false'], - ['chatflow', AppModeEnum.ADVANCED_CHAT, 'true'], - ])('should expose run details only for %s conversation answers', async (_, mode, expected) => { - mockChatConversationDetail = { - id: 'conversation-1', - created_at: 1710000000, - model_config: { - model: 'gpt-4o', - configs: { - introduction: 'Hello there', + ['chatbot', 'chat', 'false'], + ['agent', 'agent-chat', 'false'], + ['chatflow', 'advanced-chat', 'true'], + ] as const)( + 'should expose run details only for %s conversation answers', + async (_, mode, expected) => { + mockChatConversationDetail = { + id: 'conversation-1', + created_at: 1710000000, + model_config: { + model: 'gpt-4o', + configs: { + introduction: 'Hello there', + }, + user_input_form: [], }, - user_input_form: [], - }, - message: { - inputs: {}, - }, - } - mockFetchChatMessages.mockResolvedValue({ - data: [createChatMessage('message-1')], - has_more: false, - }) + message: { + inputs: {}, + }, + } + mockFetchChatMessages.mockResolvedValue({ + data: [createChatMessage('message-1')], + has_more: false, + }) - renderConversationList({ - appDetail: { id: 'app-1', mode } as any, - searchParams: '?conversation_id=conversation-1', - }) + renderConversationList({ + appDetail: { id: 'app-1', mode } as any, + searchParams: '?conversation_id=conversation-1', + }) - expect(await screen.findByTestId('chat-panel')).toHaveAttribute('data-log-enabled', expected) - }) + expect(await screen.findByTestId('chat-panel')).toHaveAttribute('data-log-enabled', expected) + }, + ) it('should render completion details and refetch after feedback updates', async () => { mockCompletionConversationDetail = { @@ -575,7 +577,7 @@ describe('ConversationList', () => { } renderConversationList({ - appDetail: { id: 'app-1', mode: AppModeEnum.COMPLETION } as any, + appDetail: { id: 'app-1', mode: 'completion' as const } as any, logs: createCompletionLogs() as any, searchParams: '?page=2&conversation_id=conversation-1', }) @@ -605,7 +607,7 @@ describe('ConversationList', () => { it('should render chatflow status cells and feedback counters for advanced chat logs', () => { renderConversationList({ - appDetail: { id: 'app-1', mode: AppModeEnum.ADVANCED_CHAT } as any, + appDetail: { id: 'app-1', mode: 'advanced-chat' as const } as any, logs: { data: [ { @@ -744,7 +746,7 @@ describe('ConversationList', () => { }) }) - it.each(['workflow', 'agent', 'prompt'])( + it.each(['workflow', 'agent', 'prompt'] as const)( 'should open and close the selected %s log within the conversation', async (kind) => { mockChatConversationDetail = { @@ -753,7 +755,7 @@ describe('ConversationList', () => { message: { inputs: {} }, } renderConversationList({ - appDetail: { id: 'app-1', mode: AppModeEnum.ADVANCED_CHAT }, + appDetail: { id: 'app-1', mode: 'advanced-chat' as const }, searchParams: '?conversation_id=conversation-1', }) const user = userEvent.setup() @@ -776,7 +778,7 @@ describe('ConversationList', () => { message: { inputs: {} }, } renderConversationList({ - appDetail: { id: 'app-1', mode: AppModeEnum.ADVANCED_CHAT }, + appDetail: { id: 'app-1', mode: 'advanced-chat' as const }, searchParams: '?conversation_id=conversation-1', }) const user = userEvent.setup() @@ -791,7 +793,7 @@ describe('ConversationList', () => { it('should clear a selected log when URL navigation changes or closes the conversation', async () => { const user = userEvent.setup() const queryClient = createAccountProfileQueryClient({ timezone: 'Asia/Shanghai' }) - const appDetail = createAppDetailFixture({ mode: AppModeEnum.ADVANCED_CHAT }) + const appDetail = createAppDetailFixture({ mode: 'advanced-chat' as const }) const view = (searchParams: string) => ( diff --git a/web/app/components/app/overview/settings/__tests__/index.spec.tsx b/web/app/components/app/overview/settings/__tests__/index.spec.tsx index 7ed09d22b48..3f8e464e248 100644 --- a/web/app/components/app/overview/settings/__tests__/index.spec.tsx +++ b/web/app/components/app/overview/settings/__tests__/index.spec.tsx @@ -10,7 +10,6 @@ import { renderWithConsoleQuery as renderWithoutPricing, } from '@/test/console/query-data' import { createAppSiteFixture } from '@/test/fixtures/app' -import { AppModeEnum } from '@/types/app' import { SettingsDialog } from '../index' const onPricingUrlUpdate = vi.hoisted(() => vi.fn()) @@ -68,7 +67,7 @@ const mockAppInfo = { show_workflow_steps: true, use_icon_as_answer_icon: true, }, - mode: AppModeEnum.ADVANCED_CHAT, + mode: 'advanced-chat' as const, } satisfies SettingsAppInfo const triggerName = 'navigation.settings.settings' diff --git a/web/app/components/app/switch-app-modal/__tests__/index.spec.tsx b/web/app/components/app/switch-app-modal/__tests__/index.spec.tsx index ef1220263bb..e0642948ec4 100644 --- a/web/app/components/app/switch-app-modal/__tests__/index.spec.tsx +++ b/web/app/components/app/switch-app-modal/__tests__/index.spec.tsx @@ -5,7 +5,6 @@ import userEvent from '@testing-library/user-event' import * as React from 'react' import { renderWithConsoleQuery } from '@/test/console/query-data' import { mockEmojiData } from '@/test/emoji-picker' -import { AppModeEnum } from '@/types/app' import SwitchAppModal from '../index' const mockPush = vi.fn() @@ -84,7 +83,7 @@ const createMockApp = (overrides: Partial = {}): AppPartial => ({ icon_background: '#FFEAD5', icon_url: null, use_icon_as_answer_icon: false, - mode: AppModeEnum.COMPLETION, + mode: 'completion' as const, created_at: Date.now(), updated_at: Date.now(), tags: [], diff --git a/web/app/components/app/type-selector/__tests__/index.spec.tsx b/web/app/components/app/type-selector/__tests__/index.spec.tsx index 54cfe4b2de3..9681c0115e1 100644 --- a/web/app/components/app/type-selector/__tests__/index.spec.tsx +++ b/web/app/components/app/type-selector/__tests__/index.spec.tsx @@ -1,6 +1,6 @@ +import type { AppMode } from '@dify/contracts/api/console/apps/types.gen' import { fireEvent, render, screen, waitFor, within } from '@testing-library/react' import * as React from 'react' -import { AppModeEnum } from '@/types/app' import AppTypeSelector, { AppTypeLabel } from '../index' describe('AppTypeSelector', () => { @@ -21,16 +21,14 @@ describe('AppTypeSelector', () => { // Covers prop-driven trigger variants (empty, single, multiple). describe('Props', () => { it('should render selected type label and clear button when a single type is selected', () => { - render() + render() expect(screen.getByText('app.typeSelector.chatbot')).toBeInTheDocument() expect(screen.getByRole('button', { name: 'common.operation.clear' })).toBeInTheDocument() }) it('should render icon-only trigger when multiple types are selected', () => { - render( - , - ) + render() expect(screen.queryByText('app.typeSelector.all')).not.toBeInTheDocument() expect(screen.queryByText('app.typeSelector.chatbot')).not.toBeInTheDocument() @@ -65,12 +63,12 @@ describe('AppTypeSelector', () => { within(screen.getByRole('list')).getByRole('button', { name: 'app.typeSelector.workflow' }), ) - expect(onChange).toHaveBeenCalledWith([AppModeEnum.WORKFLOW]) + expect(onChange).toHaveBeenCalledWith(['workflow'] as const) }) it('should call onChange with removed type when selecting an already-selected item', () => { const onChange = vi.fn() - render() + render() fireEvent.click(screen.getByRole('button', { name: 'app.typeSelector.workflow' })) fireEvent.click( @@ -82,19 +80,19 @@ describe('AppTypeSelector', () => { it('should call onChange with appended type when selecting an additional item', () => { const onChange = vi.fn() - render() + render() fireEvent.click(screen.getByRole('button', { name: 'app.typeSelector.chatbot' })) fireEvent.click( within(screen.getByRole('list')).getByRole('button', { name: 'app.typeSelector.agent' }), ) - expect(onChange).toHaveBeenCalledWith([AppModeEnum.CHAT, AppModeEnum.AGENT_CHAT]) + expect(onChange).toHaveBeenCalledWith(['chat', 'agent-chat'] as const) }) it('should clear selection without opening the dropdown when clicking clear button', () => { const onChange = vi.fn() - render() + render() fireEvent.click(screen.getByRole('button', { name: 'common.operation.clear' })) @@ -111,11 +109,11 @@ describe('AppTypeLabel', () => { // Covers label mapping for each supported app type. it.each([ - [AppModeEnum.CHAT, 'app.typeSelector.chatbot'], - [AppModeEnum.AGENT_CHAT, 'app.typeSelector.agent'], - [AppModeEnum.COMPLETION, 'app.typeSelector.completion'], - [AppModeEnum.ADVANCED_CHAT, 'app.typeSelector.advanced'], - [AppModeEnum.WORKFLOW, 'app.typeSelector.workflow'], + ['chat', 'app.typeSelector.chatbot'], + ['agent-chat', 'app.typeSelector.agent'], + ['completion', 'app.typeSelector.completion'], + ['advanced-chat', 'app.typeSelector.advanced'], + ['workflow', 'app.typeSelector.workflow'], ] as const)('should render label %s for type %s', (_type, expectedLabel) => { render() expect(screen.getByText(expectedLabel)).toBeInTheDocument() @@ -123,7 +121,7 @@ describe('AppTypeLabel', () => { // Covers fallback behavior for unexpected app mode values. it('should render empty label for unknown type', () => { - const { container } = render() + const { container } = render() expect(container.textContent).toBe('') }) }) diff --git a/web/app/components/app/workflow-log/__tests__/index.spec.tsx b/web/app/components/app/workflow-log/__tests__/index.spec.tsx index fdebf47c65b..6be089cf741 100644 --- a/web/app/components/app/workflow-log/__tests__/index.spec.tsx +++ b/web/app/components/app/workflow-log/__tests__/index.spec.tsx @@ -1,4 +1,4 @@ -import type { AppDetailWithSite } from '@dify/contracts/api/console/apps/types.gen' +import type { AppDetailWithSite, AppMode } from '@dify/contracts/api/console/apps/types.gen' import type { UseQueryResult } from '@tanstack/react-query' /** * Logs Container Component Tests @@ -19,7 +19,6 @@ import type { MockedFunction } from 'vite-plus/test' import type { CloudSandboxPlanState } from '../../log/cloud-sandbox-retention' import type { ILogsProps } from '../index' import type { WorkflowAppLogDetail, WorkflowLogsResponse, WorkflowRunDetail } from '@/models/log' -import type { AppModeEnum } from '@/types/app' import { screen, waitFor, within } from '@testing-library/react' import userEvent from '@testing-library/user-event' import dayjs from 'dayjs' @@ -612,7 +611,7 @@ describe('Logs Container', () => { }), ) - const chatApp = createMockApp({ mode: 'advanced-chat' as AppModeEnum }) + const chatApp = createMockApp({ mode: 'advanced-chat' as AppMode }) // Act renderWithQueryClient() diff --git a/web/app/components/app/workflow-log/__tests__/list.spec.tsx b/web/app/components/app/workflow-log/__tests__/list.spec.tsx index 3a672ae8e9e..5f872d76da4 100644 --- a/web/app/components/app/workflow-log/__tests__/list.spec.tsx +++ b/web/app/components/app/workflow-log/__tests__/list.spec.tsx @@ -1,4 +1,4 @@ -import type { AppDetailWithSite } from '@dify/contracts/api/console/apps/types.gen' +import type { AppDetailWithSite, AppMode } from '@dify/contracts/api/console/apps/types.gen' import type { WorkflowAppLogDetail, WorkflowLogsResponse, WorkflowRunDetail } from '@/models/log' /** * WorkflowAppLogList Component Tests @@ -10,7 +10,6 @@ import type { WorkflowAppLogDetail, WorkflowLogsResponse, WorkflowRunDetail } fr * - Drawer with run details * - Loading states */ -import type { AppModeEnum } from '@/types/app' import { render, screen, waitFor, within } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { APP_PAGE_LIMIT } from '@/config' @@ -203,7 +202,7 @@ describe('WorkflowAppLogList', () => { it('should render trigger column for workflow apps', () => { const logs = createMockLogsResponse([createMockWorkflowLog()]) - const workflowApp = createMockApp({ mode: 'workflow' as AppModeEnum }) + const workflowApp = createMockApp({ mode: 'workflow' as AppMode }) render( , @@ -214,7 +213,7 @@ describe('WorkflowAppLogList', () => { it('should not render trigger column for non-workflow apps', () => { const logs = createMockLogsResponse([createMockWorkflowLog()]) - const chatApp = createMockApp({ mode: 'advanced-chat' as AppModeEnum }) + const chatApp = createMockApp({ mode: 'advanced-chat' as AppMode }) render() @@ -855,7 +854,7 @@ describe('WorkflowAppLogList', () => { }), }), ]) - const chatApp = createMockApp({ mode: 'advanced-chat' as AppModeEnum }) + const chatApp = createMockApp({ mode: 'advanced-chat' as AppMode }) render() diff --git a/web/app/components/apps/__tests__/app-card.spec.tsx b/web/app/components/apps/__tests__/app-card.spec.tsx index 40e9a3360d9..408810505cc 100644 --- a/web/app/components/apps/__tests__/app-card.spec.tsx +++ b/web/app/components/apps/__tests__/app-card.spec.tsx @@ -6,7 +6,6 @@ import * as React from 'react' import { STEP_BY_STEP_TOUR_TARGETS } from '@/app/components/step-by-step-tour/target-registry' import { AccessMode } from '@/models/access-control' import { renderWithConsoleQuery } from '@/test/console/query-data' -import { AppModeEnum } from '@/types/app' import { AppACLPermission } from '@/utils/permission' import { AppCard } from '../app-card' import { StarredAppCard } from '../starred-app-card' @@ -414,7 +413,7 @@ const createMockApp = (overrides: Partial = {}): AppPartial => ({ id: 'test-app-id', name: 'Test App', description: 'Test app description', - mode: AppModeEnum.CHAT, + mode: 'chat', icon: '🤖', icon_type: 'emoji' as const, icon_background: '#FFEAD5', @@ -608,7 +607,7 @@ describe('AppCard', () => { describe('Props', () => { it('should handle different app modes', () => { - const workflowApp = { ...mockApp, mode: AppModeEnum.WORKFLOW } + const workflowApp = { ...mockApp, mode: 'workflow' as const } render() expect(screen.getByRole('link', { name: 'Test App' })).toBeInTheDocument() }) @@ -874,7 +873,7 @@ describe('AppCard', () => { }) it('should show switch option for chat mode apps', async () => { - const chatApp = { ...mockApp, mode: AppModeEnum.CHAT } + const chatApp = { ...mockApp, mode: 'chat' as const } render() fireEvent.click(getOperationsTrigger()) @@ -889,7 +888,7 @@ describe('AppCard', () => { const editableChatApp = createMockApp({ created_by: 'another-user', maintainer: 'another-user', - mode: AppModeEnum.CHAT, + mode: 'chat', permission_keys: [AppACLPermission.Edit], }) render() @@ -903,7 +902,7 @@ describe('AppCard', () => { }) it('should show switch option for completion mode apps', async () => { - const completionApp = { ...mockApp, mode: AppModeEnum.COMPLETION } + const completionApp = { ...mockApp, mode: 'completion' as const } render() fireEvent.click(getOperationsTrigger()) @@ -914,7 +913,7 @@ describe('AppCard', () => { }) it('should not show switch option for workflow mode apps', async () => { - const workflowApp = { ...mockApp, mode: AppModeEnum.WORKFLOW } + const workflowApp = { ...mockApp, mode: 'workflow' as const } render() fireEvent.click(getOperationsTrigger()) @@ -1236,7 +1235,7 @@ describe('AppCard', () => { describe('Switch Modal', () => { it('should open switch modal when switch button is clicked', async () => { - const chatApp = { ...mockApp, mode: AppModeEnum.CHAT } + const chatApp = { ...mockApp, mode: 'chat' as const } render() fireEvent.click(getOperationsTrigger()) @@ -1250,7 +1249,7 @@ describe('AppCard', () => { }) it('should close switch modal when close button is clicked', async () => { - const chatApp = { ...mockApp, mode: AppModeEnum.CHAT } + const chatApp = { ...mockApp, mode: 'chat' as const } render() fireEvent.click(getOperationsTrigger()) @@ -1270,7 +1269,7 @@ describe('AppCard', () => { }) it('should open switch modal for completion mode apps', async () => { - const completionApp = { ...mockApp, mode: AppModeEnum.COMPLETION } + const completionApp = { ...mockApp, mode: 'completion' as const } render() fireEvent.click(getOperationsTrigger()) @@ -1313,7 +1312,7 @@ describe('AppCard', () => { describe('Workflow Export with Environment Variables', () => { it('should use the workflow export command for workflow apps', async () => { - const workflowApp = { ...mockApp, mode: AppModeEnum.WORKFLOW } + const workflowApp = { ...mockApp, mode: 'workflow' as const } render() fireEvent.click(getOperationsTrigger()) @@ -1336,7 +1335,7 @@ describe('AppCard', () => { secretEnvList: [{ value_type: 'secret', name: 'API_KEY', value: 'secret' }], }) - const workflowApp = { ...mockApp, mode: AppModeEnum.WORKFLOW } + const workflowApp = { ...mockApp, mode: 'workflow' as const } render() fireEvent.click(getOperationsTrigger()) @@ -1350,7 +1349,7 @@ describe('AppCard', () => { }) it('should not open a modal when the workflow command downloads directly', async () => { - const workflowApp = { ...mockApp, mode: AppModeEnum.WORKFLOW } + const workflowApp = { ...mockApp, mode: 'workflow' as const } render() fireEvent.click(getOperationsTrigger()) @@ -1369,7 +1368,7 @@ describe('AppCard', () => { }) it('should use the workflow export command for advanced chat apps', async () => { - const advancedChatApp = { ...mockApp, mode: AppModeEnum.ADVANCED_CHAT } + const advancedChatApp = { ...mockApp, mode: 'advanced-chat' as const } render() fireEvent.click(getOperationsTrigger()) @@ -1391,7 +1390,7 @@ describe('AppCard', () => { secretEnvList: [{ value_type: 'secret', name: 'API_KEY', value: 'secret' }], }) - const workflowApp = { ...mockApp, mode: AppModeEnum.WORKFLOW } + const workflowApp = { ...mockApp, mode: 'workflow' as const } render() fireEvent.click(getOperationsTrigger()) @@ -1543,7 +1542,7 @@ describe('AppCard', () => { describe('Open in Explore - No App Found', () => { it('should tell workflow users to publish before opening in explore', async () => { const workflowApp = createMockApp({ - mode: AppModeEnum.WORKFLOW, + mode: 'workflow' as const, workflow: undefined, }) render() @@ -1698,7 +1697,7 @@ describe('AppCard', () => { mockUserCanAccessApp.result = false mockUserCanAccessApp.isLoading = true const workflowApp = createMockApp({ - mode: AppModeEnum.WORKFLOW, + mode: 'workflow' as const, workflow: undefined, }) diff --git a/web/app/components/apps/__tests__/list.spec.tsx b/web/app/components/apps/__tests__/list.spec.tsx index 1b81fd0f6fb..aeaeec51f07 100644 --- a/web/app/components/apps/__tests__/list.spec.tsx +++ b/web/app/components/apps/__tests__/list.spec.tsx @@ -14,7 +14,6 @@ import { import { createConsoleQueryWrapper } from '@/test/console/query-data' import { seedRegisteredConsoleStateFixture } from '@/test/console/state-fixture' import { renderWithNuqs } from '@/test/nuqs-testing' -import { AppModeEnum } from '@/types/app' import { List } from '../list' vi.mock('react-i18next', async () => { @@ -185,7 +184,7 @@ const defaultAppData = { id: 'app-1', name: 'Test App 1', description: 'Description 1', - mode: AppModeEnum.CHAT, + mode: 'chat', icon: '🤖', icon_type: 'emoji', icon_background: '#FFEAD5', @@ -198,7 +197,7 @@ const defaultAppData = { id: 'app-2', name: 'Test App 2', description: 'Description 2', - mode: AppModeEnum.WORKFLOW, + mode: 'workflow' as const, icon: '⚙️', icon_type: 'emoji', icon_background: '#E4FBCC', @@ -657,7 +656,7 @@ describe('List', () => { id: 'starred-app-1', name: 'Starred App', description: 'Starred description', - mode: AppModeEnum.CHAT, + mode: 'chat', icon: '⭐', icon_type: 'emoji', icon_background: '#FFEAD5', @@ -712,7 +711,7 @@ describe('List', () => { id: 'starred-app-1', name: 'Starred App', description: 'Starred description', - mode: AppModeEnum.CHAT, + mode: 'chat', icon: '⭐', icon_type: 'emoji', icon_background: '#FFEAD5', @@ -766,7 +765,7 @@ describe('List', () => { id: 'starred-app-1', name: 'Starred App', description: 'Starred description', - mode: AppModeEnum.CHAT, + mode: 'chat', icon: '⭐', icon_type: 'emoji', icon_background: '#FFEAD5', @@ -1054,9 +1053,7 @@ describe('List', () => { await user.click(await screen.findByRole('option', { name: 'app.types.workflow' })) await waitFor(() => expect(onUrlUpdate).toHaveBeenCalled()) - expect(onUrlUpdate.mock.calls.at(-1)?.[0].searchParams.get('category')).toBe( - AppModeEnum.WORKFLOW, - ) + expect(onUrlUpdate.mock.calls.at(-1)?.[0].searchParams.get('category')).toBe('workflow') }) it('should update category when all option is selected', async () => { @@ -1123,7 +1120,7 @@ describe('List', () => { sort_by: 'last_modified', tag_ids: ['tag-1'], creator_ids: ['creator-1'], - mode: AppModeEnum.WORKFLOW, + mode: 'workflow' as const, }, }) expect(options.getNextPageParam({ has_more: true, page: 2 })).toBe(3) @@ -1149,7 +1146,7 @@ describe('List', () => { sort_by: 'last_modified', tag_ids: ['tag-1'], creator_ids: ['creator-1'], - mode: AppModeEnum.WORKFLOW, + mode: 'workflow' as const, }, }) }) @@ -1233,11 +1230,11 @@ describe('List', () => { describe('App Type Select Options', () => { it('should update category for each app type option click', async () => { const appTypeTexts = [ - { mode: AppModeEnum.WORKFLOW, text: 'app.types.workflow' }, - { mode: AppModeEnum.ADVANCED_CHAT, text: 'app.types.advanced' }, - { mode: AppModeEnum.CHAT, text: 'app.types.chatbot' }, - { mode: AppModeEnum.AGENT_CHAT, text: 'app.types.agent' }, - { mode: AppModeEnum.COMPLETION, text: 'app.newApp.completeApp' }, + { mode: 'workflow' as const, text: 'app.types.workflow' }, + { mode: 'advanced-chat' as const, text: 'app.types.advanced' }, + { mode: 'chat' as const, text: 'app.types.chatbot' }, + { mode: 'agent-chat' as const, text: 'app.types.agent' }, + { mode: 'completion' as const, text: 'app.newApp.completeApp' }, ] for (const { mode, text } of appTypeTexts) { diff --git a/web/app/components/base/features/new-feature-panel/moderation/__tests__/moderation-setting-modal.spec.tsx b/web/app/components/base/features/new-feature-panel/moderation/__tests__/moderation-setting-modal.spec.tsx index bec6a0864a9..1d5d5101bed 100644 --- a/web/app/components/base/features/new-feature-panel/moderation/__tests__/moderation-setting-modal.spec.tsx +++ b/web/app/components/base/features/new-feature-panel/moderation/__tests__/moderation-setting-modal.spec.tsx @@ -11,7 +11,7 @@ import { createFeaturesStore } from '@/app/components/base/features/store' import { renderWithConsoleQuery as render } from '@/test/console/query-data' import { createAppModelConfigFixture } from '@/test/fixtures/app' import { withSelectorKey } from '@/test/i18n-mock' -import { AppModeEnum, ModelModeType } from '@/types/app' +import { ModelModeType } from '@/types/app' import ModerationSettingModal from '../moderation-setting-modal' const mockNotify = vi.fn() @@ -1081,7 +1081,7 @@ describe('ModerationSettingModal', () => { backendModelConfig, collectionList: [], datasetConfigs: buildConfigurationDatasetConfigs({ backendModelConfig, nextDataSets: [] }), - mode: AppModeEnum.CHAT, + mode: 'chat' as const, nextDataSets: [], }) const publish = createPublishHandler({ @@ -1096,7 +1096,7 @@ describe('ModerationSettingModal', () => { hasSetBlockStatus: { history: true, query: true }, isAdvancedMode: false, isFunctionCall: false, - mode: AppModeEnum.CHAT, + mode: 'chat' as const, modelConfig: config.modelConfig, promptEmpty: false, promptMode: 'simple', diff --git a/web/app/components/base/linked-apps-panel/__tests__/index.spec.tsx b/web/app/components/base/linked-apps-panel/__tests__/index.spec.tsx index ea92cc5ea66..518a468a010 100644 --- a/web/app/components/base/linked-apps-panel/__tests__/index.spec.tsx +++ b/web/app/components/base/linked-apps-panel/__tests__/index.spec.tsx @@ -1,7 +1,6 @@ import { render, screen } from '@testing-library/react' import * as React from 'react' import { vi } from 'vite-plus/test' -import { AppModeEnum } from '@/types/app' import LinkedAppsPanel from '../index' vi.mock('@/next/link', () => ({ @@ -25,7 +24,7 @@ describe('LinkedAppsPanel Component', () => { { id: 'app-1', name: 'Chatbot App', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, icon_type: 'emoji' as const, icon: '🤖', icon_background: '#FFEAD5', @@ -34,7 +33,7 @@ describe('LinkedAppsPanel Component', () => { { id: 'app-2', name: 'Workflow App', - mode: AppModeEnum.WORKFLOW, + mode: 'workflow' as const, icon_type: 'image' as const, icon: 'file-id', icon_background: '#E4FBCC', @@ -43,7 +42,7 @@ describe('LinkedAppsPanel Component', () => { { id: 'app-3', name: '', - mode: AppModeEnum.AGENT_CHAT, + mode: 'agent-chat' as const, icon_type: 'emoji' as const, icon: '🕵️', icon_background: '#D3F8DF', diff --git a/web/app/components/explore/create-app-modal/__tests__/index.spec.tsx b/web/app/components/explore/create-app-modal/__tests__/index.spec.tsx index 3d237a7df22..a6e521f0197 100644 --- a/web/app/components/explore/create-app-modal/__tests__/index.spec.tsx +++ b/web/app/components/explore/create-app-modal/__tests__/index.spec.tsx @@ -6,7 +6,6 @@ import userEvent from '@testing-library/user-event' import * as React from 'react' import { renderWithConsoleQuery } from '@/test/console/query-data' import { mockEmojiData } from '@/test/emoji-picker' -import { AppModeEnum } from '@/types/app' import CreateAppModal from '../index' let deploymentEdition: 'CLOUD' | 'COMMUNITY' = 'COMMUNITY' @@ -29,7 +28,7 @@ const setup = async (overrides: Partial = {}) => { appIcon: '🤖', appIconBackground: '#FFEAD5', appIconUrl: null, - appMode: AppModeEnum.CHAT, + appMode: 'chat' as const, appUseIconAsAnswerIcon: false, max_active_requests: null, onConfirm, @@ -124,7 +123,7 @@ describe('CreateAppModal', () => { }) it('should render edit-only fields when editing a chat app', async () => { - await setup({ isEditModal: true, appMode: AppModeEnum.CHAT, max_active_requests: 5 }) + await setup({ isEditModal: true, appMode: 'chat' as const, max_active_requests: 5 }) expect(screen.getByText('app.editAppTitle'))!.toBeInTheDocument() expect(screen.getByRole('button', { name: /common\.operation\.save/ }))!.toBeInTheDocument() @@ -132,7 +131,7 @@ describe('CreateAppModal', () => { expect((screen.getByRole('spinbutton') as HTMLInputElement).value).toBe('5') }) - it.each([AppModeEnum.ADVANCED_CHAT, AppModeEnum.AGENT_CHAT])( + it.each(['advanced-chat', 'agent-chat'] as const)( 'should render answer icon switch when editing %s app', async (mode) => { await setup({ isEditModal: true, appMode: mode }) @@ -142,7 +141,7 @@ describe('CreateAppModal', () => { ) it('should not render answer icon switch when editing a non-chat app', async () => { - await setup({ isEditModal: true, appMode: AppModeEnum.COMPLETION }) + await setup({ isEditModal: true, appMode: 'completion' as const }) expect(screen.queryByRole('switch')).not.toBeInTheDocument() }) @@ -587,7 +586,7 @@ describe('CreateAppModal', () => { it('should include max_active_requests and updated answer icon when saving', async () => { const { onConfirm } = await setup({ isEditModal: true, - appMode: AppModeEnum.CHAT, + appMode: 'chat' as const, appUseIconAsAnswerIcon: false, max_active_requests: 3, }) diff --git a/web/app/components/explore/installed-app/__tests__/index.spec.tsx b/web/app/components/explore/installed-app/__tests__/index.spec.tsx index 0e8a46e27f9..f65bab1f716 100644 --- a/web/app/components/explore/installed-app/__tests__/index.spec.tsx +++ b/web/app/components/explore/installed-app/__tests__/index.spec.tsx @@ -170,7 +170,7 @@ describe('InstalledApp', () => { }) }) - it.each(['chat', 'advanced-chat', 'agent-chat'])( + it.each(['chat', 'advanced-chat', 'agent-chat'] as const)( 'renders chat for the supported %s mode', async (mode) => { mocks.getInstalledApp.mockResolvedValue(createInstalledApp(mode)) diff --git a/web/app/components/explore/learn-dify/__tests__/item.spec.tsx b/web/app/components/explore/learn-dify/__tests__/item.spec.tsx index f785d5cdcdf..c8f76444baa 100644 --- a/web/app/components/explore/learn-dify/__tests__/item.spec.tsx +++ b/web/app/components/explore/learn-dify/__tests__/item.spec.tsx @@ -5,7 +5,6 @@ import { screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { trackEvent } from '@/app/components/base/amplitude' import { renderWithConsoleQuery } from '@/test/console/query-data' -import { AppModeEnum } from '@/types/app' import LearnDifyItem from '../item' let deploymentEdition: DeploymentEdition = 'CLOUD' @@ -19,7 +18,7 @@ vi.mock('@/app/components/base/amplitude', () => ({ const createApp = (overrides: Partial = {}): RecommendedAppResponse => ({ app: { id: 'app-basic-id', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, icon_type: 'emoji', icon: '😀', icon_background: '#fff', diff --git a/web/app/components/explore/try-app/app/__tests__/index.spec.tsx b/web/app/components/explore/try-app/app/__tests__/index.spec.tsx index ce6d29331a6..d0e14e44b44 100644 --- a/web/app/components/explore/try-app/app/__tests__/index.spec.tsx +++ b/web/app/components/explore/try-app/app/__tests__/index.spec.tsx @@ -20,7 +20,7 @@ const createApp = (mode: string): TrialAppDetailResponse => }) as TrialAppDetailResponse describe('TryApp', () => { - it.each(['chat', 'advanced-chat', 'agent-chat'])( + it.each(['chat', 'advanced-chat', 'agent-chat'] as const)( 'uses the chat experience for %s apps', (mode) => { render() @@ -32,7 +32,7 @@ describe('TryApp', () => { it.each([ ['completion', 'Completion preview'], ['workflow', 'Workflow preview'], - ])('uses the text generation experience for %s apps', (mode, name) => { + ] as const)('uses the text generation experience for %s apps', (mode, name) => { render() expect(screen.getByRole('region', { name })).toBeInTheDocument() diff --git a/web/app/components/explore/try-app/preview/__tests__/index.spec.tsx b/web/app/components/explore/try-app/preview/__tests__/index.spec.tsx index bcb0ddfbf5f..9059f569efd 100644 --- a/web/app/components/explore/try-app/preview/__tests__/index.spec.tsx +++ b/web/app/components/explore/try-app/preview/__tests__/index.spec.tsx @@ -19,7 +19,7 @@ describe('Preview', () => { expect(screen.getByRole('region', { name: 'Basic app preview' })).toBeInTheDocument() }) - it.each(['workflow', 'advanced-chat'])('uses the flow preview for %s apps', (mode) => { + it.each(['workflow', 'advanced-chat'] as const)('uses the flow preview for %s apps', (mode) => { render() expect(screen.getByRole('region', { name: 'Flow app preview' })).toBeInTheDocument() diff --git a/web/app/components/integrations/__tests__/tool-provider-list.spec.tsx b/web/app/components/integrations/__tests__/tool-provider-list.spec.tsx index 2f1830cd2d2..7af17b95e3d 100644 --- a/web/app/components/integrations/__tests__/tool-provider-list.spec.tsx +++ b/web/app/components/integrations/__tests__/tool-provider-list.spec.tsx @@ -357,7 +357,7 @@ describe('getToolType', () => { ['workflow', ToolType.Workflow], ['mcp', ToolType.MCP], ['unknown', ToolType.BuiltIn], - ])('returns correct ToolType for "%s"', (input, expected) => { + ] as const)('returns correct ToolType for "%s"', (input, expected) => { expect(getToolType(input)).toBe(expected) }) }) diff --git a/web/app/components/main-nav/__tests__/index.spec.tsx b/web/app/components/main-nav/__tests__/index.spec.tsx index 1797ee011d1..3a54d1a34dc 100644 --- a/web/app/components/main-nav/__tests__/index.spec.tsx +++ b/web/app/components/main-nav/__tests__/index.spec.tsx @@ -37,7 +37,6 @@ import { renderWithConsoleQuery as renderWithoutPricing, } from '@/test/console/query-data' import { seedRegisteredConsoleStateFixture } from '@/test/console/state-fixture' -import { AppModeEnum } from '@/types/app' import { MainNav } from '../index' const onPricingUrlUpdate = vi.hoisted(() => vi.fn()) @@ -471,7 +470,7 @@ const createInstalledApp = ( is_pinned: overrides.is_pinned ?? false, app: { id: overrides.app?.id ?? 'app-1', - mode: overrides.app?.mode ?? AppModeEnum.CHAT, + mode: overrides.app?.mode ?? 'chat', icon_type: overrides.app?.icon_type ?? 'emoji', icon: overrides.app?.icon ?? '🤖', icon_background: overrides.app?.icon_background ?? '#fff', diff --git a/web/app/components/plugins/plugin-detail-panel/app-selector/__tests__/app-picker.spec.tsx b/web/app/components/plugins/plugin-detail-panel/app-selector/__tests__/app-picker.spec.tsx index 8b79cc49196..cb8b16fc75e 100644 --- a/web/app/components/plugins/plugin-detail-panel/app-selector/__tests__/app-picker.spec.tsx +++ b/web/app/components/plugins/plugin-detail-panel/app-selector/__tests__/app-picker.spec.tsx @@ -2,7 +2,6 @@ import type { AppPartial } from '@dify/contracts/api/console/apps/types.gen' import { render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { useState } from 'react' -import { AppModeEnum } from '@/types/app' import { AppPicker } from '../app-picker' vi.mock('@/app/components/base/app-icon', () => ({ @@ -12,7 +11,7 @@ vi.mock('@/app/components/base/app-icon', () => ({ const app = { id: 'app-1', name: 'Workflow App', - mode: AppModeEnum.WORKFLOW, + mode: 'workflow' as const, icon_type: 'emoji', icon: 'W', icon_background: '#FFFFFF', diff --git a/web/app/components/plugins/plugin-detail-panel/app-selector/__tests__/index.spec.tsx b/web/app/components/plugins/plugin-detail-panel/app-selector/__tests__/index.spec.tsx index 8e239b9f955..d44cba5a1fc 100644 --- a/web/app/components/plugins/plugin-detail-panel/app-selector/__tests__/index.spec.tsx +++ b/web/app/components/plugins/plugin-detail-panel/app-selector/__tests__/index.spec.tsx @@ -6,14 +6,13 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { useState } from 'react' import { beforeEach, describe, expect, it, vi } from 'vite-plus/test' -import { AppModeEnum } from '@/types/app' import { AppSelector } from '../index' const apps = [ { id: 'app-1', name: 'Support Bot', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, icon_type: 'emoji', icon: '🤖', icon_background: '#FFEAD5', @@ -22,7 +21,7 @@ const apps = [ { id: 'app-2', name: 'Workflow App', - mode: AppModeEnum.WORKFLOW, + mode: 'workflow' as const, icon_type: 'emoji', icon: '⚙️', icon_background: '#E0EAFF', @@ -31,7 +30,7 @@ const apps = [ { id: 'app-3', name: 'Sales Bot', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, icon_type: 'emoji', icon: '💼', icon_background: '#FEE4E2', diff --git a/web/app/components/plugins/plugin-detail-panel/app-selector/hooks/__tests__/use-app-inputs-form-schema.spec.ts b/web/app/components/plugins/plugin-detail-panel/app-selector/hooks/__tests__/use-app-inputs-form-schema.spec.ts index f8862288758..b709e55deb3 100644 --- a/web/app/components/plugins/plugin-detail-panel/app-selector/hooks/__tests__/use-app-inputs-form-schema.spec.ts +++ b/web/app/components/plugins/plugin-detail-panel/app-selector/hooks/__tests__/use-app-inputs-form-schema.spec.ts @@ -1,7 +1,7 @@ import { renderHook } from '@testing-library/react' import { beforeEach, describe, expect, it, vi } from 'vite-plus/test' import { BlockEnum, InputVarType, SupportUploadFileTypes } from '@/app/components/workflow/types' -import { AppModeEnum, Resolution } from '@/types/app' +import { Resolution } from '@/types/app' import { useAppInputsFormSchema } from '../use-app-inputs-form-schema' let mockAppDetailData: Record | null = null @@ -54,7 +54,7 @@ describe('useAppInputsFormSchema', () => { it('should build basic app schemas and append image upload support', () => { mockAppDetailData = { id: 'app-1', - mode: AppModeEnum.COMPLETION, + mode: 'completion' as const, model_config: { user_input_form: [ { @@ -84,7 +84,7 @@ describe('useAppInputsFormSchema', () => { useAppInputsFormSchema({ appDetail: { id: 'app-1', - mode: AppModeEnum.COMPLETION, + mode: 'completion' as const, } as never, }), ) @@ -108,7 +108,7 @@ describe('useAppInputsFormSchema', () => { it('should build workflow schemas from start node variables', () => { mockAppDetailData = { id: 'app-2', - mode: AppModeEnum.WORKFLOW, + mode: 'workflow' as const, } mockAppWorkflowData = { graph: { @@ -134,7 +134,7 @@ describe('useAppInputsFormSchema', () => { useAppInputsFormSchema({ appDetail: { id: 'app-2', - mode: AppModeEnum.WORKFLOW, + mode: 'workflow' as const, } as never, }), ) @@ -153,7 +153,7 @@ describe('useAppInputsFormSchema', () => { useAppInputsFormSchema({ appDetail: { id: 'missing-app', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, } as never, }), ) @@ -166,7 +166,7 @@ describe('useAppInputsFormSchema', () => { mockFileUploadConfigIsFetching = true mockAppDetailData = { id: 'app-1', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, model_config: { user_input_form: [ { @@ -183,7 +183,7 @@ describe('useAppInputsFormSchema', () => { useAppInputsFormSchema({ appDetail: { id: 'app-1', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, } as never, }), ) @@ -203,7 +203,7 @@ describe('useAppInputsFormSchema', () => { useAppInputsFormSchema({ appDetail: { id: 'app-2', - mode: AppModeEnum.WORKFLOW, + mode: 'workflow' as const, } as never, }), ) diff --git a/web/app/components/rag-pipeline/components/__tests__/rag-pipeline-main.spec.tsx b/web/app/components/rag-pipeline/components/__tests__/rag-pipeline-main.spec.tsx index 52a36c9f801..bb31fd60ce1 100644 --- a/web/app/components/rag-pipeline/components/__tests__/rag-pipeline-main.spec.tsx +++ b/web/app/components/rag-pipeline/components/__tests__/rag-pipeline-main.spec.tsx @@ -68,16 +68,10 @@ vi.mock('../../hooks/use-pipeline-start-run', () => ({ }), })) -vi.mock('../../hooks/use-configs-map', () => ({ - useConfigsMap: () => ({ - flowId: 'test-flow-id', - flowType: 'ragPipeline', - fileSettings: {}, - }), -})) +vi.mock('../../hooks/use-file-settings', () => ({ useFileSettings: () => ({ enabled: true }) })) -vi.mock('../../hooks/use-inspect-vars-crud', () => ({ - useInspectVarsCrud: () => ({ +vi.mock('@/app/components/workflow/hooks/use-inspect-vars-crud-common', () => ({ + useInspectVarsCrudCommon: () => ({ hasNodeInspectVars: vi.fn(), hasSetInspectVar: vi.fn(), fetchInspectVarValue: vi.fn(), diff --git a/web/app/components/rag-pipeline/components/__tests__/update-dsl-modal.spec.tsx b/web/app/components/rag-pipeline/components/__tests__/update-dsl-modal.spec.tsx index 361e0d7b324..799e9ec048f 100644 --- a/web/app/components/rag-pipeline/components/__tests__/update-dsl-modal.spec.tsx +++ b/web/app/components/rag-pipeline/components/__tests__/update-dsl-modal.spec.tsx @@ -1,5 +1,6 @@ -import { act, fireEvent, render, screen, waitFor } from '@testing-library/react' +import { act, fireEvent, screen, waitFor } from '@testing-library/react' import { beforeEach, describe, expect, it, vi } from 'vite-plus/test' +import { renderEditor as render } from '@/app/components/workflow/editor-resource/__tests__/render' import { DSLImportStatus } from '@/models/app' import UpdateDSLModal from '../update-dsl-modal' @@ -133,43 +134,57 @@ describe('UpdateDSLModal', () => { describe('rendering', () => { it('should render title', () => { - render() + render(, { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) expect(screen.getByText('workflow.common.importDSL')).toBeInTheDocument() }) it('should render warning tip', () => { - render() + render(, { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) expect(screen.getByText('workflow.common.importDSLTip')).toBeInTheDocument() }) it('should render uploader', () => { - render() + render(, { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) expect(screen.getByTestId('uploader')).toBeInTheDocument() }) it('should render backup button', () => { - render() + render(, { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) expect(screen.getByText('workflow.common.backupCurrentDraft')).toBeInTheDocument() }) it('should render cancel button', () => { - render() + render(, { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) expect(screen.getByText('app.newApp.Cancel')).toBeInTheDocument() }) it('should render import button', () => { - render() + render(, { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) expect(screen.getByText('workflow.common.overwriteAndImport')).toBeInTheDocument() }) it('should render choose DSL section', () => { - render() + render(, { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) expect(screen.getByText('workflow.common.chooseDSL')).toBeInTheDocument() }) @@ -177,7 +192,9 @@ describe('UpdateDSLModal', () => { describe('user interactions', () => { it('should call onCancel when cancel button is clicked', () => { - render() + render(, { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) const cancelButton = screen.getByText('app.newApp.Cancel') fireEvent.click(cancelButton) @@ -186,7 +203,9 @@ describe('UpdateDSLModal', () => { }) it('should call onBackup when backup button is clicked', () => { - render() + render(, { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) const backupButton = screen.getByText('workflow.common.backupCurrentDraft') fireEvent.click(backupButton) @@ -195,7 +214,9 @@ describe('UpdateDSLModal', () => { }) it('should handle file upload', async () => { - render() + render(, { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) const fileInput = screen.getByTestId('file-input') const file = new File(['test content'], 'test.pipeline', { type: 'text/yaml' }) @@ -208,7 +229,9 @@ describe('UpdateDSLModal', () => { }) it('should clear file when clear button is clicked', () => { - render() + render(, { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) const clearButton = screen.getByTestId('clear-file') fireEvent.click(clearButton) @@ -219,14 +242,18 @@ describe('UpdateDSLModal', () => { describe('import functionality', () => { it('should show import button disabled when no file is selected', () => { - render() + render(, { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) const importButton = screen.getByText('workflow.common.overwriteAndImport') expect(importButton).toBeDisabled() }) it('should enable import button when file is selected', async () => { - render() + render(, { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) const fileInput = screen.getByTestId('file-input') const file = new File(['test content'], 'test.pipeline', { type: 'text/yaml' }) @@ -240,7 +267,9 @@ describe('UpdateDSLModal', () => { }) it('should disable import button after file is cleared', async () => { - render() + render(, { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) const fileInput = screen.getByTestId('file-input') const file = new File(['test content'], 'test.pipeline', { type: 'text/yaml' }) @@ -263,7 +292,9 @@ describe('UpdateDSLModal', () => { describe('import flow', () => { it('should call importDSL when import button is clicked with file content', async () => { - render() + render(, { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) const fileInput = screen.getByTestId('file-input') const file = new File(['test content'], 'test.pipeline', { type: 'text/yaml' }) @@ -289,7 +320,9 @@ describe('UpdateDSLModal', () => { pipeline_id: 'test-pipeline-id', }) - render() + render(, { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) const fileInput = screen.getByTestId('file-input') const file = new File(['test content'], 'test.pipeline', { type: 'text/yaml' }) @@ -319,7 +352,9 @@ describe('UpdateDSLModal', () => { pipeline_id: 'test-pipeline-id', }) - render() + render(, { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) const fileInput = screen.getByTestId('file-input') const file = new File(['test content'], 'test.pipeline', { type: 'text/yaml' }) @@ -345,7 +380,9 @@ describe('UpdateDSLModal', () => { pipeline_id: 'test-pipeline-id', }) - render() + render(, { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) const fileInput = screen.getByTestId('file-input') const file = new File(['test content'], 'test.pipeline', { type: 'text/yaml' }) @@ -377,7 +414,9 @@ describe('UpdateDSLModal', () => { pipeline_id: 'test-pipeline-id', }) - render() + render(, { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) const fileInput = screen.getByTestId('file-input') const file = new File(['test content'], 'test.pipeline', { type: 'text/yaml' }) @@ -407,7 +446,9 @@ describe('UpdateDSLModal', () => { pipeline_id: 'test-pipeline-id', }) - render() + render(, { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) const fileInput = screen.getByTestId('file-input') const file = new File(['test content'], 'test.pipeline', { type: 'text/yaml' }) @@ -437,7 +478,9 @@ describe('UpdateDSLModal', () => { pipeline_id: undefined, }) - render() + render(, { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) const fileInput = screen.getByTestId('file-input') const file = new File(['test content'], 'test.pipeline', { type: 'text/yaml' }) @@ -463,7 +506,9 @@ describe('UpdateDSLModal', () => { it('should show error notification when import throws exception', async () => { mockImportDSL.mockRejectedValue(new Error('Import failed')) - render() + render(, { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) const fileInput = screen.getByTestId('file-input') const file = new File(['test content'], 'test.pipeline', { type: 'text/yaml' }) @@ -493,7 +538,9 @@ describe('UpdateDSLModal', () => { pipeline_id: 'test-pipeline-id', }) - render() + render(, { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) const fileInput = screen.getByTestId('file-input') const file = new File(['test content'], 'test.pipeline', { type: 'text/yaml' }) @@ -523,7 +570,9 @@ describe('UpdateDSLModal', () => { pipeline_id: 'test-pipeline-id', }) - render() + render(, { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) const fileInput = screen.getByTestId('file-input') const file = new File(['test content'], 'test.pipeline', { type: 'text/yaml' }) @@ -553,7 +602,9 @@ describe('UpdateDSLModal', () => { current_dsl_version: '2.0.0', }) - render() + render(, { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) const fileInput = screen.getByTestId('file-input') const file = new File(['test content'], 'test.pipeline', { type: 'text/yaml' }) @@ -588,7 +639,9 @@ describe('UpdateDSLModal', () => { current_dsl_version: '2.0.0', }) - render() + render(, { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) const fileInput = screen.getByTestId('file-input') const file = new File(['test content'], 'test.pipeline', { type: 'text/yaml' }) @@ -620,7 +673,9 @@ describe('UpdateDSLModal', () => { current_dsl_version: '2.0.0', }) - render() + render(, { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) const fileInput = screen.getByTestId('file-input') const file = new File(['test content'], 'test.pipeline', { type: 'text/yaml' }) @@ -665,7 +720,9 @@ describe('UpdateDSLModal', () => { pipeline_id: 'test-pipeline-id', }) - render() + render(, { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) const fileInput = screen.getByTestId('file-input') const file = new File(['test content'], 'test.pipeline', { type: 'text/yaml' }) @@ -715,7 +772,9 @@ describe('UpdateDSLModal', () => { pipeline_id: 'test-pipeline-id', }) - render() + render(, { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) const fileInput = screen.getByTestId('file-input') const file = new File(['test content'], 'test.pipeline', { type: 'text/yaml' }) @@ -762,7 +821,9 @@ describe('UpdateDSLModal', () => { pipeline_id: 'test-pipeline-id', }) - render() + render(, { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) const fileInput = screen.getByTestId('file-input') const file = new File(['test content'], 'test.pipeline', { type: 'text/yaml' }) @@ -806,7 +867,9 @@ describe('UpdateDSLModal', () => { mockImportDSLConfirm.mockRejectedValue(new Error('Confirm failed')) - render() + render(, { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) const fileInput = screen.getByTestId('file-input') const file = new File(['test content'], 'test.pipeline', { type: 'text/yaml' }) @@ -853,7 +916,9 @@ describe('UpdateDSLModal', () => { pipeline_id: undefined, }) - render() + render(, { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) const fileInput = screen.getByTestId('file-input') const file = new File(['test content'], 'test.pipeline', { type: 'text/yaml' }) @@ -900,7 +965,9 @@ describe('UpdateDSLModal', () => { pipeline_id: 'test-pipeline-id', }) - render() + render(, { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) const fileInput = screen.getByTestId('file-input') const file = new File(['test content'], 'test.pipeline', { type: 'text/yaml' }) @@ -945,7 +1012,9 @@ describe('UpdateDSLModal', () => { pipeline_id: 'test-pipeline-id', }) - render() + render(, { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) const fileInput = screen.getByTestId('file-input') const file = new File(['test content'], 'test.pipeline', { type: 'text/yaml' }) @@ -990,7 +1059,9 @@ describe('UpdateDSLModal', () => { current_dsl_version: undefined, }) - render() + render(, { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) const fileInput = screen.getByTestId('file-input') const file = new File(['test content'], 'test.pipeline', { type: 'text/yaml' }) @@ -1013,7 +1084,9 @@ describe('UpdateDSLModal', () => { }) it('should not call importDSLConfirm when importId is not set', async () => { - render() + render(, { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) expect(mockImportDSLConfirm).not.toHaveBeenCalled() }) diff --git a/web/app/components/rag-pipeline/components/panel/__tests__/index.spec.tsx b/web/app/components/rag-pipeline/components/panel/__tests__/index.spec.tsx index d580ece22e8..4d7ad3a0384 100644 --- a/web/app/components/rag-pipeline/components/panel/__tests__/index.spec.tsx +++ b/web/app/components/rag-pipeline/components/panel/__tests__/index.spec.tsx @@ -1,5 +1,6 @@ import type { PanelProps } from '@/app/components/workflow/panel' -import { render, screen } from '@testing-library/react' +import { screen } from '@testing-library/react' +import { renderEditor as render } from '@/app/components/workflow/editor-resource/__tests__/render' import RagPipelinePanel from '../index' const { dynamicComponent } = vi.hoisted(() => { @@ -54,7 +55,9 @@ vi.mock('@/app/components/workflow/panel', () => ({ describe('RagPipelinePanel', () => { it('renders enabled panels and supplies pipeline version endpoints', () => { - render() + render(, { + resource: { kind: 'knowledge-pipeline', pipelineId: 'pipeline-1' }, + }) expect(screen.getByText('Record panel')).toBeInTheDocument() expect(screen.getByText('Test run panel')).toBeInTheDocument() diff --git a/web/app/components/rag-pipeline/components/panel/input-field/preview/__tests__/data-source.spec.tsx b/web/app/components/rag-pipeline/components/panel/input-field/preview/__tests__/data-source.spec.tsx index c838b2acd77..e4ee6f52ec5 100644 --- a/web/app/components/rag-pipeline/components/panel/input-field/preview/__tests__/data-source.spec.tsx +++ b/web/app/components/rag-pipeline/components/panel/input-field/preview/__tests__/data-source.spec.tsx @@ -1,5 +1,6 @@ import type { Datasource } from '../../../test-run/types' -import { fireEvent, render, screen } from '@testing-library/react' +import { fireEvent, screen } from '@testing-library/react' +import { renderEditor as render } from '@/app/components/workflow/editor-resource/__tests__/render' import DataSource from '../data-source' const { mockOnSelect, mockUseDraftPipelinePreProcessingParams } = vi.hoisted(() => ({ @@ -48,7 +49,9 @@ describe('DataSource preview', () => { }) it('should render the datasource selection step and forward selected values', () => { - render() + render(, { + resource: { kind: 'knowledge-pipeline', pipelineId: 'pipeline-1' }, + }) fireEvent.click(screen.getByText('select datasource')) diff --git a/web/app/components/rag-pipeline/components/panel/input-field/preview/__tests__/index.spec.tsx b/web/app/components/rag-pipeline/components/panel/input-field/preview/__tests__/index.spec.tsx index c34c535c3f9..3d3f7d0780c 100644 --- a/web/app/components/rag-pipeline/components/panel/input-field/preview/__tests__/index.spec.tsx +++ b/web/app/components/rag-pipeline/components/panel/input-field/preview/__tests__/index.spec.tsx @@ -1,8 +1,9 @@ import type { Datasource, DataSourceOption } from '../../../test-run/types' import type { RAGPipelineVariable, RAGPipelineVariables } from '@/models/pipeline' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' -import { fireEvent, render, screen, waitFor } from '@testing-library/react' +import { fireEvent, screen, waitFor } from '@testing-library/react' import * as React from 'react' +import { renderEditor as render } from '@/app/components/workflow/editor-resource/__tests__/render' import { PipelineInputVarType } from '@/models/pipeline' import DataSource from '../data-source' import Form from '../form' @@ -210,7 +211,10 @@ const TestWrapper = ({ children }: { children: React.ReactNode }) => { } const renderWithProviders = (ui: React.ReactElement) => { - return render(ui, { wrapper: TestWrapper }) + return render(ui, { + resource: { kind: 'knowledge-pipeline', pipelineId: 'pipeline-1' }, + wrapper: TestWrapper, + }) } describe('PreviewPanel', () => { diff --git a/web/app/components/rag-pipeline/components/panel/input-field/preview/__tests__/process-documents.spec.tsx b/web/app/components/rag-pipeline/components/panel/input-field/preview/__tests__/process-documents.spec.tsx index f3fa9c24b78..4a0219b54b8 100644 --- a/web/app/components/rag-pipeline/components/panel/input-field/preview/__tests__/process-documents.spec.tsx +++ b/web/app/components/rag-pipeline/components/panel/input-field/preview/__tests__/process-documents.spec.tsx @@ -1,4 +1,5 @@ -import { render, screen } from '@testing-library/react' +import { screen } from '@testing-library/react' +import { renderEditor as render } from '@/app/components/workflow/editor-resource/__tests__/render' import ProcessDocuments from '../process-documents' const mockUseDraftPipelineProcessingParams = vi.hoisted(() => @@ -30,7 +31,9 @@ describe('ProcessDocuments preview', () => { }) it('should render the processing step and its variables', () => { - render() + render(, { + resource: { kind: 'knowledge-pipeline', pipelineId: 'pipeline-1' }, + }) expect( screen.getByText('datasetPipeline.inputFieldPanel.preview.stepTwoTitle'), diff --git a/web/app/components/rag-pipeline/components/panel/test-run/preparation/document-processing/__tests__/hooks.spec.ts b/web/app/components/rag-pipeline/components/panel/test-run/preparation/document-processing/__tests__/hooks.spec.ts index 61e68cd1f90..598ec4fb862 100644 --- a/web/app/components/rag-pipeline/components/panel/test-run/preparation/document-processing/__tests__/hooks.spec.ts +++ b/web/app/components/rag-pipeline/components/panel/test-run/preparation/document-processing/__tests__/hooks.spec.ts @@ -1,4 +1,4 @@ -import { renderHook } from '@testing-library/react' +import { renderEditorHook as renderHook } from '@/app/components/workflow/editor-resource/__tests__/render' import { useInputVariables } from '../hooks' const mockUseDraftPipelineProcessingParams = vi.hoisted(() => @@ -23,7 +23,9 @@ describe('useInputVariables', () => { }) it('should query processing params with the current pipeline id and datasource node id', () => { - const { result } = renderHook(() => useInputVariables('datasource-node')) + const { result } = renderHook(() => useInputVariables('datasource-node'), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'pipeline-1' }, + }) expect(mockUseDraftPipelineProcessingParams).toHaveBeenCalledWith({ pipeline_id: 'pipeline-1', diff --git a/web/app/components/rag-pipeline/components/panel/test-run/preparation/document-processing/__tests__/index.spec.tsx b/web/app/components/rag-pipeline/components/panel/test-run/preparation/document-processing/__tests__/index.spec.tsx index d30eef3e949..70e5d0b0d57 100644 --- a/web/app/components/rag-pipeline/components/panel/test-run/preparation/document-processing/__tests__/index.spec.tsx +++ b/web/app/components/rag-pipeline/components/panel/test-run/preparation/document-processing/__tests__/index.spec.tsx @@ -7,9 +7,10 @@ import type { RAGPipelineVariables, } from '@/models/pipeline' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' -import { fireEvent, render, screen } from '@testing-library/react' +import { fireEvent, screen } from '@testing-library/react' import * as React from 'react' import { BaseFieldType } from '@/app/components/base/form/form-scenarios/base/types' +import { renderEditor as render } from '@/app/components/workflow/editor-resource/__tests__/render' import { WorkflowRunningStatus } from '@/app/components/workflow/types' import { PipelineInputVarType } from '@/models/pipeline' import { expectLoadingButton } from '@/test/button' @@ -193,7 +194,9 @@ const createQueryClient = () => const renderWithQueryClient = (component: React.ReactElement) => { const queryClient = createQueryClient() - return render({component}) + return render({component}, { + resource: { kind: 'knowledge-pipeline', pipelineId: 'pipeline-1' }, + }) } describe('DocumentProcessing', () => { @@ -475,7 +478,9 @@ describe('Actions', () => { it('should render back button', () => { const mockFormParams = createMockFormParams() - render() + render(, { + resource: { kind: 'knowledge-pipeline', pipelineId: 'pipeline-1' }, + }) expect(screen.getByText('datasetPipeline.operations.backToDataSource')).toBeInTheDocument() }) @@ -483,7 +488,9 @@ describe('Actions', () => { it('should render process button', () => { const mockFormParams = createMockFormParams() - render() + render(, { + resource: { kind: 'knowledge-pipeline', pipelineId: 'pipeline-1' }, + }) expect(screen.getByText('datasetPipeline.operations.process')).toBeInTheDocument() }) @@ -493,7 +500,9 @@ describe('Actions', () => { it('should disable process button when runDisabled is true', () => { const mockFormParams = createMockFormParams() - render() + render(, { + resource: { kind: 'knowledge-pipeline', pipelineId: 'pipeline-1' }, + }) const processButton = screen.getByText('datasetPipeline.operations.process').closest('button') expect(processButton).toBeDisabled() @@ -502,7 +511,9 @@ describe('Actions', () => { it('should disable process button when isSubmitting is true', () => { const mockFormParams = createMockFormParams({ isSubmitting: true }) - render() + render(, { + resource: { kind: 'knowledge-pipeline', pipelineId: 'pipeline-1' }, + }) const processButton = screen.getByText('datasetPipeline.operations.process').closest('button') expectLoadingButton(processButton) @@ -511,7 +522,9 @@ describe('Actions', () => { it('should disable process button when canSubmit is false', () => { const mockFormParams = createMockFormParams({ canSubmit: false }) - render() + render(, { + resource: { kind: 'knowledge-pipeline', pipelineId: 'pipeline-1' }, + }) const processButton = screen.getByText('datasetPipeline.operations.process').closest('button') expect(processButton).toBeDisabled() @@ -523,7 +536,9 @@ describe('Actions', () => { } const mockFormParams = createMockFormParams() - render() + render(, { + resource: { kind: 'knowledge-pipeline', pipelineId: 'pipeline-1' }, + }) const processButton = screen.getByText('datasetPipeline.operations.process').closest('button') expectLoadingButton(processButton) @@ -535,7 +550,9 @@ describe('Actions', () => { } const mockFormParams = createMockFormParams() - render() + render(, { + resource: { kind: 'knowledge-pipeline', pipelineId: 'pipeline-1' }, + }) const processButton = screen.getByText('datasetPipeline.operations.process').closest('button') expect(processButton).not.toBeDisabled() @@ -547,7 +564,9 @@ describe('Actions', () => { const mockOnBack = vi.fn() const mockFormParams = createMockFormParams() - render() + render(, { + resource: { kind: 'knowledge-pipeline', pipelineId: 'pipeline-1' }, + }) fireEvent.click(screen.getByText('datasetPipeline.operations.backToDataSource')) @@ -558,7 +577,9 @@ describe('Actions', () => { const mockSubmit = vi.fn() const mockFormParams = createMockFormParams({ handleSubmit: mockSubmit }) - render() + render(, { + resource: { kind: 'knowledge-pipeline', pipelineId: 'pipeline-1' }, + }) fireEvent.click(screen.getByText('datasetPipeline.operations.process')) @@ -570,7 +591,9 @@ describe('Actions', () => { it('should handle undefined runDisabled prop', () => { const mockFormParams = createMockFormParams() - render() + render(, { + resource: { kind: 'knowledge-pipeline', pipelineId: 'pipeline-1' }, + }) const processButton = screen.getByText('datasetPipeline.operations.process').closest('button') expect(processButton).not.toBeDisabled() @@ -580,7 +603,9 @@ describe('Actions', () => { mockWorkflowRunningData = undefined const mockFormParams = createMockFormParams() - render() + render(, { + resource: { kind: 'knowledge-pipeline', pipelineId: 'pipeline-1' }, + }) const processButton = screen.getByText('datasetPipeline.operations.process').closest('button') expect(processButton).not.toBeDisabled() @@ -604,7 +629,9 @@ describe('Options', () => { onSubmit: vi.fn(), } - const { container } = render() + const { container } = render(, { + resource: { kind: 'knowledge-pipeline', pipelineId: 'pipeline-1' }, + }) expect(container.querySelector('form')).toBeInTheDocument() }) @@ -622,7 +649,9 @@ describe('Options', () => { onSubmit: vi.fn(), } - render() + render(, { + resource: { kind: 'knowledge-pipeline', pipelineId: 'pipeline-1' }, + }) expect(screen.getByTestId('field-name')).toBeInTheDocument() expect(screen.getByTestId('field-email')).toBeInTheDocument() @@ -637,7 +666,9 @@ describe('Options', () => { onSubmit: vi.fn(), } - render() + render(, { + resource: { kind: 'knowledge-pipeline', pipelineId: 'pipeline-1' }, + }) expect(screen.getByTestId('custom-action')).toBeInTheDocument() }) @@ -654,7 +685,9 @@ describe('Options', () => { onSubmit: mockOnSubmit, } - const { container } = render() + const { container } = render(, { + resource: { kind: 'knowledge-pipeline', pipelineId: 'pipeline-1' }, + }) const form = container.querySelector('form')! const submitEvent = new Event('submit', { bubbles: true, cancelable: true }) const preventDefaultSpy = vi.spyOn(submitEvent, 'preventDefault') @@ -674,7 +707,9 @@ describe('Options', () => { onSubmit: mockOnSubmit, } - const { container } = render() + const { container } = render(, { + resource: { kind: 'knowledge-pipeline', pipelineId: 'pipeline-1' }, + }) const form = container.querySelector('form')! const submitEvent = new Event('submit', { bubbles: true, cancelable: true }) const stopPropagationSpy = vi.spyOn(submitEvent, 'stopPropagation') @@ -694,7 +729,9 @@ describe('Options', () => { onSubmit: mockOnSubmit, } - const { container } = render() + const { container } = render(, { + resource: { kind: 'knowledge-pipeline', pipelineId: 'pipeline-1' }, + }) const form = container.querySelector('form')! fireEvent.submit(form) @@ -719,7 +756,9 @@ describe('Options', () => { onSubmit: mockOnSubmit, } - const { container } = render() + const { container } = render(, { + resource: { kind: 'knowledge-pipeline', pipelineId: 'pipeline-1' }, + }) const form = container.querySelector('form')! fireEvent.submit(form) @@ -743,7 +782,9 @@ describe('Options', () => { onSubmit: vi.fn(), } - const { container } = render() + const { container } = render(, { + resource: { kind: 'knowledge-pipeline', pipelineId: 'pipeline-1' }, + }) const form = container.querySelector('form')! fireEvent.submit(form) @@ -770,7 +811,9 @@ describe('Options', () => { onSubmit: vi.fn(), } - const { container } = render() + const { container } = render(, { + resource: { kind: 'knowledge-pipeline', pipelineId: 'pipeline-1' }, + }) const form = container.querySelector('form')! fireEvent.submit(form) @@ -801,7 +844,9 @@ describe('Options', () => { onSubmit: vi.fn(), } - const { container } = render() + const { container } = render(, { + resource: { kind: 'knowledge-pipeline', pipelineId: 'pipeline-1' }, + }) const form = container.querySelector('form')! fireEvent.submit(form) @@ -829,7 +874,9 @@ describe('Options', () => { onSubmit: vi.fn(), } - const { container } = render() + const { container } = render(, { + resource: { kind: 'knowledge-pipeline', pipelineId: 'pipeline-1' }, + }) const form = container.querySelector('form')! fireEvent.submit(form) @@ -855,7 +902,9 @@ describe('Options', () => { onSubmit: vi.fn(), } - render() + render(, { + resource: { kind: 'knowledge-pipeline', pipelineId: 'pipeline-1' }, + }) expect(screen.getByTestId('field-first')).toBeInTheDocument() expect(screen.getByTestId('field-second')).toBeInTheDocument() @@ -883,7 +932,9 @@ describe('Options', () => { onSubmit: vi.fn(), } - render() + render(, { + resource: { kind: 'knowledge-pipeline', pipelineId: 'pipeline-1' }, + }) expect(screen.getByTestId('field-label-test')).toHaveTextContent('Test Label') expect(screen.getByTestId('field-type-test')).toHaveTextContent(BaseFieldType.textInput) @@ -901,7 +952,9 @@ describe('Options', () => { onSubmit: vi.fn(), } - const { container } = render() + const { container } = render(, { + resource: { kind: 'knowledge-pipeline', pipelineId: 'pipeline-1' }, + }) expect(container.querySelector('form')).toBeInTheDocument() }) @@ -915,7 +968,9 @@ describe('Options', () => { onSubmit: vi.fn(), } - render() + render(, { + resource: { kind: 'knowledge-pipeline', pipelineId: 'pipeline-1' }, + }) expect(screen.queryByTestId(/^field-/)).not.toBeInTheDocument() }) @@ -942,7 +997,9 @@ describe('Options', () => { onSubmit: vi.fn(), } - render() + render(, { + resource: { kind: 'knowledge-pipeline', pipelineId: 'pipeline-1' }, + }) expect(screen.getByTestId('field-text')).toBeInTheDocument() expect(screen.getByTestId('field-paragraph')).toBeInTheDocument() @@ -963,7 +1020,9 @@ describe('Options', () => { onSubmit: vi.fn(), } - render() + render(, { + resource: { kind: 'knowledge-pipeline', pipelineId: 'pipeline-1' }, + }) expect(screen.getAllByTestId(/^field-field_/)).toHaveLength(20) }) diff --git a/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/__tests__/index.spec.tsx b/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/__tests__/index.spec.tsx index c2bd364bb11..1174ee6a88c 100644 --- a/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/__tests__/index.spec.tsx +++ b/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/__tests__/index.spec.tsx @@ -4,9 +4,9 @@ import { fireEvent, screen, waitFor } from '@testing-library/react' import { NuqsTestingAdapter } from 'nuqs/adapters/testing' import * as React from 'react' import { beforeEach, describe, expect, it, vi } from 'vite-plus/test' +import { renderEditor as renderWithoutPricing } from '@/app/components/workflow/editor-resource/__tests__/render' import { seedAccountProfileQuery } from '@/test/console/account-profile' import { seedFeatures, seedSystemFeatures } from '@/test/console/query-data' -import { render as renderWithoutPricing } from '@/test/console/render' import Publisher from '../index' import { Popup } from '../popup' @@ -219,7 +219,10 @@ const renderWithQueryClient = (ui: React.ReactElement) => { function render(...args: Parameters) { args[0] = {args[0]} - return renderWithoutPricing(...args) + return renderWithoutPricing(args[0], { + ...args[1], + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) } vi.mock('@/next/navigation', () => ({ useParams: () => ({ datasetId: 'test-dataset-id' }) })) @@ -556,35 +559,6 @@ describe('publisher', () => { }) }) }) - - it('should publish as template with empty pipeline id fallback', async () => { - mockPublishedAt.mockReturnValue(1700000000) - mockPipelineId.mockReturnValue(undefined as unknown as string) - mockPublishAsCustomizedPipeline.mockResolvedValue({}) - renderWithQueryClient() - - fireEvent.click(screen.getByText('workflow.common.publish')) - - const publishAsButton = screen - .getAllByRole('button') - .find((btn) => btn.textContent?.includes('pipeline.common.publishAs')) - fireEvent.click(publishAsButton!) - - await waitFor(() => { - expect(screen.getByTestId('publish-as-knowledge-pipeline-modal')).toBeInTheDocument() - }) - - fireEvent.click(screen.getByTestId('modal-confirm')) - - await waitFor(() => { - expect(mockPublishAsCustomizedPipeline).toHaveBeenCalledWith({ - pipelineId: '', - name: 'Test Pipeline', - icon_info: { type: 'emoji', emoji: '📚', background: '#fff' }, - description: 'Test description', - }) - }) - }) }) describe('API Calls and Async Operations', () => { diff --git a/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/__tests__/popup.spec.tsx b/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/__tests__/popup.spec.tsx index 7642b08b641..e0c0ec74132 100644 --- a/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/__tests__/popup.spec.tsx +++ b/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/__tests__/popup.spec.tsx @@ -1,8 +1,8 @@ import { fireEvent, screen, waitFor } from '@testing-library/react' import { NuqsTestingAdapter } from 'nuqs/adapters/testing' import { afterEach, beforeEach, describe, expect, it, vi } from 'vite-plus/test' +import { renderEditor as renderWithConsoleState } from '@/app/components/workflow/editor-resource/__tests__/render' import { createConsoleQueryWrapper } from '@/test/console/query-data' -import { render as renderWithConsoleState } from '@/test/console/render' import { Popup } from '../popup' const onPricingUrlUpdate = vi.hoisted(() => vi.fn()) @@ -14,7 +14,10 @@ const renderWithoutPricing = (ui: React.ReactElement) => { systemFeatures: { deployment_edition: 'CLOUD' }, features: { knowledge_pipeline: { publish_enabled: mockIsAllowPublishAsCustom } }, }) - return renderWithConsoleState(ui, { wrapper }) + return renderWithConsoleState(ui, { + resource: { kind: 'knowledge-pipeline', pipelineId: 'pipeline-1' }, + wrapper, + }) } const mockPublishWorkflow = vi.fn().mockResolvedValue({ created_at: '2024-01-01T00:00:00Z' }) diff --git a/web/app/components/rag-pipeline/hooks/__tests__/use-DSL.spec.ts b/web/app/components/rag-pipeline/hooks/__tests__/use-DSL.spec.ts index 5c47c92a466..6b4b9ef5c1e 100644 --- a/web/app/components/rag-pipeline/hooks/__tests__/use-DSL.spec.ts +++ b/web/app/components/rag-pipeline/hooks/__tests__/use-DSL.spec.ts @@ -1,8 +1,9 @@ import type { ReactNode } from 'react' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' -import { act, renderHook, waitFor } from '@testing-library/react' +import { act, waitFor } from '@testing-library/react' import { createElement } from 'react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vite-plus/test' +import { renderEditorHook as renderHook } from '@/app/components/workflow/editor-resource/__tests__/render' import { useDSLByCanEdit } from '../use-DSL' const toastMocks = vi.hoisted(() => ({ @@ -47,7 +48,8 @@ vi.mock('@/app/components/workflow/store', () => ({ })) const mockExportPipelineConfig = vi.fn() -vi.mock('@/service/console', () => ({ +vi.mock('@/service/console', async (importOriginal) => ({ + ...(await importOriginal()), consoleClient: { rag: { pipelines: { @@ -140,7 +142,10 @@ describe('useDSLByCanEdit', () => { callback.onError() return null }) - const { result } = renderHook(() => useDSLByCanEdit(true), { wrapper }) + const { result } = renderHook(() => useDSLByCanEdit(true), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + wrapper, + }) await act(async () => { expect(await result.current.handleExportDSL()).toBe(false) }) @@ -152,7 +157,10 @@ describe('useDSLByCanEdit', () => { it('allows export when draft synchronization is skipped for read-only access', async () => { mockDoSyncWorkflowDraft.mockResolvedValue(null) - const { result } = renderHook(() => useDSLByCanEdit(false), { wrapper }) + const { result } = renderHook(() => useDSLByCanEdit(false), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + wrapper, + }) await act(async () => { expect(await result.current.handleExportDSL()).toBe(true) }) @@ -160,20 +168,11 @@ describe('useDSLByCanEdit', () => { }) describe('handleExportDSL', () => { - it('should return early when pipelineId is not set', async () => { - mockGetState.mockReturnValue({ pipelineId: null, knowledgeName: 'test' }) - - const { result } = renderHook(() => useDSLByCanEdit(true), { wrapper }) - - await act(async () => { - await result.current.handleExportDSL() - }) - - expect(mockDoSyncWorkflowDraft).not.toHaveBeenCalled() - }) - it('should create and download file', async () => { - const { result } = renderHook(() => useDSLByCanEdit(true), { wrapper }) + const { result } = renderHook(() => useDSLByCanEdit(true), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + wrapper, + }) await act(async () => { await result.current.handleExportDSL() @@ -183,7 +182,10 @@ describe('useDSLByCanEdit', () => { }) it('should set correct download filename', async () => { - const { result } = renderHook(() => useDSLByCanEdit(true), { wrapper }) + const { result } = renderHook(() => useDSLByCanEdit(true), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + wrapper, + }) await act(async () => { await result.current.handleExportDSL() @@ -197,7 +199,10 @@ describe('useDSLByCanEdit', () => { }) it('should pass blob data to downloadBlob', async () => { - const { result } = renderHook(() => useDSLByCanEdit(true), { wrapper }) + const { result } = renderHook(() => useDSLByCanEdit(true), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + wrapper, + }) await act(async () => { await result.current.handleExportDSL() @@ -213,7 +218,10 @@ describe('useDSLByCanEdit', () => { it('should handle export error', async () => { mockExportPipelineConfig.mockRejectedValue(new Error('Export failed')) - const { result } = renderHook(() => useDSLByCanEdit(true), { wrapper }) + const { result } = renderHook(() => useDSLByCanEdit(true), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + wrapper, + }) await act(async () => { expect(await result.current.handleExportDSL()).toBe(false) @@ -228,7 +236,10 @@ describe('useDSLByCanEdit', () => { }) it('should pass include parameter', async () => { - const { result } = renderHook(() => useDSLByCanEdit(true), { wrapper }) + const { result } = renderHook(() => useDSLByCanEdit(true), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + wrapper, + }) await act(async () => { await result.current.handleExportDSL(true) @@ -244,22 +255,13 @@ describe('useDSLByCanEdit', () => { }) describe('exportCheck', () => { - it('should return early when pipelineId is not set', async () => { - mockGetState.mockReturnValue({ pipelineId: null }) - - const { result } = renderHook(() => useDSLByCanEdit(true), { wrapper }) - - await act(async () => { - await result.current.exportCheck() - }) - - expect(mockFetchWorkflowDraft).not.toHaveBeenCalled() - }) - it('should call handleExportDSL directly when no secret variables', async () => { mockFetchWorkflowDraft.mockResolvedValue({ environment_variables: [] }) - const { result } = renderHook(() => useDSLByCanEdit(true), { wrapper }) + const { result } = renderHook(() => useDSLByCanEdit(true), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + wrapper, + }) await act(async () => { await result.current.exportCheck() @@ -277,7 +279,10 @@ describe('useDSLByCanEdit', () => { const secretVars = [{ value_type: 'secret', name: 'API_KEY' }] mockFetchWorkflowDraft.mockResolvedValue({ environment_variables: secretVars }) - const { result } = renderHook(() => useDSLByCanEdit(true), { wrapper }) + const { result } = renderHook(() => useDSLByCanEdit(true), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + wrapper, + }) await act(async () => { await result.current.exportCheck() @@ -296,7 +301,10 @@ describe('useDSLByCanEdit', () => { it('should handle export check error', async () => { mockFetchWorkflowDraft.mockRejectedValue(new Error('Fetch failed')) - const { result } = renderHook(() => useDSLByCanEdit(true), { wrapper }) + const { result } = renderHook(() => useDSLByCanEdit(true), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + wrapper, + }) await act(async () => { await result.current.exportCheck() diff --git a/web/app/components/rag-pipeline/hooks/__tests__/use-configs-map.spec.ts b/web/app/components/rag-pipeline/hooks/__tests__/use-configs-map.spec.ts deleted file mode 100644 index cb7b7fdaf3c..00000000000 --- a/web/app/components/rag-pipeline/hooks/__tests__/use-configs-map.spec.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { renderHook } from '@testing-library/react' -import { describe, expect, it, vi } from 'vite-plus/test' -import { useConfigsMap } from '../use-configs-map' - -const mockPipelineId = 'pipeline-xyz' -const mockFileUploadConfig = { max_size: 10 } - -vi.mock('@/app/components/workflow/store', () => ({ - useStore: (selector: (state: Record) => unknown) => { - const state = { - pipelineId: mockPipelineId, - fileUploadConfig: mockFileUploadConfig, - } - return selector(state) - }, -})) - -vi.mock('@/types/app', () => ({ - Resolution: { high: 'high' }, - TransferMethod: { local_file: 'local_file', remote_url: 'remote_url' }, -})) - -vi.mock('@/types/common', () => ({ - FlowType: { ragPipeline: 'rag-pipeline' }, -})) - -describe('useConfigsMap', () => { - it('should return flowId from pipelineId', () => { - const { result } = renderHook(() => useConfigsMap()) - - expect(result.current.flowId).toBe('pipeline-xyz') - }) - - it('should return ragPipeline as flowType', () => { - const { result } = renderHook(() => useConfigsMap()) - - expect(result.current.flowType).toBe('rag-pipeline') - }) - - it('should include file settings with image disabled', () => { - const { result } = renderHook(() => useConfigsMap()) - - expect(result.current.fileSettings.image.enabled).toBe(false) - }) - - it('should set image detail to high resolution', () => { - const { result } = renderHook(() => useConfigsMap()) - - expect(result.current.fileSettings.image.detail).toBe('high') - }) - - it('should set image number_limits to 3', () => { - const { result } = renderHook(() => useConfigsMap()) - - expect(result.current.fileSettings.image.number_limits).toBe(3) - }) - - it('should include both transfer methods for image', () => { - const { result } = renderHook(() => useConfigsMap()) - - expect(result.current.fileSettings.image.transfer_methods).toEqual(['local_file', 'remote_url']) - }) - - it('should pass through fileUploadConfig from store', () => { - const { result } = renderHook(() => useConfigsMap()) - - expect(result.current.fileSettings.fileUploadConfig).toEqual({ max_size: 10 }) - }) -}) diff --git a/web/app/components/rag-pipeline/hooks/__tests__/use-file-settings.spec.ts b/web/app/components/rag-pipeline/hooks/__tests__/use-file-settings.spec.ts new file mode 100644 index 00000000000..b6afa47f6be --- /dev/null +++ b/web/app/components/rag-pipeline/hooks/__tests__/use-file-settings.spec.ts @@ -0,0 +1,53 @@ +import { renderHook } from '@testing-library/react' +import { describe, expect, it, vi } from 'vite-plus/test' +import { useFileSettings } from '../use-file-settings' + +const mockPipelineId = 'pipeline-xyz' +const mockFileUploadConfig = { max_size: 10 } + +vi.mock('@/app/components/workflow/store', () => ({ + useStore: (selector: (state: Record) => unknown) => { + const state = { + pipelineId: mockPipelineId, + fileUploadConfig: mockFileUploadConfig, + } + return selector(state) + }, +})) + +vi.mock('@/types/app', () => ({ + Resolution: { high: 'high' }, + TransferMethod: { local_file: 'local_file', remote_url: 'remote_url' }, +})) + +describe('useFileSettings', () => { + it('should include file settings with image disabled', () => { + const { result } = renderHook(() => useFileSettings()) + + expect(result.current.image.enabled).toBe(false) + }) + + it('should set image detail to high resolution', () => { + const { result } = renderHook(() => useFileSettings()) + + expect(result.current.image.detail).toBe('high') + }) + + it('should set image number_limits to 3', () => { + const { result } = renderHook(() => useFileSettings()) + + expect(result.current.image.number_limits).toBe(3) + }) + + it('should include both transfer methods for image', () => { + const { result } = renderHook(() => useFileSettings()) + + expect(result.current.image.transfer_methods).toEqual(['local_file', 'remote_url']) + }) + + it('should pass through fileUploadConfig from store', () => { + const { result } = renderHook(() => useFileSettings()) + + expect(result.current.fileUploadConfig).toEqual({ max_size: 10 }) + }) +}) diff --git a/web/app/components/rag-pipeline/hooks/__tests__/use-get-run-and-trace-url.spec.ts b/web/app/components/rag-pipeline/hooks/__tests__/use-get-run-and-trace-url.spec.ts index c4502b42675..4feab0ddc98 100644 --- a/web/app/components/rag-pipeline/hooks/__tests__/use-get-run-and-trace-url.spec.ts +++ b/web/app/components/rag-pipeline/hooks/__tests__/use-get-run-and-trace-url.spec.ts @@ -1,5 +1,5 @@ -import { renderHook } from '@testing-library/react' import { describe, expect, it, vi } from 'vite-plus/test' +import { renderEditorHook as renderHook } from '@/app/components/workflow/editor-resource/__tests__/render' import { useGetRunAndTraceUrl } from '../use-get-run-and-trace-url' vi.mock('@/app/components/workflow/store', () => ({ @@ -12,21 +12,27 @@ vi.mock('@/app/components/workflow/store', () => ({ describe('useGetRunAndTraceUrl', () => { it('should generate correct runUrl', () => { - const { result } = renderHook(() => useGetRunAndTraceUrl()) + const { result } = renderHook(() => useGetRunAndTraceUrl(), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'pipeline-test-123' }, + }) const { runUrl } = result.current.getWorkflowRunAndTraceUrl('run-abc') expect(runUrl).toBe('/rag/pipelines/pipeline-test-123/workflow-runs/run-abc') }) it('should generate correct traceUrl', () => { - const { result } = renderHook(() => useGetRunAndTraceUrl()) + const { result } = renderHook(() => useGetRunAndTraceUrl(), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'pipeline-test-123' }, + }) const { traceUrl } = result.current.getWorkflowRunAndTraceUrl('run-abc') expect(traceUrl).toBe('/rag/pipelines/pipeline-test-123/workflow-runs/run-abc/node-executions') }) it('should handle different runIds', () => { - const { result } = renderHook(() => useGetRunAndTraceUrl()) + const { result } = renderHook(() => useGetRunAndTraceUrl(), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'pipeline-test-123' }, + }) const r1 = result.current.getWorkflowRunAndTraceUrl('id-1') const r2 = result.current.getWorkflowRunAndTraceUrl('id-2') diff --git a/web/app/components/rag-pipeline/hooks/__tests__/use-inspect-vars-crud.spec.ts b/web/app/components/rag-pipeline/hooks/__tests__/use-inspect-vars-crud.spec.ts deleted file mode 100644 index 495531a1fe4..00000000000 --- a/web/app/components/rag-pipeline/hooks/__tests__/use-inspect-vars-crud.spec.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { renderHook } from '@testing-library/react' -import { beforeEach, describe, expect, it, vi } from 'vite-plus/test' -import { useInspectVarsCrud } from '../use-inspect-vars-crud' - -// Mock return value for useInspectVarsCrudCommon -const mockApis = { - hasNodeInspectVars: vi.fn(), - hasSetInspectVar: vi.fn(), - fetchInspectVarValue: vi.fn(), - editInspectVarValue: vi.fn(), - renameInspectVarName: vi.fn(), - appendNodeInspectVars: vi.fn(), - deleteInspectVar: vi.fn(), - deleteNodeInspectorVars: vi.fn(), - deleteAllInspectorVars: vi.fn(), - isInspectVarEdited: vi.fn(), - resetToLastRunVar: vi.fn(), - invalidateSysVarValues: vi.fn(), - resetConversationVar: vi.fn(), - invalidateConversationVarValues: vi.fn(), -} - -const mockUseInspectVarsCrudCommon = vi.fn(() => mockApis) -vi.mock('@/app/components/workflow/hooks/use-inspect-vars-crud-common', () => ({ - useInspectVarsCrudCommon: (...args: Parameters) => - mockUseInspectVarsCrudCommon(...args), -})) - -const mockConfigsMap = { - flowId: 'pipeline-123', - flowType: 'rag_pipeline', - fileSettings: { - image: { enabled: false }, - fileUploadConfig: {}, - }, -} - -vi.mock('../use-configs-map', () => ({ - useConfigsMap: () => mockConfigsMap, -})) - -describe('useInspectVarsCrud', () => { - beforeEach(() => { - vi.clearAllMocks() - }) - - // Verify the hook composes useConfigsMap with useInspectVarsCrudCommon - describe('Composition', () => { - it('should pass configsMap to useInspectVarsCrudCommon', () => { - renderHook(() => useInspectVarsCrud()) - - expect(mockUseInspectVarsCrudCommon).toHaveBeenCalledWith( - expect.objectContaining({ - flowId: 'pipeline-123', - flowType: 'rag_pipeline', - }), - ) - }) - - it('should return all APIs from useInspectVarsCrudCommon', () => { - const { result } = renderHook(() => useInspectVarsCrud()) - - expect(result.current.hasNodeInspectVars).toBe(mockApis.hasNodeInspectVars) - expect(result.current.fetchInspectVarValue).toBe(mockApis.fetchInspectVarValue) - expect(result.current.editInspectVarValue).toBe(mockApis.editInspectVarValue) - expect(result.current.deleteInspectVar).toBe(mockApis.deleteInspectVar) - expect(result.current.deleteAllInspectorVars).toBe(mockApis.deleteAllInspectorVars) - expect(result.current.resetToLastRunVar).toBe(mockApis.resetToLastRunVar) - expect(result.current.resetConversationVar).toBe(mockApis.resetConversationVar) - }) - }) - - // Verify the hook spreads all returned properties - describe('API Surface', () => { - it('should expose all expected API methods', () => { - const { result } = renderHook(() => useInspectVarsCrud()) - - const expectedKeys = [ - 'hasNodeInspectVars', - 'hasSetInspectVar', - 'fetchInspectVarValue', - 'editInspectVarValue', - 'renameInspectVarName', - 'appendNodeInspectVars', - 'deleteInspectVar', - 'deleteNodeInspectorVars', - 'deleteAllInspectorVars', - 'isInspectVarEdited', - 'resetToLastRunVar', - 'invalidateSysVarValues', - 'resetConversationVar', - 'invalidateConversationVarValues', - ] - - for (const key of expectedKeys) expect(result.current).toHaveProperty(key) - }) - }) -}) diff --git a/web/app/components/rag-pipeline/hooks/__tests__/use-nodes-sync-draft.spec.ts b/web/app/components/rag-pipeline/hooks/__tests__/use-nodes-sync-draft.spec.ts index 46a4f75b925..029fd026923 100644 --- a/web/app/components/rag-pipeline/hooks/__tests__/use-nodes-sync-draft.spec.ts +++ b/web/app/components/rag-pipeline/hooks/__tests__/use-nodes-sync-draft.spec.ts @@ -1,6 +1,6 @@ -import { renderHook } from '@testing-library/react' import { act } from 'react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vite-plus/test' +import { renderEditorHook as renderHook } from '@/app/components/workflow/editor-resource/__tests__/render' import { createHttpError } from '@/service/http-error' import { useNodesSyncDraft } from '../use-nodes-sync-draft' @@ -30,7 +30,7 @@ vi.mock('@/app/components/workflow/hooks/use-workflow', () => ({ const mockSyncWorkflowDraft = vi.fn() // Stand in for the draft sync request at the client boundary: record the request it sends // and validate rejections against the contract, as the console link does. -vi.mock('@/service/console', async () => { +vi.mock('@/service/console', async (importOriginal) => { const { rag } = await import('@dify/contracts/api/console/rag/orpc.gen') const { validateDefinedErrors } = await import('@/service/console/defined-errors') const validate = validateDefinedErrors({ rag }) @@ -46,6 +46,7 @@ vi.mock('@/service/console', async () => { }), }) return { + ...(await importOriginal()), consoleClient: { rag: { pipelines: { byPipelineId: { workflows: { draft: { post } } } } } }, } }) @@ -57,12 +58,14 @@ vi.mock('../use-pipeline-refresh-draft', () => ({ }), })) -vi.mock('@/config', () => ({ +vi.mock('@/config', async (importOriginal) => ({ + ...(await importOriginal()), API_PREFIX: '/api', })) const mockPostWithKeepalive = vi.fn() -vi.mock('@/service/fetch', () => ({ +vi.mock('@/service/fetch', async (importOriginal) => ({ + ...(await importOriginal()), postWithKeepalive: (...args: unknown[]) => mockPostWithKeepalive(...args), })) @@ -103,14 +106,18 @@ describe('useNodesSyncDraft', () => { describe('hook initialization', () => { it('should return doSyncWorkflowDraft function', () => { - const { result } = renderHook(() => useNodesSyncDraft()) + const { result } = renderHook(() => useNodesSyncDraft(), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) expect(result.current.doSyncWorkflowDraft).toBeDefined() expect(typeof result.current.doSyncWorkflowDraft).toBe('function') }) it('should return syncWorkflowDraftWhenPageClose function', () => { - const { result } = renderHook(() => useNodesSyncDraft()) + const { result } = renderHook(() => useNodesSyncDraft(), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) expect(result.current.syncWorkflowDraftWhenPageClose).toBeDefined() expect(typeof result.current.syncWorkflowDraftWhenPageClose).toBe('function') @@ -121,7 +128,9 @@ describe('useNodesSyncDraft', () => { it('should not call postWithKeepalive when nodes are read only', () => { mockGetNodesReadOnly.mockReturnValue(true) - const { result } = renderHook(() => useNodesSyncDraft()) + const { result } = renderHook(() => useNodesSyncDraft(), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) act(() => { result.current.syncWorkflowDraftWhenPageClose() @@ -136,7 +145,9 @@ describe('useNodesSyncDraft', () => { { id: 'node-1', data: { type: 'start' }, position: { x: 0, y: 0 } }, ]) - const { result } = renderHook(() => useNodesSyncDraft()) + const { result } = renderHook(() => useNodesSyncDraft(), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) act(() => { result.current.syncWorkflowDraftWhenPageClose() @@ -151,27 +162,12 @@ describe('useNodesSyncDraft', () => { ) }) - it('should not call postWithKeepalive when pipelineId is missing', () => { - mockWorkflowStoreGetState.mockReturnValue({ - pipelineId: undefined, - environmentVariables: [], - syncWorkflowDraftHash: 'test-hash', - ragPipelineVariables: [], - }) - - const { result } = renderHook(() => useNodesSyncDraft()) - - act(() => { - result.current.syncWorkflowDraftWhenPageClose() - }) - - expect(mockPostWithKeepalive).not.toHaveBeenCalled() - }) - it('should not call postWithKeepalive when nodes array is empty', () => { mockGetNodes.mockReturnValue([]) - const { result } = renderHook(() => useNodesSyncDraft()) + const { result } = renderHook(() => useNodesSyncDraft(), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) act(() => { result.current.syncWorkflowDraftWhenPageClose() @@ -185,7 +181,9 @@ describe('useNodesSyncDraft', () => { { id: 'node-1', data: { type: 'start', _isTempNode: true }, position: { x: 0, y: 0 } }, ]) - const { result } = renderHook(() => useNodesSyncDraft()) + const { result } = renderHook(() => useNodesSyncDraft(), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) act(() => { result.current.syncWorkflowDraftWhenPageClose() @@ -199,7 +197,9 @@ describe('useNodesSyncDraft', () => { { id: 'node-1', data: { type: 'start', _privateData: 'secret' }, position: { x: 0, y: 0 } }, ]) - const { result } = renderHook(() => useNodesSyncDraft()) + const { result } = renderHook(() => useNodesSyncDraft(), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) act(() => { result.current.syncWorkflowDraftWhenPageClose() @@ -215,7 +215,9 @@ describe('useNodesSyncDraft', () => { it('should not sync when nodes are read only', async () => { mockGetNodesReadOnly.mockReturnValue(true) - const { result } = renderHook(() => useNodesSyncDraft()) + const { result } = renderHook(() => useNodesSyncDraft(), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) await act(async () => { await result.current.doSyncWorkflowDraft() @@ -230,7 +232,9 @@ describe('useNodesSyncDraft', () => { { id: 'node-1', data: { type: 'start' }, position: { x: 0, y: 0 } }, ]) - const { result } = renderHook(() => useNodesSyncDraft()) + const { result } = renderHook(() => useNodesSyncDraft(), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) await act(async () => { await result.current.doSyncWorkflowDraft() @@ -258,7 +262,9 @@ describe('useNodesSyncDraft', () => { transform: [10, 20, 1.5], }) - const { result } = renderHook(() => useNodesSyncDraft()) + const { result } = renderHook(() => useNodesSyncDraft(), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) let syncPromise!: ReturnType act(() => { @@ -310,7 +316,9 @@ describe('useNodesSyncDraft', () => { .mockResolvedValueOnce({ hash: 'hash-after-first-sync', updated_at: 'first-update' }) .mockResolvedValueOnce({ hash: 'hash-after-second-sync', updated_at: 'second-update' }) - const { result } = renderHook(() => useNodesSyncDraft()) + const { result } = renderHook(() => useNodesSyncDraft(), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) let firstSync!: ReturnType let secondSync!: ReturnType @@ -343,7 +351,9 @@ describe('useNodesSyncDraft', () => { { id: 'node-1', data: { type: 'start' }, position: { x: 0, y: 0 } }, ]) - const { result } = renderHook(() => useNodesSyncDraft()) + const { result } = renderHook(() => useNodesSyncDraft(), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) await act(async () => { await result.current.doSyncWorkflowDraft() @@ -365,7 +375,9 @@ describe('useNodesSyncDraft', () => { ]) const onSuccess = vi.fn() - const { result } = renderHook(() => useNodesSyncDraft()) + const { result } = renderHook(() => useNodesSyncDraft(), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) await act(async () => { await result.current.doSyncWorkflowDraft(false, { onSuccess }) @@ -381,7 +393,9 @@ describe('useNodesSyncDraft', () => { ]) const onSettled = vi.fn() - const { result } = renderHook(() => useNodesSyncDraft()) + const { result } = renderHook(() => useNodesSyncDraft(), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) await act(async () => { await result.current.doSyncWorkflowDraft(false, { onSettled }) @@ -398,7 +412,9 @@ describe('useNodesSyncDraft', () => { mockSyncWorkflowDraft.mockRejectedValue(new Error('Sync failed')) const onError = vi.fn() - const { result } = renderHook(() => useNodesSyncDraft()) + const { result } = renderHook(() => useNodesSyncDraft(), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) await act(async () => { await result.current.doSyncWorkflowDraft(false, { onError }) @@ -424,7 +440,9 @@ describe('useNodesSyncDraft', () => { setDraftUpdatedAt: mockSetDraftUpdatedAt, }) - const { result } = renderHook(() => useNodesSyncDraft()) + const { result } = renderHook(() => useNodesSyncDraft(), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) await act(async () => { await result.current.doSyncWorkflowDraft() @@ -447,7 +465,9 @@ describe('useNodesSyncDraft', () => { }) mockSyncWorkflowDraft.mockRejectedValue(mockJsonError) - const { result } = renderHook(() => useNodesSyncDraft()) + const { result } = renderHook(() => useNodesSyncDraft(), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) await act(async () => { await result.current.doSyncWorkflowDraft(false) @@ -471,7 +491,9 @@ describe('useNodesSyncDraft', () => { }) mockSyncWorkflowDraft.mockRejectedValue(mockJsonError) - const { result } = renderHook(() => useNodesSyncDraft()) + const { result } = renderHook(() => useNodesSyncDraft(), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) await act(async () => { await result.current.doSyncWorkflowDraft(true) @@ -494,7 +516,9 @@ describe('useNodesSyncDraft', () => { { id: 'node-1', data: { type: 'start' }, position: { x: 0, y: 0 } }, ]) - const { result } = renderHook(() => useNodesSyncDraft()) + const { result } = renderHook(() => useNodesSyncDraft(), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) act(() => { result.current.syncWorkflowDraftWhenPageClose() @@ -517,7 +541,9 @@ describe('useNodesSyncDraft', () => { { id: 'node-1', data: { type: 'start' }, position: { x: 0, y: 0 } }, ]) - const { result } = renderHook(() => useNodesSyncDraft()) + const { result } = renderHook(() => useNodesSyncDraft(), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) act(() => { result.current.syncWorkflowDraftWhenPageClose() @@ -540,7 +566,9 @@ describe('useNodesSyncDraft', () => { { id: 'node-1', data: { type: 'start' }, position: { x: 0, y: 0 } }, ]) - const { result } = renderHook(() => useNodesSyncDraft()) + const { result } = renderHook(() => useNodesSyncDraft(), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) act(() => { result.current.syncWorkflowDraftWhenPageClose() @@ -555,7 +583,9 @@ describe('useNodesSyncDraft', () => { { id: 'node-1', data: { type: 'start' }, position: { x: 0, y: 0 } }, ]) - const { result } = renderHook(() => useNodesSyncDraft()) + const { result } = renderHook(() => useNodesSyncDraft(), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) act(() => { result.current.syncWorkflowDraftWhenPageClose() @@ -582,7 +612,9 @@ describe('useNodesSyncDraft', () => { { id: 'node-1', data: { type: 'start' }, position: { x: 0, y: 0 } }, ]) - const { result } = renderHook(() => useNodesSyncDraft()) + const { result } = renderHook(() => useNodesSyncDraft(), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) act(() => { result.current.syncWorkflowDraftWhenPageClose() diff --git a/web/app/components/rag-pipeline/hooks/__tests__/use-pipeline-config.spec.ts b/web/app/components/rag-pipeline/hooks/__tests__/use-pipeline-config.spec.ts index fdb26ea5525..2d43132f608 100644 --- a/web/app/components/rag-pipeline/hooks/__tests__/use-pipeline-config.spec.ts +++ b/web/app/components/rag-pipeline/hooks/__tests__/use-pipeline-config.spec.ts @@ -4,6 +4,7 @@ import { act, renderHook, waitFor } from '@testing-library/react' import { createElement } from 'react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vite-plus/test' import { WorkflowContext } from '@/app/components/workflow/context' +import { EditorResourceScope } from '@/app/components/workflow/editor-resource/provider' import { createWorkflowStore } from '@/app/components/workflow/store' import { consoleQuery } from '@/service/console' import { createDatasourceProvider } from '../../__tests__/datasource-fixtures' @@ -20,20 +21,24 @@ vi.mock('@/service/use-workflow', () => ({ useWorkflowConfig: mocks.workflowConf const catalog = consoleQuery.rag.pipelines.datasourcePlugins.get describe('usePipelineConfig', () => { + let pipelineId: string let client: QueryClient let store: ReturnType const wrapper = ({ children }: PropsWithChildren) => createElement( QueryClientProvider, { client }, - createElement(WorkflowContext.Provider, { value: store }, children), + createElement(EditorResourceScope, { + identity: { kind: 'knowledge-pipeline', pipelineId }, + children: createElement(WorkflowContext.Provider, { value: store }, children), + }), ) beforeEach(() => { vi.clearAllMocks() client = new QueryClient({ defaultOptions: { queries: { retry: false, gcTime: Infinity } } }) store = createWorkflowStore({ injectWorkflowStoreSliceFn: createRagPipelineSliceSlice }) - store.setState({ pipelineId: 'pipeline-1' }) + pipelineId = 'pipeline-1' mocks.request.mockResolvedValue(Response.json([])) }) afterEach(() => client.clear()) @@ -74,7 +79,7 @@ describe('usePipelineConfig', () => { }) it('does not request or hydrate datasource state before a pipeline is available', () => { - store.setState({ pipelineId: '' }) + pipelineId = '' client.setQueryData(catalog.queryKey(), [createDatasourceProvider()]) renderHook(usePipelineConfig, { wrapper }) @@ -84,12 +89,13 @@ describe('usePipelineConfig', () => { }) it('fetches and synchronizes when the pipeline becomes available', async () => { - store.setState({ pipelineId: '' }) + pipelineId = '' const providers = [createDatasourceProvider()] mocks.request.mockResolvedValue(Response.json(providers)) - renderHook(usePipelineConfig, { wrapper }) + const { rerender } = renderHook(usePipelineConfig, { wrapper }) - act(() => store.setState({ pipelineId: 'pipeline-2' })) + pipelineId = 'pipeline-2' + rerender() await waitFor(() => expect(store.getState().dataSourceList).toEqual(providers)) }) diff --git a/web/app/components/rag-pipeline/hooks/__tests__/use-pipeline-init.spec.ts b/web/app/components/rag-pipeline/hooks/__tests__/use-pipeline-init.spec.ts index 499777d5ec3..7d8e724cc9b 100644 --- a/web/app/components/rag-pipeline/hooks/__tests__/use-pipeline-init.spec.ts +++ b/web/app/components/rag-pipeline/hooks/__tests__/use-pipeline-init.spec.ts @@ -1,5 +1,6 @@ -import { renderHook, waitFor } from '@testing-library/react' +import { waitFor } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vite-plus/test' +import { renderEditorHook as renderHook } from '@/app/components/workflow/editor-resource/__tests__/render' import { createHttpError } from '@/service/http-error' import { usePipelineInit } from '../use-pipeline-init' @@ -90,17 +91,20 @@ describe('usePipelineInit', () => { describe('hook initialization', () => { it('should return data and isLoading', async () => { - const { result } = renderHook(() => usePipelineInit()) + const { result } = renderHook(() => usePipelineInit(), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) expect(result.current.isLoading).toBe(true) expect(result.current.data).toBeUndefined() }) - it('should set pipelineId in workflow store on mount', () => { - renderHook(() => usePipelineInit()) + it('should initialize the knowledge presentation metadata', () => { + renderHook(() => usePipelineInit(), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) expect(mockWorkflowStoreSetState).toHaveBeenCalledWith({ - pipelineId: 'test-pipeline-id', knowledgeName: 'Test Knowledge', knowledgeIcon: { icon: 'test-icon' }, }) @@ -109,7 +113,9 @@ describe('usePipelineInit', () => { describe('data fetching', () => { it('should fetch workflow draft on mount', async () => { - renderHook(() => usePipelineInit()) + renderHook(() => usePipelineInit(), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) await waitFor(() => { expect(mockFetchWorkflowDraft).toHaveBeenCalledWith( @@ -119,7 +125,9 @@ describe('usePipelineInit', () => { }) it('should set data after successful fetch', async () => { - const { result } = renderHook(() => usePipelineInit()) + const { result } = renderHook(() => usePipelineInit(), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) await waitFor(() => { expect(result.current.data).toBeDefined() @@ -127,7 +135,9 @@ describe('usePipelineInit', () => { }) it('should set isLoading to false after fetch', async () => { - const { result } = renderHook(() => usePipelineInit()) + const { result } = renderHook(() => usePipelineInit(), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) await waitFor(() => { expect(result.current.isLoading).toBe(false) @@ -135,7 +145,9 @@ describe('usePipelineInit', () => { }) it('should set draft updated at', async () => { - renderHook(() => usePipelineInit()) + renderHook(() => usePipelineInit(), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) await waitFor(() => { expect(mockSetDraftUpdatedAt).toHaveBeenCalledWith('2024-01-01T00:00:00Z') @@ -143,7 +155,9 @@ describe('usePipelineInit', () => { }) it('should set tool published status', async () => { - renderHook(() => usePipelineInit()) + renderHook(() => usePipelineInit(), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) await waitFor(() => { expect(mockSetToolPublished).toHaveBeenCalledWith(true) @@ -151,7 +165,9 @@ describe('usePipelineInit', () => { }) it('should set sync hash', async () => { - renderHook(() => usePipelineInit()) + renderHook(() => usePipelineInit(), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) await waitFor(() => { expect(mockSetSyncWorkflowDraftHash).toHaveBeenCalledWith('test-hash') @@ -159,7 +175,9 @@ describe('usePipelineInit', () => { }) it('should mark workflow data as loaded after a successful fetch', async () => { - renderHook(() => usePipelineInit()) + renderHook(() => usePipelineInit(), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) await waitFor(() => { expect(mockWorkflowStoreSetState).toHaveBeenCalledWith({ isWorkflowDataLoaded: true }) @@ -181,7 +199,9 @@ describe('usePipelineInit', () => { rag_pipeline_variables: [], }) - renderHook(() => usePipelineInit()) + renderHook(() => usePipelineInit(), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) await waitFor(() => { expect(mockSetEnvSecrets).toHaveBeenCalledWith({ 'env-1': 'secret-value' }) @@ -201,7 +221,9 @@ describe('usePipelineInit', () => { rag_pipeline_variables: [], }) - renderHook(() => usePipelineInit()) + renderHook(() => usePipelineInit(), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) await waitFor(() => { expect(mockSetEnvironmentVariables).toHaveBeenCalledWith([ @@ -221,7 +243,9 @@ describe('usePipelineInit', () => { rag_pipeline_variables: [], }) - renderHook(() => usePipelineInit()) + renderHook(() => usePipelineInit(), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) await waitFor(() => { expect(mockSetEnvSecrets).toHaveBeenCalledWith({}) @@ -241,7 +265,9 @@ describe('usePipelineInit', () => { rag_pipeline_variables: [{ variable: 'query', type: 'text-input' }], }) - renderHook(() => usePipelineInit()) + renderHook(() => usePipelineInit(), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) await waitFor(() => { expect(mockSetRagPipelineVariables).toHaveBeenCalledWith([ @@ -260,7 +286,9 @@ describe('usePipelineInit', () => { rag_pipeline_variables: undefined, }) - renderHook(() => usePipelineInit()) + renderHook(() => usePipelineInit(), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) await waitFor(() => { expect(mockSetRagPipelineVariables).toHaveBeenCalledWith([]) @@ -287,7 +315,9 @@ describe('usePipelineInit', () => { rag_pipeline_variables: [], }) - renderHook(() => usePipelineInit()) + renderHook(() => usePipelineInit(), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) await waitFor(() => { expect(mockWorkflowStoreSetState).toHaveBeenCalledWith({ @@ -306,7 +336,9 @@ describe('usePipelineInit', () => { mockFetchWorkflowDraft.mockRejectedValueOnce(mockJsonError) mockSyncWorkflowDraft.mockResolvedValue({ updated_at: '2024-01-02T00:00:00Z' }) - renderHook(() => usePipelineInit()) + renderHook(() => usePipelineInit(), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) await waitFor(() => { expect(mockSyncWorkflowDraft).toHaveBeenCalledWith({ @@ -332,7 +364,9 @@ describe('usePipelineInit', () => { }, ) - renderHook(() => usePipelineInit()) + renderHook(() => usePipelineInit(), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) await waitFor(() => { expect(mockFetchWorkflowDraft).toHaveBeenCalled() diff --git a/web/app/components/rag-pipeline/hooks/__tests__/use-pipeline-refresh-draft.spec.ts b/web/app/components/rag-pipeline/hooks/__tests__/use-pipeline-refresh-draft.spec.ts index ec44c58ebcb..d1b5278017f 100644 --- a/web/app/components/rag-pipeline/hooks/__tests__/use-pipeline-refresh-draft.spec.ts +++ b/web/app/components/rag-pipeline/hooks/__tests__/use-pipeline-refresh-draft.spec.ts @@ -1,6 +1,7 @@ -import { renderHook, waitFor } from '@testing-library/react' +import { waitFor } from '@testing-library/react' import { act } from 'react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vite-plus/test' +import { renderEditorHook as renderHook } from '@/app/components/workflow/editor-resource/__tests__/render' import { usePipelineRefreshDraft } from '../use-pipeline-refresh-draft' const mockWorkflowStoreGetState = vi.fn() @@ -66,7 +67,9 @@ describe('usePipelineRefreshDraft', () => { describe('hook initialization', () => { it('should return handleRefreshWorkflowDraft function', () => { - const { result } = renderHook(() => usePipelineRefreshDraft()) + const { result } = renderHook(() => usePipelineRefreshDraft(), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) expect(result.current.handleRefreshWorkflowDraft).toBeDefined() expect(typeof result.current.handleRefreshWorkflowDraft).toBe('function') @@ -75,7 +78,9 @@ describe('usePipelineRefreshDraft', () => { describe('handleRefreshWorkflowDraft', () => { it('should set syncing state to true at start', async () => { - const { result } = renderHook(() => usePipelineRefreshDraft()) + const { result } = renderHook(() => usePipelineRefreshDraft(), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) act(() => { result.current.handleRefreshWorkflowDraft() @@ -85,7 +90,9 @@ describe('usePipelineRefreshDraft', () => { }) it('should fetch workflow draft with correct URL', async () => { - const { result } = renderHook(() => usePipelineRefreshDraft()) + const { result } = renderHook(() => usePipelineRefreshDraft(), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) act(() => { result.current.handleRefreshWorkflowDraft() @@ -97,7 +104,9 @@ describe('usePipelineRefreshDraft', () => { }) it('should update workflow canvas with response data', async () => { - const { result } = renderHook(() => usePipelineRefreshDraft()) + const { result } = renderHook(() => usePipelineRefreshDraft(), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) act(() => { result.current.handleRefreshWorkflowDraft() @@ -109,7 +118,9 @@ describe('usePipelineRefreshDraft', () => { }) it('should update sync hash after fetch', async () => { - const { result } = renderHook(() => usePipelineRefreshDraft()) + const { result } = renderHook(() => usePipelineRefreshDraft(), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) act(() => { result.current.handleRefreshWorkflowDraft() @@ -132,7 +143,9 @@ describe('usePipelineRefreshDraft', () => { rag_pipeline_variables: [{ variable: 'query', type: 'text-input' }], }) - const { result } = renderHook(() => usePipelineRefreshDraft()) + const { result } = renderHook(() => usePipelineRefreshDraft(), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) act(() => { result.current.handleRefreshWorkflowDraft() @@ -146,7 +159,9 @@ describe('usePipelineRefreshDraft', () => { }) it('should set syncing state to false after completion', async () => { - const { result } = renderHook(() => usePipelineRefreshDraft()) + const { result } = renderHook(() => usePipelineRefreshDraft(), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) act(() => { result.current.handleRefreshWorkflowDraft() @@ -171,7 +186,9 @@ describe('usePipelineRefreshDraft', () => { ], }) - const { result } = renderHook(() => usePipelineRefreshDraft()) + const { result } = renderHook(() => usePipelineRefreshDraft(), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) act(() => { result.current.handleRefreshWorkflowDraft() @@ -196,7 +213,9 @@ describe('usePipelineRefreshDraft', () => { ], }) - const { result } = renderHook(() => usePipelineRefreshDraft()) + const { result } = renderHook(() => usePipelineRefreshDraft(), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) act(() => { result.current.handleRefreshWorkflowDraft() @@ -221,7 +240,9 @@ describe('usePipelineRefreshDraft', () => { environment_variables: [], }) - const { result } = renderHook(() => usePipelineRefreshDraft()) + const { result } = renderHook(() => usePipelineRefreshDraft(), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) act(() => { result.current.handleRefreshWorkflowDraft() @@ -244,7 +265,9 @@ describe('usePipelineRefreshDraft', () => { environment_variables: undefined, }) - const { result } = renderHook(() => usePipelineRefreshDraft()) + const { result } = renderHook(() => usePipelineRefreshDraft(), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) act(() => { result.current.handleRefreshWorkflowDraft() diff --git a/web/app/components/rag-pipeline/hooks/__tests__/use-pipeline-run.spec.ts b/web/app/components/rag-pipeline/hooks/__tests__/use-pipeline-run.spec.ts index 7853a84f46f..ecf56bd1a29 100644 --- a/web/app/components/rag-pipeline/hooks/__tests__/use-pipeline-run.spec.ts +++ b/web/app/components/rag-pipeline/hooks/__tests__/use-pipeline-run.spec.ts @@ -1,7 +1,7 @@ import type { VersionHistory } from '@/types/workflow' -import { renderHook } from '@testing-library/react' import { act } from 'react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vite-plus/test' +import { renderEditorHook as renderHook } from '@/app/components/workflow/editor-resource/__tests__/render' import { WorkflowRunningStatus } from '@/app/components/workflow/types' import { usePipelineRunByCanEdit } from '../use-pipeline-run' @@ -80,16 +80,10 @@ vi.mock('@/service/workflow', () => ({ const mockInvalidAllLastRun = vi.fn() const mockInvalidateRunHistory = vi.fn() vi.mock('@/service/use-workflow', () => ({ - useInvalidAllLastRun: () => mockInvalidAllLastRun, + useInvalidateAllLastRun: () => mockInvalidAllLastRun, useInvalidateWorkflowRunHistory: () => mockInvalidateRunHistory, })) -vi.mock('@/types/common', () => ({ - FlowType: { - ragPipeline: 'rag-pipeline', - }, -})) - describe('usePipelineRunByCanEdit', () => { const mockSetNodes = vi.fn() const mockGetNodes = vi.fn() @@ -149,35 +143,45 @@ describe('usePipelineRunByCanEdit', () => { describe('hook initialization', () => { it('should return handleBackupDraft function', () => { - const { result } = renderHook(() => usePipelineRunByCanEdit(true)) + const { result } = renderHook(() => usePipelineRunByCanEdit(true), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) expect(result.current.handleBackupDraft).toBeDefined() expect(typeof result.current.handleBackupDraft).toBe('function') }) it('should return handleLoadBackupDraft function', () => { - const { result } = renderHook(() => usePipelineRunByCanEdit(true)) + const { result } = renderHook(() => usePipelineRunByCanEdit(true), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) expect(result.current.handleLoadBackupDraft).toBeDefined() expect(typeof result.current.handleLoadBackupDraft).toBe('function') }) it('should return handleRun function', () => { - const { result } = renderHook(() => usePipelineRunByCanEdit(true)) + const { result } = renderHook(() => usePipelineRunByCanEdit(true), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) expect(result.current.handleRun).toBeDefined() expect(typeof result.current.handleRun).toBe('function') }) it('should return handleStopRun function', () => { - const { result } = renderHook(() => usePipelineRunByCanEdit(true)) + const { result } = renderHook(() => usePipelineRunByCanEdit(true), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) expect(result.current.handleStopRun).toBeDefined() expect(typeof result.current.handleStopRun).toBe('function') }) it('should return handleRestoreFromPublishedWorkflow function', () => { - const { result } = renderHook(() => usePipelineRunByCanEdit(true)) + const { result } = renderHook(() => usePipelineRunByCanEdit(true), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) expect(result.current.handleRestoreFromPublishedWorkflow).toBeDefined() expect(typeof result.current.handleRestoreFromPublishedWorkflow).toBe('function') @@ -186,7 +190,9 @@ describe('usePipelineRunByCanEdit', () => { describe('handleBackupDraft', () => { it('should backup draft when no backup exists', () => { - const { result } = renderHook(() => usePipelineRunByCanEdit(true)) + const { result } = renderHook(() => usePipelineRunByCanEdit(true), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) act(() => { result.current.handleBackupDraft() @@ -207,7 +213,9 @@ describe('usePipelineRunByCanEdit', () => { setWorkflowRunningData: mockSetWorkflowRunningData, }) - const { result } = renderHook(() => usePipelineRunByCanEdit(true)) + const { result } = renderHook(() => usePipelineRunByCanEdit(true), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) act(() => { result.current.handleBackupDraft() @@ -236,7 +244,9 @@ describe('usePipelineRunByCanEdit', () => { setWorkflowRunningData: mockSetWorkflowRunningData, }) - const { result } = renderHook(() => usePipelineRunByCanEdit(true)) + const { result } = renderHook(() => usePipelineRunByCanEdit(true), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) act(() => { result.current.handleLoadBackupDraft() @@ -262,7 +272,9 @@ describe('usePipelineRunByCanEdit', () => { setWorkflowRunningData: mockSetWorkflowRunningData, }) - const { result } = renderHook(() => usePipelineRunByCanEdit(true)) + const { result } = renderHook(() => usePipelineRunByCanEdit(true), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) act(() => { result.current.handleLoadBackupDraft() @@ -274,7 +286,9 @@ describe('usePipelineRunByCanEdit', () => { describe('handleStopRun', () => { it('should call stop workflow run service', () => { - const { result } = renderHook(() => usePipelineRunByCanEdit(true)) + const { result } = renderHook(() => usePipelineRunByCanEdit(true), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) act(() => { result.current.handleStopRun('task-123') @@ -298,7 +312,9 @@ describe('usePipelineRunByCanEdit', () => { rag_pipeline_variables: [{ variable: 'input', type: 'text-input' }], } - const { result } = renderHook(() => usePipelineRunByCanEdit(true)) + const { result } = renderHook(() => usePipelineRunByCanEdit(true), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) act(() => { result.current.handleRestoreFromPublishedWorkflow( @@ -324,7 +340,9 @@ describe('usePipelineRunByCanEdit', () => { rag_pipeline_variables: [], } - const { result } = renderHook(() => usePipelineRunByCanEdit(true)) + const { result } = renderHook(() => usePipelineRunByCanEdit(true), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) act(() => { result.current.handleRestoreFromPublishedWorkflow( @@ -346,7 +364,9 @@ describe('usePipelineRunByCanEdit', () => { rag_pipeline_variables: [{ variable: 'query', type: 'text-input' }], } - const { result } = renderHook(() => usePipelineRunByCanEdit(true)) + const { result } = renderHook(() => usePipelineRunByCanEdit(true), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) act(() => { result.current.handleRestoreFromPublishedWorkflow( @@ -370,7 +390,9 @@ describe('usePipelineRunByCanEdit', () => { rag_pipeline_variables: undefined, } - const { result } = renderHook(() => usePipelineRunByCanEdit(true)) + const { result } = renderHook(() => usePipelineRunByCanEdit(true), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) act(() => { result.current.handleRestoreFromPublishedWorkflow( @@ -385,7 +407,9 @@ describe('usePipelineRunByCanEdit', () => { describe('handleRun', () => { it('should sync workflow draft before running', async () => { - const { result } = renderHook(() => usePipelineRunByCanEdit(true)) + const { result } = renderHook(() => usePipelineRunByCanEdit(true), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) await act(async () => { await result.current.handleRun({ inputs: {} }) @@ -395,7 +419,9 @@ describe('usePipelineRunByCanEdit', () => { }) it('should reset node selection and running status', async () => { - const { result } = renderHook(() => usePipelineRunByCanEdit(true)) + const { result } = renderHook(() => usePipelineRunByCanEdit(true), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) await act(async () => { await result.current.handleRun({ inputs: {} }) @@ -405,7 +431,9 @@ describe('usePipelineRunByCanEdit', () => { }) it('should clear history workflow data', async () => { - const { result } = renderHook(() => usePipelineRunByCanEdit(true)) + const { result } = renderHook(() => usePipelineRunByCanEdit(true), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) await act(async () => { await result.current.handleRun({ inputs: {} }) @@ -415,7 +443,9 @@ describe('usePipelineRunByCanEdit', () => { }) it('should set initial running data', async () => { - const { result } = renderHook(() => usePipelineRunByCanEdit(true)) + const { result } = renderHook(() => usePipelineRunByCanEdit(true), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) await act(async () => { await result.current.handleRun({ inputs: {} }) @@ -434,7 +464,9 @@ describe('usePipelineRunByCanEdit', () => { }) it('should call ssePost with correct URL', async () => { - const { result } = renderHook(() => usePipelineRunByCanEdit(true)) + const { result } = renderHook(() => usePipelineRunByCanEdit(true), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) await act(async () => { await result.current.handleRun({ inputs: { query: 'test' } }) @@ -455,7 +487,9 @@ describe('usePipelineRunByCanEdit', () => { capturedCallbacks = callbacks }) - const { result } = renderHook(() => usePipelineRunByCanEdit(true)) + const { result } = renderHook(() => usePipelineRunByCanEdit(true), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) await act(async () => { await result.current.handleRun({ inputs: {} }, { onWorkflowStarted }) @@ -477,7 +511,9 @@ describe('usePipelineRunByCanEdit', () => { capturedCallbacks = callbacks }) - const { result } = renderHook(() => usePipelineRunByCanEdit(true)) + const { result } = renderHook(() => usePipelineRunByCanEdit(true), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) await act(async () => { await result.current.handleRun({ inputs: {} }, { onWorkflowFinished }) @@ -499,7 +535,9 @@ describe('usePipelineRunByCanEdit', () => { capturedCallbacks = callbacks }) - const { result } = renderHook(() => usePipelineRunByCanEdit(true)) + const { result } = renderHook(() => usePipelineRunByCanEdit(true), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) await act(async () => { await result.current.handleRun({ inputs: {} }, { onError }) @@ -521,7 +559,9 @@ describe('usePipelineRunByCanEdit', () => { capturedCallbacks = callbacks }) - const { result } = renderHook(() => usePipelineRunByCanEdit(true)) + const { result } = renderHook(() => usePipelineRunByCanEdit(true), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) await act(async () => { await result.current.handleRun({ inputs: {} }, { onNodeStarted }) @@ -542,7 +582,9 @@ describe('usePipelineRunByCanEdit', () => { capturedCallbacks = callbacks }) - const { result } = renderHook(() => usePipelineRunByCanEdit(true)) + const { result } = renderHook(() => usePipelineRunByCanEdit(true), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) await act(async () => { await result.current.handleRun({ inputs: {} }, { onNodeFinished }) @@ -563,7 +605,9 @@ describe('usePipelineRunByCanEdit', () => { capturedCallbacks = callbacks }) - const { result } = renderHook(() => usePipelineRunByCanEdit(true)) + const { result } = renderHook(() => usePipelineRunByCanEdit(true), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) await act(async () => { await result.current.handleRun({ inputs: {} }, { onIterationStart }) @@ -584,7 +628,9 @@ describe('usePipelineRunByCanEdit', () => { capturedCallbacks = callbacks }) - const { result } = renderHook(() => usePipelineRunByCanEdit(true)) + const { result } = renderHook(() => usePipelineRunByCanEdit(true), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) await act(async () => { await result.current.handleRun({ inputs: {} }, { onIterationNext }) @@ -605,7 +651,9 @@ describe('usePipelineRunByCanEdit', () => { capturedCallbacks = callbacks }) - const { result } = renderHook(() => usePipelineRunByCanEdit(true)) + const { result } = renderHook(() => usePipelineRunByCanEdit(true), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) await act(async () => { await result.current.handleRun({ inputs: {} }, { onIterationFinish }) @@ -626,7 +674,9 @@ describe('usePipelineRunByCanEdit', () => { capturedCallbacks = callbacks }) - const { result } = renderHook(() => usePipelineRunByCanEdit(true)) + const { result } = renderHook(() => usePipelineRunByCanEdit(true), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) await act(async () => { await result.current.handleRun({ inputs: {} }, { onLoopStart }) @@ -647,7 +697,9 @@ describe('usePipelineRunByCanEdit', () => { capturedCallbacks = callbacks }) - const { result } = renderHook(() => usePipelineRunByCanEdit(true)) + const { result } = renderHook(() => usePipelineRunByCanEdit(true), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) await act(async () => { await result.current.handleRun({ inputs: {} }, { onLoopNext }) @@ -668,7 +720,9 @@ describe('usePipelineRunByCanEdit', () => { capturedCallbacks = callbacks }) - const { result } = renderHook(() => usePipelineRunByCanEdit(true)) + const { result } = renderHook(() => usePipelineRunByCanEdit(true), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) await act(async () => { await result.current.handleRun({ inputs: {} }, { onLoopFinish }) @@ -689,7 +743,9 @@ describe('usePipelineRunByCanEdit', () => { capturedCallbacks = callbacks }) - const { result } = renderHook(() => usePipelineRunByCanEdit(true)) + const { result } = renderHook(() => usePipelineRunByCanEdit(true), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) await act(async () => { await result.current.handleRun({ inputs: {} }, { onNodeRetry }) @@ -710,7 +766,9 @@ describe('usePipelineRunByCanEdit', () => { capturedCallbacks = callbacks }) - const { result } = renderHook(() => usePipelineRunByCanEdit(true)) + const { result } = renderHook(() => usePipelineRunByCanEdit(true), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) await act(async () => { await result.current.handleRun({ inputs: {} }, { onAgentLog }) @@ -730,7 +788,9 @@ describe('usePipelineRunByCanEdit', () => { capturedCallbacks = callbacks }) - const { result } = renderHook(() => usePipelineRunByCanEdit(true)) + const { result } = renderHook(() => usePipelineRunByCanEdit(true), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) await act(async () => { await result.current.handleRun({ inputs: {} }) @@ -750,7 +810,9 @@ describe('usePipelineRunByCanEdit', () => { capturedCallbacks = callbacks }) - const { result } = renderHook(() => usePipelineRunByCanEdit(true)) + const { result } = renderHook(() => usePipelineRunByCanEdit(true), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) await act(async () => { await result.current.handleRun({ inputs: {} }) @@ -771,7 +833,9 @@ describe('usePipelineRunByCanEdit', () => { capturedCallbacks = callbacks }) - const { result } = renderHook(() => usePipelineRunByCanEdit(true)) + const { result } = renderHook(() => usePipelineRunByCanEdit(true), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) await act(async () => { await result.current.handleRun({ inputs: {} }, { @@ -789,7 +853,9 @@ describe('usePipelineRunByCanEdit', () => { capturedCallbacks = callbacks }) - const { result } = renderHook(() => usePipelineRunByCanEdit(true)) + const { result } = renderHook(() => usePipelineRunByCanEdit(true), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) await act(async () => { await result.current.handleRun({ inputs: {} }) diff --git a/web/app/components/rag-pipeline/hooks/__tests__/use-update-dsl-modal.spec.ts b/web/app/components/rag-pipeline/hooks/__tests__/use-update-dsl-modal.spec.ts index 847702e82be..72f72cdcf8d 100644 --- a/web/app/components/rag-pipeline/hooks/__tests__/use-update-dsl-modal.spec.ts +++ b/web/app/components/rag-pipeline/hooks/__tests__/use-update-dsl-modal.spec.ts @@ -1,5 +1,6 @@ -import { act, renderHook } from '@testing-library/react' +import { act } from '@testing-library/react' import { beforeEach, describe, expect, it, vi } from 'vite-plus/test' +import { renderEditorHook as renderHook } from '@/app/components/workflow/editor-resource/__tests__/render' import { DSLImportMode, DSLImportStatus } from '@/models/app' import { httpErrorFromResponse } from '@/service/http-error' import { useUpdateDSLModal } from '../use-update-dsl-modal' @@ -100,11 +101,13 @@ describe('useUpdateDSLModal', () => { const mockOnImport = vi.fn() const renderUpdateDSLModal = (overrides?: { onImport?: () => void }) => - renderHook(() => - useUpdateDSLModal({ - onCancel: mockOnCancel, - onImport: overrides?.onImport ?? mockOnImport, - }), + renderHook( + () => + useUpdateDSLModal({ + onCancel: mockOnCancel, + onImport: overrides?.onImport ?? mockOnImport, + }), + { resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' } }, ) beforeEach(() => { @@ -599,7 +602,9 @@ describe('useUpdateDSLModal', () => { describe('optional onImport', () => { it('should work without onImport callback', async () => { - const { result } = renderHook(() => useUpdateDSLModal({ onCancel: mockOnCancel })) + const { result } = renderHook(() => useUpdateDSLModal({ onCancel: mockOnCancel }), { + resource: { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline-id' }, + }) act(() => { result.current.handleFile(createFile()) diff --git a/web/app/components/snippets/__tests__/index.spec.tsx b/web/app/components/snippets/__tests__/index.spec.tsx index a168799ac86..3fa561e563c 100644 --- a/web/app/components/snippets/__tests__/index.spec.tsx +++ b/web/app/components/snippets/__tests__/index.spec.tsx @@ -15,13 +15,9 @@ vi.mock('../hooks/use-snippet-init', () => ({ })) vi.mock('../components/snippet-main', () => ({ - default: ({ - hasPublishedWorkflow, - snippetId, - }: { - hasPublishedWorkflow: boolean - snippetId: string - }) =>
{`${snippetId}:${String(hasPublishedWorkflow)}`}
, + default: ({ hasPublishedWorkflow }: { hasPublishedWorkflow: boolean }) => ( +
{String(hasPublishedWorkflow)}
+ ), })) vi.mock('@/next/navigation', () => ({ @@ -163,7 +159,8 @@ describe('SnippetPage', () => { render() expect(screen.getByTestId('workflow-default-context')).toBeInTheDocument() - expect(screen.getByTestId('snippet-main')).toHaveTextContent('snippet-1') + expect(mockUseSnippetInit).toHaveBeenCalledWith('snippet-1') + expect(screen.getByTestId('snippet-main')).toHaveTextContent('true') expect(screen.queryByRole('main')).not.toBeInTheDocument() }) @@ -176,7 +173,7 @@ describe('SnippetPage', () => { expect(capturedWorkflowDefaultContextProps?.edges).toEqual( mockPublishedSnippetDetail.graph.edges, ) - expect(screen.getByTestId('snippet-main')).toHaveTextContent('snippet-1:true') + expect(screen.getByTestId('snippet-main')).toHaveTextContent('true') }) it('should initialize workflow context with draft graph data when the published workflow is empty', () => { @@ -198,7 +195,7 @@ describe('SnippetPage', () => { expect(capturedWorkflowDefaultContextProps?.nodes).toEqual(mockDraftSnippetDetail.graph.nodes) expect(capturedWorkflowDefaultContextProps?.edges).toEqual(mockDraftSnippetDetail.graph.edges) - expect(screen.getByTestId('snippet-main')).toHaveTextContent('snippet-1:false') + expect(screen.getByTestId('snippet-main')).toHaveTextContent('false') }) it('should render loading fallback when orchestrate data is unavailable', () => { diff --git a/web/app/components/snippets/components/__tests__/snippet-main.spec.tsx b/web/app/components/snippets/components/__tests__/snippet-main.spec.tsx index adba9b918d2..3ffad9db548 100644 --- a/web/app/components/snippets/components/__tests__/snippet-main.spec.tsx +++ b/web/app/components/snippets/components/__tests__/snippet-main.spec.tsx @@ -83,13 +83,7 @@ vi.mock('@/service/use-snippet-workflows', () => ({ }), })) -vi.mock('../../hooks/use-configs-map', () => ({ - useConfigsMap: () => ({ - flowId: 'snippet-1', - flowType: 'snippet', - fileSettings: {}, - }), -})) +vi.mock('../../hooks/use-file-settings', () => ({ useFileSettings: () => ({ enabled: true }) })) vi.mock('@/app/components/workflow/hooks/use-fetch-workflow-inspect-vars', () => ({ useSetWorkflowVarsWithValue: () => ({ @@ -103,12 +97,12 @@ vi.mock('@/app/components/workflow/hooks/use-checklist', () => ({ }), })) -vi.mock('@/app/components/workflow-app/hooks/use-available-nodes-meta-data', () => ({ - useAvailableNodesMetaData: () => mockUseAvailableNodesMetaData(), +vi.mock('@/app/components/workflow/node-catalog', () => ({ + useWorkflowNodeCatalog: () => mockUseAvailableNodesMetaData(), })) -vi.mock('../../hooks/use-inspect-vars-crud', () => ({ - useInspectVarsCrud: () => mockInspectVarsCrud, +vi.mock('@/app/components/workflow/hooks/use-inspect-vars-crud-common', () => ({ + useInspectVarsCrudCommon: () => mockInspectVarsCrud, })) vi.mock('../../hooks/use-nodes-sync-draft', () => ({ @@ -234,7 +228,6 @@ const renderSnippetMain = ({ draftPayload={payload} hasInitialDraftChanges={hasInitialDraftChanges} hasPublishedWorkflow={hasPublishedWorkflow} - snippetId="snippet-1" nodes={workflowNodes} edges={[] as WorkflowProps['edges']} viewport={{ x: 0, y: 0, zoom: 1 }} @@ -243,6 +236,7 @@ const renderSnippetMain = ({ draftViewport={{ x: 0, y: 0, zoom: 1 }} />, { + resource: { kind: 'snippet', snippetId: 'snippet-1' }, initialStoreState: { nodes: currentNodes, }, diff --git a/web/app/components/snippets/hooks/__tests__/use-configs-map.spec.ts b/web/app/components/snippets/hooks/__tests__/use-configs-map.spec.ts deleted file mode 100644 index 571516a4355..00000000000 --- a/web/app/components/snippets/hooks/__tests__/use-configs-map.spec.ts +++ /dev/null @@ -1,51 +0,0 @@ -import type { FileUploadConfigResponse } from '@/models/common' -import { renderWorkflowHook } from '@/app/components/workflow/__tests__/workflow-test-env' -import { Resolution, TransferMethod } from '@/types/app' -import { FlowType } from '@/types/common' -import { useConfigsMap } from '../use-configs-map' - -describe('useConfigsMap', () => { - it('should build snippet workflow configs from the snippet id and workflow file upload config', () => { - const fileUploadConfig = { - batch_count_limit: 5, - image_file_batch_limit: 3, - file_size_limit: 15, - workflow_file_upload_limit: 10, - } as FileUploadConfigResponse - - const { result, rerender } = renderWorkflowHook( - ({ snippetId }: { snippetId: string }) => useConfigsMap(snippetId), - { - initialProps: { snippetId: 'snippet-1' }, - initialStoreState: { - fileUploadConfig, - }, - }, - ) - - expect(result.current).toEqual({ - flowId: 'snippet-1', - flowType: FlowType.snippet, - fileSettings: { - image: { - enabled: false, - detail: Resolution.high, - number_limits: 3, - transfer_methods: [TransferMethod.local_file, TransferMethod.remote_url], - }, - fileUploadConfig, - }, - }) - - const firstConfigs = result.current - - rerender({ snippetId: 'snippet-1' }) - - expect(result.current).toBe(firstConfigs) - - rerender({ snippetId: 'snippet-2' }) - - expect(result.current.flowId).toBe('snippet-2') - expect(result.current).not.toBe(firstConfigs) - }) -}) diff --git a/web/app/components/snippets/hooks/__tests__/use-file-settings.spec.ts b/web/app/components/snippets/hooks/__tests__/use-file-settings.spec.ts new file mode 100644 index 00000000000..5a5a681e058 --- /dev/null +++ b/web/app/components/snippets/hooks/__tests__/use-file-settings.spec.ts @@ -0,0 +1,28 @@ +import type { FileUploadConfigResponse } from '@/models/common' +import { act } from '@testing-library/react' +import { renderWorkflowHook } from '@/app/components/workflow/__tests__/workflow-test-env' +import { Resolution, TransferMethod } from '@/types/app' +import { useFileSettings } from '../use-file-settings' + +describe('useFileSettings', () => { + it('updates upload limits from the editor while keeping snippet image inputs disabled', () => { + const fileUploadConfig = { workflow_file_upload_limit: 10 } as FileUploadConfigResponse + const { result, store } = renderWorkflowHook(useFileSettings, { + resource: { kind: 'snippet', snippetId: 'snippet-1' }, + initialStoreState: { fileUploadConfig }, + }) + expect(result.current).toEqual({ + image: { + enabled: false, + detail: Resolution.high, + number_limits: 3, + transfer_methods: [TransferMethod.local_file, TransferMethod.remote_url], + }, + fileUploadConfig, + }) + act(() => + store.setState({ fileUploadConfig: { ...fileUploadConfig, workflow_file_upload_limit: 20 } }), + ) + expect(result.current.fileUploadConfig?.workflow_file_upload_limit).toBe(20) + }) +}) diff --git a/web/app/components/snippets/hooks/__tests__/use-inspect-vars-crud.spec.ts b/web/app/components/snippets/hooks/__tests__/use-inspect-vars-crud.spec.ts deleted file mode 100644 index 7b19f6c9f0a..00000000000 --- a/web/app/components/snippets/hooks/__tests__/use-inspect-vars-crud.spec.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { renderHook } from '@testing-library/react' -import { beforeEach, describe, expect, it, vi } from 'vite-plus/test' -import { useInspectVarsCrud } from '../use-inspect-vars-crud' - -const mockApis = { - hasNodeInspectVars: vi.fn(), - hasSetInspectVar: vi.fn(), - fetchInspectVarValue: vi.fn(), - editInspectVarValue: vi.fn(), - renameInspectVarName: vi.fn(), - appendNodeInspectVars: vi.fn(), - deleteInspectVar: vi.fn(), - deleteNodeInspectorVars: vi.fn(), - deleteAllInspectorVars: vi.fn(), - isInspectVarEdited: vi.fn(), - resetToLastRunVar: vi.fn(), - invalidateSysVarValues: vi.fn(), - resetConversationVar: vi.fn(), - invalidateConversationVarValues: vi.fn(), -} - -const mockUseInspectVarsCrudCommon = vi.fn(() => mockApis) -vi.mock('@/app/components/workflow/hooks/use-inspect-vars-crud-common', () => ({ - useInspectVarsCrudCommon: (...args: Parameters) => - mockUseInspectVarsCrudCommon(...args), -})) - -const mockConfigsMap = { - flowId: 'snippet-123', - flowType: 'snippet', - fileSettings: { - image: { enabled: false }, - fileUploadConfig: {}, - }, -} - -vi.mock('../use-configs-map', () => ({ - useConfigsMap: () => mockConfigsMap, -})) - -describe('useInspectVarsCrud', () => { - beforeEach(() => { - vi.clearAllMocks() - }) - - describe('Composition', () => { - it('should pass configsMap to useInspectVarsCrudCommon', () => { - renderHook(() => useInspectVarsCrud('snippet-123')) - - expect(mockUseInspectVarsCrudCommon).toHaveBeenCalledWith( - expect.objectContaining({ - flowId: 'snippet-123', - flowType: 'snippet', - }), - ) - }) - - it('should return all APIs from useInspectVarsCrudCommon', () => { - const { result } = renderHook(() => useInspectVarsCrud('snippet-123')) - - expect(result.current.hasNodeInspectVars).toBe(mockApis.hasNodeInspectVars) - expect(result.current.fetchInspectVarValue).toBe(mockApis.fetchInspectVarValue) - expect(result.current.editInspectVarValue).toBe(mockApis.editInspectVarValue) - expect(result.current.deleteInspectVar).toBe(mockApis.deleteInspectVar) - expect(result.current.deleteAllInspectorVars).toBe(mockApis.deleteAllInspectorVars) - expect(result.current.resetToLastRunVar).toBe(mockApis.resetToLastRunVar) - expect(result.current.resetConversationVar).toBe(mockApis.resetConversationVar) - }) - }) - - describe('API Surface', () => { - it('should expose all expected API methods', () => { - const { result } = renderHook(() => useInspectVarsCrud('snippet-123')) - - const expectedKeys = [ - 'hasNodeInspectVars', - 'hasSetInspectVar', - 'fetchInspectVarValue', - 'editInspectVarValue', - 'renameInspectVarName', - 'appendNodeInspectVars', - 'deleteInspectVar', - 'deleteNodeInspectorVars', - 'deleteAllInspectorVars', - 'isInspectVarEdited', - 'resetToLastRunVar', - 'invalidateSysVarValues', - 'resetConversationVar', - 'invalidateConversationVarValues', - ] - - for (const key of expectedKeys) expect(result.current).toHaveProperty(key) - }) - }) -}) diff --git a/web/app/components/snippets/hooks/__tests__/use-snippet-run.spec.ts b/web/app/components/snippets/hooks/__tests__/use-snippet-run.spec.ts index 6c0edfe37ae..e9904cb641c 100644 --- a/web/app/components/snippets/hooks/__tests__/use-snippet-run.spec.ts +++ b/web/app/components/snippets/hooks/__tests__/use-snippet-run.spec.ts @@ -1,4 +1,5 @@ -import { act, renderHook } from '@testing-library/react' +import { act } from '@testing-library/react' +import { renderEditorHook as renderHook } from '@/app/components/workflow/editor-resource/__tests__/render' import { WorkflowRunningStatus } from '@/app/components/workflow/types' import { useSnippetRun } from '../use-snippet-run' @@ -104,7 +105,7 @@ vi.mock('@/service/base', () => ({ })) vi.mock('@/service/use-workflow', () => ({ - useInvalidAllLastRun: () => mocks.mockInvalidateAllLastRun, + useInvalidateAllLastRun: () => mocks.mockInvalidateAllLastRun, useInvalidateWorkflowRunHistory: () => mocks.mockInvalidateRunHistory, })) @@ -139,7 +140,9 @@ describe('useSnippetRun', () => { it('stops a snippet workflow with the provided task id', () => { mocks.workflowStoreState.workflowRunningData = createRunningData() - const { result } = renderHook(() => useSnippetRun('snippet-1')) + const { result } = renderHook(() => useSnippetRun(), { + resource: { kind: 'snippet', snippetId: 'snippet-1' }, + }) act(() => { result.current.handleStopRun('task-1') @@ -155,7 +158,9 @@ describe('useSnippetRun', () => { it('does not fall back to the workflow running task id when stop is called without one', () => { mocks.workflowStoreState.workflowRunningData = createRunningData('task-from-store') - const { result } = renderHook(() => useSnippetRun('snippet-1')) + const { result } = renderHook(() => useSnippetRun(), { + resource: { kind: 'snippet', snippetId: 'snippet-1' }, + }) act(() => { result.current.handleStopRun('') @@ -169,7 +174,9 @@ describe('useSnippetRun', () => { it('does not call the stop endpoint when task id is unavailable', () => { mocks.workflowStoreState.workflowRunningData = createRunningData() - const { result } = renderHook(() => useSnippetRun('snippet-1')) + const { result } = renderHook(() => useSnippetRun(), { + resource: { kind: 'snippet', snippetId: 'snippet-1' }, + }) act(() => { result.current.handleStopRun('') diff --git a/web/app/components/workflow-app/__tests__/index.spec.tsx b/web/app/components/workflow-app/__tests__/index.spec.tsx index 7aa1cf62a96..16a851138be 100644 --- a/web/app/components/workflow-app/__tests__/index.spec.tsx +++ b/web/app/components/workflow-app/__tests__/index.spec.tsx @@ -7,7 +7,6 @@ import { createQueryClientWrapper } from '@/test/console/query-client' import { seedAppDetail } from '@/test/console/query-data' import { render as renderWithConsoleState } from '@/test/console/render' import { createAppDetailFixture } from '@/test/fixtures/app' -import { AppModeEnum } from '@/types/app' import { AppACLPermission } from '@/utils/permission' import WorkflowApp from '../index' @@ -229,7 +228,7 @@ describe('WorkflowApp', () => { mockIsWorkflowDataLoaded = true appDetail = createAppDetailFixture({ id: 'app-1', - mode: AppModeEnum.WORKFLOW, + mode: 'workflow' as const, permission_keys: [AppACLPermission.TestAndRun], }) workflowInitState = { @@ -359,7 +358,7 @@ describe('WorkflowApp', () => { searchParamsValue = 'run-1' appDetail = createAppDetailFixture({ id: 'app-1', - mode: AppModeEnum.WORKFLOW, + mode: 'workflow' as const, permission_keys: [AppACLPermission.ViewLayout], }) diff --git a/web/app/components/workflow-app/__tests__/provider-contract.spec.tsx b/web/app/components/workflow-app/__tests__/provider-contract.spec.tsx index 41b4e233c08..71b3be6ec12 100644 --- a/web/app/components/workflow-app/__tests__/provider-contract.spec.tsx +++ b/web/app/components/workflow-app/__tests__/provider-contract.spec.tsx @@ -6,7 +6,6 @@ import { createQueryClientWrapper } from '@/test/console/query-client' import { seedAppDetail } from '@/test/console/query-data' import { render as renderWithConsoleState } from '@/test/console/render' import { createAppDetailFixture } from '@/test/fixtures/app' -import { AppModeEnum } from '@/types/app' import WorkflowApp from '../index' const render = (ui: ReactElement) => { @@ -16,7 +15,7 @@ const render = (ui: ReactElement) => { createAppDetailFixture({ id: 'app-1', name: 'Workflow App', - mode: AppModeEnum.WORKFLOW, + mode: 'workflow' as const, permission_keys: [], }), ) @@ -66,11 +65,11 @@ vi.mock('../hooks/use-workflow-init', async () => { return { useWorkflowInit: () => { - useStore((state) => state.appId) + useStore((state) => state.syncWorkflowDraftHash) const workflowStore = useWorkflowStore() React.useEffect(() => { - workflowStore.setState({ appId: 'initialized-app' }) + workflowStore.setState({ syncWorkflowDraftHash: 'initialized-draft' }) }, [workflowStore]) return { @@ -115,7 +114,7 @@ vi.mock('../components/workflow-main', async () => { await import('@/app/components/workflow/workflow-history-store') const WorkflowMainProbe = () => { - const appId = useStore((state) => state.appId) + const appId = useStore((state) => state.syncWorkflowDraftHash) const hasWorkflowSlice = useStore((state) => typeof state.setNotInitialWorkflow === 'function') const { store } = useWorkflowHistoryStore() @@ -139,6 +138,8 @@ describe('WorkflowApp provider contract', () => { , ) - expect(await screen.findByText('app:initialized-app history:1 slice:true')).toBeInTheDocument() + expect( + await screen.findByText('app:initialized-draft history:1 slice:true'), + ).toBeInTheDocument() }) }) diff --git a/web/app/components/workflow-app/components/__tests__/workflow-children.spec.tsx b/web/app/components/workflow-app/components/__tests__/workflow-children.spec.tsx index 64003955eae..4e81561637d 100644 --- a/web/app/components/workflow-app/components/__tests__/workflow-children.spec.tsx +++ b/web/app/components/workflow-app/components/__tests__/workflow-children.spec.tsx @@ -1,7 +1,8 @@ -import { act, render, screen } from '@testing-library/react' +import { act, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' import * as React from 'react' import { DSL_EXPORT_CHECK } from '@/app/components/workflow/constants' +import { renderEditor as render } from '@/app/components/workflow/editor-resource/__tests__/render' import { BlockEnum } from '@/app/components/workflow/types' import WorkflowChildren from '../workflow-children' @@ -135,8 +136,8 @@ vi.mock('@/app/components/workflow/utils', async (importOriginal) => { } }) -vi.mock('../../hooks/use-available-nodes-meta-data', () => ({ - useAvailableNodesMetaData: () => ({ +vi.mock('@/app/components/workflow/node-catalog', () => ({ + useWorkflowNodeCatalog: () => ({ nodesMap: { [BlockEnum.Start]: { defaultValue: { @@ -160,9 +161,12 @@ vi.mock('../../hooks/use-available-nodes-meta-data', () => ({ }), })) -vi.mock('../../hooks/use-is-chat-mode', () => ({ - useIsChatMode: () => false, -})) +vi.mock('@/app/components/workflow/editor-resource/state', async (importOriginal) => { + const actual = + await importOriginal() + const { atom } = await import('jotai') + return { ...actual, isChatflowAtom: atom(() => false) } +}) vi.mock('../../hooks/use-auto-onboarding', () => ({ useAutoOnboarding: () => ({ @@ -366,7 +370,7 @@ describe('WorkflowChildren', () => { showImportDSLModal: true, } - render() + render(, { resource: { kind: 'app', appId: 'app-1', mode: 'workflow' } }) expect(screen.getByTestId('plugin-dependency')).toBeInTheDocument() expect(screen.getByTestId('workflow-header')).toBeInTheDocument() @@ -386,7 +390,7 @@ describe('WorkflowChildren', () => { it('should react to DSL export check events by showing the confirm modal and closing it', async () => { const user = userEvent.setup() - render() + render(, { resource: { kind: 'app', appId: 'app-1', mode: 'workflow' } }) await act(async () => { eventSubscription?.({ @@ -410,7 +414,7 @@ describe('WorkflowChildren', () => { }) it('should ignore unrelated workflow events when listening for DSL export checks', async () => { - render() + render(, { resource: { kind: 'app', appId: 'app-1', mode: 'workflow' } }) await act(async () => { eventSubscription?.({ @@ -431,7 +435,7 @@ describe('WorkflowChildren', () => { showOnboarding: true, } - render() + render(, { resource: { kind: 'app', appId: 'app-1', mode: 'workflow' } }) expect(await screen.findByTestId('workflow-onboarding-modal')).toBeInTheDocument() @@ -447,7 +451,7 @@ describe('WorkflowChildren', () => { showOnboarding: true, } - render() + render(, { resource: { kind: 'app', appId: 'app-1', mode: 'workflow' } }) expect(screen.queryByTestId('workflow-onboarding-modal')).not.toBeInTheDocument() expect(mockSetNodes).not.toHaveBeenCalled() @@ -461,7 +465,7 @@ describe('WorkflowChildren', () => { showOnboarding: true, } - render() + render(, { resource: { kind: 'app', appId: 'app-1', mode: 'workflow' } }) await user.click(await screen.findByRole('button', { name: /^select-start-node$/i })) @@ -490,7 +494,7 @@ describe('WorkflowChildren', () => { showOnboarding: true, } - render() + render(, { resource: { kind: 'app', appId: 'app-1', mode: 'workflow' } }) await user.click(await screen.findByRole('button', { name: /select-start-node-with-config/i })) @@ -514,7 +518,7 @@ describe('WorkflowChildren', () => { showOnboarding: true, } - render() + render(, { resource: { kind: 'app', appId: 'app-1', mode: 'workflow' } }) await user.click(await screen.findByRole('button', { name: /^select-trigger-plugin$/i })) @@ -550,7 +554,7 @@ describe('WorkflowChildren', () => { showOnboarding: true, } - render() + render(, { resource: { kind: 'app', appId: 'app-1', mode: 'workflow' } }) await user.click(await screen.findByRole('button', { name: /select-trigger-plugin-fallback/i })) diff --git a/web/app/components/workflow-app/components/__tests__/workflow-main.spec.tsx b/web/app/components/workflow-app/components/__tests__/workflow-main.spec.tsx index 66bec7abc3f..74d8af69306 100644 --- a/web/app/components/workflow-app/components/__tests__/workflow-main.spec.tsx +++ b/web/app/components/workflow-app/components/__tests__/workflow-main.spec.tsx @@ -1,20 +1,14 @@ import type { AppDetailWithSite } from '@dify/contracts/api/console/apps/types.gen' import type { ReactElement, ReactNode } from 'react' import type { WorkflowProps } from '@/app/components/workflow' -import { - act, - fireEvent, - render as renderWithQueryClient, - screen, - waitFor, -} from '@testing-library/react' +import { act, fireEvent, screen, waitFor } from '@testing-library/react' +import { renderEditor as renderWithQueryClient } from '@/app/components/workflow/editor-resource/__tests__/render' import { ChatVarType } from '@/app/components/workflow/panel/chat-variable-panel/type' import { BlockEnum } from '@/app/components/workflow/types' import { createAccountProfileQueryClient } from '@/test/console/account-profile' import { createQueryClientWrapper } from '@/test/console/query-client' import { seedAppDetail } from '@/test/console/query-data' import { createAppDetailFixture } from '@/test/fixtures/app' -import { AppModeEnum } from '@/types/app' import { AppACLPermission } from '@/utils/permission' import WorkflowMain from '../workflow-main' @@ -23,7 +17,10 @@ let appDetail: AppDetailWithSite const render = (ui: ReactElement) => { const queryClient = createAccountProfileQueryClient() seedAppDetail(queryClient, appDetail) - return renderWithQueryClient(ui, { wrapper: createQueryClientWrapper(queryClient) }) + return renderWithQueryClient(ui, { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + wrapper: createQueryClientWrapper(queryClient), + }) } vi.mock('@/next/navigation', async (importOriginal) => ({ @@ -129,6 +126,8 @@ vi.mock('@/context/workspace-state', async () => { }) vi.mock('@/app/components/base/features/hooks', () => ({ + useFeatures: (selector: (state: { features: { file: { enabled: boolean } } }) => unknown) => + selector({ features: { file: { enabled: true } } }), useFeaturesStore: () => ({ getState: () => ({ setFeatures: mockSetFeatures, @@ -302,17 +301,13 @@ vi.mock('@/app/components/workflow', () => ({ }, })) -vi.mock('../../hooks/use-available-nodes-meta-data', () => ({ - useAvailableNodesMetaData: () => ({ +vi.mock('@/app/components/workflow/node-catalog', () => ({ + useWorkflowNodeCatalog: () => ({ nodes: [{ id: 'start' }], nodesMap: { start: { id: 'start' } }, }), })) -vi.mock('../../hooks/use-configs-map', () => ({ - useConfigsMap: () => ({ flowId: 'app-1', flowType: 'app-flow', fileSettings: { enabled: true } }), -})) - vi.mock('../../hooks/use-DSL', () => ({ useDSL: () => ({ exportCheck: hookFns.exportCheck, handleExportDSL: hookFns.handleExportDSL }), useDSLByCanEdit: () => ({ @@ -325,8 +320,8 @@ vi.mock('../../hooks/use-get-run-and-trace-url', () => ({ useGetRunAndTraceUrl: () => ({ getWorkflowRunAndTraceUrl: hookFns.getWorkflowRunAndTraceUrl }), })) -vi.mock('../../hooks/use-inspect-vars-crud', () => ({ - useInspectVarsCrud: () => ({ +vi.mock('@/app/components/workflow/hooks/use-inspect-vars-crud-common', () => ({ + useInspectVarsCrudCommon: () => ({ hasNodeInspectVars: hookFns.hasNodeInspectVars, hasSetInspectVar: hookFns.hasSetInspectVar, fetchInspectVarValue: hookFns.fetchInspectVarValue, @@ -472,7 +467,7 @@ describe('WorkflowMain', () => { mockReplaceGraphFromReactFlow.mockReturnValue(true) hookFns.doSyncWorkflowDraft.mockResolvedValue({ hash: 'saved-hash', updatedAt: 2 }) hookFns.handleRefreshWorkflowDraft.mockResolvedValue(true) - appDetail = createAppDetailFixture({ id: 'app-1', mode: AppModeEnum.WORKFLOW }) + appDetail = createAppDetailFixture({ id: 'app-1', mode: 'workflow' as const }) }) it('should render the inner workflow context with children and forwarded graph props', () => { @@ -567,7 +562,7 @@ describe('WorkflowMain', () => { exportCheck: hookFns.exportCheck, handleExportDSL: hookFns.handleExportDSL, fetchInspectVars: hookFns.fetchInspectVars, - configsMap: { flowId: 'app-1', flowType: 'app-flow', fileSettings: { enabled: true } }, + fileSettings: { enabled: true }, }) }) @@ -616,7 +611,7 @@ describe('WorkflowMain', () => { it('disables collaboration for view-only apps', () => { appDetail = createAppDetailFixture({ id: 'app-1', - mode: AppModeEnum.WORKFLOW, + mode: 'workflow' as const, permission_keys: [AppACLPermission.ViewLayout], }) diff --git a/web/app/components/workflow-app/components/__tests__/workflow-panel.spec.tsx b/web/app/components/workflow-app/components/__tests__/workflow-panel.spec.tsx index aafaa97550c..381c6669c17 100644 --- a/web/app/components/workflow-app/components/__tests__/workflow-panel.spec.tsx +++ b/web/app/components/workflow-app/components/__tests__/workflow-panel.spec.tsx @@ -1,3 +1,4 @@ +import type { AppMode } from '@dify/contracts/api/console/apps/types.gen' import type { ReactNode } from 'react' import type { Shape } from '@/app/components/workflow/store/workflow' import { act, screen, waitFor } from '@testing-library/react' @@ -6,7 +7,6 @@ import * as React from 'react' import { renderWorkflowComponent } from '@/app/components/workflow/__tests__/workflow-test-env' import { createConsoleQueryWrapper, seedAppDetail } from '@/test/console/query-data' import { createAppDetailFixture } from '@/test/fixtures/app' -import { AppModeEnum } from '@/types/app' import WorkflowPanel from '../workflow-panel' type WorkflowStoreState = Partial @@ -26,11 +26,15 @@ const renderPanel = (initialStoreState: WorkflowStoreState) => { queryClient, createAppDetailFixture({ id: 'app-123', - mode: AppModeEnum.WORKFLOW, + mode: 'workflow' as const, workflow: { id: 'workflow-version-id' }, }), ) - return renderWorkflowComponent(, { initialStoreState, queryClient }) + return renderWorkflowComponent(, { + initialStoreState, + queryClient, + resource: { kind: 'app', appId: 'app-123', mode: 'workflow' }, + }) } vi.mock('@/app/components/workflow/panel', () => ({ @@ -48,7 +52,7 @@ vi.mock('@/app/components/workflow/panel', () => ({ restoreVersionUrl: (versionId: string) => string updateVersionUrl: (versionId: string) => string latestVersionId?: string - appMode?: AppModeEnum + appMode?: AppMode } }) => (
({ default: () =>
global-variable
, })) -vi.mock('../../hooks/use-is-chat-mode', () => ({ - useIsChatMode: () => mockUseIsChatMode(), -})) +vi.mock('@/app/components/workflow/editor-resource/state', async (importOriginal) => { + const actual = + await importOriginal() + const { atom } = await import('jotai') + return { ...actual, isChatflowAtom: atom(() => mockUseIsChatMode()) } +}) describe('WorkflowPanel', () => { beforeEach(() => { @@ -136,7 +143,7 @@ describe('WorkflowPanel', () => { ) expect(panel).toHaveAttribute('data-update-version-url', '/apps/app-123/workflows/version-1') expect(panel).toHaveAttribute('data-latest-version-id', 'workflow-version-id') - expect(panel).toHaveAttribute('data-app-mode', AppModeEnum.WORKFLOW) + expect(panel).toHaveAttribute('data-app-mode', 'workflow') }) it('should close the selected log and reopen on the default detail tab', async () => { diff --git a/web/app/components/workflow-app/components/workflow-header/__tests__/chat-variable-trigger.spec.tsx b/web/app/components/workflow-app/components/workflow-header/__tests__/chat-variable-trigger.spec.tsx index 343a6d26314..23ea3891639 100644 --- a/web/app/components/workflow-app/components/workflow-header/__tests__/chat-variable-trigger.spec.tsx +++ b/web/app/components/workflow-app/components/workflow-header/__tests__/chat-variable-trigger.spec.tsx @@ -1,4 +1,5 @@ -import { render, screen } from '@testing-library/react' +import { screen } from '@testing-library/react' +import { renderEditor as render } from '@/app/components/workflow/editor-resource/__tests__/render' import ChatVariableTrigger from '../chat-variable-trigger' const mockUseNodesReadOnly = vi.fn() @@ -8,9 +9,12 @@ vi.mock('@/app/components/workflow/hooks/use-workflow', () => ({ useNodesReadOnly: () => mockUseNodesReadOnly(), })) -vi.mock('../../../hooks/use-is-chat-mode', () => ({ - useIsChatMode: () => mockUseIsChatMode(), -})) +vi.mock('@/app/components/workflow/editor-resource/state', async (importOriginal) => { + const actual = + await importOriginal() + const { atom } = await import('jotai') + return { ...actual, isChatflowAtom: atom(() => mockUseIsChatMode()) } +}) vi.mock('@/app/components/workflow/header/chat-variable-button', () => ({ default: ({ disabled }: { disabled: boolean }) => ( @@ -33,7 +37,9 @@ describe('ChatVariableTrigger', () => { mockUseNodesReadOnly.mockReturnValue({ nodesReadOnly: false }) // Act - render() + render(, { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + }) // Assert expect(screen.queryByRole('button', { name: 'ChatVariableButton' })).not.toBeInTheDocument() @@ -48,7 +54,9 @@ describe('ChatVariableTrigger', () => { mockUseNodesReadOnly.mockReturnValue({ nodesReadOnly: false }) // Act - render() + render(, { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + }) // Assert expect(screen.getByRole('button', { name: 'ChatVariableButton' })).toBeEnabled() @@ -60,7 +68,9 @@ describe('ChatVariableTrigger', () => { mockUseNodesReadOnly.mockReturnValue({ nodesReadOnly: true }) // Act - render() + render(, { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + }) // Assert expect(screen.getByRole('button', { name: 'ChatVariableButton' })).toBeDisabled() diff --git a/web/app/components/workflow-app/components/workflow-header/__tests__/features-trigger.spec.tsx b/web/app/components/workflow-app/components/workflow-header/__tests__/features-trigger.spec.tsx index 0879fed0d1e..3e8be595d84 100644 --- a/web/app/components/workflow-app/components/workflow-header/__tests__/features-trigger.spec.tsx +++ b/web/app/components/workflow-app/components/workflow-header/__tests__/features-trigger.spec.tsx @@ -2,8 +2,9 @@ import type { CloudPlan } from '@dify/contracts/api/console/features/types.gen' import type { DeploymentEdition } from '@dify/contracts/api/console/system-features/types.gen' import type { ReactElement } from 'react' import type { AppPublisherProps } from '@/app/components/app/app-publisher/types' -import { render, screen, waitFor } from '@testing-library/react' +import { screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' +import { renderEditor as render } from '@/app/components/workflow/editor-resource/__tests__/render' import { BlockEnum, InputVarType } from '@/app/components/workflow/types' import { consoleQuery } from '@/service/console' import { createConsoleQueryWrapper, seedFeatures } from '@/test/console/query-data' @@ -73,7 +74,6 @@ const mockWorkflowStore = { vi.mock('@/app/components/workflow/hooks/use-workflow', () => ({ useNodesReadOnly: () => mockUseNodesReadOnly(), - useIsChatMode: () => mockUseIsChatMode(), })) vi.mock('@/app/components/workflow/hooks/use-checklist', () => ({ @@ -244,7 +244,17 @@ const renderWithToast = ( }) seedFeatures(queryClient, { billing: { subscription: { plan } } }) vi.spyOn(queryClient, 'invalidateQueries').mockImplementation(mockInvalidateQueries) - return { queryClient, ...render(ui, { wrapper }) } + return { + ...render(ui, { + resource: { + kind: 'app', + appId: 'app-id', + mode: mockUseIsChatMode() ? 'advanced-chat' : 'workflow', + }, + wrapper, + queryClient, + }), + } } describe('FeaturesTrigger', () => { diff --git a/web/app/components/workflow-app/hooks/__tests__/use-DSL.spec.ts b/web/app/components/workflow-app/hooks/__tests__/use-DSL.spec.ts index 157f1c488c6..2ab099dad65 100644 --- a/web/app/components/workflow-app/hooks/__tests__/use-DSL.spec.ts +++ b/web/app/components/workflow-app/hooks/__tests__/use-DSL.spec.ts @@ -1,8 +1,9 @@ import type { ReactNode } from 'react' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' -import { act, renderHook, waitFor } from '@testing-library/react' +import { act, waitFor } from '@testing-library/react' import { createElement } from 'react' import { DSL_EXPORT_CHECK } from '@/app/components/workflow/constants' +import { renderEditorHook as renderHook } from '@/app/components/workflow/editor-resource/__tests__/render' import { consoleQuery } from '@/service/console' import { httpErrorFromResponse } from '@/service/http-error' import { createAppDetailFixture } from '@/test/fixtures/app' @@ -129,7 +130,10 @@ describe('useDSLByCanEdit', () => { }) it('exports with the latest name from the detail query', async () => { - const { result } = renderHook(() => useDSLByCanEdit(true), { wrapper }) + const { result } = renderHook(() => useDSLByCanEdit(true), { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + wrapper, + }) const beforeRename = result.current act(() => { queryClient.setQueryData( @@ -152,7 +156,10 @@ describe('useDSLByCanEdit', () => { callback.onError() return null }) - const { result } = renderHook(() => useDSLByCanEdit(true), { wrapper }) + const { result } = renderHook(() => useDSLByCanEdit(true), { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + wrapper, + }) await act(async () => { expect(await result.current.handleExportDSL()).toBe(false) }) @@ -164,7 +171,10 @@ describe('useDSLByCanEdit', () => { it('allows export when draft synchronization is skipped for read-only access', async () => { mockDoSyncWorkflowDraft.mockResolvedValue(null) - const { result } = renderHook(() => useDSLByCanEdit(false), { wrapper }) + const { result } = renderHook(() => useDSLByCanEdit(false), { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + wrapper, + }) await act(async () => { expect(await result.current.handleExportDSL()).toBe(true) }) @@ -174,7 +184,10 @@ describe('useDSLByCanEdit', () => { it('downloads the default package bytes and forwards the selected workflow version', async () => { const file = new File([new Uint8Array([0x50, 0x4b, 0xff])], 'workflow.ifpkg') mockExportAppConfig.mockResolvedValue(file) - const { result } = renderHook(() => useDSLByCanEdit(true), { wrapper }) + const { result } = renderHook(() => useDSLByCanEdit(true), { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + wrapper, + }) await act(async () => { expect(await result.current.handleExportDSL(true, 'revision-1')).toBe(true) }) @@ -190,7 +203,10 @@ describe('useDSLByCanEdit', () => { }) it('should export workflow dsl and download the yaml blob when no secret env is present', async () => { - const { result } = renderHook(() => useDSLByCanEdit(true), { wrapper }) + const { result } = renderHook(() => useDSLByCanEdit(true), { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + wrapper, + }) await act(async () => { await result.current.exportCheck() @@ -232,7 +248,10 @@ workflow: package_ref: agent_1 ` mockExportAppConfig.mockResolvedValue({ data: agentDSL }) - const { result } = renderHook(() => useDSLByCanEdit(true), { wrapper }) + const { result } = renderHook(() => useDSLByCanEdit(true), { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + wrapper, + }) await act(async () => { await result.current.handleExportDSL() @@ -246,7 +265,10 @@ workflow: }) it('should forward include and workflow id arguments when exporting dsl directly', async () => { - const { result } = renderHook(() => useDSLByCanEdit(true), { wrapper }) + const { result } = renderHook(() => useDSLByCanEdit(true), { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + wrapper, + }) await act(async () => { await result.current.handleExportDSL(true, 'workflow-1') @@ -265,7 +287,10 @@ workflow: const secretVars = [{ id: 'env-1', value_type: 'secret', value: 'secret-token' }] mockFetchEnvironmentVariables.mockResolvedValue({ items: secretVars }) - const { result } = renderHook(() => useDSLByCanEdit(true), { wrapper }) + const { result } = renderHook(() => useDSLByCanEdit(true), { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + wrapper, + }) await act(async () => { await result.current.exportCheck() @@ -280,22 +305,6 @@ workflow: expect(mockExportAppConfig).not.toHaveBeenCalled() }) - it('should return early when app detail is unavailable', async () => { - appQueryFixture = {} - - const { result } = renderHook(() => useDSLByCanEdit(true), { wrapper }) - - await act(async () => { - await result.current.exportCheck() - await result.current.handleExportDSL() - }) - - expect(mockFetchEnvironmentVariables).not.toHaveBeenCalled() - expect(mockDoSyncWorkflowDraft).not.toHaveBeenCalled() - expect(mockExportAppConfig).not.toHaveBeenCalled() - expect(mockEmit).not.toHaveBeenCalled() - }) - it('should notify when export fails', async () => { mockExportAppConfig.mockRejectedValue( await httpErrorFromResponse( @@ -305,7 +314,10 @@ workflow: ), ) - const { result } = renderHook(() => useDSLByCanEdit(true), { wrapper }) + const { result } = renderHook(() => useDSLByCanEdit(true), { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + wrapper, + }) await act(async () => { expect(await result.current.handleExportDSL()).toBe(false) @@ -323,7 +335,10 @@ workflow: it('should notify when exportCheck cannot load the workflow draft', async () => { mockFetchEnvironmentVariables.mockRejectedValue(new Error('draft fetch failed')) - const { result } = renderHook(() => useDSLByCanEdit(true), { wrapper }) + const { result } = renderHook(() => useDSLByCanEdit(true), { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + wrapper, + }) await act(async () => { await result.current.exportCheck() @@ -345,7 +360,10 @@ workflow: mockDoSyncWorkflowDraft.mockReturnValue(sync.promise) mockExportAppConfig.mockReturnValue(deferred.promise) - const { result } = renderHook(() => useDSLByCanEdit(true), { wrapper }) + const { result } = renderHook(() => useDSLByCanEdit(true), { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + wrapper, + }) let firstExportPromise!: Promise act(() => { diff --git a/web/app/components/workflow-app/hooks/__tests__/use-configs-map.spec.ts b/web/app/components/workflow-app/hooks/__tests__/use-configs-map.spec.ts deleted file mode 100644 index 3613475b7b0..00000000000 --- a/web/app/components/workflow-app/hooks/__tests__/use-configs-map.spec.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { renderHook } from '@testing-library/react' -import { FlowType } from '@/types/common' -import { useConfigsMap } from '../use-configs-map' - -const mockUseFeatures = vi.fn() - -vi.mock('@/app/components/base/features/hooks', () => ({ - useFeatures: (selector: (state: { features: { file: Record } }) => unknown) => - mockUseFeatures(selector), -})) - -vi.mock('@/app/components/workflow/store', () => ({ - useStore: (selector: (state: { appId: string }) => T) => selector({ appId: 'app-1' }), -})) - -describe('useConfigsMap', () => { - beforeEach(() => { - vi.clearAllMocks() - mockUseFeatures.mockImplementation( - (selector: (state: { features: { file: Record } }) => unknown) => - selector({ - features: { - file: { - enabled: true, - number_limits: 3, - }, - }, - }), - ) - }) - - it('should map workflow app id and feature file settings into inspect-var configs', () => { - const { result } = renderHook(() => useConfigsMap()) - - expect(result.current).toEqual({ - flowId: 'app-1', - flowType: FlowType.appFlow, - fileSettings: { - enabled: true, - number_limits: 3, - }, - }) - }) -}) diff --git a/web/app/components/workflow-app/hooks/__tests__/use-get-run-and-trace-url.spec.ts b/web/app/components/workflow-app/hooks/__tests__/use-get-run-and-trace-url.spec.ts index 196ad8c915b..100b8bd9c4f 100644 --- a/web/app/components/workflow-app/hooks/__tests__/use-get-run-and-trace-url.spec.ts +++ b/web/app/components/workflow-app/hooks/__tests__/use-get-run-and-trace-url.spec.ts @@ -1,4 +1,4 @@ -import { renderHook } from '@testing-library/react' +import { renderEditorHook as renderHook } from '@/app/components/workflow/editor-resource/__tests__/render' import { useGetRunAndTraceUrl } from '../use-get-run-and-trace-url' const mockWorkflowStore = { @@ -18,7 +18,9 @@ describe('useGetRunAndTraceUrl', () => { }) it('should build workflow run and trace urls from the current app id', () => { - const { result } = renderHook(() => useGetRunAndTraceUrl()) + const { result } = renderHook(() => useGetRunAndTraceUrl(), { + resource: { kind: 'app', appId: 'app-123', mode: 'workflow' }, + }) expect(result.current.getWorkflowRunAndTraceUrl('run-1')).toEqual({ runUrl: '/apps/app-123/workflow-runs/run-1', diff --git a/web/app/components/workflow-app/hooks/__tests__/use-is-chat-mode.spec.ts b/web/app/components/workflow-app/hooks/__tests__/use-is-chat-mode.spec.ts deleted file mode 100644 index 1c2abe40ff9..00000000000 --- a/web/app/components/workflow-app/hooks/__tests__/use-is-chat-mode.spec.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { - createConsoleQueryClient, - renderHookWithConsoleQuery, - seedAppDetail, -} from '@/test/console/query-data' -import { createAppDetailFixture } from '@/test/fixtures/app' -import { AppModeEnum } from '@/types/app' -import { useIsChatMode } from '../use-is-chat-mode' - -vi.mock('@/next/navigation', async (importOriginal) => ({ - ...(await importOriginal()), - useParams: () => ({ appId: 'app-1' }), -})) - -const renderWithAppMode = (mode: AppModeEnum) => { - const queryClient = createConsoleQueryClient() - seedAppDetail(queryClient, createAppDetailFixture({ id: 'app-1', mode })) - return renderHookWithConsoleQuery(() => useIsChatMode(), { queryClient }) -} - -describe('useIsChatMode', () => { - it('should return true when the app mode is ADVANCED_CHAT', () => { - const { result } = renderWithAppMode(AppModeEnum.ADVANCED_CHAT) - - expect(result.current).toBe(true) - }) - - it('should return false when the app mode is WORKFLOW', () => { - const { result } = renderWithAppMode(AppModeEnum.WORKFLOW) - - expect(result.current).toBe(false) - }) -}) diff --git a/web/app/components/workflow-app/hooks/__tests__/use-nodes-sync-draft.spec.ts b/web/app/components/workflow-app/hooks/__tests__/use-nodes-sync-draft.spec.ts index 1cd9bd37d6b..bc9606d1124 100644 --- a/web/app/components/workflow-app/hooks/__tests__/use-nodes-sync-draft.spec.ts +++ b/web/app/components/workflow-app/hooks/__tests__/use-nodes-sync-draft.spec.ts @@ -1,15 +1,16 @@ +import type { AppMode } from '@dify/contracts/api/console/apps/types.gen' import type { WorkflowSliceShape } from '@/app/components/workflow/store/workflow/workflow-slice' import type { EnvironmentVariablePatch } from '@/service/workflow' import { noop } from '@tanstack/react-query' import { act } from '@testing-library/react' import { beforeEach, describe, expect, it, vi } from 'vite-plus/test' +import { renderEditorHook as renderHookWithConsoleQuery } from '@/app/components/workflow/editor-resource/__tests__/render' import { BlockEnum } from '@/app/components/workflow/types' import { markAppDeletionFailed, markAppDeletionStarted } from '@/service/app-deletion' import { consoleQuery } from '@/service/console' import { createHttpError } from '@/service/http-error' -import { createConsoleQueryClient, renderHookWithConsoleQuery } from '@/test/console/query-data' +import { createConsoleQueryClient } from '@/test/console/query-data' import { createAppDetailFixture } from '@/test/fixtures/app' -import { AppModeEnum } from '@/types/app' import { useNodesSyncDraft } from '../use-nodes-sync-draft' const mockGetNodes = vi.fn() @@ -25,7 +26,7 @@ const mockCollaborationCanFlushGraphOnPageClose = vi.fn() const mockCollaborationCanUseLocalDraftFallback = vi.fn() const mockSetShowConfirm = vi.fn<(confirmation: WorkflowSliceShape['showConfirm']) => void>() let isCollaborationEnabled = false -let appMode: AppModeEnum | undefined +let appMode: AppMode | undefined let reactFlowState: { getNodes: typeof mockGetNodes @@ -200,7 +201,7 @@ describe('useNodesSyncDraft', () => { updatedAt: 2, }) isCollaborationEnabled = false - appMode = AppModeEnum.WORKFLOW + appMode = 'workflow' }) it.each(['draft sync', 'page close'] as const)( @@ -260,7 +261,7 @@ describe('useNodesSyncDraft', () => { }, ) - it.each([AppModeEnum.ADVANCED_CHAT, undefined])( + it.each(['advanced-chat'] as const)( 'should preserve LLM memory when app mode is %s', async (mode) => { appMode = mode @@ -544,24 +545,6 @@ describe('useNodesSyncDraft', () => { expect(callbacks.onSettled).toHaveBeenCalledOnce() }) - it('should not apply empty-canvas consent to another app', async () => { - mockGetNodes.mockReturnValue([]) - const { result } = renderUseNodesSyncDraft() - let syncPromise!: ReturnType - - act(() => { - syncPromise = result.current.doSyncWorkflowDraft() - }) - - workflowStoreState.appId = 'app-2' - await act(async () => { - workflowStoreState.showConfirm?.onConfirm() - await expect(syncPromise).resolves.toBeNull() - }) - - expect(mockSyncWorkflowDraft).not.toHaveBeenCalled() - }) - it('should cancel a pending empty save when its owning hook unmounts', async () => { mockGetNodes.mockReturnValue([]) const callbacks = { onError: vi.fn(), onSettled: vi.fn() } diff --git a/web/app/components/workflow-app/hooks/__tests__/use-workflow-draft-graph-for-canvas.spec.ts b/web/app/components/workflow-app/hooks/__tests__/use-workflow-draft-graph-for-canvas.spec.ts index 3d5c8a4cf97..59db726cbe5 100644 --- a/web/app/components/workflow-app/hooks/__tests__/use-workflow-draft-graph-for-canvas.spec.ts +++ b/web/app/components/workflow-app/hooks/__tests__/use-workflow-draft-graph-for-canvas.spec.ts @@ -1,7 +1,6 @@ import type { Node } from '@/app/components/workflow/types' import { renderHook } from '@testing-library/react' import { BlockEnum } from '@/app/components/workflow/types' -import { AppModeEnum } from '@/types/app' import { useWorkflowDraftGraphForCanvas } from '../use-workflow-draft-graph-for-canvas' let generateNewNodeCalls: Array> = [] @@ -34,7 +33,7 @@ describe('useWorkflowDraftGraphForCanvas', () => { }) it('should restore a local start placeholder for workflow graphs without an entry node', () => { - const { result } = renderHook(() => useWorkflowDraftGraphForCanvas(AppModeEnum.WORKFLOW)) + const { result } = renderHook(() => useWorkflowDraftGraphForCanvas('workflow')) const graph = result.current.getWorkflowDraftGraphForCanvas({ nodes: [], @@ -64,7 +63,7 @@ describe('useWorkflowDraftGraphForCanvas', () => { BlockEnum.StartPlaceholder, ])('should preserve existing %s entry nodes', (type) => { const node = { id: 'entry', data: { type } } - const { result } = renderHook(() => useWorkflowDraftGraphForCanvas(AppModeEnum.WORKFLOW)) + const { result } = renderHook(() => useWorkflowDraftGraphForCanvas('workflow')) const graph = result.current.getWorkflowDraftGraphForCanvas({ nodes: [node] as never, @@ -76,7 +75,7 @@ describe('useWorkflowDraftGraphForCanvas', () => { }) it('should not restore a start placeholder for non-workflow app modes', () => { - const { result } = renderHook(() => useWorkflowDraftGraphForCanvas(AppModeEnum.ADVANCED_CHAT)) + const { result } = renderHook(() => useWorkflowDraftGraphForCanvas('advanced-chat')) const graph = result.current.getWorkflowDraftGraphForCanvas({ nodes: [], @@ -93,7 +92,7 @@ describe('useWorkflowDraftGraphForCanvas', () => { data: { type: BlockEnum.StartPlaceholder }, } const draftNode = { id: 'llm', data: { type: BlockEnum.LLM } } - const { result } = renderHook(() => useWorkflowDraftGraphForCanvas(AppModeEnum.WORKFLOW)) + const { result } = renderHook(() => useWorkflowDraftGraphForCanvas('workflow')) const graph = result.current.getWorkflowDraftGraphForCanvas( { @@ -141,7 +140,7 @@ describe('useWorkflowDraftGraphForCanvas', () => { }, ] const originalNodes = structuredClone(nodes) - const { result } = renderHook(() => useWorkflowDraftGraphForCanvas(AppModeEnum.WORKFLOW)) + const { result } = renderHook(() => useWorkflowDraftGraphForCanvas('workflow')) const graph = result.current.getWorkflowDraftGraphForCanvas({ nodes, edges: [] }) @@ -159,7 +158,7 @@ describe('useWorkflowDraftGraphForCanvas', () => { expect(nodes).toEqual(originalNodes) }) - it.each([AppModeEnum.ADVANCED_CHAT, undefined])( + it.each(['advanced-chat', undefined] as const)( 'preserves memory when app mode is %s', (appMode) => { const nodes: Node<{ memory: { window: { enabled: boolean; size: number } } }>[] = [ diff --git a/web/app/components/workflow-app/hooks/__tests__/use-workflow-init.spec.ts b/web/app/components/workflow-app/hooks/__tests__/use-workflow-init.spec.ts index 399e5c4b02e..6d2c21c170e 100644 --- a/web/app/components/workflow-app/hooks/__tests__/use-workflow-init.spec.ts +++ b/web/app/components/workflow-app/hooks/__tests__/use-workflow-init.spec.ts @@ -1,12 +1,12 @@ import type { AppDetailWithSite } from '@dify/contracts/api/console/apps/types.gen' import { waitFor } from '@testing-library/react' import { beforeEach, describe, expect, it, vi } from 'vite-plus/test' +import { renderEditorHook as renderHookWithConsoleState } from '@/app/components/workflow/editor-resource/__tests__/render' import { BlockEnum } from '@/app/components/workflow/types' import { createHttpError } from '@/service/http-error' import { createAccountProfileQueryClient } from '@/test/console/account-profile' import { createQueryClientWrapper } from '@/test/console/query-client' import { seedAppDetail } from '@/test/console/query-data' -import { renderHook as renderHookWithConsoleState } from '@/test/console/render' import { createAppDetailFixture } from '@/test/fixtures/app' import { AppACLPermission } from '@/utils/permission' import { useWorkflowInit } from '../use-workflow-init' @@ -29,6 +29,7 @@ const renderHook = (callback: () => Result) => { const queryClient = createAccountProfileQueryClient({ id: 'user-1' }) seedAppDetail(queryClient, appDetail) return renderHookWithConsoleState(callback, { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, wrapper: createQueryClientWrapper(queryClient), }) } @@ -346,7 +347,6 @@ describe('useWorkflowInit', () => { expect(result.current.data?.hash).toBe('server-hash') }) - expect(mockWorkflowStoreSetState).toHaveBeenCalledWith({ appId: 'app-1', appName: 'Test' }) expect(mockWorkflowStoreSetState).toHaveBeenCalledWith( expect.objectContaining({ envSecrets: { 'env-secret': 'top-secret' }, diff --git a/web/app/components/workflow-app/hooks/__tests__/use-workflow-refresh-draft.spec.ts b/web/app/components/workflow-app/hooks/__tests__/use-workflow-refresh-draft.spec.ts index ff8b4c48ef9..288c4926406 100644 --- a/web/app/components/workflow-app/hooks/__tests__/use-workflow-refresh-draft.spec.ts +++ b/web/app/components/workflow-app/hooks/__tests__/use-workflow-refresh-draft.spec.ts @@ -1,13 +1,10 @@ +import type { AppMode } from '@dify/contracts/api/console/apps/types.gen' import { act, waitFor } from '@testing-library/react' import { beforeEach, describe, expect, it, vi } from 'vite-plus/test' +import { renderEditorHook as renderHookWithConsoleQuery } from '@/app/components/workflow/editor-resource/__tests__/render' import { BlockEnum } from '@/app/components/workflow/types' -import { - createConsoleQueryClient, - renderHookWithConsoleQuery, - seedAppDetail, -} from '@/test/console/query-data' +import { createConsoleQueryClient, seedAppDetail } from '@/test/console/query-data' import { createAppDetailFixture } from '@/test/fixtures/app' -import { AppModeEnum } from '@/types/app' import { useWorkflowRefreshDraft } from '../use-workflow-refresh-draft' const mockHandleUpdateWorkflowCanvas = vi.fn() @@ -18,7 +15,7 @@ const mockSetEnvSecrets = vi.fn() const mockSetConversationVariables = vi.fn() const mockSetIsWorkflowDataLoaded = vi.fn() const mockCancel = vi.fn() -let appMode: AppModeEnum +let appMode: AppMode let workflowStoreState: { appId: string @@ -79,7 +76,7 @@ describe('useWorkflowRefreshDraft — notUpdateCanvas parameter', () => { setConversationVariables: mockSetConversationVariables, setIsWorkflowDataLoaded: mockSetIsWorkflowDataLoaded, } - appMode = AppModeEnum.ADVANCED_CHAT + appMode = 'advanced-chat' mockFetchWorkflowDraft.mockResolvedValue(draftResponse) }) @@ -236,7 +233,7 @@ describe('useWorkflowRefreshDraft — notUpdateCanvas parameter', () => { }) it('should restore a local start placeholder for workflow drafts without an entry node', async () => { - appMode = AppModeEnum.WORKFLOW + appMode = 'workflow' mockFetchWorkflowDraft.mockResolvedValue({ hash: 'server-hash', graph: { diff --git a/web/app/components/workflow-app/hooks/__tests__/use-workflow-run-utils.spec.ts b/web/app/components/workflow-app/hooks/__tests__/use-workflow-run-utils.spec.ts index b88fea15858..66d852f7d9b 100644 --- a/web/app/components/workflow-app/hooks/__tests__/use-workflow-run-utils.spec.ts +++ b/web/app/components/workflow-app/hooks/__tests__/use-workflow-run-utils.spec.ts @@ -1,7 +1,6 @@ import { TriggerType } from '@/app/components/workflow/header/test-run-menu' import { WorkflowRunningStatus } from '@/app/components/workflow/types' import { httpErrorFromResponse } from '@/service/http-error' -import { AppModeEnum } from '@/types/app' import { applyRunningStateForMode, applyStoppedState, @@ -54,36 +53,32 @@ describe('useWorkflowRun utils', () => { }) it('should resolve run history urls and run endpoints for workflow modes', () => { - expect(buildRunHistoryUrl({ id: 'app-1', mode: AppModeEnum.WORKFLOW })).toBe( + expect(buildRunHistoryUrl({ id: 'app-1', mode: 'workflow' as const })).toBe( '/apps/app-1/workflow-runs', ) - expect(buildRunHistoryUrl({ id: 'app-1', mode: AppModeEnum.ADVANCED_CHAT })).toBe( + expect(buildRunHistoryUrl({ id: 'app-1', mode: 'advanced-chat' as const })).toBe( '/apps/app-1/advanced-chat/workflow-runs', ) expect( resolveWorkflowRunUrl( - { id: 'app-1', mode: AppModeEnum.WORKFLOW }, + { id: 'app-1', mode: 'workflow' as const }, TriggerType.UserInput, true, ), ).toBe('/apps/app-1/workflows/draft/run') expect( resolveWorkflowRunUrl( - { id: 'app-1', mode: AppModeEnum.ADVANCED_CHAT }, + { id: 'app-1', mode: 'advanced-chat' as const }, TriggerType.UserInput, false, ), ).toBe('/apps/app-1/advanced-chat/workflows/draft/run') expect( - resolveWorkflowRunUrl( - { id: 'app-1', mode: AppModeEnum.WORKFLOW }, - TriggerType.Schedule, - true, - ), + resolveWorkflowRunUrl({ id: 'app-1', mode: 'workflow' as const }, TriggerType.Schedule, true), ).toBe('/apps/app-1/workflows/draft/trigger/run') expect( - resolveWorkflowRunUrl({ id: 'app-1', mode: AppModeEnum.WORKFLOW }, TriggerType.All, true), + resolveWorkflowRunUrl({ id: 'app-1', mode: 'workflow' as const }, TriggerType.All, true), ).toBe('/apps/app-1/workflows/draft/trigger/run-all') }) @@ -124,7 +119,7 @@ describe('useWorkflowRun utils', () => { expect(resolveWorkflowRunUrl(undefined, TriggerType.All, true)).toBe('') expect( resolveWorkflowRunUrl( - { id: 'app-1', mode: AppModeEnum.WORKFLOW }, + { id: 'app-1', mode: 'workflow' as const }, TriggerType.UserInput, false, ), diff --git a/web/app/components/workflow-app/hooks/__tests__/use-workflow-run.spec.ts b/web/app/components/workflow-app/hooks/__tests__/use-workflow-run.spec.ts index 3c57c38fc69..82c3adf8f16 100644 --- a/web/app/components/workflow-app/hooks/__tests__/use-workflow-run.spec.ts +++ b/web/app/components/workflow-app/hooks/__tests__/use-workflow-run.spec.ts @@ -1,11 +1,8 @@ import { act } from '@testing-library/react' +import { renderEditorHook as renderHookWithConsoleQuery } from '@/app/components/workflow/editor-resource/__tests__/render' import { TriggerType } from '@/app/components/workflow/header/test-run-menu' import { WorkflowRunningStatus } from '@/app/components/workflow/types' -import { - createConsoleQueryClient, - renderHookWithConsoleQuery, - seedAppDetail, -} from '@/test/console/query-data' +import { createConsoleQueryClient, seedAppDetail } from '@/test/console/query-data' import { createAppDetailFixture } from '@/test/fixtures/app' import { useWorkflowRun } from '../use-workflow-run' @@ -173,7 +170,7 @@ vi.mock('@/service/base', () => ({ })) vi.mock('@/service/use-workflow', () => ({ - useInvalidAllLastRun: () => mocks.mockInvalidateAllLastRun, + useInvalidateAllLastRun: () => mocks.mockInvalidateAllLastRun, useInvalidateWorkflowRunHistory: () => mocks.mockInvalidateRunHistory, useInvalidateConversationVarValues: () => vi.fn(), useInvalidateSysVarValues: () => vi.fn(), @@ -189,13 +186,6 @@ vi.mock('@/app/components/workflow/hooks/use-fetch-workflow-inspect-vars', () => }), })) -vi.mock('../use-configs-map', () => ({ - useConfigsMap: () => ({ - flowId: 'flow-1', - flowType: 'workflow', - }), -})) - vi.mock('../use-nodes-sync-draft', () => ({ useNodesSyncDraft: () => ({ doSyncWorkflowDraft: mocks.mockDoSyncWorkflowDraft, @@ -456,7 +446,7 @@ describe('useWorkflowRun', () => { ) expect(mocks.mockTrackEvent).toHaveBeenCalledWith('workflow_run_failed', { - workflow_id: 'flow-1', + workflow_id: 'app-1', reason: 'failed', node_type: 'llm', data: { @@ -470,7 +460,7 @@ describe('useWorkflowRun', () => { baseCallbackFactoryContext.trackWorkflowRunFailed('Server Error', workflowData) expect(mocks.mockTrackEvent).toHaveBeenCalledWith('workflow_run_failed', { - workflow_id: 'flow-1', + workflow_id: 'app-1', reason: 'Server Error', node_type: undefined, data: { @@ -515,7 +505,7 @@ describe('useWorkflowRun', () => { if (!chunks) throw new Error('Expected workflow data chunks to be tracked') expect(mocks.mockTrackEvent).toHaveBeenCalledWith('workflow_run_failed', { - workflow_id: 'flow-1', + workflow_id: 'app-1', reason: 'failed', node_type: 'llm', data: { @@ -546,7 +536,7 @@ describe('useWorkflowRun', () => { baseCallbackFactoryContext.trackWorkflowRunFailed(new Error('network down'), undefined) expect(mocks.mockTrackEvent).toHaveBeenCalledWith('workflow_run_failed', { - workflow_id: 'flow-1', + workflow_id: 'app-1', reason: 'network down', node_type: undefined, data: { @@ -568,7 +558,7 @@ describe('useWorkflowRun', () => { ) expect(mocks.mockTrackEvent).toHaveBeenCalledWith('workflow_run_failed', { - workflow_id: 'flow-1', + workflow_id: 'app-1', reason: undefined, node_type: undefined, data: { diff --git a/web/app/components/workflow-app/hooks/__tests__/use-workflow-start-run.spec.tsx b/web/app/components/workflow-app/hooks/__tests__/use-workflow-start-run.spec.tsx index 30339007207..49004059bf1 100644 --- a/web/app/components/workflow-app/hooks/__tests__/use-workflow-start-run.spec.tsx +++ b/web/app/components/workflow-app/hooks/__tests__/use-workflow-start-run.spec.tsx @@ -1,4 +1,5 @@ -import { act, renderHook } from '@testing-library/react' +import { act } from '@testing-library/react' +import { renderEditorHook as renderHook } from '@/app/components/workflow/editor-resource/__tests__/render' import { TriggerType } from '@/app/components/workflow/header/test-run-menu' import { BlockEnum, WorkflowRunningStatus } from '@/app/components/workflow/types' import { useWorkflowStartRunByCanEdit } from '../use-workflow-start-run' @@ -50,7 +51,7 @@ vi.mock('@/app/components/workflow/store', () => ({ })) vi.mock('@/app/components/workflow/hooks/use-workflow', () => ({ - useIsChatMode: () => mockUseIsChatMode(), + useIsChatflow: () => mockUseIsChatMode(), })) vi.mock('../use-nodes-sync-draft', () => ({ @@ -102,7 +103,9 @@ describe('useWorkflowStartRunByCanEdit', () => { }) it('should run the workflow immediately when there are no start variables and no image upload input', async () => { - const { result } = renderHook(() => useWorkflowStartRunByCanEdit(true)) + const { result } = renderHook(() => useWorkflowStartRunByCanEdit(true), { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + }) await act(async () => { await result.current.handleWorkflowStartRunInWorkflow() @@ -121,7 +124,9 @@ describe('useWorkflowStartRunByCanEdit', () => { { id: 'start-placeholder', data: { type: BlockEnum.StartPlaceholder } }, ]) - const { result } = renderHook(() => useWorkflowStartRunByCanEdit(true)) + const { result } = renderHook(() => useWorkflowStartRunByCanEdit(true), { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + }) await act(async () => { await result.current.handleWorkflowStartRunInWorkflow() @@ -138,7 +143,9 @@ describe('useWorkflowStartRunByCanEdit', () => { { id: 'inset-s-1', data: { type: BlockEnum.Start, variables: [{ name: 'query' }] } }, ]) - const { result } = renderHook(() => useWorkflowStartRunByCanEdit(true)) + const { result } = renderHook(() => useWorkflowStartRunByCanEdit(true), { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + }) await act(async () => { await result.current.handleWorkflowStartRunInWorkflow() @@ -161,7 +168,9 @@ describe('useWorkflowStartRunByCanEdit', () => { }, }) - const { result } = renderHook(() => useWorkflowStartRunByCanEdit(true)) + const { result } = renderHook(() => useWorkflowStartRunByCanEdit(true), { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + }) await act(async () => { await result.current.handleWorkflowStartRunInWorkflow() @@ -178,7 +187,9 @@ describe('useWorkflowStartRunByCanEdit', () => { showDebugAndPreviewPanel: true, }) - const { result } = renderHook(() => useWorkflowStartRunByCanEdit(true)) + const { result } = renderHook(() => useWorkflowStartRunByCanEdit(true), { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + }) await act(async () => { await result.current.handleWorkflowStartRunInWorkflow() @@ -198,7 +209,9 @@ describe('useWorkflowStartRunByCanEdit', () => { }, }) - const { result } = renderHook(() => useWorkflowStartRunByCanEdit(true)) + const { result } = renderHook(() => useWorkflowStartRunByCanEdit(true), { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + }) await act(async () => { await result.current.handleWorkflowStartRunInWorkflow() @@ -212,7 +225,9 @@ describe('useWorkflowStartRunByCanEdit', () => { it('should configure schedule trigger runs and execute the workflow with schedule options', async () => { mockGetNodes.mockReturnValue([{ id: 'schedule-1', data: { type: BlockEnum.TriggerSchedule } }]) - const { result } = renderHook(() => useWorkflowStartRunByCanEdit(true)) + const { result } = renderHook(() => useWorkflowStartRunByCanEdit(true), { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + }) await act(async () => { await result.current.handleWorkflowTriggerScheduleRunInWorkflow('schedule-1') @@ -239,7 +254,9 @@ describe('useWorkflowStartRunByCanEdit', () => { }) mockGetNodes.mockReturnValue([{ id: 'schedule-1', data: { type: BlockEnum.TriggerSchedule } }]) - const { result } = renderHook(() => useWorkflowStartRunByCanEdit(true)) + const { result } = renderHook(() => useWorkflowStartRunByCanEdit(true), { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + }) await act(async () => { await result.current.handleWorkflowTriggerScheduleRunInWorkflow('schedule-1') @@ -267,7 +284,9 @@ describe('useWorkflowStartRunByCanEdit', () => { hook.handleWorkflowTriggerPluginRunInWorkflow(''), }, ])('should ignore $title trigger execution when the node id is empty', async ({ invoke }) => { - const { result } = renderHook(() => useWorkflowStartRunByCanEdit(true)) + const { result } = renderHook(() => useWorkflowStartRunByCanEdit(true), { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + }) await act(async () => { await invoke(result.current) @@ -302,7 +321,9 @@ describe('useWorkflowStartRunByCanEdit', () => { const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) mockGetNodes.mockReturnValue([{ id: 'other-node', data: { type: BlockEnum.Start } }]) - const { result } = renderHook(() => useWorkflowStartRunByCanEdit(true)) + const { result } = renderHook(() => useWorkflowStartRunByCanEdit(true), { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + }) await act(async () => { await invoke(result.current) @@ -340,7 +361,9 @@ describe('useWorkflowStartRunByCanEdit', () => { async ({ nodeId, nodeType, invoke, expectedParams, expectedOptions }) => { mockGetNodes.mockReturnValue([{ id: nodeId, data: { type: nodeType } }]) - const { result } = renderHook(() => useWorkflowStartRunByCanEdit(true)) + const { result } = renderHook(() => useWorkflowStartRunByCanEdit(true), { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + }) await act(async () => { await invoke(result.current) @@ -360,7 +383,9 @@ describe('useWorkflowStartRunByCanEdit', () => { ) it('should run all triggers and mark the listener state as global', async () => { - const { result } = renderHook(() => useWorkflowStartRunByCanEdit(true)) + const { result } = renderHook(() => useWorkflowStartRunByCanEdit(true), { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + }) await act(async () => { await result.current.handleWorkflowRunAllTriggersInWorkflow(['trigger-1', 'trigger-2']) @@ -385,7 +410,9 @@ describe('useWorkflowStartRunByCanEdit', () => { }) it('should ignore run-all requests when there are no trigger nodes', async () => { - const { result } = renderHook(() => useWorkflowStartRunByCanEdit(true)) + const { result } = renderHook(() => useWorkflowStartRunByCanEdit(true), { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + }) await act(async () => { await result.current.handleWorkflowRunAllTriggersInWorkflow([]) @@ -399,7 +426,9 @@ describe('useWorkflowStartRunByCanEdit', () => { it('should route handleStartWorkflowRun to the chatflow path when chat mode is enabled', async () => { mockUseIsChatMode.mockReturnValue(true) - const { result } = renderHook(() => useWorkflowStartRunByCanEdit(true)) + const { result } = renderHook(() => useWorkflowStartRunByCanEdit(true), { + resource: { kind: 'app', appId: 'app-1', mode: 'advanced-chat' }, + }) await act(async () => { result.current.handleStartWorkflowRun() diff --git a/web/app/components/workflow-app/hooks/__tests__/use-workflow-template.spec.ts b/web/app/components/workflow-app/hooks/__tests__/use-workflow-template.spec.ts index 4f1317c8707..1f14a1c1be6 100644 --- a/web/app/components/workflow-app/hooks/__tests__/use-workflow-template.spec.ts +++ b/web/app/components/workflow-app/hooks/__tests__/use-workflow-template.spec.ts @@ -1,24 +1,24 @@ -import { - createConsoleQueryClient, - renderHookWithConsoleQuery, - seedAppDetail, -} from '@/test/console/query-data' +import type { AppMode } from '@dify/contracts/api/console/apps/types.gen' +import { renderEditorHook as renderHookWithConsoleQuery } from '@/app/components/workflow/editor-resource/__tests__/render' +import { createConsoleQueryClient, seedAppDetail } from '@/test/console/query-data' import { createAppDetailFixture } from '@/test/fixtures/app' -import { AppModeEnum } from '@/types/app' import { useWorkflowTemplate } from '../use-workflow-template' const mockUseIsChatMode = vi.fn() let generateNewNodeCalls: Array> = [] -const renderTemplateHook = (mode: AppModeEnum) => { +const renderTemplateHook = (mode: AppMode) => { const queryClient = createConsoleQueryClient() seedAppDetail(queryClient, createAppDetailFixture({ id: 'app-1', mode })) return renderHookWithConsoleQuery(() => useWorkflowTemplate(), { queryClient }) } -vi.mock('../use-is-chat-mode', () => ({ - useIsChatMode: () => mockUseIsChatMode(), -})) +vi.mock('@/app/components/workflow/editor-resource/state', async (importOriginal) => { + const actual = + await importOriginal() + const { atom } = await import('jotai') + return { ...actual, isChatflowAtom: atom(() => mockUseIsChatMode()) } +}) vi.mock('@/next/navigation', async (importOriginal) => ({ ...(await importOriginal()), @@ -55,7 +55,7 @@ describe('useWorkflowTemplate', () => { it('should return only the start placeholder template in workflow mode', () => { mockUseIsChatMode.mockReturnValue(false) - const { result } = renderTemplateHook(AppModeEnum.WORKFLOW) + const { result } = renderTemplateHook('workflow') expect(result.current.nodes).toHaveLength(1) expect(result.current.edges).toEqual([]) @@ -71,7 +71,7 @@ describe('useWorkflowTemplate', () => { it('should return the start node template for non-workflow app modes', () => { mockUseIsChatMode.mockReturnValue(false) - const { result } = renderTemplateHook(AppModeEnum.COMPLETION) + const { result } = renderTemplateHook('completion') expect(result.current.nodes).toHaveLength(1) expect(result.current.edges).toEqual([]) @@ -85,7 +85,7 @@ describe('useWorkflowTemplate', () => { it('should build start, llm, and answer templates with linked edges in chat mode', () => { mockUseIsChatMode.mockReturnValue(true) - const { result } = renderTemplateHook(AppModeEnum.ADVANCED_CHAT) + const { result } = renderTemplateHook('advanced-chat') expect(result.current.nodes).toHaveLength(3) expect(result.current.nodes.map((node) => node.id)).toEqual(['generated-1', 'llm', 'answer']) diff --git a/web/app/components/workflow/__tests__/edge-insert-focus.browser.spec.tsx b/web/app/components/workflow/__tests__/edge-insert-focus.browser.spec.tsx index 6dfddd9018c..5d5e4f9e144 100644 --- a/web/app/components/workflow/__tests__/edge-insert-focus.browser.spec.tsx +++ b/web/app/components/workflow/__tests__/edge-insert-focus.browser.spec.tsx @@ -5,6 +5,7 @@ import { page, userEvent } from 'vite-plus/test/browser' import { render } from 'vitest-browser-react' import { WorkflowContext } from '../context' import CustomEdge from '../custom-edge' +import { EditorResourceScope } from '../editor-resource/provider' import { createWorkflowStore } from '../store/workflow' import { BlockEnum } from '../types' import 'reactflow/dist/style.css' @@ -53,20 +54,22 @@ const edges = [ function Fixture() { const [store] = useState(() => createWorkflowStore({})) return ( - - - -
- -
-
-
+ + + + +
+ +
+
+
+
) } diff --git a/web/app/components/workflow/__tests__/features.spec.tsx b/web/app/components/workflow/__tests__/features.spec.tsx index 5d9da98e74c..4b68fefa8ad 100644 --- a/web/app/components/workflow/__tests__/features.spec.tsx +++ b/web/app/components/workflow/__tests__/features.spec.tsx @@ -35,7 +35,7 @@ vi.mock('../hooks/use-workflow', async (importOriginal) => { const actual = await importOriginal() return { ...actual, - useIsChatMode: () => mockIsChatMode, + useNodesReadOnly: () => ({ nodesReadOnly: mockNodesReadOnly, }), @@ -230,3 +230,10 @@ describe('Features', () => { }) }) }) + +vi.mock('@/app/components/workflow/editor-resource/state', async (importOriginal) => { + const actual = + await importOriginal() + const { atom } = await import('jotai') + return { ...actual, isChatflowAtom: atom(() => mockIsChatMode) } +}) diff --git a/web/app/components/workflow-app/hooks/__tests__/use-available-nodes-meta-data.spec.ts b/web/app/components/workflow/__tests__/node-catalog.spec.ts similarity index 74% rename from web/app/components/workflow-app/hooks/__tests__/use-available-nodes-meta-data.spec.ts rename to web/app/components/workflow/__tests__/node-catalog.spec.ts index 8d105f403b7..057d2392ff0 100644 --- a/web/app/components/workflow-app/hooks/__tests__/use-available-nodes-meta-data.spec.ts +++ b/web/app/components/workflow/__tests__/node-catalog.spec.ts @@ -1,15 +1,11 @@ -import { renderHook } from '@testing-library/react' +import { renderWorkflowHook } from '@/app/components/workflow/__tests__/workflow-test-env' +import { useWorkflowNodeCatalog } from '@/app/components/workflow/node-catalog' import { BlockEnum } from '@/app/components/workflow/types' -import { useAvailableNodesMetaData } from '../use-available-nodes-meta-data' const mockUseIsChatMode = vi.fn() const mockIsAgentV2Enabled = vi.hoisted(() => vi.fn(() => true)) const mockIsAgentV2InChatflowEnabled = vi.hoisted(() => vi.fn(() => false)) -vi.mock('../use-is-chat-mode', () => ({ - useIsChatMode: () => mockUseIsChatMode(), -})) - vi.mock('@/features/agent-v2/feature-flag', () => ({ isAgentV2Enabled: () => mockIsAgentV2Enabled(), isAgentV2InChatflowEnabled: () => mockIsAgentV2InChatflowEnabled(), @@ -19,7 +15,7 @@ vi.mock('@/context/i18n', () => ({ useDocLink: () => (path: string) => `/docs${path}`, })) -describe('useAvailableNodesMetaData', () => { +describe('useWorkflowNodeCatalog', () => { beforeEach(() => { vi.clearAllMocks() mockIsAgentV2Enabled.mockReturnValue(true) @@ -29,7 +25,13 @@ describe('useAvailableNodesMetaData', () => { it('should include chat-specific nodes and make the start node undeletable in chat mode', () => { mockUseIsChatMode.mockReturnValue(true) - const { result } = renderHook(() => useAvailableNodesMetaData()) + const { result } = renderWorkflowHook(() => useWorkflowNodeCatalog(), { + resource: { + kind: 'app', + appId: 'app-1', + mode: mockUseIsChatMode() ? 'advanced-chat' : 'workflow', + }, + }) expect(result.current.nodesMap?.[BlockEnum.Start]?.metaData.isUndeletable).toBe(true) expect(result.current.nodesMap?.[BlockEnum.Answer]).toBeDefined() @@ -46,7 +48,13 @@ describe('useAvailableNodesMetaData', () => { it('should expose only legacy Agent in chat mode by default even when Agent v2 is enabled', () => { mockUseIsChatMode.mockReturnValue(true) - const { result } = renderHook(() => useAvailableNodesMetaData()) + const { result } = renderWorkflowHook(() => useWorkflowNodeCatalog(), { + resource: { + kind: 'app', + appId: 'app-1', + mode: mockUseIsChatMode() ? 'advanced-chat' : 'workflow', + }, + }) const nodeTypes = result.current.nodes.map((node) => node.metaData.type) expect(nodeTypes).toContain(BlockEnum.Agent) @@ -59,7 +67,13 @@ describe('useAvailableNodesMetaData', () => { mockUseIsChatMode.mockReturnValue(true) mockIsAgentV2InChatflowEnabled.mockReturnValue(true) - const { result } = renderHook(() => useAvailableNodesMetaData()) + const { result } = renderWorkflowHook(() => useWorkflowNodeCatalog(), { + resource: { + kind: 'app', + appId: 'app-1', + mode: mockUseIsChatMode() ? 'advanced-chat' : 'workflow', + }, + }) const nodeTypes = result.current.nodes.map((node) => node.metaData.type) expect(nodeTypes).toContain(BlockEnum.AgentV2) @@ -73,7 +87,13 @@ describe('useAvailableNodesMetaData', () => { mockIsAgentV2Enabled.mockReturnValue(false) mockIsAgentV2InChatflowEnabled.mockReturnValue(true) - const { result } = renderHook(() => useAvailableNodesMetaData()) + const { result } = renderWorkflowHook(() => useWorkflowNodeCatalog(), { + resource: { + kind: 'app', + appId: 'app-1', + mode: mockUseIsChatMode() ? 'advanced-chat' : 'workflow', + }, + }) const nodeTypes = result.current.nodes.map((node) => node.metaData.type) expect(nodeTypes).toContain(BlockEnum.Agent) @@ -85,7 +105,13 @@ describe('useAvailableNodesMetaData', () => { it('should include workflow-specific trigger and end nodes outside chat mode', () => { mockUseIsChatMode.mockReturnValue(false) - const { result } = renderHook(() => useAvailableNodesMetaData()) + const { result } = renderWorkflowHook(() => useWorkflowNodeCatalog(), { + resource: { + kind: 'app', + appId: 'app-1', + mode: mockUseIsChatMode() ? 'advanced-chat' : 'workflow', + }, + }) expect(result.current.nodesMap?.[BlockEnum.Start]?.metaData.isUndeletable).toBe(false) expect(result.current.nodesMap?.[BlockEnum.End]).toBeDefined() @@ -103,7 +129,13 @@ describe('useAvailableNodesMetaData', () => { it('should use explicit docs pages and skip nodes without generated docs pages', () => { mockUseIsChatMode.mockReturnValue(false) - const { result } = renderHook(() => useAvailableNodesMetaData()) + const { result } = renderWorkflowHook(() => useWorkflowNodeCatalog(), { + resource: { + kind: 'app', + appId: 'app-1', + mode: mockUseIsChatMode() ? 'advanced-chat' : 'workflow', + }, + }) expect(result.current.nodesMap?.[BlockEnum.End]?.metaData.helpLinkUri).toBe( '/docs/use-dify/nodes/output', @@ -121,7 +153,13 @@ describe('useAvailableNodesMetaData', () => { it('should hide legacy Agent from the node picker but retain its validator when Agent v2 is enabled', () => { mockUseIsChatMode.mockReturnValue(false) - const { result } = renderHook(() => useAvailableNodesMetaData()) + const { result } = renderWorkflowHook(() => useWorkflowNodeCatalog(), { + resource: { + kind: 'app', + appId: 'app-1', + mode: mockUseIsChatMode() ? 'advanced-chat' : 'workflow', + }, + }) const nodeTypes = result.current.nodes.map((node) => node.metaData.type) expect(nodeTypes).toContain(BlockEnum.AgentV2) @@ -137,7 +175,13 @@ describe('useAvailableNodesMetaData', () => { mockUseIsChatMode.mockReturnValue(false) mockIsAgentV2Enabled.mockReturnValue(false) - const { result } = renderHook(() => useAvailableNodesMetaData()) + const { result } = renderWorkflowHook(() => useWorkflowNodeCatalog(), { + resource: { + kind: 'app', + appId: 'app-1', + mode: mockUseIsChatMode() ? 'advanced-chat' : 'workflow', + }, + }) const nodeTypes = result.current.nodes.map((node) => node.metaData.type) expect(nodeTypes).toContain(BlockEnum.Agent) diff --git a/web/app/components/workflow/__tests__/panel-contextmenu.spec.tsx b/web/app/components/workflow/__tests__/panel-contextmenu.spec.tsx index 7a418254010..a81bf60dcb8 100644 --- a/web/app/components/workflow/__tests__/panel-contextmenu.spec.tsx +++ b/web/app/components/workflow/__tests__/panel-contextmenu.spec.tsx @@ -3,7 +3,6 @@ import { detectPlatform } from '@tanstack/react-hotkeys' import { act, fireEvent, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { withSelectorKey } from '@/test/i18n-mock' -import { FlowType } from '@/types/common' import { fullWorkflowAccessControl } from '../hooks-store' import { PanelContextmenu } from '../panel-contextmenu' import { BlockEnum } from '../types' @@ -76,7 +75,7 @@ vi.mock('../hooks/use-workflow', async (importOriginal) => { return { ...actual, - useIsChatMode: () => mockUseIsChatMode(), + useNodesReadOnly: () => mockUseNodesReadOnly(), } }) @@ -237,12 +236,9 @@ describe('PanelContextmenu', () => { initialStoreState: { contextMenuTarget: { type: 'panel' }, }, + resource: { kind: 'snippet' as const, snippetId: 'snippet-1' }, hooksStoreProps: { - configsMap: { - flowId: 'snippet-1', - flowType: FlowType.snippet, - fileSettings: {}, - }, + fileSettings: {}, }, }) @@ -293,3 +289,10 @@ describe('PanelContextmenu', () => { expect(screen.queryByText('nodes.note.addNote')).not.toBeInTheDocument() }) }) + +vi.mock('@/app/components/workflow/editor-resource/state', async (importOriginal) => { + const actual = + await importOriginal() + const { atom } = await import('jotai') + return { ...actual, isChatflowAtom: atom(() => mockUseIsChatMode()) } +}) diff --git a/web/app/components/workflow/__tests__/update-dsl-modal.helpers.spec.ts b/web/app/components/workflow/__tests__/update-dsl-modal.helpers.spec.ts index a8a91801f70..95e6c46fe11 100644 --- a/web/app/components/workflow/__tests__/update-dsl-modal.helpers.spec.ts +++ b/web/app/components/workflow/__tests__/update-dsl-modal.helpers.spec.ts @@ -1,5 +1,4 @@ import { DSLImportStatus } from '@/models/app' -import { AppModeEnum } from '@/types/app' import { BlockEnum } from '../types' import { getImportNotificationPayload, @@ -20,7 +19,7 @@ workflow: type: trigger-webhook ` - expect(validateDSLContent(content, AppModeEnum.ADVANCED_CHAT)).toBe(false) + expect(validateDSLContent(content, 'advanced-chat')).toBe(false) }) it('should reject disallowed node types inherited through YAML merge keys', () => { @@ -34,11 +33,11 @@ workflow: <<: *trigger ` - expect(validateDSLContent(content, AppModeEnum.ADVANCED_CHAT)).toBe(false) + expect(validateDSLContent(content, 'advanced-chat')).toBe(false) }) it('should reject malformed yaml and answer nodes in non-advanced mode', () => { - expect(validateDSLContent('[', AppModeEnum.CHAT)).toBe(false) + expect(validateDSLContent('[', 'chat')).toBe(false) expect( validateDSLContent( ` @@ -48,7 +47,7 @@ workflow: - data: type: answer `, - AppModeEnum.CHAT, + 'chat', ), ).toBe(false) }) @@ -63,28 +62,28 @@ workflow: - data: type: tool `, - AppModeEnum.ADVANCED_CHAT, + 'advanced-chat', ), ).toBe(true) }) it('should accept empty yaml content', () => { - expect(validateDSLContent('', AppModeEnum.CHAT)).toBe(true) + expect(validateDSLContent('', 'chat')).toBe(true) }) it('should accept comment-only yaml content', () => { - expect(validateDSLContent('# No document', AppModeEnum.CHAT)).toBe(true) + expect(validateDSLContent('# No document', 'chat')).toBe(true) }) it('should reject yaml streams with multiple documents', () => { - expect(validateDSLContent('---\n{}\n---\n{}', AppModeEnum.CHAT)).toBe(false) + expect(validateDSLContent('---\n{}\n---\n{}', 'chat')).toBe(false) }) it('should expose the invalid node sets per mode', () => { - expect(getInvalidNodeTypes(AppModeEnum.ADVANCED_CHAT)).toEqual( + expect(getInvalidNodeTypes('advanced-chat')).toEqual( expect.arrayContaining([BlockEnum.End, BlockEnum.TriggerWebhook]), ) - expect(getInvalidNodeTypes(AppModeEnum.CHAT)).toEqual([BlockEnum.Answer]) + expect(getInvalidNodeTypes('chat')).toEqual([BlockEnum.Answer]) }) }) diff --git a/web/app/components/workflow/__tests__/workflow-edge-events.spec.tsx b/web/app/components/workflow/__tests__/workflow-edge-events.spec.tsx index 706e6378461..9d34dc5bb7d 100644 --- a/web/app/components/workflow/__tests__/workflow-edge-events.spec.tsx +++ b/web/app/components/workflow/__tests__/workflow-edge-events.spec.tsx @@ -3,7 +3,6 @@ import { act, fireEvent, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import * as React from 'react' import { BaseEdge, internalsSymbol, Position, ReactFlowProvider, useStoreApi } from 'reactflow' -import { FlowType } from '@/types/common' import { WORKFLOW_DATA_UPDATE } from '../constants' import { Workflow } from '../index' import { ControlMode } from '../types' @@ -415,7 +414,7 @@ vi.mock('../hooks/use-workflow', () => ({ nodesReadOnly: false, getNodesReadOnly: () => false, }), - useIsChatMode: () => false, + useWorkflow: () => ({ isValidConnection: workflowHookMocks.isValidConnection, }), @@ -512,12 +511,9 @@ function renderSubject(options?: { , { initialStoreState, + resource: { kind: 'app' as const, appId: 'flow-1', mode: 'workflow' as const }, hooksStoreProps: { - configsMap: { - flowId: 'flow-1', - flowType: FlowType.appFlow, - fileSettings: {}, - }, + fileSettings: {}, }, }, ) @@ -939,3 +935,10 @@ describe('Workflow edge event wiring', () => { }) }) }) + +vi.mock('@/app/components/workflow/editor-resource/state', async (importOriginal) => { + const actual = + await importOriginal() + const { atom } = await import('jotai') + return { ...actual, isChatflowAtom: atom(() => false) } +}) diff --git a/web/app/components/workflow/__tests__/workflow-test-env.spec.tsx b/web/app/components/workflow/__tests__/workflow-test-env.spec.tsx index 529a2aadc50..27d6e9f8358 100644 --- a/web/app/components/workflow/__tests__/workflow-test-env.spec.tsx +++ b/web/app/components/workflow/__tests__/workflow-test-env.spec.tsx @@ -5,7 +5,6 @@ import type { Shape } from '../store/workflow' import { act, screen } from '@testing-library/react' import * as React from 'react' import { useNodes } from 'reactflow' -import { FlowType } from '@/types/common' import { useHooksStore } from '../hooks-store/store' import { useStore, useWorkflowStore } from '../store/workflow' import { createNode } from './fixtures' @@ -43,8 +42,12 @@ function StoreWriter() { } function HooksStoreReader() { - const flowId = useHooksStore((s) => s.configsMap?.flowId ?? 'none') - return React.createElement('div', { 'data-testid': 'hooks-reader' }, flowId) + const fileSettings = useHooksStore((s) => s.fileSettings) + return React.createElement( + 'div', + { 'data-testid': 'hooks-reader' }, + fileSettings ? 'configured' : 'none', + ) } function NodeRenderer(props: { id: string; data: { title: string }; selected?: boolean }) { @@ -104,11 +107,12 @@ describe('renderWorkflowComponent', () => { it('should provide HooksStoreContext when hooksStoreProps given', () => { renderWorkflowComponent(React.createElement(HooksStoreReader), { + resource: { kind: 'app' as const, appId: 'test-123', mode: 'workflow' as const }, hooksStoreProps: { - configsMap: { flowId: 'test-123', flowType: FlowType.appFlow, fileSettings: {} }, + fileSettings: {}, }, }) - expect(screen.getByTestId('hooks-reader'))!.toHaveTextContent('test-123') + expect(screen.getByTestId('hooks-reader'))!.toHaveTextContent('configured') }) it('should provide HooksStoreContext with default store', () => { diff --git a/web/app/components/workflow/__tests__/workflow-test-env.tsx b/web/app/components/workflow/__tests__/workflow-test-env.tsx index caba2808b04..251a408852f 100644 --- a/web/app/components/workflow/__tests__/workflow-test-env.tsx +++ b/web/app/components/workflow/__tests__/workflow-test-env.tsx @@ -66,6 +66,7 @@ import type { RenderOptions, RenderResult, } from '@testing-library/react' +import type { EditorResource } from '../editor-resource/resource' import type { Shape as HooksStoreShape } from '../hooks-store/store' import type { Shape } from '../store/workflow' import type { WorkflowHistoryState } from '../store/workflow/history-slice' @@ -73,11 +74,14 @@ import type { Edge, Node, WorkflowRunningData } from '../types' import { QueryClient } from '@tanstack/react-query' import * as React from 'react' import ReactFlow, { ReactFlowProvider } from 'reactflow' +import { consoleQuery } from '@/service/console' import { seedAccountProfileQuery } from '@/test/console/account-profile' import { createQueryClientWrapper } from '@/test/console/query-client' -import { seedAppDslVersion, seedSystemFeatures } from '@/test/console/query-data' +import { seedAppDetail, seedAppDslVersion, seedSystemFeatures } from '@/test/console/query-data' import { render, renderHook } from '@/test/console/render' +import { createAppDetailFixture } from '@/test/fixtures/app' import { WorkflowContext } from '../context' +import { EditorResourceScope } from '../editor-resource/provider' import { HooksStoreContext } from '../hooks-store/provider' import { createHooksStore } from '../hooks-store/store' import { createWorkflowStore } from '../store/workflow' @@ -133,6 +137,7 @@ type HistoryStoreConfig = { } type WorkflowProviderOptions = { + resource?: EditorResource initialStoreState?: Partial hooksStoreProps?: Partial historyStore?: HistoryStoreConfig @@ -154,6 +159,7 @@ function createWorkflowWrapper( stores: StoreInstances, historyConfig?: HistoryStoreConfig, externalQueryClient?: QueryClient, + resource: EditorResource = { kind: 'app', appId: 'app-1', mode: 'workflow' }, ) { if (historyConfig) { stores.store.temporal.getState().pause() @@ -176,6 +182,25 @@ function createWorkflowWrapper( if (!externalQueryClient) seedSystemFeatures(queryClient) if (!externalQueryClient) seedAppDslVersion(queryClient) if (!externalQueryClient) seedAccountProfileQuery(queryClient) + if (resource.kind === 'app') { + const input = { input: { params: { app_id: resource.appId } } } + for (const endpoint of [ + consoleQuery.apps.byAppId.workflows.draft.systemVariables.get, + consoleQuery.apps.byAppId.workflows.draft.conversationVariables.get, + ]) { + const variableKey = endpoint.queryKey(input) + if (!queryClient.getQueryData(variableKey)) + queryClient.setQueryData(variableKey, { items: [] }) + } + const key = consoleQuery.apps.byAppId.get.queryKey({ + input: { params: { app_id: resource.appId } }, + }) + if (!queryClient.getQueryData(key)) + seedAppDetail( + queryClient, + createAppDetailFixture({ id: resource.appId, mode: resource.mode }), + ) + } const QueryClientWrapper = createQueryClientWrapper(queryClient) return ({ children }: { children: React.ReactNode }) => { @@ -188,7 +213,10 @@ function createWorkflowWrapper( return React.createElement( QueryClientWrapper, null, - React.createElement(WorkflowContext.Provider, { value: stores.store }, inner), + React.createElement(EditorResourceScope, { + identity: resource, + children: React.createElement(WorkflowContext.Provider, { value: stores.store }, inner), + }), ) } } @@ -218,11 +246,12 @@ export function renderWorkflowHook( hooksStoreProps, historyStore: historyConfig, queryClient, + resource, ...rest } = options ?? {} const stores = createStoresFromOptions({ initialStoreState, hooksStoreProps }) - const wrapper = createWorkflowWrapper(stores, historyConfig, queryClient) + const wrapper = createWorkflowWrapper(stores, historyConfig, queryClient, resource) const renderResult = renderHook(hook, { wrapper, ...rest }) return { ...renderResult, ...stores } @@ -253,11 +282,12 @@ export function renderWorkflowComponent( hooksStoreProps, historyStore: historyConfig, queryClient, + resource, ...renderOptions } = options ?? {} const stores = createStoresFromOptions({ initialStoreState, hooksStoreProps }) - const wrapper = createWorkflowWrapper(stores, historyConfig, queryClient) + const wrapper = createWorkflowWrapper(stores, historyConfig, queryClient, resource) const renderResult = render(ui, { wrapper, ...renderOptions }) return { ...renderResult, ...stores } @@ -285,9 +315,11 @@ function createWorkflowFlowWrapper( edges = [], reactFlowProps, canvasStyle, + resource, + queryClient, }: WorkflowFlowOptions, ) { - const workflowWrapper = createWorkflowWrapper(stores, historyConfig) + const workflowWrapper = createWorkflowWrapper(stores, historyConfig, queryClient, resource) return ({ children }: { children: React.ReactNode }) => React.createElement( @@ -318,6 +350,8 @@ export function renderWorkflowFlowComponent( edges, reactFlowProps, canvasStyle, + resource, + queryClient, ...renderOptions } = options ?? {} @@ -328,6 +362,8 @@ export function renderWorkflowFlowComponent( edges, reactFlowProps, canvasStyle, + resource, + queryClient, }) const renderResult = render(ui, { wrapper, ...renderOptions }) @@ -346,6 +382,8 @@ export function renderWorkflowFlowHook( edges, reactFlowProps, canvasStyle, + resource, + queryClient, ...rest } = options ?? {} @@ -356,6 +394,8 @@ export function renderWorkflowFlowHook( edges, reactFlowProps, canvasStyle, + resource, + queryClient, }) const renderResult = renderHook(hook, { wrapper, ...rest }) diff --git a/web/app/components/workflow/block-selector/__tests__/agent-selector.spec.tsx b/web/app/components/workflow/block-selector/__tests__/agent-selector.spec.tsx index ce256df8513..5849aec25c3 100644 --- a/web/app/components/workflow/block-selector/__tests__/agent-selector.spec.tsx +++ b/web/app/components/workflow/block-selector/__tests__/agent-selector.spec.tsx @@ -1,8 +1,10 @@ import type { AgentRosterNodeData } from '../types' +import type { EditorIdentity } from '@/app/components/workflow/editor-resource/state' import { Button } from '@langgenius/dify-ui/button' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { render, screen, waitFor, within } from '@testing-library/react' import userEvent from '@testing-library/user-event' +import { EditorResourceScope } from '@/app/components/workflow/editor-resource/provider' import { AgentSelectorMenu, AgentSelectorMenuContent, @@ -23,23 +25,28 @@ vi.mock('@/app/notifications', () => ({ toast: { error: mocks.toastError }, })) -vi.mock('@/service/console', () => ({ - consoleQuery: { - agent: { - inviteOptions: { - get: { - queryOptions: (args: unknown) => { - mocks.queryOptions(args) - return { - queryKey: ['agent-invite-options', args], - queryFn: async () => ({ data: mocks.agents }), - } +vi.mock('@/service/console', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + consoleQuery: { + ...actual.consoleQuery, + agent: { + inviteOptions: { + get: { + queryOptions: (args: unknown) => { + mocks.queryOptions(args) + return { + queryKey: ['agent-invite-options', args], + queryFn: async () => ({ data: mocks.agents }), + } + }, }, }, }, }, - }, -})) + } +}) const createAgent = (overrides: Record = {}) => ({ id: 'agent-1', @@ -56,19 +63,23 @@ const createAgent = (overrides: Record = {}) => ({ const renderMenu = async ({ onSelect = vi.fn<(agent: AgentRosterNodeData) => void>(), onStartFromScratch, + identity = { kind: 'app', appId: 'app-1' }, }: { onSelect?: ReturnType void>> onStartFromScratch?: () => void + identity?: EditorIdentity } = {}) => { const user = userEvent.setup() const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }) render( - - Open agents - - + + + Open agents + + + , ) @@ -85,6 +96,13 @@ describe('AgentSelectorMenu', () => { mocks.toastError.mockClear() }) + it('does not send a snippet ID as the workflow app filter', async () => { + await renderMenu({ identity: { kind: 'snippet', snippetId: 'snippet-1' } }) + expect(mocks.queryOptions).toHaveBeenCalledWith({ + input: { query: { limit: 8, page: 1 } }, + }) + }) + it('lists agents and keeps the footer actions inside the menu', async () => { mocks.agents = [createAgent(), createAgent({ id: 'agent-2', name: 'Bob' })] const { menu } = await renderMenu({ onStartFromScratch: vi.fn() }) @@ -149,12 +167,14 @@ describe('AgentSelectorMenu', () => { render( - - - }> - Open agents - - + + + + }> + Open agents + + + , ) diff --git a/web/app/components/workflow/block-selector/__tests__/all-start-blocks.spec.tsx b/web/app/components/workflow/block-selector/__tests__/all-start-blocks.spec.tsx index 98a4868765d..afce1f62939 100644 --- a/web/app/components/workflow/block-selector/__tests__/all-start-blocks.spec.tsx +++ b/web/app/components/workflow/block-selector/__tests__/all-start-blocks.spec.tsx @@ -6,7 +6,7 @@ import { useLocale } from '#i18n' import { useMarketplacePlugins } from '@/app/components/plugins/marketplace/query' import { PluginCategoryEnum } from '@/app/components/plugins/types' import { CollectionType } from '@/app/components/tools/types' -import { useAvailableNodesMetaData } from '@/app/components/workflow-app/hooks/use-available-nodes-meta-data' +import { useWorkflowNodeCatalog } from '@/app/components/workflow/node-catalog' import { useGetLanguage } from '@/context/i18n' import useTheme from '@/hooks/use-theme' import { useFeaturedTriggersRecommendations } from '@/service/use-plugins' @@ -48,8 +48,8 @@ vi.mock('../../store/workflow/use-nodes', () => ({ default: vi.fn(), })) -vi.mock('@/app/components/workflow-app/hooks/use-available-nodes-meta-data', () => ({ - useAvailableNodesMetaData: vi.fn(), +vi.mock('@/app/components/workflow/node-catalog', () => ({ + useWorkflowNodeCatalog: vi.fn(), })) vi.mock('@/utils/var', async (importOriginal) => { @@ -75,7 +75,7 @@ const mockUseAllTriggerPlugins = vi.mocked(useAllTriggerPlugins) const mockUseInvalidateAllTriggerPlugins = vi.mocked(useInvalidateAllTriggerPlugins) const mockUseFeaturedTriggersRecommendations = vi.mocked(useFeaturedTriggersRecommendations) const mockUseNodes = vi.mocked(useNodes) -const mockUseAvailableNodesMetaData = vi.mocked(useAvailableNodesMetaData) +const mockUseAvailableNodesMetaData = vi.mocked(useWorkflowNodeCatalog) type UseMarketplacePluginsReturn = ReturnType type UseAllTriggerPluginsReturn = ReturnType @@ -175,10 +175,10 @@ const createFeaturedTriggersRecommendationsMock = ( ...overrides, }) -const createAvailableNodesMetaData = (): ReturnType => +const createAvailableNodesMetaData = (): ReturnType => ({ nodes: [], - }) as unknown as ReturnType + }) as unknown as ReturnType describe('AllStartBlocks', () => { beforeEach(() => { diff --git a/web/app/components/workflow/block-selector/__tests__/blocks.spec.tsx b/web/app/components/workflow/block-selector/__tests__/blocks.spec.tsx index 659890e3852..539a5830b80 100644 --- a/web/app/components/workflow/block-selector/__tests__/blocks.spec.tsx +++ b/web/app/components/workflow/block-selector/__tests__/blocks.spec.tsx @@ -2,11 +2,12 @@ import type { AgentInviteOptionResponse, AgentInviteOptionsResponse, } from '@dify/contracts/api/console/agent/types.gen' +import type { ReactElement } from 'react' import type { NodeDefault } from '../../types' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' -import { render, screen, waitFor } from '@testing-library/react' +import { render as baseRender, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' -import { FlowType } from '@/types/common' +import { EditorResourceScope } from '../../editor-resource/provider' import { HooksStoreContext } from '../../hooks-store/provider' import { createHooksStore } from '../../hooks-store/store' import { BlockEnum } from '../../types' @@ -173,11 +174,7 @@ describe('Blocks', () => { }, }) const hooksStore = createHooksStore({ - configsMap: { - flowId: 'app-1', - flowType: FlowType.appFlow, - fileSettings: {} as never, - }, + fileSettings: {} as never, }) render( @@ -221,11 +218,7 @@ describe('Blocks', () => { }, }) const hooksStore = createHooksStore({ - configsMap: { - flowId: 'app-1', - flowType: FlowType.appFlow, - fileSettings: {} as never, - }, + fileSettings: {} as never, }) render( @@ -313,11 +306,7 @@ describe('Blocks', () => { }, }) const hooksStore = createHooksStore({ - configsMap: { - flowId: 'app-1', - flowType: FlowType.appFlow, - fileSettings: {} as never, - }, + fileSettings: {} as never, }) render( @@ -371,11 +360,7 @@ describe('Blocks', () => { }, }) const hooksStore = createHooksStore({ - configsMap: { - flowId: 'app-1', - flowType: FlowType.appFlow, - fileSettings: {} as never, - }, + fileSettings: {} as never, }) render( @@ -414,11 +399,7 @@ describe('Blocks', () => { }, }) const hooksStore = createHooksStore({ - configsMap: { - flowId: 'app-1', - flowType: FlowType.appFlow, - fileSettings: {} as never, - }, + fileSettings: {} as never, }) render( @@ -467,11 +448,7 @@ describe('Blocks', () => { }, }) const hooksStore = createHooksStore({ - configsMap: { - flowId: 'app-1', - flowType: FlowType.appFlow, - fileSettings: {} as never, - }, + fileSettings: {} as never, }) render( @@ -503,3 +480,9 @@ describe('Blocks', () => { }) }) }) + +function render(ui: ReactElement) { + return baseRender( + {ui}, + ) +} diff --git a/web/app/components/workflow/block-selector/__tests__/index.spec.tsx b/web/app/components/workflow/block-selector/__tests__/index.spec.tsx index 65673beaebf..a9a019821a3 100644 --- a/web/app/components/workflow/block-selector/__tests__/index.spec.tsx +++ b/web/app/components/workflow/block-selector/__tests__/index.spec.tsx @@ -4,7 +4,6 @@ import { Button } from '@langgenius/dify-ui/button' import { act, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { useState } from 'react' -import { FlowType } from '@/types/common' import BlockSelector from '..' import { renderWorkflowComponent } from '../../__tests__/workflow-test-env' import { BlockEnum } from '../../types' @@ -64,14 +63,10 @@ type RenderBlockSelectorOptions = Parameters[1] const renderBlockSelector = (ui: ReactElement, options?: RenderBlockSelectorOptions) => { return renderWorkflowComponent(ui, { ...options, + resource: { kind: 'app' as const, appId: 'app-1', mode: 'workflow' as const }, hooksStoreProps: { ...options?.hooksStoreProps, - configsMap: { - flowId: 'app-1', - flowType: FlowType.appFlow, - fileSettings: {} as never, - ...options?.hooksStoreProps?.configsMap, - }, + fileSettings: {} as never, }, }) } diff --git a/web/app/components/workflow/block-selector/__tests__/start-blocks.spec.tsx b/web/app/components/workflow/block-selector/__tests__/start-blocks.spec.tsx index bf757ae1a05..65d69d6e0e1 100644 --- a/web/app/components/workflow/block-selector/__tests__/start-blocks.spec.tsx +++ b/web/app/components/workflow/block-selector/__tests__/start-blocks.spec.tsx @@ -1,7 +1,7 @@ import type { CommonNodeType } from '../../types' import { render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' -import { useAvailableNodesMetaData } from '@/app/components/workflow-app/hooks/use-available-nodes-meta-data' +import { useWorkflowNodeCatalog } from '@/app/components/workflow/node-catalog' import useNodes from '@/app/components/workflow/store/workflow/use-nodes' import { BlockEnum } from '../../types' import StartBlocks from '../start-blocks' @@ -10,22 +10,22 @@ vi.mock('@/app/components/workflow/store/workflow/use-nodes', () => ({ default: vi.fn(), })) -vi.mock('@/app/components/workflow-app/hooks/use-available-nodes-meta-data', () => ({ - useAvailableNodesMetaData: vi.fn(), +vi.mock('@/app/components/workflow/node-catalog', () => ({ + useWorkflowNodeCatalog: vi.fn(), })) const mockUseNodes = vi.mocked(useNodes) -const mockUseAvailableNodesMetaData = vi.mocked(useAvailableNodesMetaData) +const mockUseAvailableNodesMetaData = vi.mocked(useWorkflowNodeCatalog) const createNode = (type: BlockEnum) => ({ data: { type } as Pick, }) as ReturnType[number] -const createAvailableNodesMetaData = (): ReturnType => +const createAvailableNodesMetaData = (): ReturnType => ({ nodes: [], - }) as unknown as ReturnType + }) as unknown as ReturnType describe('StartBlocks', () => { beforeEach(() => { diff --git a/web/app/components/workflow/comment/__tests__/comment-close-focus.spec.tsx b/web/app/components/workflow/comment/__tests__/comment-close-focus.spec.tsx index 05a6411ac35..c4cf70a4c34 100644 --- a/web/app/components/workflow/comment/__tests__/comment-close-focus.spec.tsx +++ b/web/app/components/workflow/comment/__tests__/comment-close-focus.spec.tsx @@ -18,10 +18,6 @@ vi.mock('reactflow', async () => (await import('../../__tests__/reactflow-mock-state')).createReactFlowModuleMock(), ) -vi.mock('@/next/navigation', () => ({ - useParams: () => ({ appId: 'app-1' }), -})) - vi.mock('@/hooks/use-format-time-from-now', () => ({ useFormatTimeFromNow: () => ({ formatTimeFromNow: () => 'just now' }), })) diff --git a/web/app/components/workflow/comment/__tests__/comment-thread-focus.spec.tsx b/web/app/components/workflow/comment/__tests__/comment-thread-focus.spec.tsx index 8bde90723c2..3599b30e2e3 100644 --- a/web/app/components/workflow/comment/__tests__/comment-thread-focus.spec.tsx +++ b/web/app/components/workflow/comment/__tests__/comment-thread-focus.spec.tsx @@ -2,20 +2,22 @@ import type { UserProfile, WorkflowCommentDetail, WorkflowCommentList } from '.. import { act, screen, waitFor, within } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { useState } from 'react' -import { renderWithAccountProfile } from '@/test/console/account-profile' +import { renderWithAccountProfile as renderAccountProfile } from '@/test/console/account-profile' +import { EditorResourceScope } from '../../editor-resource/provider' import { CommentIcon } from '../comment-icon' import { CommentInput } from '../comment-input' import { CommentThread } from '../thread' +const renderWithAccountProfile = (ui: Parameters[0]) => + renderAccountProfile( + {ui}, + ) + const storeState = vi.hoisted(() => ({ mentionableUsersCache: { 'app-1': [] } as Record, setCommentPreviewHovering: vi.fn(), })) -vi.mock('@/next/navigation', () => ({ - useParams: () => ({ appId: 'app-1' }), -})) - vi.mock('reactflow', () => ({ useReactFlow: () => ({ flowToScreenPosition: (position: { x: number; y: number }) => position, diff --git a/web/app/components/workflow/comment/mention-input.spec.tsx b/web/app/components/workflow/comment/mention-input.spec.tsx index 06af12b3c3b..0fbfa60538d 100644 --- a/web/app/components/workflow/comment/mention-input.spec.tsx +++ b/web/app/components/workflow/comment/mention-input.spec.tsx @@ -1,8 +1,15 @@ +import type { ReactElement } from 'react' import type { UserProfile } from '@/app/components/workflow/comment/types' -import { act, fireEvent, render, screen, waitFor } from '@testing-library/react' +import { act, fireEvent, render as renderComponent, screen, waitFor } from '@testing-library/react' import { useState } from 'react' +import { EditorResourceScope } from '../editor-resource/provider' import { MentionInput } from './mention-input' +const render = (ui: ReactElement) => + renderComponent( + {ui}, + ) + const mockFetchMentionableUsers = vi.hoisted(() => vi.fn()) const mockSetMentionableUsersLoading = vi.hoisted(() => vi.fn()) const mockSetMentionableUsersCache = vi.hoisted(() => vi.fn()) @@ -19,10 +26,6 @@ const mentionStoreState = vi.hoisted(() => ({ mentionStoreState.mentionableUsersCache[appId] = users }, })) -vi.mock('@/next/navigation', () => ({ - useParams: () => ({ appId: 'app-1' }), -})) - vi.mock('@/service/console', () => ({ consoleClient: { apps: { diff --git a/web/app/components/workflow/comment/placement-preview.spec.tsx b/web/app/components/workflow/comment/placement-preview.spec.tsx index aa45ea15116..2d91e902d19 100644 --- a/web/app/components/workflow/comment/placement-preview.spec.tsx +++ b/web/app/components/workflow/comment/placement-preview.spec.tsx @@ -4,8 +4,6 @@ import userEvent from '@testing-library/user-event' import { renderWorkflowComponent } from '../__tests__/workflow-test-env' import { CommentPlacementPreview } from './placement-preview' -vi.mock('next/navigation', () => ({ useParams: () => ({ appId: 'app-1' }) })) - function renderPlacement(placing = true) { const onCancel = vi.fn() const result = renderWorkflowComponent( diff --git a/web/app/components/workflow/comment/reply-deletion.browser.spec.tsx b/web/app/components/workflow/comment/reply-deletion.browser.spec.tsx index 704b63499ad..68ff98a715f 100644 --- a/web/app/components/workflow/comment/reply-deletion.browser.spec.tsx +++ b/web/app/components/workflow/comment/reply-deletion.browser.spec.tsx @@ -3,9 +3,9 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { Suspense, useState } from 'react' import { page, userEvent } from 'vite-plus/test/browser' import { render } from 'vitest-browser-react' +import { EditorResourceScope } from '../editor-resource/provider' import { CommentThread } from './thread' -vi.mock('@/next/navigation', () => ({ useParams: () => ({ appId: 'app-1' }) })) vi.mock('reactflow', () => ({ useReactFlow: () => ({ flowToScreenPosition: (position: { x: number; y: number }) => position }), useViewport: () => ({ x: 0, y: 0, zoom: 1 }), @@ -54,18 +54,20 @@ function Fixture({ onDelete }: { onDelete: (id: string) => void }) { ) return ( - Loading profile

}> - -
- {}} - onReply={() => {}} - onReplyEdit={() => {}} - onReplyDeleteDirect={onDelete} - /> -
-
+ + Loading profile

}> + +
+ {}} + onReply={() => {}} + onReplyEdit={() => {}} + onReplyDeleteDirect={onDelete} + /> +
+
+
) } diff --git a/web/app/components/workflow/comment/thread.spec.tsx b/web/app/components/workflow/comment/thread.spec.tsx index c955415b3f4..aded2465954 100644 --- a/web/app/components/workflow/comment/thread.spec.tsx +++ b/web/app/components/workflow/comment/thread.spec.tsx @@ -4,6 +4,7 @@ import { fireEvent, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { createAccountProfileQueryWrapper } from '@/test/console/account-profile' import { render as renderWithConsoleState } from '@/test/console/render' +import { EditorResourceScope } from '../editor-resource/provider' import { CommentThread } from './thread' const mockSetCommentPreviewHovering = vi.hoisted(() => vi.fn()) @@ -19,9 +20,12 @@ const mockConsoleState = vi.hoisted(() => ({ })) const render = (ui: ReactElement) => - renderWithConsoleState(ui, { - wrapper: createAccountProfileQueryWrapper(mockConsoleState.userProfile), - }) + renderWithConsoleState( + {ui}, + { + wrapper: createAccountProfileQueryWrapper(mockConsoleState.userProfile), + }, + ) const storeState = vi.hoisted(() => ({ mentionableUsersCache: { @@ -32,10 +36,6 @@ const storeState = vi.hoisted(() => ({ } as Record>, setCommentPreviewHovering: (...args: unknown[]) => mockSetCommentPreviewHovering(...args), })) -vi.mock('@/next/navigation', () => ({ - useParams: () => ({ appId: 'app-1' }), -})) - vi.mock('@/hooks/use-format-time-from-now', () => ({ useFormatTimeFromNow: () => ({ formatTimeFromNow: () => 'just now', diff --git a/web/app/components/workflow/editor-resource/__tests__/render.ts b/web/app/components/workflow/editor-resource/__tests__/render.ts new file mode 100644 index 00000000000..59f62546abf --- /dev/null +++ b/web/app/components/workflow/editor-resource/__tests__/render.ts @@ -0,0 +1,66 @@ +import type { RenderHookOptions, RenderOptions } from '@testing-library/react' +import type { ReactElement, ReactNode } from 'react' +import type { EditorResource } from '../resource' +import type { ConsoleQueryTestOptions } from '@/test/console/query-data' +import { useQueryClient } from '@tanstack/react-query' +import { createElement } from 'react' +import { consoleQuery } from '@/service/console' +import { createConsoleQueryWrapper, seedAppDetail } from '@/test/console/query-data' +import { QueryClientTestProvider } from '@/test/console/query-provider' +import { render, renderHook } from '@/test/console/render' +import { createAppDetailFixture } from '@/test/fixtures/app' +import { EditorResourceScope } from '../provider' + +type ResourceOption = { resource?: EditorResource } + +function createWrapper({ + resource = { kind: 'app', appId: 'app-1', mode: 'workflow' }, + wrapper: Wrapper, + ...options +}: ResourceOption & ConsoleQueryTestOptions & Pick) { + const { wrapper: ConsoleWrapper, ...query } = createConsoleQueryWrapper(options) + function ResourceFixture({ children }: { children: ReactNode }) { + const client = useQueryClient() + if ( + resource.kind === 'app' && + !client.getQueryData( + consoleQuery.apps.byAppId.get.queryKey({ input: { params: { app_id: resource.appId } } }), + ) + ) + seedAppDetail(client, createAppDetailFixture({ id: resource.appId, mode: resource.mode })) + return createElement( + QueryClientTestProvider, + { queryClient: client }, + createElement(EditorResourceScope, { identity: resource, children }), + ) + } + function EditorWrapper({ children }: { children: ReactNode }) { + const content = createElement(ResourceFixture, { children }) + return createElement( + ConsoleWrapper, + undefined, + Wrapper ? createElement(Wrapper, undefined, content) : content, + ) + } + return { ...query, wrapper: EditorWrapper } +} + +export function renderEditor( + ui: ReactElement, + { resource, wrapper, ...options }: RenderOptions & ConsoleQueryTestOptions & ResourceOption = {}, +) { + const { wrapper: EditorWrapper, ...query } = createWrapper({ ...options, resource, wrapper }) + return { ...render(ui, { ...options, wrapper: EditorWrapper }), ...query } +} + +export function renderEditorHook( + callback: (props: Props) => Result, + { + resource, + wrapper, + ...options + }: RenderHookOptions & ConsoleQueryTestOptions & ResourceOption = {}, +) { + const { wrapper: EditorWrapper, ...query } = createWrapper({ ...options, resource, wrapper }) + return { ...renderHook(callback, { ...options, wrapper: EditorWrapper }), ...query } +} diff --git a/web/app/components/workflow/editor-resource/__tests__/scope.spec.tsx b/web/app/components/workflow/editor-resource/__tests__/scope.spec.tsx new file mode 100644 index 00000000000..5e3ebee5a80 --- /dev/null +++ b/web/app/components/workflow/editor-resource/__tests__/scope.spec.tsx @@ -0,0 +1,146 @@ +import { act, render, screen, waitFor } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { useAtomValue } from 'jotai' +import { useState } from 'react' +import { WorkflowContextProvider } from '@/app/components/workflow/context' +import { useStore } from '@/app/components/workflow/store' +import { createConsoleQueryClient, seedAppDetail } from '@/test/console/query-data' +import { QueryClientTestProvider } from '@/test/console/query-provider' +import { createAppDetailFixture } from '@/test/fixtures/app' +import { EditorResourceScope } from '../provider' +import { getEditorResourceId } from '../resource' +import { editorAppNameAtom, editorResourceAtom, isChatflowAtom } from '../state' + +function ResourceSummary({ label }: { label: string }) { + const id = getEditorResourceId(useAtomValue(editorResourceAtom)) + const isChatflow = useAtomValue(isChatflowAtom) + const name = useAtomValue(editorAppNameAtom) + return ( + {`${id}:${isChatflow ? 'chatflow' : 'other'}:${name ?? ''}`} + ) +} + +function EditorSession() { + const [draft, setDraft] = useState('') + const showPreview = useStore((state) => state.showDebugAndPreviewPanel) + const setShowPreview = useStore((state) => state.setShowDebugAndPreviewPanel) + + return ( + <> + + setDraft(event.target.value)} + /> + + {showPreview &&
Preview is open
} + + ) +} + +describe('workflow editor resource scope', () => { + it('isolates simultaneous app, pipeline and snippet editors', () => { + const queryClient = createConsoleQueryClient() + seedAppDetail( + queryClient, + createAppDetailFixture({ id: 'app-1', mode: 'advanced-chat', name: 'Chatflow' }), + ) + render( + + + + + + + + + + + , + ) + expect(screen.getByLabelText('App')).toHaveTextContent('app-1:chatflow:Chatflow') + expect(screen.getByLabelText('Pipeline')).toHaveTextContent('pipeline-1:other:') + expect(screen.getByLabelText('Snippet')).toHaveTextContent('snippet-1:other:') + }) + + it('updates app facts from the query cache and switches resource identity', async () => { + const queryClient = createConsoleQueryClient() + seedAppDetail( + queryClient, + createAppDetailFixture({ id: 'app-1', mode: 'workflow', name: 'First' }), + ) + seedAppDetail( + queryClient, + createAppDetailFixture({ id: 'app-2', mode: 'advanced-chat', name: 'Second' }), + ) + const view = (appId: string) => ( + + + + + + ) + const { rerender } = render(view('app-1')) + expect(screen.getByLabelText('Editor')).toHaveTextContent('app-1:other:First') + await act(async () => { + seedAppDetail( + queryClient, + createAppDetailFixture({ id: 'app-1', mode: 'workflow', name: 'Renamed' }), + ) + }) + await waitFor(() => + expect(screen.getByLabelText('Editor')).toHaveTextContent('app-1:other:Renamed'), + ) + rerender(view('app-2')) + expect(screen.getByLabelText('Editor')).toHaveTextContent('app-2:chatflow:Second') + }) + + it('preserves the editor session when app facts refresh and resets it when the resource changes', async () => { + const user = userEvent.setup() + const queryClient = createConsoleQueryClient() + seedAppDetail( + queryClient, + createAppDetailFixture({ id: 'app-1', mode: 'workflow', name: 'First' }), + ) + seedAppDetail( + queryClient, + createAppDetailFixture({ id: 'app-2', mode: 'advanced-chat', name: 'Second' }), + ) + const view = (appId: string) => ( + + + + + + + + ) + const { rerender } = render(view('app-1')) + + await user.type(screen.getByRole('textbox', { name: 'Unsaved draft' }), 'Unpublished changes') + await user.click(screen.getByRole('button', { name: 'Open preview' })) + + await act(async () => { + seedAppDetail( + queryClient, + createAppDetailFixture({ id: 'app-1', mode: 'workflow', name: 'Renamed' }), + ) + }) + await waitFor(() => + expect(screen.getByLabelText('Editor')).toHaveTextContent('app-1:other:Renamed'), + ) + rerender(view('app-1')) + expect(screen.getByRole('textbox', { name: 'Unsaved draft' })).toHaveValue( + 'Unpublished changes', + ) + expect(screen.getByRole('region', { name: 'Preview' })).toBeVisible() + + rerender(view('app-2')) + expect(screen.getByLabelText('Editor')).toHaveTextContent('app-2:chatflow:Second') + expect(screen.getByRole('textbox', { name: 'Unsaved draft' })).toHaveValue('') + expect(screen.queryByRole('region', { name: 'Preview' })).not.toBeInTheDocument() + }) +}) diff --git a/web/app/components/workflow/header/__tests__/header-in-restoring.spec.tsx b/web/app/components/workflow/header/__tests__/header-in-restoring.spec.tsx index 315b4f5bea0..f2c42b707a3 100644 --- a/web/app/components/workflow/header/__tests__/header-in-restoring.spec.tsx +++ b/web/app/components/workflow/header/__tests__/header-in-restoring.spec.tsx @@ -8,7 +8,6 @@ import { seedFeatures, seedSystemFeatures, } from '@/test/console/query-data' -import { FlowType } from '@/types/common' import { renderWorkflowComponent as renderWorkflow } from '../../__tests__/workflow-test-env' import { WorkflowVersion } from '../../types' import HeaderInRestoring from '../header-in-restoring' @@ -40,7 +39,7 @@ vi.mock('@/hooks/use-format-time-from-now', () => ({ })) vi.mock('@/service/use-workflow', () => ({ - useInvalidAllLastRun: () => mockInvalidAllLastRun, + useInvalidateAllLastRun: () => mockInvalidAllLastRun, useResetWorkflowVersionHistory: () => mockResetWorkflowVersionHistory, useRestoreWorkflow: () => ({ mutateAsync: mockRestoreWorkflow, @@ -92,31 +91,12 @@ describe('HeaderInRestoring', () => { deploymentEdition = 'CLOUD' }) - it('should disable restore when the flow id is not ready yet', () => { - renderWorkflowComponent(, { - initialStoreState: { - currentVersion: createVersion(), - }, - hooksStoreProps: { - configsMap: undefined, - }, - }) - - expect(screen.getByRole('button', { name: 'workflow.common.restore' })).toBeDisabled() - }) - it('should enable restore when version and flow id are both ready', () => { renderWorkflowComponent(, { initialStoreState: { currentVersion: createVersion(), }, - hooksStoreProps: { - configsMap: { - flowId: 'app-1', - flowType: undefined as never, - fileSettings: {} as never, - }, - }, + hooksStoreProps: {}, }) expect(screen.getByRole('button', { name: 'workflow.common.restore' })).toBeEnabled() @@ -129,12 +109,9 @@ describe('HeaderInRestoring', () => { version: WorkflowVersion.Draft, }), }, + resource: { kind: 'app' as const, appId: 'app-1', mode: 'workflow' as const }, hooksStoreProps: { - configsMap: { - flowId: 'app-1', - flowType: FlowType.appFlow, - fileSettings: {} as never, - }, + fileSettings: {} as never, }, }) @@ -147,12 +124,9 @@ describe('HeaderInRestoring', () => { initialStoreState: { currentVersion: createVersion(), }, + resource: { kind: 'app' as const, appId: 'app-1', mode: 'workflow' as const }, hooksStoreProps: { - configsMap: { - flowId: 'app-1', - flowType: FlowType.appFlow, - fileSettings: {} as never, - }, + fileSettings: {} as never, }, }) diff --git a/web/app/components/workflow/header/__tests__/header-layouts.spec.tsx b/web/app/components/workflow/header/__tests__/header-layouts.spec.tsx index 08ad1a58dac..ab6c5897c1a 100644 --- a/web/app/components/workflow/header/__tests__/header-layouts.spec.tsx +++ b/web/app/components/workflow/header/__tests__/header-layouts.spec.tsx @@ -2,7 +2,6 @@ import type { Shape } from '../../store/workflow' import { fireEvent, screen, waitFor } from '@testing-library/react' import { createAccountProfileQueryClient } from '@/test/console/account-profile' import { seedSystemFeatures } from '@/test/console/query-data' -import { FlowType } from '@/types/common' import { renderWorkflowComponent } from '../../__tests__/workflow-test-env' import { WorkflowVersion } from '../../types' import HeaderInNormal from '../header-in-normal' @@ -71,7 +70,7 @@ vi.mock('@/hooks/use-theme', () => ({ })) vi.mock('@/service/use-workflow', () => ({ - useInvalidAllLastRun: () => mockInvalidAllLastRun, + useInvalidateAllLastRun: () => mockInvalidAllLastRun, useResetWorkflowVersionHistory: () => mockResetWorkflowVersionHistory, useRestoreWorkflow: () => ({ mutateAsync: mockRestoreWorkflow, @@ -269,12 +268,9 @@ describe('Header layout components', () => { isRestoring: true, showWorkflowVersionHistoryPanel: true, }, + resource: { kind: 'app' as const, appId: 'flow-1', mode: 'workflow' as const }, hooksStoreProps: { - configsMap: { - flowType: FlowType.appFlow, - flowId: 'flow-1', - fileSettings: {}, - }, + fileSettings: {}, }, }) @@ -303,12 +299,9 @@ describe('Header layout components', () => { currentVersion, deleteAllInspectVars, }, + resource: { kind: 'app' as const, appId: 'flow-1', mode: 'workflow' as const }, hooksStoreProps: { - configsMap: { - flowType: FlowType.appFlow, - flowId: 'flow-1', - fileSettings: {}, - }, + fileSettings: {}, }, queryClient, }, @@ -354,12 +347,9 @@ describe('Header layout components', () => { backupDraft: createBackupDraft(), currentVersion, }, + resource: { kind: 'knowledge-pipeline' as const, pipelineId: 'pipeline-1' }, hooksStoreProps: { - configsMap: { - flowType: FlowType.ragPipeline, - flowId: 'pipeline-1', - fileSettings: {}, - }, + fileSettings: {}, }, }) @@ -386,12 +376,9 @@ describe('Header layout components', () => { backupDraft: createBackupDraft(), currentVersion, }, + resource: { kind: 'snippet' as const, snippetId: 'snippet-1' }, hooksStoreProps: { - configsMap: { - flowType: FlowType.snippet, - flowId: 'snippet-1', - fileSettings: {}, - }, + fileSettings: {}, }, }) diff --git a/web/app/components/workflow/header/__tests__/running-title.spec.tsx b/web/app/components/workflow/header/__tests__/running-title.spec.tsx index bdc62573a2c..aff8025c396 100644 --- a/web/app/components/workflow/header/__tests__/running-title.spec.tsx +++ b/web/app/components/workflow/header/__tests__/running-title.spec.tsx @@ -4,9 +4,7 @@ import RunningTitle from '../running-title' let mockIsChatMode = false const mockFormatWorkflowRunIdentifier = vi.fn() -vi.mock('../../hooks/use-workflow', () => ({ - useIsChatMode: () => mockIsChatMode, -})) +vi.mock('../../hooks/use-workflow', () => ({})) vi.mock('../../utils', () => ({ formatWorkflowRunIdentifier: (finishedAt?: number) => mockFormatWorkflowRunIdentifier(finishedAt), @@ -59,3 +57,10 @@ describe('RunningTitle', () => { expect(container).toHaveTextContent('Test Run (14:30:25)') }) }) + +vi.mock('@/app/components/workflow/editor-resource/state', async (importOriginal) => { + const actual = + await importOriginal() + const { atom } = await import('jotai') + return { ...actual, isChatflowAtom: atom(() => mockIsChatMode) } +}) diff --git a/web/app/components/workflow/header/__tests__/view-history.spec.tsx b/web/app/components/workflow/header/__tests__/view-history.spec.tsx index 4674bef1111..a51e96d952b 100644 --- a/web/app/components/workflow/header/__tests__/view-history.spec.tsx +++ b/web/app/components/workflow/header/__tests__/view-history.spec.tsx @@ -17,9 +17,7 @@ const mockFormatWorkflowRunIdentifier = vi.fn( let mockIsChatMode = false -vi.mock('../../hooks/use-workflow', () => ({ - useIsChatMode: () => mockIsChatMode, -})) +vi.mock('../../hooks/use-workflow', () => ({})) vi.mock('../../hooks/use-nodes-interactions', () => ({ useNodesInteractions: () => ({ @@ -256,3 +254,10 @@ describe('ViewHistory', () => { expect(screen.queryByTestId('popover-content')).not.toBeInTheDocument() }) }) + +vi.mock('@/app/components/workflow/editor-resource/state', async (importOriginal) => { + const actual = + await importOriginal() + const { atom } = await import('jotai') + return { ...actual, isChatflowAtom: atom(() => mockIsChatMode) } +}) diff --git a/web/app/components/workflow/header/checklist/__tests__/index.spec.tsx b/web/app/components/workflow/header/checklist/__tests__/index.spec.tsx index 4cdef83f1a1..fa9f29bb3eb 100644 --- a/web/app/components/workflow/header/checklist/__tests__/index.spec.tsx +++ b/web/app/components/workflow/header/checklist/__tests__/index.spec.tsx @@ -1,6 +1,7 @@ import type { ChecklistItem } from '../../../hooks/use-checklist' -import { render, screen } from '@testing-library/react' +import { screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' +import { renderEditor as render } from '@/app/components/workflow/editor-resource/__tests__/render' import { BlockEnum } from '../../../types' import WorkflowChecklist from '../index' @@ -56,10 +57,10 @@ vi.mock('../../../store', () => ({ })) vi.mock('../../../hooks-store/store', () => ({ - useHooksStore: (selector: (state: { configsMap: { flowType: string } }) => unknown) => + useHooksStore: (selector: (state: { configsMap: { resourceKind: string } }) => unknown) => selector({ configsMap: { - flowType: 'workflow', + resourceKind: 'workflow', }, }), })) @@ -109,7 +110,9 @@ describe('WorkflowChecklist', () => { it('should split checklist items into plugin and node groups and delegate clicks to node selection by default', async () => { const user = userEvent.setup() - render() + render(, { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + }) expect(screen.getByText('2')).toBeInTheDocument() await user.click(screen.getByRole('button', { name: 'workflow.panel.checklist 2' })) @@ -123,7 +126,9 @@ describe('WorkflowChecklist', () => { it('should use the custom item click handler when provided', async () => { const user = userEvent.setup() const onItemClick = vi.fn() - render() + render(, { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + }) await user.click(screen.getByRole('button', { name: 'workflow.panel.checklist 2' })) await user.click(screen.getByTestId('node-group-Broken Node')) @@ -134,7 +139,9 @@ describe('WorkflowChecklist', () => { it('should show the tooltip without replacing the warning count in the accessible name', async () => { const user = userEvent.setup() - render() + render(, { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + }) const trigger = screen.getByRole('button', { name: 'workflow.panel.checklist 2' }) await user.hover(trigger) @@ -153,7 +160,9 @@ describe('WorkflowChecklist', () => { title: 'Inline Agent', openInlineAgentPanel: true, } - render() + render(, { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + }) await user.click(screen.getByRole('button', { name: 'workflow.panel.checklist 2' })) await user.click(screen.getByTestId('node-group-Inline Agent')) @@ -166,14 +175,18 @@ describe('WorkflowChecklist', () => { const user = userEvent.setup() mockChecklistItems = [] - render() + render(, { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + }) await user.click(screen.getByRole('button', { name: 'workflow.panel.checklist' })) expect(screen.getByText(/checklistResolved/i)).toBeInTheDocument() }) it('should ignore popover open changes when the checklist is disabled', () => { - render() + render(, { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + }) expect(screen.getByText('2').closest('button')).toBeDisabled() }) diff --git a/web/app/components/workflow/hooks/__tests__/use-auto-generate-webhook-url.spec.ts b/web/app/components/workflow/hooks/__tests__/use-auto-generate-webhook-url.spec.ts index 6de78fb9557..701ce6c596d 100644 --- a/web/app/components/workflow/hooks/__tests__/use-auto-generate-webhook-url.spec.ts +++ b/web/app/components/workflow/hooks/__tests__/use-auto-generate-webhook-url.spec.ts @@ -1,8 +1,15 @@ +import type { EditorIdentity } from '../../editor-resource/state' import type { Node } from '../../types' -import { act, waitFor } from '@testing-library/react' +import { act, screen, waitFor } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { createElement } from 'react' import { useNodes } from 'reactflow' import { createNode } from '../../__tests__/fixtures' -import { renderWorkflowFlowHook } from '../../__tests__/workflow-test-env' +import { + renderWorkflowFlowComponent, + renderWorkflowFlowHook, +} from '../../__tests__/workflow-test-env' +import { EditorResourceScope } from '../../editor-resource/provider' import { BlockEnum } from '../../types' import { useAutoGenerateWebhookUrl } from '../use-auto-generate-webhook-url' @@ -13,14 +20,8 @@ type WebhookFlowNode = Node & { } } -const routeParams = vi.hoisted(() => ({ appId: 'app-123' as string | undefined })) const mockRequest = vi.hoisted(() => vi.fn()) -vi.mock('@/next/navigation', async (importOriginal) => ({ - ...(await importOriginal()), - useParams: () => routeParams, -})) - vi.mock('@/service/console/browser', () => ({ consoleBrowserLink: { call: mockRequest }, })) @@ -45,13 +46,13 @@ describe('useAutoGenerateWebhookUrl', () => { nodes: useNodes(), }), { + resource: { kind: 'app', appId: 'app-123', mode: 'workflow' }, nodes: createFlowNodes(), edges: [], }, ) beforeEach(() => { - routeParams.appId = 'app-123' mockRequest.mockReset() }) @@ -82,29 +83,40 @@ describe('useAutoGenerateWebhookUrl', () => { }) }) - it('uses the current route after switching apps and skips routes without an app', async () => { - routeParams.appId = undefined - const { result, rerender } = renderAutoGenerateWebhookUrlHook() - - await act(async () => { - await result.current.autoGenerateWebhookUrl('webhook-1') - }) + it('uses the scoped app after switching resources and skips snippets', async () => { + const user = userEvent.setup() + function GenerateButton() { + const generate = useAutoGenerateWebhookUrl() + return createElement( + 'button', + { type: 'button', onClick: () => generate('webhook-1') }, + 'Generate URL', + ) + } + const view = (identity: EditorIdentity) => + createElement(EditorResourceScope, { identity, children: createElement(GenerateButton) }) + const { rerender } = renderWorkflowFlowComponent( + view({ kind: 'snippet', snippetId: 'snippet-1' }), + { + nodes: createFlowNodes(), + edges: [], + }, + ) + await user.click(screen.getByRole('button', { name: 'Generate URL' })) expect(mockRequest).not.toHaveBeenCalled() - routeParams.appId = 'app-456' - rerender() mockRequest.mockResolvedValue({ webhook_url: 'https://example.com/other-webhook', webhook_debug_url: 'https://example.com/other-debug', }) - await act(async () => { - await result.current.autoGenerateWebhookUrl('webhook-1') - }) - - expect(mockRequest).toHaveBeenCalledWith( - ['apps', 'byAppId', 'workflows', 'triggers', 'webhook', 'get'], - { params: { app_id: 'app-456' }, query: { node_id: 'webhook-1' } }, - expect.any(Object), + rerender(view({ kind: 'app', appId: 'app-456' })) + await user.click(screen.getByRole('button', { name: 'Generate URL' })) + await waitFor(() => + expect(mockRequest).toHaveBeenCalledWith( + ['apps', 'byAppId', 'workflows', 'triggers', 'webhook', 'get'], + { params: { app_id: 'app-456' }, query: { node_id: 'webhook-1' } }, + expect.any(Object), + ), ) }) diff --git a/web/app/components/workflow/hooks/__tests__/use-checklist.spec.ts b/web/app/components/workflow/hooks/__tests__/use-checklist.spec.ts index 054a4d6d729..be597ff9f66 100644 --- a/web/app/components/workflow/hooks/__tests__/use-checklist.spec.ts +++ b/web/app/components/workflow/hooks/__tests__/use-checklist.spec.ts @@ -14,8 +14,6 @@ import { createElement, Fragment } from 'react' import { CollectionType } from '@/app/components/tools/types' import { consoleQuery } from '@/service/console' import { createAppDetailFixture } from '@/test/fixtures/app' -import { AppModeEnum } from '@/types/app' -import { FlowType } from '@/types/common' import { createEdge, createNode, resetFixtureCounters } from '../../__tests__/fixtures' import { resetReactFlowMockState, rfState } from '../../__tests__/reactflow-mock-state' import { renderWorkflowComponent, renderWorkflowHook } from '../../__tests__/workflow-test-env' @@ -323,12 +321,9 @@ function buildInlineAgentGraph({ nodes: [startNode, agentNode], options: { queryClient, + resource: { kind: 'app' as const, appId, mode: 'workflow' as const }, hooksStoreProps: { - configsMap: { - flowId: appId, - flowType: FlowType.appFlow, - fileSettings: {} as never, - }, + fileSettings: {} as never, }, }, } @@ -555,14 +550,14 @@ describe('useChecklist', () => { const edges = [createEdge({ source: 'start', target: 'llm' })] - renderWorkflowHook(() => - useChecklist([startNode, llmNode], edges, { flowType: FlowType.snippet }), - ) + renderWorkflowHook(() => useChecklist([startNode, llmNode], edges), { + resource: { kind: 'snippet', snippetId: 'snippet-1' }, + }) expect(checkValid).toHaveBeenCalledWith( expect.objectContaining({ type: BlockEnum.LLM }), expect.any(Function), - expect.objectContaining({ flowType: FlowType.snippet }), + expect.objectContaining({ resourceKind: 'snippet' }), ) }) @@ -863,7 +858,7 @@ vi.mock('@tanstack/react-query', async (importOriginal) => { select?: (data: AppDetailWithSite) => unknown }) => { if (options.queryKey[0].join('.') === 'console.apps.byAppId.get') { - const appDetail = createAppDetailFixture({ mode: AppModeEnum.WORKFLOW }) + const appDetail = createAppDetailFixture({ mode: 'workflow' as const }) return { data: options.select ? options.select(appDetail) : appDetail } } if (options.queryKey[0].includes('modelProviders') && options.queryKey[0].includes('summary')) diff --git a/web/app/components/workflow/hooks/__tests__/use-config-vision.spec.ts b/web/app/components/workflow/hooks/__tests__/use-config-vision.spec.ts index 6670bd68d4e..16a222e8487 100644 --- a/web/app/components/workflow/hooks/__tests__/use-config-vision.spec.ts +++ b/web/app/components/workflow/hooks/__tests__/use-config-vision.spec.ts @@ -12,9 +12,7 @@ vi.mock('@/app/components/header/account-setting/model-provider-page/hooks', () mockUseTextGenerationCurrentProviderAndModelAndModelList(...args), })) -vi.mock('../use-workflow', () => ({ - useIsChatMode: () => mockUseIsChatMode(), -})) +vi.mock('../use-workflow', () => ({})) const createModel = (overrides: Partial = {}): ModelConfig => ({ provider: 'openai', @@ -181,3 +179,10 @@ describe('useConfigVision', () => { }) }) }) + +vi.mock('@/app/components/workflow/editor-resource/state', async (importOriginal) => { + const actual = + await importOriginal() + const { atom } = await import('jotai') + return { ...actual, isChatflowAtom: atom(() => mockUseIsChatMode()) } +}) diff --git a/web/app/components/workflow/hooks/__tests__/use-fetch-workflow-inspect-vars.spec.ts b/web/app/components/workflow/hooks/__tests__/use-fetch-workflow-inspect-vars.spec.ts index 553dc9f224f..624a6cecb8c 100644 --- a/web/app/components/workflow/hooks/__tests__/use-fetch-workflow-inspect-vars.spec.ts +++ b/web/app/components/workflow/hooks/__tests__/use-fetch-workflow-inspect-vars.spec.ts @@ -1,7 +1,6 @@ import type { SchemaTypeDefinition } from '@/service/use-common' import type { VarInInspect } from '@/types/workflow' import { act, waitFor } from '@testing-library/react' -import { FlowType } from '@/types/common' import { createNode } from '../../__tests__/fixtures' import { resetReactFlowMockState, rfState } from '../../__tests__/reactflow-mock-state' import { renderWorkflowHook } from '../../__tests__/workflow-test-env' @@ -110,18 +109,11 @@ describe('use-fetch-workflow-inspect-vars', () => { }), ]) - const { result, store } = renderWorkflowHook( - () => - useSetWorkflowVarsWithValue({ - flowType: FlowType.appFlow, - flowId: 'flow-1', - }), - { - initialStoreState: { - dataSourceList: [], - }, + const { result, store } = renderWorkflowHook(() => useSetWorkflowVarsWithValue(), { + initialStoreState: { + dataSourceList: [], }, - ) + }) await act(async () => { await result.current.fetchInspectVars({}) @@ -129,7 +121,11 @@ describe('use-fetch-workflow-inspect-vars', () => { expect(mockInvalidateConversationVarValues).toHaveBeenCalledTimes(1) expect(mockInvalidateSysVarValues).toHaveBeenCalledTimes(1) - expect(mockFetchAllInspectVars).toHaveBeenCalledWith(FlowType.appFlow, 'flow-1') + expect(mockFetchAllInspectVars).toHaveBeenCalledWith({ + kind: 'app', + appId: 'app-1', + mode: 'workflow', + }) expect(mockHandleCancelAllNodeSuccessStatus).toHaveBeenCalledTimes(1) expect(store.getState().nodesWithInspectVars).toEqual([ expect.objectContaining({ @@ -163,18 +159,11 @@ describe('use-fetch-workflow-inspect-vars', () => { dataSourceList: [], } - const { result, store } = renderWorkflowHook( - () => - useSetWorkflowVarsWithValue({ - flowType: FlowType.appFlow, - flowId: 'flow-2', - }), - { - initialStoreState: { - dataSourceList: [], - }, + const { result, store } = renderWorkflowHook(() => useSetWorkflowVarsWithValue(), { + initialStoreState: { + dataSourceList: [], }, - ) + }) await act(async () => { await result.current.fetchInspectVars({ diff --git a/web/app/components/workflow/hooks/__tests__/use-inspect-vars-crud-common.spec.ts b/web/app/components/workflow/hooks/__tests__/use-inspect-vars-crud-common.spec.ts index c91c3b8f686..64f5ce1af0d 100644 --- a/web/app/components/workflow/hooks/__tests__/use-inspect-vars-crud-common.spec.ts +++ b/web/app/components/workflow/hooks/__tests__/use-inspect-vars-crud-common.spec.ts @@ -1,13 +1,15 @@ import type { SchemaTypeDefinition } from '@/service/use-common' import type { VarInInspect } from '@/types/workflow' import { act, waitFor } from '@testing-library/react' -import { FlowType } from '@/types/common' import { createNode } from '../../__tests__/fixtures' import { resetReactFlowMockState, rfState } from '../../__tests__/reactflow-mock-state' import { renderWorkflowHook } from '../../__tests__/workflow-test-env' import { BlockEnum, VarType } from '../../types' import { useInspectVarsCrudCommon } from '../use-inspect-vars-crud-common' +const mockResetInspectorVar = vi.hoisted(() => vi.fn()) +const mockDeleteInspectVar = vi.hoisted(() => vi.fn()) +const mockDeleteNodeInspectorVars = vi.hoisted(() => vi.fn()) const mockFetchNodeInspectVars = vi.hoisted(() => vi.fn()) const mockDoDeleteAllInspectorVars = vi.hoisted(() => vi.fn()) const mockInvalidateConversationVarValues = vi.hoisted(() => vi.fn()) @@ -29,17 +31,14 @@ vi.mock('reactflow', async () => (await import('../../__tests__/reactflow-mock-state')).createReactFlowModuleMock(), ) -vi.mock('@/service/use-flow', () => ({ - default: () => ({ - useInvalidateConversationVarValues: () => mockInvalidateConversationVarValues, - useInvalidateSysVarValues: () => mockInvalidateSysVarValues, - useResetConversationVar: () => ({ mutateAsync: vi.fn() }), - useResetToLastRunValue: () => ({ mutateAsync: vi.fn() }), - useDeleteAllInspectorVars: () => ({ mutateAsync: mockDoDeleteAllInspectorVars }), - useDeleteNodeInspectorVars: () => ({ mutate: vi.fn() }), - useDeleteInspectVar: () => ({ mutate: vi.fn() }), - useEditInspectorVar: () => ({ mutateAsync: vi.fn() }), - }), +vi.mock('@/service/use-workflow', () => ({ + useInvalidateConversationVarValues: () => mockInvalidateConversationVarValues, + useInvalidateSysVarValues: () => mockInvalidateSysVarValues, + useResetInspectorVar: () => ({ mutateAsync: mockResetInspectorVar }), + useDeleteAllInspectorVars: () => ({ mutateAsync: mockDoDeleteAllInspectorVars }), + useDeleteNodeInspectorVars: () => ({ mutateAsync: mockDeleteNodeInspectorVars }), + useDeleteInspectVar: () => ({ mutateAsync: mockDeleteInspectVar }), + useEditInspectorVar: () => ({ mutateAsync: vi.fn() }), })) vi.mock('@/service/use-tools', async () => @@ -118,18 +117,11 @@ describe('useInspectVarsCrudCommon', () => { }) it('invalidates cached system vars without refetching node values for system selectors', async () => { - const { result } = renderWorkflowHook( - () => - useInspectVarsCrudCommon({ - flowId: 'flow-1', - flowType: FlowType.appFlow, - }), - { - initialStoreState: { - dataSourceList: [], - }, + const { result } = renderWorkflowHook(() => useInspectVarsCrudCommon(), { + initialStoreState: { + dataSourceList: [], }, - ) + }) await act(async () => { await result.current.fetchInspectVarValue(['sys', 'query'], schemaTypeDefinitions) @@ -142,38 +134,34 @@ describe('useInspectVarsCrudCommon', () => { it('fetches node inspect vars, adds schema types, and marks the node as fetched', async () => { mockFetchNodeInspectVars.mockResolvedValue([createInspectVar()]) - const { result, store } = renderWorkflowHook( - () => - useInspectVarsCrudCommon({ - flowId: 'flow-1', - flowType: FlowType.appFlow, - }), - { - initialStoreState: { - dataSourceList: [], - nodesWithInspectVars: [ - { - nodeId: 'node-1', - nodePayload: { - type: BlockEnum.Code, - title: 'Code', - desc: '', - } as never, - nodeType: BlockEnum.Code, + const { result, store } = renderWorkflowHook(() => useInspectVarsCrudCommon(), { + initialStoreState: { + dataSourceList: [], + nodesWithInspectVars: [ + { + nodeId: 'node-1', + nodePayload: { + type: BlockEnum.Code, title: 'Code', - vars: [], - }, - ], - }, + desc: '', + } as never, + nodeType: BlockEnum.Code, + title: 'Code', + vars: [], + }, + ], }, - ) + }) await act(async () => { await result.current.fetchInspectVarValue(['node-1', 'answer'], schemaTypeDefinitions) }) await waitFor(() => { - expect(mockFetchNodeInspectVars).toHaveBeenCalledWith(FlowType.appFlow, 'flow-1', 'node-1') + expect(mockFetchNodeInspectVars).toHaveBeenCalledWith( + { kind: 'app', appId: 'app-1', mode: 'workflow' }, + 'node-1', + ) expect(store.getState().nodesWithInspectVars[0]).toMatchObject({ nodeId: 'node-1', isValueFetched: true, @@ -190,30 +178,23 @@ describe('useInspectVarsCrudCommon', () => { it('deletes all inspect vars, invalidates cached values, and clears edge running state', async () => { mockDoDeleteAllInspectorVars.mockResolvedValue(undefined) - const { result, store } = renderWorkflowHook( - () => - useInspectVarsCrudCommon({ - flowId: 'flow-1', - flowType: FlowType.appFlow, - }), - { - initialStoreState: { - nodesWithInspectVars: [ - { - nodeId: 'node-1', - nodePayload: { - type: BlockEnum.Code, - title: 'Code', - desc: '', - } as never, - nodeType: BlockEnum.Code, + const { result, store } = renderWorkflowHook(() => useInspectVarsCrudCommon(), { + initialStoreState: { + nodesWithInspectVars: [ + { + nodeId: 'node-1', + nodePayload: { + type: BlockEnum.Code, title: 'Code', - vars: [createInspectVar()], - }, - ], - }, + desc: '', + } as never, + nodeType: BlockEnum.Code, + title: 'Code', + vars: [createInspectVar()], + }, + ], }, - ) + }) await act(async () => { await result.current.deleteAllInspectorVars() @@ -225,4 +206,51 @@ describe('useInspectVarsCrudCommon', () => { expect(mockHandleEdgeCancelRunningStatus).toHaveBeenCalledTimes(1) expect(store.getState().nodesWithInspectVars).toEqual([]) }) + it.each(['variable', 'node'] as const)( + 'keeps inspected values when %s deletion fails and removes them only after success', + async (target) => { + const { result, store } = renderWorkflowHook(() => useInspectVarsCrudCommon()) + act(() => + result.current.appendNodeInspectVars( + 'node-1', + [createInspectVar()], + [createNode({ id: 'node-1', data: { type: BlockEnum.Code } })], + ), + ) + const deleteRequest = + target === 'variable' ? mockDeleteInspectVar : mockDeleteNodeInspectorVars + const deleteValues = () => + target === 'variable' + ? result.current.deleteInspectVar('node-1', 'var-1') + : result.current.deleteNodeInspectorVars('node-1') + deleteRequest.mockRejectedValueOnce(new Error('Delete failed')) + await act(async () => { + await expect(deleteValues()).resolves.toBeUndefined() + }) + expect(store.getState().nodesWithInspectVars[0]?.vars).toEqual([createInspectVar()]) + deleteRequest.mockResolvedValueOnce(undefined) + await act(async () => { + await deleteValues() + }) + expect(store.getState().nodesWithInspectVars.flatMap((node) => node.vars ?? [])).toEqual([]) + }, + ) + + it('removes a variable reset with no historical value and preserves neighboring schemas', async () => { + const { result, store } = renderWorkflowHook(() => useInspectVarsCrudCommon()) + const neighbor = createInspectVar({ id: 'var-2', name: 'other', schemaType: 'simple' }) + act(() => + result.current.appendNodeInspectVars( + 'node-1', + [createInspectVar(), neighbor], + [createNode({ id: 'node-1', data: { type: BlockEnum.Code } })], + ), + ) + mockResetInspectorVar.mockResolvedValueOnce(undefined) + await act(async () => { + await result.current.resetToLastRunVar('node-1', 'var-1') + }) + expect(store.getState().nodesWithInspectVars[0]?.vars).toEqual([neighbor]) + expect(mockFetchNodeInspectVars).not.toHaveBeenCalled() + }) }) diff --git a/web/app/components/workflow/hooks/__tests__/use-inspect-vars-crud.spec.ts b/web/app/components/workflow/hooks/__tests__/use-inspect-vars-crud.spec.ts index e9befc499ad..aed0d9ae28c 100644 --- a/web/app/components/workflow/hooks/__tests__/use-inspect-vars-crud.spec.ts +++ b/web/app/components/workflow/hooks/__tests__/use-inspect-vars-crud.spec.ts @@ -1,5 +1,4 @@ import type { VarInInspect } from '@/types/workflow' -import { FlowType } from '@/types/common' import { renderWorkflowHook } from '../../__tests__/workflow-test-env' import { BlockEnum, VarType } from '../../types' import useInspectVarsCrud from '../use-inspect-vars-crud' @@ -65,17 +64,22 @@ describe('useInspectVarsCrud', () => { it('should pass the flow id to shared variable queries for app flows', () => { renderWorkflowHook(() => useInspectVarsCrud(), { + resource: { kind: 'app' as const, appId: 'app-1', mode: 'workflow' as const }, hooksStoreProps: { - configsMap: { - flowId: 'app-1', - flowType: FlowType.appFlow, - fileSettings: {} as never, - }, + fileSettings: {} as never, }, }) - expect(mockUseConversationVarValues).toHaveBeenCalledWith(FlowType.appFlow, 'app-1') - expect(mockUseSysVarValues).toHaveBeenCalledWith(FlowType.appFlow, 'app-1') + expect(mockUseConversationVarValues).toHaveBeenCalledWith({ + kind: 'app', + appId: 'app-1', + mode: 'workflow', + }) + expect(mockUseSysVarValues).toHaveBeenCalledWith({ + kind: 'app', + appId: 'app-1', + mode: 'workflow', + }) }) it('should append query and files vars to the start node and keep other system vars separate', () => { @@ -104,12 +108,9 @@ describe('useInspectVarsCrud', () => { }, ], }, + resource: { kind: 'app' as const, appId: 'flow-1', mode: 'workflow' as const }, hooksStoreProps: { - configsMap: { - flowId: 'flow-1', - flowType: FlowType.appFlow, - fileSettings: {} as never, - }, + fileSettings: {} as never, hasNodeInspectVars, fetchInspectVarValue, editInspectVarValue: vi.fn(), @@ -139,33 +140,27 @@ describe('useInspectVarsCrud', () => { expect(result.current.deleteAllInspectorVars).toBe(deleteAllInspectorVars) }) - it('should use an empty flow id for shared variable queries in rag pipelines', () => { + it('should disable app-only variable queries in rag pipelines', () => { renderWorkflowHook(() => useInspectVarsCrud(), { + resource: { kind: 'knowledge-pipeline' as const, pipelineId: 'pipeline-1' }, hooksStoreProps: { - configsMap: { - flowId: 'pipeline-1', - flowType: FlowType.ragPipeline, - fileSettings: {} as never, - }, + fileSettings: {} as never, }, }) - expect(mockUseConversationVarValues).toHaveBeenCalledWith(FlowType.ragPipeline, '') - expect(mockUseSysVarValues).toHaveBeenCalledWith(FlowType.ragPipeline, '') + expect(mockUseConversationVarValues).toHaveBeenCalledWith(undefined) + expect(mockUseSysVarValues).toHaveBeenCalledWith(undefined) }) - it('should use an empty flow id for shared variable queries in snippets', () => { + it('should disable app-only variable queries in snippets', () => { renderWorkflowHook(() => useInspectVarsCrud(), { + resource: { kind: 'snippet' as const, snippetId: 'snippet-1' }, hooksStoreProps: { - configsMap: { - flowId: 'snippet-1', - flowType: FlowType.snippet, - fileSettings: {} as never, - }, + fileSettings: {} as never, }, }) - expect(mockUseConversationVarValues).toHaveBeenCalledWith(FlowType.snippet, '') - expect(mockUseSysVarValues).toHaveBeenCalledWith(FlowType.snippet, '') + expect(mockUseConversationVarValues).toHaveBeenCalledWith(undefined) + expect(mockUseSysVarValues).toHaveBeenCalledWith(undefined) }) }) diff --git a/web/app/components/workflow/hooks/__tests__/use-nodes-available-var-list.spec.ts b/web/app/components/workflow/hooks/__tests__/use-nodes-available-var-list.spec.ts index c2b6d41b2f6..167141e9e24 100644 --- a/web/app/components/workflow/hooks/__tests__/use-nodes-available-var-list.spec.ts +++ b/web/app/components/workflow/hooks/__tests__/use-nodes-available-var-list.spec.ts @@ -1,8 +1,8 @@ import type { Node, NodeOutPutVar, Var } from '../../types' -import { renderHook } from '@testing-library/react' +import type { EditorResource } from '@/app/components/workflow/editor-resource/resource' import { useSnippetDraftStore } from '@/app/components/snippets/draft-store' +import { renderEditorHook as renderHook } from '@/app/components/workflow/editor-resource/__tests__/render' import { PipelineInputVarType } from '@/models/pipeline' -import { FlowType } from '@/types/common' import { BlockEnum, VarType } from '../../types' import useNodesAvailableVarList, { useGetNodesAvailableVarList, @@ -12,7 +12,7 @@ const mockGetTreeLeafNodes = vi.hoisted(() => vi.fn()) const mockGetBeforeNodesInSameBranchIncludeParent = vi.hoisted(() => vi.fn()) const mockGetNodeAvailableVars = vi.hoisted(() => vi.fn()) const mockFlowType = vi.hoisted(() => ({ - value: undefined as FlowType | undefined, + value: undefined as EditorResource['kind'] | undefined, })) vi.mock('../use-workflow', async (importOriginal) => { @@ -20,7 +20,7 @@ vi.mock('../use-workflow', async (importOriginal) => { return { ...actual, - useIsChatMode: () => true, + useWorkflow: () => ({ getTreeLeafNodes: mockGetTreeLeafNodes, getBeforeNodesInSameBranchIncludeParent: mockGetBeforeNodesInSameBranchIncludeParent, @@ -40,10 +40,12 @@ vi.mock('../use-workflow-variables', async (importOriginal) => { }) vi.mock('@/app/components/workflow/hooks-store/store', () => ({ - useHooksStore: (selector: (state: { configsMap?: { flowType?: FlowType } }) => unknown) => + useHooksStore: ( + selector: (state: { configsMap?: { resourceKind?: EditorResource['kind'] } }) => unknown, + ) => selector({ configsMap: { - flowType: mockFlowType.value, + resourceKind: mockFlowType.value, }, }), })) @@ -136,12 +138,14 @@ describe('useNodesAvailableVarList', () => { }) const filterVar = vi.fn(() => true) - const { result } = renderHook(() => - useNodesAvailableVarList([loopNode, childNode], { - filterVar, - hideEnv: true, - hideChatVar: true, - }), + const { result } = renderHook( + () => + useNodesAvailableVarList([loopNode, childNode], { + filterVar, + hideEnv: true, + hideChatVar: true, + }), + { resource: { kind: 'app', appId: 'app-1', mode: 'workflow' } }, ) expect(mockGetBeforeNodesInSameBranchIncludeParent).toHaveBeenCalledWith('loop-1') @@ -155,7 +159,7 @@ describe('useNodesAvailableVarList', () => { 2, expect.objectContaining({ parentNode: loopNode, - isChatMode: true, + isChatflow: true, filterVar, hideEnv: true, hideChatVar: true, @@ -164,7 +168,6 @@ describe('useNodesAvailableVarList', () => { }) it('adds snippet input fields as virtual start variables on snippet canvases', () => { - globalThis.history.pushState({}, '', '/snippets/snippet-1/orchestrate') useSnippetDraftStore.getState().setInputFields([ { type: PipelineInputVarType.textInput, @@ -176,10 +179,12 @@ describe('useNodesAvailableVarList', () => { const currentNode = createNode({ id: 'node-a' }) - const { result } = renderHook(() => - useNodesAvailableVarList([currentNode], { - filterVar: () => true, - }), + const { result } = renderHook( + () => + useNodesAvailableVarList([currentNode], { + filterVar: () => true, + }), + { resource: { kind: 'snippet', snippetId: 'snippet-1' } }, ) expect(result.current['node-a']?.availableNodes[0]).toEqual( @@ -205,15 +210,16 @@ describe('useNodesAvailableVarList', () => { }) it('filters system variables on snippet canvases', () => { - globalThis.history.pushState({}, '', '/snippets/snippet-1/orchestrate') mockGetNodeAvailableVars.mockReturnValue(outputVarsWithSystemVars) const currentNode = createNode({ id: 'node-a' }) - const { result } = renderHook(() => - useNodesAvailableVarList([currentNode], { - filterVar: () => true, - }), + const { result } = renderHook( + () => + useNodesAvailableVarList([currentNode], { + filterVar: () => true, + }), + { resource: { kind: 'snippet', snippetId: 'snippet-1' } }, ) expect(result.current['node-a']?.availableVars).toEqual([ @@ -235,25 +241,28 @@ describe('useNodesAvailableVarList', () => { const currentNode = createNode({ id: 'node-a' }) - const { result } = renderHook(() => - useNodesAvailableVarList([currentNode], { - filterVar: () => true, - }), + const { result } = renderHook( + () => + useNodesAvailableVarList([currentNode], { + filterVar: () => true, + }), + { resource: { kind: 'app', appId: 'app-1', mode: 'workflow' } }, ) expect(result.current['node-a']?.availableVars).toEqual(outputVarsWithSystemVars) }) it('filters system variables when the current flow is a snippet', () => { - mockFlowType.value = FlowType.snippet mockGetNodeAvailableVars.mockReturnValue(outputVarsWithSystemVars) const currentNode = createNode({ id: 'node-a' }) - const { result } = renderHook(() => - useNodesAvailableVarList([currentNode], { - filterVar: () => true, - }), + const { result } = renderHook( + () => + useNodesAvailableVarList([currentNode], { + filterVar: () => true, + }), + { resource: { kind: 'snippet', snippetId: 'snippet-1' } }, ) expect(result.current['node-a']?.availableVars).toEqual([ @@ -276,7 +285,9 @@ describe('useNodesAvailableVarList', () => { const filterVar = vi.fn(() => true) const passedInAvailableNodes = [createNode({ id: 'manual-node' })] - const { result } = renderHook(() => useGetNodesAvailableVarList()) + const { result } = renderHook(() => useGetNodesAvailableVarList(), { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + }) const leafMap = result.current.getNodesAvailableVarList([firstNode], { onlyLeafNodeVar: true, @@ -292,3 +303,10 @@ describe('useNodesAvailableVarList', () => { expect(manualMap['node-b']?.availableNodes).toBe(passedInAvailableNodes) }) }) + +vi.mock('@/app/components/workflow/editor-resource/state', async (importOriginal) => { + const actual = + await importOriginal() + const { atom } = await import('jotai') + return { ...actual, isChatflowAtom: atom(() => true) } +}) diff --git a/web/app/components/workflow/hooks/__tests__/use-nodes-interactions.spec.ts b/web/app/components/workflow/hooks/__tests__/use-nodes-interactions.spec.ts index c8a4e756a8e..00548868027 100644 --- a/web/app/components/workflow/hooks/__tests__/use-nodes-interactions.spec.ts +++ b/web/app/components/workflow/hooks/__tests__/use-nodes-interactions.spec.ts @@ -30,7 +30,7 @@ const runtimeNodesMetaDataMap = vi.hoisted(() => ({ })) const runtimeState = vi.hoisted(() => ({ - isChatMode: false, + isChatflow: false, nodesReadOnly: false, workflowReadOnly: false, })) @@ -43,7 +43,6 @@ vi.mock('reactflow', async () => ) vi.mock('../use-workflow', () => ({ - useIsChatMode: () => runtimeState.isChatMode, useWorkflow: () => ({ getAfterNodesInSameBranch: () => [], }), @@ -119,7 +118,7 @@ describe('useNodesInteractions', () => { beforeEach(() => { vi.clearAllMocks() resetReactFlowMockState() - runtimeState.isChatMode = false + runtimeState.isChatflow = false runtimeState.nodesReadOnly = false runtimeState.workflowReadOnly = false mockCreateInlineAgentBinding.mockImplementation( @@ -974,13 +973,13 @@ describe('useNodesInteractions', () => { BlockEnum.ParameterExtractor, BlockEnum.Agent, ].flatMap((type) => [ - { type, isChatMode: false }, - { type, isChatMode: true }, + { type, isChatflow: false }, + { type, isChatflow: true }, ]), )( - 'pastes $type with conversation memory only when isChatMode=$isChatMode', - async ({ type, isChatMode }) => { - runtimeState.isChatMode = isChatMode + 'pastes $type with conversation memory only when isChatflow=$isChatflow', + async ({ type, isChatflow }) => { + runtimeState.isChatflow = isChatflow runtimeNodesMetaDataMap.value = { [type]: { defaultValue: { type, title: type, desc: '' } }, } @@ -997,7 +996,7 @@ describe('useNodesInteractions', () => { const pastedNodes = rfState.setNodes.mock.calls.at(-1)?.[0] as Node[] const pastedNode = pastedNodes.find((node) => node.data.type === type)! - if (isChatMode) expect(pastedNode.data).toHaveProperty('memory', memory) + if (isChatflow) expect(pastedNode.data).toHaveProperty('memory', memory) else expect(pastedNode.data).not.toHaveProperty('memory') expect(pastedNode.data).toHaveProperty('prompt_template', promptTemplate) expect(store.getState().clipboardElements[0]?.data).toHaveProperty('memory', memory) @@ -1390,3 +1389,10 @@ describe('useNodesInteractions', () => { ) }) }) + +vi.mock('@/app/components/workflow/editor-resource/state', async (importOriginal) => { + const actual = + await importOriginal() + const { atom } = await import('jotai') + return { ...actual, isChatflowAtom: atom(() => runtimeState.isChatflow) } +}) diff --git a/web/app/components/workflow/hooks/__tests__/use-workflow-comment.spec.ts b/web/app/components/workflow/hooks/__tests__/use-workflow-comment.spec.ts index cc3da3f2204..560c77c95e5 100644 --- a/web/app/components/workflow/hooks/__tests__/use-workflow-comment.spec.ts +++ b/web/app/components/workflow/hooks/__tests__/use-workflow-comment.spec.ts @@ -51,10 +51,6 @@ vi.mock('reactflow', () => ({ }), })) -vi.mock('@/next/navigation', () => ({ - useParams: () => ({ appId: 'app-1' }), -})) - vi.mock('@/service/console', async (importOriginal) => { const actual = await importOriginal() diff --git a/web/app/components/workflow/hooks/__tests__/use-workflow-variables.spec.ts b/web/app/components/workflow/hooks/__tests__/use-workflow-variables.spec.ts index 6bdb76d83f5..55c4b83a3e0 100644 --- a/web/app/components/workflow/hooks/__tests__/use-workflow-variables.spec.ts +++ b/web/app/components/workflow/hooks/__tests__/use-workflow-variables.spec.ts @@ -25,9 +25,7 @@ vi.mock('../../nodes/_base/components/variable/use-match-schema-type', () => ({ })) let mockIsChatMode = false -vi.mock('../use-workflow', () => ({ - useIsChatMode: () => mockIsChatMode, -})) +vi.mock('../use-workflow', () => ({})) describe('useWorkflowVariables', () => { beforeEach(() => { @@ -45,13 +43,13 @@ describe('useWorkflowVariables', () => { result.current.getNodeAvailableVars({ beforeNodes: [], - isChatMode: true, + isChatflow: true, filterVar: () => true, }) expect(mockToNodeAvailableVars).toHaveBeenCalledOnce() const args = mockToNodeAvailableVars.mock.calls[0]![0] - expect(args.isChatMode).toBe(true) + expect(args.isChatflow).toBe(true) expect(args.conversationVariables).toHaveLength(1) expect(args.environmentVariables).toHaveLength(1) }) @@ -65,7 +63,7 @@ describe('useWorkflowVariables', () => { result.current.getNodeAvailableVars({ beforeNodes: [], - isChatMode: false, + isChatflow: false, filterVar: () => true, hideEnv: true, }) @@ -83,7 +81,7 @@ describe('useWorkflowVariables', () => { result.current.getNodeAvailableVars({ beforeNodes: [], - isChatMode: false, + isChatflow: false, filterVar: () => true, }) @@ -101,7 +99,7 @@ describe('useWorkflowVariables', () => { const type = result.current.getCurrentVariableType({ valueSelector: ['node-1', 'output'], availableNodes: [], - isChatMode: false, + isChatflow: false, }) expect(mockGetVarType).toHaveBeenCalledOnce() @@ -143,3 +141,10 @@ describe('useWorkflowVariableType', () => { expect((args.parentNode as { id: string }).id).toBe('iter-1') }) }) + +vi.mock('@/app/components/workflow/editor-resource/state', async (importOriginal) => { + const actual = + await importOriginal() + const { atom } = await import('jotai') + return { ...actual, isChatflowAtom: atom(() => mockIsChatMode) } +}) diff --git a/web/app/components/workflow/hooks/__tests__/use-workflow.spec.ts b/web/app/components/workflow/hooks/__tests__/use-workflow.spec.ts index b53a9fd7640..8dd39891e96 100644 --- a/web/app/components/workflow/hooks/__tests__/use-workflow.spec.ts +++ b/web/app/components/workflow/hooks/__tests__/use-workflow.spec.ts @@ -1,9 +1,5 @@ import type { NodeDefault } from '../../types' import { act } from '@testing-library/react' -import { consoleQuery } from '@/service/console' -import { createAppDetailFixture } from '@/test/fixtures/app' -import { createTestQueryClient } from '@/test/query-client' -import { AppModeEnum } from '@/types/app' import { createNode } from '../../__tests__/fixtures' import { baseRunningData, @@ -13,7 +9,6 @@ import { import { BlockClassification } from '../../block-selector/types' import { BlockEnum, WorkflowRunningStatus } from '../../types' import { - useIsChatMode, useIsNodeInIteration, useIsNodeInLoop, useNodesReadOnly, @@ -41,77 +36,7 @@ function createNodeDefault(type: BlockEnum): NodeDefault { } // --------------------------------------------------------------------------- -// useIsChatMode -// --------------------------------------------------------------------------- - -describe('useIsChatMode', () => { - let queryClient: ReturnType - - beforeEach(() => { - queryClient = createTestQueryClient() - }) - - afterEach(() => { - queryClient.clear() - }) - - it.each([ - { mode: AppModeEnum.ADVANCED_CHAT, expected: true }, - { mode: AppModeEnum.WORKFLOW, expected: false }, - ])('should return $expected for the current $mode app', ({ mode, expected }) => { - queryClient.setQueryData( - consoleQuery.apps.byAppId.get.queryKey({ input: { params: { app_id: 'app-1' } } }), - createAppDetailFixture({ id: 'app-1', mode }), - ) - - const { result } = renderWorkflowHook(() => useIsChatMode(), { - initialStoreState: { appId: 'app-1' }, - queryClient, - }) - - expect(result.current).toBe(expected) - }) - - it('should return false without an app id even when another chatflow is cached', () => { - queryClient.setQueryData( - consoleQuery.apps.byAppId.get.queryKey({ input: { params: { app_id: 'chatflow-app' } } }), - createAppDetailFixture({ id: 'chatflow-app', mode: AppModeEnum.ADVANCED_CHAT }), - ) - - const { result } = renderWorkflowHook(() => useIsChatMode(), { queryClient }) - - expect(result.current).toBe(false) - expect(globalThis.fetch).not.toHaveBeenCalled() - }) - - it('should follow the current app id when switching between cached workflow and chatflow apps', () => { - for (const [appId, mode] of [ - ['workflow-app', AppModeEnum.WORKFLOW], - ['chatflow-app', AppModeEnum.ADVANCED_CHAT], - ] as const) { - queryClient.setQueryData( - consoleQuery.apps.byAppId.get.queryKey({ input: { params: { app_id: appId } } }), - createAppDetailFixture({ id: appId, mode }), - ) - } - - const { result, store } = renderWorkflowHook(() => useIsChatMode(), { - initialStoreState: { appId: 'workflow-app' }, - queryClient, - }) - - expect(result.current).toBe(false) - - act(() => store.setState({ appId: 'chatflow-app' })) - expect(result.current).toBe(true) - - act(() => store.setState({ appId: 'workflow-app' })) - expect(result.current).toBe(false) - }) -}) - -// --------------------------------------------------------------------------- -// useWorkflowReadOnly +// useIsChatflow // --------------------------------------------------------------------------- describe('useWorkflowReadOnly', () => { diff --git a/web/app/components/workflow/node-actions-menu/__tests__/details.spec.tsx b/web/app/components/workflow/node-actions-menu/__tests__/details.spec.tsx index 62334178808..345f6fd89ed 100644 --- a/web/app/components/workflow/node-actions-menu/__tests__/details.spec.tsx +++ b/web/app/components/workflow/node-actions-menu/__tests__/details.spec.tsx @@ -6,12 +6,11 @@ import { useHooksStore } from '@/app/components/workflow/hooks-store' import useNodes from '@/app/components/workflow/store/workflow/use-nodes' import { BlockEnum, NodeRunningStatus } from '@/app/components/workflow/types' import { useAllWorkflowTools } from '@/service/use-tools' -import { FlowType } from '@/types/common' import { BlockClassification } from '../../block-selector/types' import { useAvailableBlocks } from '../../hooks/use-available-blocks' import { useNodesInteractions } from '../../hooks/use-nodes-interactions' import { useNodeMetaData } from '../../hooks/use-nodes-meta-data' -import { useIsChatMode, useNodesReadOnly } from '../../hooks/use-workflow' +import { useNodesReadOnly } from '../../hooks/use-workflow' import { NodeActionsDropdown } from '../index' vi.mock('../../hooks/use-available-blocks', async (importOriginal) => { @@ -46,7 +45,7 @@ vi.mock('../../hooks/use-workflow', async (importOriginal) => { return { ...actual, - useIsChatMode: vi.fn(), + useNodesReadOnly: vi.fn(), } }) @@ -80,7 +79,7 @@ vi.mock('@/service/use-triggers', () => ({ })) const mockUseAvailableBlocks = vi.mocked(useAvailableBlocks) -const mockUseIsChatMode = vi.mocked(useIsChatMode) +const mockUseIsChatMode = vi.hoisted(() => vi.fn()) const mockUseNodeMetaData = vi.mocked(useNodeMetaData) const mockUseNodesInteractions = vi.mocked(useNodesInteractions) const mockUseNodesReadOnly = vi.mocked(useNodesReadOnly) @@ -151,7 +150,7 @@ describe('node actions menu details', () => { >) mockUseHooksStore.mockImplementation((selector: any) => selector({ - configsMap: { flowType: FlowType.appFlow }, + configsMap: { resourceKind: 'app' }, accessControl: { canRun: true }, availableNodesMetaData: { nodes: [ @@ -341,3 +340,10 @@ describe('node actions menu details', () => { expect(screen.queryByText('common.operation.delete')).not.toBeInTheDocument() }) }) + +vi.mock('@/app/components/workflow/editor-resource/state', async (importOriginal) => { + const actual = + await importOriginal() + const { atom } = await import('jotai') + return { ...actual, isChatflowAtom: atom(() => mockUseIsChatMode()) } +}) diff --git a/web/app/components/workflow/nodes/_base/components/__tests__/add-variable-popup-with-position.spec.tsx b/web/app/components/workflow/nodes/_base/components/__tests__/add-variable-popup-with-position.spec.tsx index 1ea4cfa2749..1ac77774489 100644 --- a/web/app/components/workflow/nodes/_base/components/__tests__/add-variable-popup-with-position.spec.tsx +++ b/web/app/components/workflow/nodes/_base/components/__tests__/add-variable-popup-with-position.spec.tsx @@ -35,7 +35,6 @@ vi.mock('../../../../hooks/use-node-data-update', () => ({ useNodeDataUpdate: () => ({ handleNodeDataUpdate: vi.fn() }), })) vi.mock('../../../../hooks/use-workflow', () => ({ - useIsChatMode: () => false, useWorkflow: () => ({ getBeforeNodesInSameBranch: () => [] }), })) vi.mock('../../../../hooks/use-workflow-variables', () => ({ @@ -74,3 +73,10 @@ it('selects a connection variable from the owning canvas and preserves its focus ) expect(canvas).toHaveFocus() }) + +vi.mock('@/app/components/workflow/editor-resource/state', async (importOriginal) => { + const actual = + await importOriginal() + const { atom } = await import('jotai') + return { ...actual, isChatflowAtom: atom(() => false) } +}) diff --git a/web/app/components/workflow/nodes/_base/components/__tests__/agent-strategy.spec.tsx b/web/app/components/workflow/nodes/_base/components/__tests__/agent-strategy.spec.tsx index f6a0650ad7b..080a0dc8eed 100644 --- a/web/app/components/workflow/nodes/_base/components/__tests__/agent-strategy.spec.tsx +++ b/web/app/components/workflow/nodes/_base/components/__tests__/agent-strategy.spec.tsx @@ -3,8 +3,8 @@ import { screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { useState } from 'react' import { WorkflowContext } from '@/app/components/workflow/context' +import { renderEditor as render } from '@/app/components/workflow/editor-resource/__tests__/render' import { createWorkflowStore } from '@/app/components/workflow/store' -import { renderWithConsoleQuery as render } from '@/test/console/query-data' import { createProvider, createStrategy } from '../../../agent/__tests__/strategy-fixture' import { AgentStrategy } from '../agent-strategy' diff --git a/web/app/components/workflow/nodes/_base/components/__tests__/code-generator-button.spec.tsx b/web/app/components/workflow/nodes/_base/components/__tests__/code-generator-button.spec.tsx index 42fd14813c5..7f7c9fb04a2 100644 --- a/web/app/components/workflow/nodes/_base/components/__tests__/code-generator-button.spec.tsx +++ b/web/app/components/workflow/nodes/_base/components/__tests__/code-generator-button.spec.tsx @@ -1,5 +1,6 @@ -import { render, screen } from '@testing-library/react' +import { screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' +import { renderEditor as render } from '@/app/components/workflow/editor-resource/__tests__/render' import { CodeLanguage } from '@/app/components/workflow/nodes/code/types' import CodeGenerateBtn from '../code-generator-button' @@ -15,7 +16,9 @@ vi.mock('@/app/components/workflow/hooks-store', () => ({ describe('CodeGenerateBtn', () => { it('should open the code generator from the named action', async () => { const user = userEvent.setup() - render() + render(, { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + }) await user.click(screen.getByRole('button', { name: 'appDebug.operation.automatic' })) diff --git a/web/app/components/workflow/nodes/_base/components/__tests__/form-input-item.branches.spec.tsx b/web/app/components/workflow/nodes/_base/components/__tests__/form-input-item.branches.spec.tsx index d745254c884..776f008d74c 100644 --- a/web/app/components/workflow/nodes/_base/components/__tests__/form-input-item.branches.spec.tsx +++ b/web/app/components/workflow/nodes/_base/components/__tests__/form-input-item.branches.spec.tsx @@ -39,7 +39,7 @@ vi.mock('../../../../hooks/use-workflow', async (importOriginal) => { return { ...actual, - useIsChatMode: () => false, + useWorkflow: () => ({ getTreeLeafNodes: () => [], getNodeById: () => undefined, @@ -421,3 +421,10 @@ describe('FormInputItem branches', () => { }) }) }) + +vi.mock('@/app/components/workflow/editor-resource/state', async (importOriginal) => { + const actual = + await importOriginal() + const { atom } = await import('jotai') + return { ...actual, isChatflowAtom: atom(() => false) } +}) diff --git a/web/app/components/workflow/nodes/_base/components/__tests__/form-input-item.spec.tsx b/web/app/components/workflow/nodes/_base/components/__tests__/form-input-item.spec.tsx index 38d88829de4..b707c0e4f5a 100644 --- a/web/app/components/workflow/nodes/_base/components/__tests__/form-input-item.spec.tsx +++ b/web/app/components/workflow/nodes/_base/components/__tests__/form-input-item.spec.tsx @@ -16,7 +16,7 @@ vi.mock('../../../../hooks/use-workflow', async (importOriginal) => { return { ...actual, - useIsChatMode: () => false, + useWorkflow: () => ({ getTreeLeafNodes: () => [], getNodeById: () => undefined, @@ -220,3 +220,10 @@ it.each([false, 0])( }) }, ) + +vi.mock('@/app/components/workflow/editor-resource/state', async (importOriginal) => { + const actual = + await importOriginal() + const { atom } = await import('jotai') + return { ...actual, isChatflowAtom: atom(() => false) } +}) diff --git a/web/app/components/workflow/nodes/_base/components/__tests__/node-focus.browser.spec.tsx b/web/app/components/workflow/nodes/_base/components/__tests__/node-focus.browser.spec.tsx index cacd1ca5b69..2d6e3cb8643 100644 --- a/web/app/components/workflow/nodes/_base/components/__tests__/node-focus.browser.spec.tsx +++ b/web/app/components/workflow/nodes/_base/components/__tests__/node-focus.browser.spec.tsx @@ -1,13 +1,32 @@ +import type { ReactElement } from 'react' import type { NodeProps } from 'reactflow' import type { CommonNodeType } from '../../../../types' import ReactFlow from 'reactflow' import { page, userEvent } from 'vite-plus/test/browser' -import { render } from 'vitest-browser-react' +import { render as renderBrowser } from 'vitest-browser-react' +import { consoleQuery } from '@/service/console' +import { QueryClientTestProvider } from '@/test/console/query-provider' +import { createAppDetailFixture } from '@/test/fixtures/app' +import { createTestQueryClient } from '@/test/query-client' +import { EditorResourceScope } from '../../../../editor-resource/provider' import { BlockEnum } from '../../../../types' import NodeControl from '../node-control' import { NodeSourceHandle, NodeTargetHandle } from '../node-handle' import 'reactflow/dist/style.css' +const render = (ui: ReactElement) => { + const queryClient = createTestQueryClient() + queryClient.setQueryData( + consoleQuery.apps.byAppId.get.queryKey({ input: { params: { app_id: 'app-1' } } }), + createAppDetailFixture({ id: 'app-1', mode: 'workflow' }), + ) + return renderBrowser( + + {ui} + , + ) +} + // Plugin installation is outside node trigger and popup focus ownership. vi.mock('@/app/components/plugins/install-plugin/install-from-marketplace', () => ({ default: () => null, @@ -23,7 +42,6 @@ vi.mock('../../../../hooks/use-nodes-interactions', () => ({ useNodesInteractions: () => ({ handleNodeAdd: vi.fn(), handleNodeSelect: vi.fn() }), })) vi.mock('../../../../hooks/use-workflow', () => ({ - useIsChatMode: () => false, useNodesReadOnly: () => ({ nodesReadOnly: false, getNodesReadOnly: () => false }), })) vi.mock('../../../../hooks/use-nodes-meta-data', () => ({ @@ -43,7 +61,6 @@ vi.mock('@/app/components/workflow/store', () => ({ vi.mock('@/app/components/workflow/hooks-store', () => ({ useHooksStore: (selector: (state: Record) => unknown) => selector({ - configsMap: { flowType: 'app-flow' }, availableNodesMetaData: { nodes: [] }, accessControl: { canRun: false }, }), diff --git a/web/app/components/workflow/nodes/_base/components/__tests__/node-handle.browser.spec.tsx b/web/app/components/workflow/nodes/_base/components/__tests__/node-handle.browser.spec.tsx index acb7fa3685d..41d9bf9289f 100644 --- a/web/app/components/workflow/nodes/_base/components/__tests__/node-handle.browser.spec.tsx +++ b/web/app/components/workflow/nodes/_base/components/__tests__/node-handle.browser.spec.tsx @@ -1,13 +1,18 @@ -import type { RefObject } from 'react' +import type { ReactElement, RefObject } from 'react' import type { Connection, NodeProps } from 'reactflow' import type { CommonNodeType, OnSelectBlock } from '../../../../types' import type { HumanInputNodeType } from '../../../human-input/types' import { useRef, useState } from 'react' import ReactFlow, { ReactFlowProvider, useReactFlow } from 'reactflow' import { page, userEvent } from 'vite-plus/test/browser' -import { render } from 'vitest-browser-react' +import { render as renderBrowser } from 'vitest-browser-react' +import { consoleQuery } from '@/service/console' +import { QueryClientTestProvider } from '@/test/console/query-provider' +import { createAppDetailFixture } from '@/test/fixtures/app' +import { createTestQueryClient } from '@/test/query-client' import { WorkflowContext } from '../../../../context' import CustomEdge from '../../../../custom-edge' +import { EditorResourceScope } from '../../../../editor-resource/provider' import { useWorkflowControlScale } from '../../../../hooks/use-workflow-control-scale' import { createWorkflowStore } from '../../../../store/workflow' import { BlockEnum } from '../../../../types' @@ -17,6 +22,19 @@ import { NodeSourceHandle, NodeTargetHandle } from '../node-handle' import 'reactflow/dist/style.css' import '../../../../style.css' +const render = (ui: ReactElement) => { + const queryClient = createTestQueryClient() + queryClient.setQueryData( + consoleQuery.apps.byAppId.get.queryKey({ input: { params: { app_id: 'app-1' } } }), + createAppDetailFixture({ id: 'app-1', mode: 'workflow' }), + ) + return renderBrowser( + + {ui} + , + ) +} + const { handleNodeAdd } = vi.hoisted(() => ({ handleNodeAdd: vi.fn() })) vi.mock('../../../../hooks/use-available-blocks', () => ({ @@ -29,7 +47,6 @@ vi.mock('../../../../hooks/use-nodes-interactions', () => ({ useNodesInteractions: () => ({ handleNodeAdd }), })) vi.mock('../../../../hooks/use-workflow', () => ({ - useIsChatMode: () => false, useNodesReadOnly: () => ({ getNodesReadOnly: () => false }), })) vi.mock('../../../../hooks-store', () => ({ diff --git a/web/app/components/workflow/nodes/_base/components/__tests__/node-handle.spec.tsx b/web/app/components/workflow/nodes/_base/components/__tests__/node-handle.spec.tsx index 57c7ff0c34b..daba060a013 100644 --- a/web/app/components/workflow/nodes/_base/components/__tests__/node-handle.spec.tsx +++ b/web/app/components/workflow/nodes/_base/components/__tests__/node-handle.spec.tsx @@ -1,14 +1,15 @@ import type { ReactNode } from 'react' import type { CommonNodeType } from '@/app/components/workflow/types' -import { fireEvent, render, screen, waitFor } from '@testing-library/react' +import { fireEvent, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' +import { renderEditor as render } from '@/app/components/workflow/editor-resource/__tests__/render' import { BlockEnum } from '@/app/components/workflow/types' import { NodeSourceHandle, NodeTargetHandle } from '../node-handle' type MockHooksState = { availablePrevBlocks: BlockEnum[] availableNextBlocks: BlockEnum[] - isChatMode: boolean + isChatflow: boolean isReadOnly: boolean } @@ -31,7 +32,7 @@ const { const mockHooksState: MockHooksState = { availablePrevBlocks: [], availableNextBlocks: [], - isChatMode: false, + isChatflow: false, isReadOnly: false, } const mockStoreState: MockStoreState = { @@ -112,7 +113,7 @@ vi.mock('../../../../hooks/use-workflow', async (importOriginal) => { return { ...actual, - useIsChatMode: () => mockHooksState.isChatMode, + useNodesReadOnly: () => ({ getNodesReadOnly: () => mockHooksState.isReadOnly, }), @@ -129,7 +130,7 @@ vi.mock('@/app/components/workflow/store', () => ({ vi.mock('@/app/components/workflow/hooks-store', () => ({ useHooksStore: (selector: (state: Record) => unknown) => selector({ - configsMap: { flowType: 'app-flow' }, + configsMap: { resourceKind: 'app-flow' }, availableNodesMetaData: { nodes: [] }, }), })) @@ -176,6 +177,7 @@ const renderTargetHandle = (dataOverrides: Partial = {}) => { nodeSelectorClassName="custom-selector" handleClassName="custom-target-handle" />, + { resource: { kind: 'app', appId: 'app-1', mode: 'workflow' } }, ) } @@ -192,6 +194,7 @@ const renderSourceHandle = ( handleClassName="custom-source-handle" {...propsOverrides} />, + { resource: { kind: 'app', appId: 'app-1', mode: 'workflow' } }, ) } @@ -201,7 +204,7 @@ describe('node-handle', () => { mockHooksState.availablePrevBlocks = [BlockEnum.Code] mockHooksState.availableNextBlocks = [BlockEnum.Code] - mockHooksState.isChatMode = false + mockHooksState.isChatflow = false mockHooksState.isReadOnly = false mockStoreState.shouldAutoOpenStartNodeSelector = false @@ -316,7 +319,9 @@ describe('node-handle', () => { describe('NodeSourceHandle auto-open', () => { it('restores the previous focus when the start selector opens automatically', async () => { const user = userEvent.setup() - render() + render(, { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + }) const previousAction = screen.getByRole('button', { name: 'Previous action' }) await user.click(previousAction) mockStoreState.shouldAutoOpenStartNodeSelector = true @@ -347,7 +352,7 @@ describe('node-handle', () => { }) it('should skip source auto-open in chat mode and only reset the start selector flag', () => { - mockHooksState.isChatMode = true + mockHooksState.isChatflow = true mockStoreState.shouldAutoOpenStartNodeSelector = true renderSourceHandle({ type: BlockEnum.Start }) @@ -382,3 +387,10 @@ describe('node-handle', () => { }) }) }) + +vi.mock('@/app/components/workflow/editor-resource/state', async (importOriginal) => { + const actual = + await importOriginal() + const { atom } = await import('jotai') + return { ...actual, isChatflowAtom: atom(() => mockHooksState.isChatflow) } +}) diff --git a/web/app/components/workflow/nodes/_base/components/variable/__tests__/utils.spec.ts b/web/app/components/workflow/nodes/_base/components/variable/__tests__/utils.spec.ts index 44b23ab696a..a0a0a0c6b94 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/__tests__/utils.spec.ts +++ b/web/app/components/workflow/nodes/_base/components/variable/__tests__/utils.spec.ts @@ -15,7 +15,6 @@ import { PromptRole, VarType, } from '@/app/components/workflow/types' -import { AppModeEnum } from '@/types/app' import { findUsedVarNodes, getNodeUsedVars, toNodeAvailableVars, updateNodeVars } from '../utils' const createNode = (data: Node['data']): Node => ({ @@ -38,7 +37,7 @@ const createLLMNodeData = (promptTemplate: PromptItem[]): LLMNodeType => ({ model: { provider: 'provider', name: 'model', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, completion_params: {}, }, prompt_template: promptTemplate, @@ -73,7 +72,7 @@ describe('variable utils', () => { const availableVars = toNodeAvailableVars({ beforeNodes: [], - isChatMode: false, + isChatflow: false, environmentVariables, filterVar: () => true, allPluginInfoList: {}, @@ -95,7 +94,7 @@ describe('variable utils', () => { }) const availableVars = toNodeAvailableVars({ beforeNodes: [node], - isChatMode: false, + isChatflow: false, filterVar: () => true, allPluginInfoList: {}, }) @@ -124,7 +123,7 @@ describe('variable utils', () => { }, }, }) - const options = { beforeNodes: [node], isChatMode: false, allPluginInfoList: {} } + const options = { beforeNodes: [node], isChatflow: false, allPluginInfoList: {} } const availableVars = toNodeAvailableVars({ ...options, filterVar: () => true }) expect(availableVars.find((item) => item.nodeId === node.id)?.vars).toEqual( expect.arrayContaining([ @@ -160,7 +159,7 @@ describe('variable utils', () => { const availableVars = toNodeAvailableVars({ beforeNodes: [node], - isChatMode: false, + isChatflow: false, filterVar: () => true, allPluginInfoList: {}, }) @@ -204,7 +203,7 @@ describe('variable utils', () => { const availableVars = toNodeAvailableVars({ beforeNodes: [node], - isChatMode: false, + isChatflow: false, filterVar: () => true, allPluginInfoList: {}, }) @@ -513,7 +512,7 @@ it('keeps datasource output variables typed through the public picker projection }) const options = { beforeNodes: [node], - isChatMode: false, + isChatflow: false, allPluginInfoList: { dataSourceList: [provider] }, } const all = toNodeAvailableVars({ ...options, filterVar: () => true }).find( diff --git a/web/app/components/workflow/nodes/_base/components/variable/__tests__/var-reference-picker.branches.spec.tsx b/web/app/components/workflow/nodes/_base/components/variable/__tests__/var-reference-picker.branches.spec.tsx index df425ac511e..d3f0d8ec5d7 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/__tests__/var-reference-picker.branches.spec.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable/__tests__/var-reference-picker.branches.spec.tsx @@ -27,7 +27,7 @@ vi.mock('../../../../../hooks/use-workflow', async (importOriginal) => { return { ...actual, - useIsChatMode: () => false, + useWorkflow: () => ({ getTreeLeafNodes: () => [], getNodeById: () => undefined, @@ -294,3 +294,10 @@ describe('VarReferencePicker branches', () => { expect(screen.queryByTestId('var-reference-picker-error-icon')).not.toBeInTheDocument() }) }) + +vi.mock('@/app/components/workflow/editor-resource/state', async (importOriginal) => { + const actual = + await importOriginal() + const { atom } = await import('jotai') + return { ...actual, isChatflowAtom: atom(() => false) } +}) diff --git a/web/app/components/workflow/nodes/_base/components/variable/__tests__/var-reference-picker.spec.tsx b/web/app/components/workflow/nodes/_base/components/variable/__tests__/var-reference-picker.spec.tsx index bf3c5d90851..97ae748dc60 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/__tests__/var-reference-picker.spec.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable/__tests__/var-reference-picker.spec.tsx @@ -15,7 +15,7 @@ vi.mock('../../../../../hooks/use-workflow', async (importOriginal) => { return { ...actual, - useIsChatMode: () => false, + useWorkflow: () => ({ getTreeLeafNodes: () => [], getNodeById: () => undefined, @@ -174,3 +174,10 @@ describe('VarReferencePicker', () => { expect(onChange).not.toHaveBeenCalled() }) }) + +vi.mock('@/app/components/workflow/editor-resource/state', async (importOriginal) => { + const actual = + await importOriginal() + const { atom } = await import('jotai') + return { ...actual, isChatflowAtom: atom(() => false) } +}) diff --git a/web/app/components/workflow/nodes/_base/components/workflow-panel/__tests__/helpers.spec.tsx b/web/app/components/workflow/nodes/_base/components/workflow-panel/__tests__/helpers.spec.tsx index f59d2f3d46d..2b8852e9122 100644 --- a/web/app/components/workflow/nodes/_base/components/workflow-panel/__tests__/helpers.spec.tsx +++ b/web/app/components/workflow/nodes/_base/components/workflow-panel/__tests__/helpers.spec.tsx @@ -22,8 +22,8 @@ describe('workflow-panel helpers', () => { payload: Partial, ): CustomRunFormProps => ({ nodeId: 'node-1', - flowId: 'flow-1', - flowType: 'app' as CustomRunFormProps['flowType'], + resource: { kind: 'app', appId: 'flow-1', mode: 'workflow' }, + payload: payload as CustomRunFormProps['payload'], setRunResult: vi.fn(), setIsRunAfterSingleRun: vi.fn(), diff --git a/web/app/components/workflow/nodes/_base/components/workflow-panel/__tests__/index.spec.tsx b/web/app/components/workflow/nodes/_base/components/workflow-panel/__tests__/index.spec.tsx index 5cfda71a375..b58b36c5e6c 100644 --- a/web/app/components/workflow/nodes/_base/components/workflow-panel/__tests__/index.spec.tsx +++ b/web/app/components/workflow/nodes/_base/components/workflow-panel/__tests__/index.spec.tsx @@ -182,14 +182,14 @@ vi.mock('../../../../../hooks/use-workflow-history', async (importOriginal) => { vi.mock('@/app/components/workflow/hooks-store', () => ({ useHooksStore: ( selector: (state: { - configsMap: { flowId: string; flowType: string } + configsMap: { flowId: string; resourceKind: string } accessControl: { canRun: boolean } }) => unknown, ) => selector({ configsMap: { flowId: 'flow-1', - flowType: 'app', + resourceKind: 'app', }, accessControl: { canRun: mockCanRun, diff --git a/web/app/components/workflow/nodes/_base/components/workflow-panel/last-run/__tests__/index.spec.tsx b/web/app/components/workflow/nodes/_base/components/workflow-panel/last-run/__tests__/index.spec.tsx index b9d4469cdbc..96f3fa69e97 100644 --- a/web/app/components/workflow/nodes/_base/components/workflow-panel/last-run/__tests__/index.spec.tsx +++ b/web/app/components/workflow/nodes/_base/components/workflow-panel/last-run/__tests__/index.spec.tsx @@ -1,4 +1,5 @@ -import { act, render, screen } from '@testing-library/react' +import { act, screen } from '@testing-library/react' +import { renderEditor as render } from '@/app/components/workflow/editor-resource/__tests__/render' import { NodeRunningStatus } from '@/app/components/workflow/types' import { createHttpError } from '@/service/http-error' import LastRun from '../index' @@ -13,7 +14,7 @@ vi.mock('@remixicon/react', () => ({ vi.mock('@/app/components/workflow/hooks-store', () => ({ useHooksStore: ( - selector: (state: { configsMap?: { flowType?: string; flowId?: string } }) => unknown, + selector: (state: { configsMap?: { resourceKind?: string; flowId?: string } }) => unknown, ) => mockUseHooksStore(selector), })) @@ -46,10 +47,10 @@ describe('LastRun', () => { beforeEach(() => { vi.clearAllMocks() mockUseHooksStore.mockImplementation( - (selector: (state: { configsMap?: { flowType?: string; flowId?: string } }) => unknown) => + (selector: (state: { configsMap?: { resourceKind?: string; flowId?: string } }) => unknown) => selector({ configsMap: { - flowType: 'appFlow', + resourceKind: 'appFlow', flowId: 'flow-1', }, }), @@ -75,13 +76,13 @@ describe('LastRun', () => { render( , + { resource: { kind: 'app', appId: 'app-1', mode: 'workflow' } }, ) expect(screen.getByTestId('loading-icon')).toBeInTheDocument() @@ -91,7 +92,6 @@ describe('LastRun', () => { it('should show a running result panel while a single run is still executing', () => { render( { onSingleRunClicked={onSingleRunClicked} runningStatus={NodeRunningStatus.Running} />, + { resource: { kind: 'app', appId: 'app-1', mode: 'workflow' } }, ) expect(screen.getByTestId('result-panel')).toHaveTextContent('running') @@ -119,13 +120,13 @@ describe('LastRun', () => { render( , + { resource: { kind: 'app', appId: 'app-1', mode: 'workflow' } }, ) act(() => { @@ -148,7 +149,6 @@ describe('LastRun', () => { render( { runningStatus={NodeRunningStatus.Succeeded} isPaused />, + { resource: { kind: 'app', appId: 'app-1', mode: 'workflow' } }, ) expect(screen.getByTestId('result-panel')).toHaveTextContent(NodeRunningStatus.Stopped) @@ -181,7 +182,6 @@ describe('LastRun', () => { const { rerender } = render( { onSingleRunClicked={onSingleRunClicked} runningStatus={NodeRunningStatus.Stopped} />, + { resource: { kind: 'app', appId: 'app-1', mode: 'workflow' } }, ) expect(screen.getByTestId('result-panel')).toHaveTextContent(NodeRunningStatus.Stopped) rerender( { render( { onSingleRunClicked={onSingleRunClicked} runningStatus={NodeRunningStatus.Succeeded} />, + { resource: { kind: 'app', appId: 'app-1', mode: 'workflow' } }, ) act(() => { @@ -249,14 +249,15 @@ describe('LastRun', () => { }) const props = { - appId: 'app-1', nodeId: 'node-1', canSingleRun: true, isRunAfterSingleRun: true, updateNodeRunningStatus, onSingleRunClicked, } - const { rerender } = render() + const { rerender } = render(, { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + }) rerender() act(() => { diff --git a/web/app/components/workflow/nodes/_base/components/workflow-panel/last-run/__tests__/use-last-run.spec.ts b/web/app/components/workflow/nodes/_base/components/workflow-panel/last-run/__tests__/use-last-run.spec.ts index 03e5445de37..11a41afa85e 100644 --- a/web/app/components/workflow/nodes/_base/components/workflow-panel/last-run/__tests__/use-last-run.spec.ts +++ b/web/app/components/workflow/nodes/_base/components/workflow-panel/last-run/__tests__/use-last-run.spec.ts @@ -1,7 +1,6 @@ import { act } from '@testing-library/react' import { renderWorkflowHook } from '@/app/components/workflow/__tests__/workflow-test-env' import { BlockEnum } from '@/app/components/workflow/types' -import { FlowType } from '@/types/common' import useLastRun from '../use-last-run' const mockHandleSyncWorkflowDraft = vi.fn() @@ -54,7 +53,7 @@ vi.mock('@/app/components/workflow/nodes/_base/hooks/use-one-step-run', () => ({ })) vi.mock('@/service/use-workflow', () => ({ - useInvalidLastRun: () => vi.fn(), + useInvalidateLastRun: () => vi.fn(), })) describe('useLastRun', () => { @@ -66,8 +65,8 @@ describe('useLastRun', () => { const { result } = renderWorkflowHook(() => useLastRun({ id: 'data-source-node', - flowId: 'flow-id', - flowType: FlowType.appFlow, + resource: { kind: 'app', appId: 'flow-id', mode: 'workflow' }, + data: { type: BlockEnum.DataSource, title: 'Data Source', diff --git a/web/app/components/workflow/nodes/_base/hooks/__tests__/snippet-input-field-vars.spec.ts b/web/app/components/workflow/nodes/_base/hooks/__tests__/snippet-input-field-vars.spec.ts index 4d250883b8d..3f043202820 100644 --- a/web/app/components/workflow/nodes/_base/hooks/__tests__/snippet-input-field-vars.spec.ts +++ b/web/app/components/workflow/nodes/_base/hooks/__tests__/snippet-input-field-vars.spec.ts @@ -15,17 +15,13 @@ const createNode = (id = 'node-1'): Node => }) as Node describe('appendSnippetInputFieldVars', () => { - beforeEach(() => { - globalThis.history.pushState({}, '', '/') - }) - it('should treat missing snippet input fields as empty on snippet canvases', () => { - globalThis.history.pushState({}, '', '/snippets/snippet-1/orchestrate') const availableNodes = [createNode()] expect( appendSnippetInputFieldVars({ availableNodes, + isSnippetFlow: true, fields: undefined, title: 'Snippet', }), diff --git a/web/app/components/workflow/nodes/_base/hooks/__tests__/use-available-var-list.spec.ts b/web/app/components/workflow/nodes/_base/hooks/__tests__/use-available-var-list.spec.ts index 547f84779a7..a2c56169e5e 100644 --- a/web/app/components/workflow/nodes/_base/hooks/__tests__/use-available-var-list.spec.ts +++ b/web/app/components/workflow/nodes/_base/hooks/__tests__/use-available-var-list.spec.ts @@ -1,7 +1,7 @@ +import type { EditorResource } from '@/app/components/workflow/editor-resource/resource' import type { Node, NodeOutPutVar, Var } from '@/app/components/workflow/types' -import { renderHook } from '@testing-library/react' +import { renderEditorHook as renderHook } from '@/app/components/workflow/editor-resource/__tests__/render' import { BlockEnum, VarType } from '@/app/components/workflow/types' -import { FlowType } from '@/types/common' import useAvailableVarList from '../use-available-var-list' const mockGetTreeLeafNodes = vi.hoisted(() => vi.fn()) @@ -9,7 +9,7 @@ const mockGetBeforeNodesInSameBranchIncludeParent = vi.hoisted(() => vi.fn()) const mockGetNodeById = vi.hoisted(() => vi.fn()) const mockGetNodeAvailableVars = vi.hoisted(() => vi.fn()) const mockFlowType = vi.hoisted(() => ({ - value: undefined as FlowType | undefined, + value: undefined as EditorResource['kind'] | undefined, })) vi.mock('@/app/components/snippets/draft-store', () => ({ @@ -22,7 +22,7 @@ vi.mock('../../../../hooks/use-workflow', async (importOriginal) => { return { ...actual, - useIsChatMode: () => true, + useWorkflow: () => ({ getTreeLeafNodes: mockGetTreeLeafNodes, getBeforeNodesInSameBranchIncludeParent: mockGetBeforeNodesInSameBranchIncludeParent, @@ -48,10 +48,12 @@ vi.mock('@/app/components/workflow/store', () => ({ })) vi.mock('@/app/components/workflow/hooks-store/store', () => ({ - useHooksStore: (selector: (state: { configsMap?: { flowType?: FlowType } }) => unknown) => + useHooksStore: ( + selector: (state: { configsMap?: { resourceKind?: EditorResource['kind'] } }) => unknown, + ) => selector({ configsMap: { - flowType: mockFlowType.value, + resourceKind: mockFlowType.value, }, }), })) @@ -114,12 +116,12 @@ describe('useAvailableVarList', () => { }) it('filters system variables on snippet canvases', () => { - globalThis.history.pushState({}, '', '/snippets/snippet-1/orchestrate') - - const { result } = renderHook(() => - useAvailableVarList('node-1', { - filterVar: () => true, - }), + const { result } = renderHook( + () => + useAvailableVarList('node-1', { + filterVar: () => true, + }), + { resource: { kind: 'snippet', snippetId: 'snippet-1' } }, ) expect(result.current.availableVars).toEqual([ @@ -137,22 +139,24 @@ describe('useAvailableVarList', () => { }) it('keeps system variables outside snippet canvases', () => { - const { result } = renderHook(() => - useAvailableVarList('node-1', { - filterVar: () => true, - }), + const { result } = renderHook( + () => + useAvailableVarList('node-1', { + filterVar: () => true, + }), + { resource: { kind: 'app', appId: 'app-1', mode: 'workflow' } }, ) expect(result.current.availableVars).toEqual(outputVarsWithSystemVars) }) it('filters system variables when the current flow is a snippet', () => { - mockFlowType.value = FlowType.snippet - - const { result } = renderHook(() => - useAvailableVarList('node-1', { - filterVar: () => true, - }), + const { result } = renderHook( + () => + useAvailableVarList('node-1', { + filterVar: () => true, + }), + { resource: { kind: 'snippet', snippetId: 'snippet-1' } }, ) expect(result.current.availableVars).toEqual([ @@ -169,3 +173,10 @@ describe('useAvailableVarList', () => { ]) }) }) + +vi.mock('@/app/components/workflow/editor-resource/state', async (importOriginal) => { + const actual = + await importOriginal() + const { atom } = await import('jotai') + return { ...actual, isChatflowAtom: atom(() => true) } +}) diff --git a/web/app/components/workflow/nodes/_base/hooks/__tests__/use-one-step-run.spec.ts b/web/app/components/workflow/nodes/_base/hooks/__tests__/use-one-step-run.spec.ts index 4b30f53ab70..49d15d12244 100644 --- a/web/app/components/workflow/nodes/_base/hooks/__tests__/use-one-step-run.spec.ts +++ b/web/app/components/workflow/nodes/_base/hooks/__tests__/use-one-step-run.spec.ts @@ -4,7 +4,6 @@ import { BlockEnum, InputVarType, VarType } from '@/app/components/workflow/type import { toast } from '@/app/notifications' // oxlint-disable-next-line no-restricted-imports -- Mock the request boundary while preserving the single-run error owner. import { post } from '@/service/base' -import { FlowType } from '@/types/common' import useOneStepRun from '../use-one-step-run' const mockWorkflowState = { @@ -46,7 +45,7 @@ vi.mock('../../../../hooks/use-workflow', async (importOriginal) => { return { ...actual, - useIsChatMode: () => false, + useWorkflow: () => ({ getBeforeNodesInSameBranch: () => [ { @@ -103,7 +102,7 @@ vi.mock('@/service/use-tools', () => ({ })) vi.mock('@/service/use-workflow', () => ({ - useInvalidLastRun: () => vi.fn(), + useInvalidateLastRun: () => vi.fn(), })) vi.mock('@/service/workflow', () => ({ @@ -188,8 +187,8 @@ const renderUseOneStepRun = (type: BlockEnum = BlockEnum.IfElse) => renderHook(() => useOneStepRun({ id: 'if-else-node', - flowId: 'app-id', - flowType: FlowType.appFlow, + resource: { kind: 'app', appId: 'app-id', mode: 'workflow' }, + data: { type, title: 'IF/ELSE', @@ -264,3 +263,10 @@ describe('useOneStepRun single-run input vars', () => { expect(reportError.mock.calls[0]?.[0]).toBe(error) }) }) + +vi.mock('@/app/components/workflow/editor-resource/state', async (importOriginal) => { + const actual = + await importOriginal() + const { atom } = await import('jotai') + return { ...actual, isChatflowAtom: atom(() => false) } +}) diff --git a/web/app/components/workflow/nodes/agent-v2/__tests__/hooks.spec.tsx b/web/app/components/workflow/nodes/agent-v2/__tests__/hooks.spec.tsx index 1013152d62e..f788fa77f5f 100644 --- a/web/app/components/workflow/nodes/agent-v2/__tests__/hooks.spec.tsx +++ b/web/app/components/workflow/nodes/agent-v2/__tests__/hooks.spec.tsx @@ -1,3 +1,4 @@ +import type { AppMode } from '@dify/contracts/api/console/apps/types.gen' import { QueryClient } from '@tanstack/react-query' import { act, waitFor } from '@testing-library/react' import { useSetAtom } from 'jotai' @@ -6,8 +7,6 @@ import { agentComposerDraftAtom } from '@/features/agent-v2/agent-composer/store import { AgentScope } from '@/features/agent-v2/analytics' import { createConsoleQueryClient, seedAppDetail } from '@/test/console/query-data' import { createAppDetailFixture } from '@/test/fixtures/app' -import { AppModeEnum } from '@/types/app' -import { FlowType } from '@/types/common' import { renderWorkflowHook } from '../../../__tests__/workflow-test-env' import { useWorkflowInlineAgentConfigureSync } from '../agent-soul-config' import { useCreateInlineAgentBinding, useWorkflowInlineAgentDetail } from '../hooks' @@ -168,6 +167,9 @@ vi.mock('@/service/console', async (importOriginal) => { get: actual.consoleQuery.apps.byAppId.get, workflows: { draft: { + systemVariables: actual.consoleQuery.apps.byAppId.workflows.draft.systemVariables, + conversationVariables: + actual.consoleQuery.apps.byAppId.workflows.draft.conversationVariables, nodes: { byNodeId: { agentComposer: { @@ -222,7 +224,7 @@ vi.mock('@/service/console', async (importOriginal) => { } }) -const createInlineAgentBindingQueryClient = (appMode: AppModeEnum = AppModeEnum.WORKFLOW) => { +const createInlineAgentBindingQueryClient = (appMode: AppMode = 'workflow') => { const queryClient = createConsoleQueryClient() seedAppDetail(queryClient, createAppDetailFixture({ id: 'app-1', mode: appMode })) return queryClient @@ -249,12 +251,9 @@ describe('useWorkflowInlineAgentDetail', () => { () => useWorkflowInlineAgentDetail('node-1', 'snippet-agent'), { queryClient, + resource: { kind: 'snippet' as const, snippetId: 'snippet-1' }, hooksStoreProps: { - configsMap: { - flowId: 'snippet-1', - flowType: FlowType.snippet, - fileSettings: {} as never, - }, + fileSettings: {} as never, }, }, ) @@ -288,12 +287,9 @@ describe('useWorkflowInlineAgentDetail', () => { }), { queryClient, + resource: { kind: 'app' as const, appId: 'app-1', mode: 'workflow' as const }, hooksStoreProps: { - configsMap: { - flowId: 'app-1', - flowType: FlowType.appFlow, - fileSettings: {} as never, - }, + fileSettings: {} as never, }, }, ) @@ -321,12 +317,9 @@ describe('useWorkflowInlineAgentDetail', () => { }), { queryClient, + resource: { kind: 'app' as const, appId: 'app-1', mode: 'workflow' as const }, hooksStoreProps: { - configsMap: { - flowId: 'app-1', - flowType: FlowType.appFlow, - fileSettings: {} as never, - }, + fileSettings: {} as never, }, }, ) @@ -361,12 +354,9 @@ describe('useCreateInlineAgentBinding', () => { const queryClient = createInlineAgentBindingQueryClient() const { result } = renderWorkflowHook(() => useCreateInlineAgentBinding(), { queryClient, + resource: { kind: 'app' as const, appId: 'app-1', mode: 'workflow' as const }, hooksStoreProps: { - configsMap: { - flowId: 'app-1', - flowType: FlowType.appFlow, - fileSettings: {} as never, - }, + fileSettings: {} as never, }, }) @@ -432,15 +422,12 @@ describe('useCreateInlineAgentBinding', () => { }) it('tracks inline agent creation with the chatflow scope', async () => { - const queryClient = createInlineAgentBindingQueryClient(AppModeEnum.ADVANCED_CHAT) + const queryClient = createInlineAgentBindingQueryClient('advanced-chat') const { result } = renderWorkflowHook(() => useCreateInlineAgentBinding(), { queryClient, + resource: { kind: 'app' as const, appId: 'chatflow-1', mode: 'advanced-chat' as const }, hooksStoreProps: { - configsMap: { - flowId: 'chatflow-1', - flowType: FlowType.appFlow, - fileSettings: {} as never, - }, + fileSettings: {} as never, }, }) @@ -462,12 +449,9 @@ describe('useCreateInlineAgentBinding', () => { const queryClient = createInlineAgentBindingQueryClient() const { result } = renderWorkflowHook(() => useCreateInlineAgentBinding(), { queryClient, + resource: { kind: 'snippet' as const, snippetId: 'snippet-1' }, hooksStoreProps: { - configsMap: { - flowId: 'snippet-1', - flowType: FlowType.snippet, - fileSettings: {} as never, - }, + fileSettings: {} as never, }, }) @@ -515,12 +499,9 @@ describe('useCreateInlineAgentBinding', () => { const queryClient = createInlineAgentBindingQueryClient() const { result } = renderWorkflowHook(() => useCreateInlineAgentBinding(), { queryClient, + resource: { kind: 'app' as const, appId: 'app-1', mode: 'workflow' as const }, hooksStoreProps: { - configsMap: { - flowId: 'app-1', - flowType: FlowType.appFlow, - fileSettings: {} as never, - }, + fileSettings: {} as never, }, }) @@ -575,12 +556,9 @@ describe('useCreateInlineAgentBinding', () => { const queryClient = createInlineAgentBindingQueryClient() const { result, unmount } = renderWorkflowHook(() => useCreateInlineAgentBinding(), { queryClient, + resource: { kind: 'app' as const, appId: 'app-1', mode: 'workflow' as const }, hooksStoreProps: { - configsMap: { - flowId: 'app-1', - flowType: FlowType.appFlow, - fileSettings: {} as never, - }, + fileSettings: {} as never, }, }) @@ -651,12 +629,9 @@ describe('useWorkflowInlineAgentConfigureSync', () => { }), { queryClient, + resource: { kind: 'app' as const, appId: 'app-1', mode: 'workflow' as const }, hooksStoreProps: { - configsMap: { - flowId: 'app-1', - flowType: FlowType.appFlow, - fileSettings: {} as never, - }, + fileSettings: {} as never, }, }, ) @@ -728,12 +703,9 @@ describe('useWorkflowInlineAgentConfigureSync', () => { }), { queryClient, + resource: { kind: 'snippet' as const, snippetId: 'snippet-1' }, hooksStoreProps: { - configsMap: { - flowId: 'snippet-1', - flowType: FlowType.snippet, - fileSettings: {} as never, - }, + fileSettings: {} as never, }, }, ) @@ -802,12 +774,9 @@ describe('useWorkflowInlineAgentConfigureSync', () => { }), { queryClient, + resource: { kind: 'app' as const, appId: 'app-1', mode: 'workflow' as const }, hooksStoreProps: { - configsMap: { - flowId: 'app-1', - flowType: FlowType.appFlow, - fileSettings: {} as never, - }, + fileSettings: {} as never, }, }, ) @@ -866,12 +835,9 @@ describe('useWorkflowInlineAgentConfigureSync', () => { }), { queryClient, + resource: { kind: 'app' as const, appId: 'app-1', mode: 'workflow' as const }, hooksStoreProps: { - configsMap: { - flowId: 'app-1', - flowType: FlowType.appFlow, - fileSettings: {} as never, - }, + fileSettings: {} as never, }, }, ) @@ -910,12 +876,9 @@ describe('useWorkflowInlineAgentConfigureSync', () => { }), { queryClient, + resource: { kind: 'app' as const, appId: 'app-1', mode: 'workflow' as const }, hooksStoreProps: { - configsMap: { - flowId: 'app-1', - flowType: FlowType.appFlow, - fileSettings: {} as never, - }, + fileSettings: {} as never, }, }, ) diff --git a/web/app/components/workflow/nodes/agent-v2/__tests__/panel.spec.tsx b/web/app/components/workflow/nodes/agent-v2/__tests__/panel.spec.tsx index fcf2e7e0776..f476261d0b3 100644 --- a/web/app/components/workflow/nodes/agent-v2/__tests__/panel.spec.tsx +++ b/web/app/components/workflow/nodes/agent-v2/__tests__/panel.spec.tsx @@ -2,12 +2,12 @@ import type { AgentInviteOptionsResponse } from '@dify/contracts/api/console/age import type { ReactNode } from 'react' import type { AgentV2NodeType } from '../types' import type { PromptEditorProps } from '@/app/components/base/prompt-editor' +import type { EditorResource } from '@/app/components/workflow/editor-resource/resource' import type { NodePanelProps } from '@/app/components/workflow/types' import { act, fireEvent, screen, within } from '@testing-library/react' import userEvent from '@testing-library/user-event' +import { renderEditor as render } from '@/app/components/workflow/editor-resource/__tests__/render' import { BlockEnum } from '@/app/components/workflow/types' -import { renderWithConsoleQuery as render } from '@/test/console/query-data' -import { FlowType } from '@/types/common' import { AgentV2Panel } from '../panel' vi.mock('../components/agent-output-routes', () => ({ AgentOutputRoutes: () => null })) @@ -57,7 +57,7 @@ const { }, mockConfigsMap: { flowId: 'app-1', - flowType: 'appFlow' as FlowType, + resourceKind: 'appFlow' as EditorResource['kind'], }, mockCreateInlineAgentBinding: vi.fn(), mockSetInputs: vi.fn(), @@ -341,7 +341,7 @@ describe('agent/panel', () => { mockStoreState.openInlineAgentPanelNodeId = undefined mockCopyFromRosterState.isPending = false mockConfigsMap.flowId = 'app-1' - mockConfigsMap.flowType = FlowType.appFlow + mockConfigsMap.resourceKind = 'app' mockCopyFromRosterMutate.mockImplementation( ( _variables, @@ -524,9 +524,11 @@ describe('agent/panel', () => { it('copies a roster agent through the snippet composer API', () => { mockConfigsMap.flowId = 'snippet-1' - mockConfigsMap.flowType = FlowType.snippet + mockConfigsMap.resourceKind = 'snippet' mockStoreState.appId = undefined as never - render() + render(, { + resource: { kind: 'snippet', snippetId: 'snippet-1' }, + }) fireEvent.click( screen.getByRole('button', { name: /^workflowAgent\.nodes\.agent\.roster\.openPanel/ }), diff --git a/web/app/components/workflow/nodes/agent-v2/components/__tests__/agent-orchestrate-panel-content.spec.tsx b/web/app/components/workflow/nodes/agent-v2/components/__tests__/agent-orchestrate-panel-content.spec.tsx index 34965ca8289..453732e9627 100644 --- a/web/app/components/workflow/nodes/agent-v2/components/__tests__/agent-orchestrate-panel-content.spec.tsx +++ b/web/app/components/workflow/nodes/agent-v2/components/__tests__/agent-orchestrate-panel-content.spec.tsx @@ -6,16 +6,11 @@ import type { ReactNode, Ref } from 'react' import type { AgentBuildDraftChangeSummary } from '@/features/agent-v2/agent-detail/configure/components/orchestrate/build-draft-changes-context' import { act, fireEvent, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' +import { renderEditor as renderWithConsoleQuery } from '@/app/components/workflow/editor-resource/__tests__/render' import { AgentScope } from '@/features/agent-v2/analytics' import { createHttpError } from '@/service/http-error' -import { - createConsoleQueryClient, - renderWithConsoleQuery, - seedAppDetail, -} from '@/test/console/query-data' +import { createConsoleQueryClient, seedAppDetail } from '@/test/console/query-data' import { createAppDetailFixture } from '@/test/fixtures/app' -import { AppModeEnum } from '@/types/app' -import { FlowType } from '@/types/common' import { WorkflowInlineAgentConfigureWorkspace } from '../agent-orchestrate-panel-content' const mocks = vi.hoisted(() => ({ @@ -496,7 +491,7 @@ function createInlineComposerState({ const render: typeof renderWithConsoleQuery = (ui, options = {}) => { const queryClient = createConsoleQueryClient() - seedAppDetail(queryClient, createAppDetailFixture({ id: 'app-1', mode: AppModeEnum.WORKFLOW })) + seedAppDetail(queryClient, createAppDetailFixture({ id: 'app-1', mode: 'workflow' as const })) return renderWithConsoleQuery(ui, { queryClient, ...options }) } @@ -570,8 +565,7 @@ describe('WorkflowInlineAgentConfigureWorkspace', () => { return render( { render( , @@ -605,8 +598,7 @@ describe('WorkflowInlineAgentConfigureWorkspace', () => { render( { rerender( ({ @@ -88,8 +87,7 @@ const renderDialog = (agent: AgentComposerAgentResponse = inlineAgent) => { render( { const user = userEvent.setup() render( { } const { rerender } = render( { rerender( { rerender( { rerender( { it('hides memory config when chat mode support is unavailable', () => { mockUseConfig.mockReturnValue( createConfigResult({ - isChatMode: false, + isChatflow: false, currentStrategy: { ...createConfigResult().currentStrategy!, features: [], diff --git a/web/app/components/workflow/nodes/agent/__tests__/use-config.spec.ts b/web/app/components/workflow/nodes/agent/__tests__/use-config.spec.ts index 426e05f6778..26dc39ff6eb 100644 --- a/web/app/components/workflow/nodes/agent/__tests__/use-config.spec.ts +++ b/web/app/components/workflow/nodes/agent/__tests__/use-config.spec.ts @@ -16,7 +16,6 @@ const { request, setInputs, marketplaceRefetch, marketplaceQuery } = vi.hoisted( vi.mock('@/service/base', () => ({ request })) vi.mock('../../../hooks/use-workflow', () => ({ useNodesReadOnly: () => ({ nodesReadOnly: false }), - useIsChatMode: () => true, })) vi.mock('../../_base/hooks/use-node-crud', () => ({ default: (_id: string, inputs: AgentNodeType) => ({ inputs, setInputs }), @@ -197,3 +196,10 @@ describe('agent strategy query and configuration', () => { await waitFor(() => expect(setInputs).toHaveBeenCalledWith(inputs)) }) }) + +vi.mock('@/app/components/workflow/editor-resource/state', async (importOriginal) => { + const actual = + await importOriginal() + const { atom } = await import('jotai') + return { ...actual, isChatflowAtom: atom(() => true) } +}) diff --git a/web/app/components/workflow/nodes/assigner/__tests__/use-config.spec.tsx b/web/app/components/workflow/nodes/assigner/__tests__/use-config.spec.tsx index 8a94ecb4f5a..c430647c903 100644 --- a/web/app/components/workflow/nodes/assigner/__tests__/use-config.spec.tsx +++ b/web/app/components/workflow/nodes/assigner/__tests__/use-config.spec.tsx @@ -15,7 +15,7 @@ vi.mock('../../../hooks/use-workflow', async (importOriginal) => { return { ...actual, useNodesReadOnly: () => ({ nodesReadOnly: false }), - useIsChatMode: () => false, + useWorkflow: () => ({ getBeforeNodesInSameBranchIncludeParent: () => [ { id: 'start-node', data: { title: 'Start', type: BlockEnum.Start } }, @@ -157,3 +157,10 @@ describe('useConfig', () => { ]) }) }) + +vi.mock('@/app/components/workflow/editor-resource/state', async (importOriginal) => { + const actual = + await importOriginal() + const { atom } = await import('jotai') + return { ...actual, isChatflowAtom: atom(() => false) } +}) diff --git a/web/app/components/workflow/nodes/assigner/components/var-list/__tests__/index.spec.tsx b/web/app/components/workflow/nodes/assigner/components/var-list/__tests__/index.spec.tsx index 9aa5f16d473..253eef8ad4e 100644 --- a/web/app/components/workflow/nodes/assigner/components/var-list/__tests__/index.spec.tsx +++ b/web/app/components/workflow/nodes/assigner/components/var-list/__tests__/index.spec.tsx @@ -12,7 +12,7 @@ vi.mock('../../../../../hooks/use-workflow', async (importOriginal) => { return { ...actual, - useIsChatMode: () => false, + useWorkflow: () => ({ getTreeLeafNodes: () => [], getNodeById: () => undefined, @@ -173,3 +173,10 @@ describe('assigner/var-list', () => { expect(handleChange).toHaveBeenLastCalledWith([]) }) }) + +vi.mock('@/app/components/workflow/editor-resource/state', async (importOriginal) => { + const actual = + await importOriginal() + const { atom } = await import('jotai') + return { ...actual, isChatflowAtom: atom(() => false) } +}) diff --git a/web/app/components/workflow/nodes/code/__tests__/use-config.spec.ts b/web/app/components/workflow/nodes/code/__tests__/use-config.spec.ts index 3f1dc0a4c0e..6201046f21a 100644 --- a/web/app/components/workflow/nodes/code/__tests__/use-config.spec.ts +++ b/web/app/components/workflow/nodes/code/__tests__/use-config.spec.ts @@ -1,6 +1,7 @@ import type { CodeNodeType, OutputVar } from '../types' import type { Var, Variable } from '@/app/components/workflow/types' -import { act, renderHook, waitFor } from '@testing-library/react' +import { act, waitFor } from '@testing-library/react' +import { renderEditorHook as renderHook } from '@/app/components/workflow/editor-resource/__tests__/render' import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud' import { useStore } from '@/app/components/workflow/store' import { BlockEnum, VarType } from '@/app/components/workflow/types' @@ -162,7 +163,9 @@ describe('code/use-config', () => { outputs: {}, }) - const { result } = renderHook(() => useConfig('code-node', currentInputs)) + const { result } = renderHook(() => useConfig('code-node', currentInputs), { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + }) await waitFor(() => { expect(mockSetInputs).toHaveBeenCalledWith( @@ -185,61 +188,67 @@ describe('code/use-config', () => { expect(result.current.filterVar({ type: VarType.secret } as Var)).toBe(true) }) - it('fetches app and pipeline defaults, switches language, and updates code and output vars together', async () => { - workflowStoreState.appId = 'app-1' - workflowStoreState.pipelineId = 'pipeline-1' + it.each(['app', 'knowledge-pipeline'] as const)( + 'loads %s defaults and updates code with its variables', + async (kind) => { + workflowStoreState.appId = 'app-1' + workflowStoreState.pipelineId = 'pipeline-1' - const { result } = renderHook(() => useConfig('code-node', currentInputs)) + const { result } = renderHook(() => useConfig('code-node', currentInputs), { + resource: + kind === 'app' + ? { kind, appId: 'app-1', mode: 'workflow' } + : { kind, pipelineId: 'pipeline-1' }, + }) - await waitFor(() => { - expect(mockFetchNodeDefault).toHaveBeenCalledWith('app-1', BlockEnum.Code, { - code_language: CodeLanguage.javascript, + const fetchDefaults = kind === 'app' ? mockFetchNodeDefault : mockFetchPipelineNodeDefault + const otherFetch = kind === 'app' ? mockFetchPipelineNodeDefault : mockFetchNodeDefault + const resourceId = kind === 'app' ? 'app-1' : 'pipeline-1' + await waitFor(() => { + expect(fetchDefaults).toHaveBeenCalledWith(resourceId, BlockEnum.Code, { + code_language: CodeLanguage.javascript, + }) + expect(fetchDefaults).toHaveBeenCalledWith(resourceId, BlockEnum.Code, { + code_language: CodeLanguage.python3, + }) }) - expect(mockFetchNodeDefault).toHaveBeenCalledWith('app-1', BlockEnum.Code, { - code_language: CodeLanguage.python3, - }) - expect(mockFetchPipelineNodeDefault).toHaveBeenCalledWith('pipeline-1', BlockEnum.Code, { - code_language: CodeLanguage.javascript, - }) - expect(mockFetchPipelineNodeDefault).toHaveBeenCalledWith('pipeline-1', BlockEnum.Code, { - code_language: CodeLanguage.python3, - }) - }) + expect(otherFetch).not.toHaveBeenCalled() - mockSetInputs.mockClear() + mockSetInputs.mockClear() - act(() => { - result.current.handleCodeLanguageChange(CodeLanguage.python3) - result.current.handleCodeChange('function main({ bar }) { return { result: bar } }') - result.current.handleCodeAndVarsChange( - 'function main({ amount }) { return { total: amount } }', - [createVariable('amount', VarType.number)], - createOutputs('total', VarType.number), + act(() => { + result.current.handleCodeLanguageChange(CodeLanguage.python3) + result.current.handleCodeChange('function main({ bar }) { return { result: bar } }') + result.current.handleCodeAndVarsChange( + 'function main({ amount }) { return { total: amount } }', + [createVariable('amount', VarType.number)], + createOutputs('total', VarType.number), + ) + }) + + expect(mockSetInputs).toHaveBeenCalledWith( + expect.objectContaining({ + code_language: CodeLanguage.python3, + code: pythonConfig.code, + variables: pythonConfig.variables, + outputs: pythonConfig.outputs, + }), ) - }) - - expect(mockSetInputs).toHaveBeenCalledWith( - expect.objectContaining({ - code_language: CodeLanguage.python3, - code: pythonConfig.code, - variables: pythonConfig.variables, - outputs: pythonConfig.outputs, - }), - ) - expect(mockSetInputs).toHaveBeenCalledWith( - expect.objectContaining({ - code: 'function main({ bar }) { return { result: bar } }', - }), - ) - expect(mockSetInputs).toHaveBeenCalledWith( - expect.objectContaining({ - code: 'function main({ amount }) { return { total: amount } }', - variables: [expect.objectContaining({ variable: 'amount' })], - outputs: createOutputs('total', VarType.number), - }), - ) - expect(result.current.outputKeyOrders).toEqual(['total']) - }) + expect(mockSetInputs).toHaveBeenCalledWith( + expect.objectContaining({ + code: 'function main({ bar }) { return { result: bar } }', + }), + ) + expect(mockSetInputs).toHaveBeenCalledWith( + expect.objectContaining({ + code: 'function main({ amount }) { return { total: amount } }', + variables: [expect.objectContaining({ variable: 'amount' })], + outputs: createOutputs('total', VarType.number), + }), + ) + expect(result.current.outputKeyOrders).toEqual(['total']) + }, + ) it('syncs javascript and python function signatures and keeps json code unchanged', () => { currentInputs = createData({ @@ -248,7 +257,9 @@ describe('code/use-config', () => { variables: [createVariable('foo'), createVariable('bar')], }) - const { result, rerender } = renderHook(() => useConfig('code-node', currentInputs)) + const { result, rerender } = renderHook(() => useConfig('code-node', currentInputs), { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + }) act(() => { result.current.handleSyncFunctionSignature() @@ -320,7 +331,9 @@ describe('code/use-config', () => { workflowStoreState.appId = undefined workflowStoreState.pipelineId = undefined - const { result } = renderHook(() => useConfig('code-node', currentInputs)) + const { result } = renderHook(() => useConfig('code-node', currentInputs), { + resource: { kind: 'snippet', snippetId: 'snippet-1' }, + }) await waitFor(() => { expect(result.current.outputKeyOrders).toEqual(['summary', 'count']) diff --git a/web/app/components/workflow/nodes/data-source/__tests__/before-run-form.spec.tsx b/web/app/components/workflow/nodes/data-source/__tests__/before-run-form.spec.tsx index 732dfca9b34..b2bf919ef94 100644 --- a/web/app/components/workflow/nodes/data-source/__tests__/before-run-form.spec.tsx +++ b/web/app/components/workflow/nodes/data-source/__tests__/before-run-form.spec.tsx @@ -3,7 +3,6 @@ import type { CustomRunFormProps, DataSourceNodeType } from '../types' import { render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { DatasourceType } from '@/models/pipeline' -import { FlowType } from '@/types/common' import { BlockEnum } from '../../../types' import BeforeRunForm from '../before-run-form' import useBeforeRunForm from '../hooks/use-before-run-form' @@ -120,8 +119,8 @@ const createData = (overrides: Partial = {}): DataSourceNode const createProps = (overrides: Partial = {}): CustomRunFormProps => ({ nodeId: 'data-source-node', - flowId: 'flow-id', - flowType: FlowType.ragPipeline, + resource: { kind: 'knowledge-pipeline', pipelineId: 'flow-id' }, + payload: createData(), setRunResult: vi.fn(), setIsRunAfterSingleRun: vi.fn(), diff --git a/web/app/components/workflow/nodes/data-source/hooks/__tests__/use-before-run-form.branches.spec.tsx b/web/app/components/workflow/nodes/data-source/hooks/__tests__/use-before-run-form.branches.spec.tsx index bb05f8dc2a3..04485b7e498 100644 --- a/web/app/components/workflow/nodes/data-source/hooks/__tests__/use-before-run-form.branches.spec.tsx +++ b/web/app/components/workflow/nodes/data-source/hooks/__tests__/use-before-run-form.branches.spec.tsx @@ -9,9 +9,8 @@ import { import { BlockEnum, NodeRunningStatus } from '@/app/components/workflow/types' import { DatasourceType } from '@/models/pipeline' import { useDatasourceSingleRun } from '@/service/use-pipeline' -import { useInvalidLastRun } from '@/service/use-workflow' +import { useInvalidateLastRun } from '@/service/use-workflow' import { fetchNodeInspectVars } from '@/service/workflow' -import { FlowType } from '@/types/common' import { useNodeDataUpdate } from '../../../../hooks/use-node-data-update' import { useNodesSyncDraft } from '../../../../hooks/use-nodes-sync-draft' import useBeforeRunForm from '../use-before-run-form' @@ -82,7 +81,7 @@ vi.mock('@/service/use-pipeline', () => ({ })) vi.mock('@/service/use-workflow', () => ({ - useInvalidLastRun: vi.fn(), + useInvalidateLastRun: vi.fn(), })) vi.mock('@/service/workflow', () => ({ @@ -98,7 +97,7 @@ const mockUseStoreApi = vi.mocked(useStoreApi) const mockUseNodeDataUpdateHook = vi.mocked(useNodeDataUpdate) const mockUseNodesSyncDraftHook = vi.mocked(useNodesSyncDraft) const mockUseDatasourceSingleRunHook = vi.mocked(useDatasourceSingleRun) -const mockUseInvalidLastRunHook = vi.mocked(useInvalidLastRun) +const mockUseInvalidLastRunHook = vi.mocked(useInvalidateLastRun) const mockFetchNodeInspectVarsFn = vi.mocked(fetchNodeInspectVars) const mockUseDataSourceStoreHook = vi.mocked(useDataSourceStore) const mockUseDataSourceStoreWithSelectorHook = vi.mocked(useDataSourceStoreWithSelector) @@ -120,8 +119,8 @@ const createData = (overrides: Partial = {}): DataSourceNode const createProps = (overrides: Partial = {}): CustomRunFormProps => ({ nodeId: 'data-source-node', - flowId: 'flow-id', - flowType: FlowType.ragPipeline, + resource: { kind: 'knowledge-pipeline', pipelineId: 'flow-id' }, + payload: createData(), setRunResult: vi.fn(), setIsRunAfterSingleRun: vi.fn(), diff --git a/web/app/components/workflow/nodes/data-source/hooks/__tests__/use-before-run-form.spec.tsx b/web/app/components/workflow/nodes/data-source/hooks/__tests__/use-before-run-form.spec.tsx index 9c73404978a..7c4f76df6c4 100644 --- a/web/app/components/workflow/nodes/data-source/hooks/__tests__/use-before-run-form.spec.tsx +++ b/web/app/components/workflow/nodes/data-source/hooks/__tests__/use-before-run-form.spec.tsx @@ -1,18 +1,18 @@ import type { CustomRunFormProps, DataSourceNodeType } from '../../types' import type { NodeRunResult, VarInInspect } from '@/types/workflow' -import { act, renderHook } from '@testing-library/react' +import { act } from '@testing-library/react' import { useStoreApi } from 'reactflow' import { useDataSourceStore, useDataSourceStoreWithSelector, } from '@/app/components/datasets/documents/create-from-pipeline/data-source/store' +import { renderEditorHook as renderHook } from '@/app/components/workflow/editor-resource/__tests__/render' import { BlockEnum, NodeRunningStatus } from '@/app/components/workflow/types' import { DatasourceType } from '@/models/pipeline' import { useDatasourceSingleRun } from '@/service/use-pipeline' -import { useInvalidLastRun } from '@/service/use-workflow' +import { useInvalidateLastRun } from '@/service/use-workflow' import { fetchNodeInspectVars } from '@/service/workflow' import { TransferMethod } from '@/types/app' -import { FlowType } from '@/types/common' import { useNodeDataUpdate } from '../../../../hooks/use-node-data-update' import { useNodesSyncDraft } from '../../../../hooks/use-nodes-sync-draft' import useBeforeRunForm from '../use-before-run-form' @@ -83,7 +83,7 @@ vi.mock('@/service/use-pipeline', () => ({ })) vi.mock('@/service/use-workflow', () => ({ - useInvalidLastRun: vi.fn(), + useInvalidateLastRun: vi.fn(), })) vi.mock('@/service/workflow', () => ({ @@ -99,7 +99,7 @@ const mockUseStoreApi = vi.mocked(useStoreApi) const mockUseNodeDataUpdateHook = vi.mocked(useNodeDataUpdate) const mockUseNodesSyncDraftHook = vi.mocked(useNodesSyncDraft) const mockUseDatasourceSingleRunHook = vi.mocked(useDatasourceSingleRun) -const mockUseInvalidLastRunHook = vi.mocked(useInvalidLastRun) +const mockUseInvalidLastRunHook = vi.mocked(useInvalidateLastRun) const mockFetchNodeInspectVarsFn = vi.mocked(fetchNodeInspectVars) const mockUseDataSourceStoreHook = vi.mocked(useDataSourceStore) const mockUseDataSourceStoreWithSelectorHook = vi.mocked(useDataSourceStoreWithSelector) @@ -121,8 +121,8 @@ const createData = (overrides: Partial = {}): DataSourceNode const createProps = (overrides: Partial = {}): CustomRunFormProps => ({ nodeId: 'data-source-node', - flowId: 'flow-id', - flowType: FlowType.ragPipeline, + resource: { kind: 'knowledge-pipeline', pipelineId: 'flow-id' }, + payload: createData(), setRunResult: vi.fn(), setIsRunAfterSingleRun: vi.fn(), @@ -205,6 +205,7 @@ describe('data-source/hooks/use-before-run-form', () => { const { result, rerender } = renderHook( ({ payload }) => useBeforeRunForm(createProps({ payload })), { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, initialProps: { payload: createData(), }, @@ -257,7 +258,9 @@ describe('data-source/hooks/use-before-run-form', () => { }) const props = createProps() - const { result } = renderHook(() => useBeforeRunForm(props)) + const { result } = renderHook(() => useBeforeRunForm(props), { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + }) await act(async () => { result.current.handleRunWithSyncDraft() @@ -284,8 +287,7 @@ describe('data-source/hooks/use-before-run-form', () => { expect.any(Object), ) expect(mockFetchNodeInspectVars).toHaveBeenCalledWith( - FlowType.ragPipeline, - 'flow-id', + { kind: 'knowledge-pipeline', pipelineId: 'flow-id' }, 'data-source-node', ) expect(props.appendNodeInspectVars).toHaveBeenCalledWith( @@ -324,14 +326,16 @@ describe('data-source/hooks/use-before-run-form', () => { return Promise.resolve(undefined) }) - const { result } = renderHook(() => - useBeforeRunForm( - createProps({ - payload: createData({ - provider_type: DatasourceType.onlineDrive, + const { result } = renderHook( + () => + useBeforeRunForm( + createProps({ + payload: createData({ + provider_type: DatasourceType.onlineDrive, + }), }), - }), - ), + ), + { resource: { kind: 'app', appId: 'app-1', mode: 'workflow' } }, ) await act(async () => { diff --git a/web/app/components/workflow/nodes/document-extractor/__tests__/use-config.spec.ts b/web/app/components/workflow/nodes/document-extractor/__tests__/use-config.spec.ts index e181164fa73..1cfd66ff442 100644 --- a/web/app/components/workflow/nodes/document-extractor/__tests__/use-config.spec.ts +++ b/web/app/components/workflow/nodes/document-extractor/__tests__/use-config.spec.ts @@ -3,14 +3,14 @@ import { renderHook } from '@testing-library/react' import { useStoreApi } from 'reactflow' import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud' import { BlockEnum, VarType } from '@/app/components/workflow/types' -import { useIsChatMode, useNodesReadOnly, useWorkflow } from '../../../hooks/use-workflow' +import { useNodesReadOnly, useWorkflow } from '../../../hooks/use-workflow' import { useWorkflowVariables } from '../../../hooks/use-workflow-variables' import useConfig from '../use-config' const mockUseStoreApi = vi.mocked(useStoreApi) const mockUseNodesReadOnly = vi.mocked(useNodesReadOnly) const mockUseNodeCrud = vi.mocked(useNodeCrud) -const mockUseIsChatMode = vi.mocked(useIsChatMode) +const mockUseIsChatMode = vi.hoisted(() => vi.fn()) const mockUseWorkflow = vi.mocked(useWorkflow) const mockUseWorkflowVariables = vi.mocked(useWorkflowVariables) @@ -27,7 +27,7 @@ vi.mock('../../../hooks/use-workflow', async (importOriginal) => { return { ...actual, - useIsChatMode: vi.fn(), + useNodesReadOnly: vi.fn(), useWorkflow: vi.fn(), } @@ -109,3 +109,10 @@ describe('document-extractor/use-config', () => { expect(result.current.filterVar({ type: VarType.string } as never)).toBe(false) }) }) + +vi.mock('@/app/components/workflow/editor-resource/state', async (importOriginal) => { + const actual = + await importOriginal() + const { atom } = await import('jotai') + return { ...actual, isChatflowAtom: atom(() => mockUseIsChatMode()) } +}) diff --git a/web/app/components/workflow/nodes/end/__tests__/node.spec.tsx b/web/app/components/workflow/nodes/end/__tests__/node.spec.tsx index 87c1050eda2..02a9b94e00a 100644 --- a/web/app/components/workflow/nodes/end/__tests__/node.spec.tsx +++ b/web/app/components/workflow/nodes/end/__tests__/node.spec.tsx @@ -3,7 +3,7 @@ import { screen } from '@testing-library/react' import { createNode, createStartNode } from '@/app/components/workflow/__tests__/fixtures' import { renderNodeComponent } from '@/app/components/workflow/__tests__/workflow-test-env' import { BlockEnum } from '@/app/components/workflow/types' -import { useIsChatMode, useWorkflow } from '../../../hooks/use-workflow' +import { useWorkflow } from '../../../hooks/use-workflow' import { useWorkflowVariables } from '../../../hooks/use-workflow-variables' import Node from '../node' @@ -13,7 +13,6 @@ vi.mock('../../../hooks/use-workflow', async (importOriginal) => { return { ...actual, useWorkflow: vi.fn(), - useIsChatMode: vi.fn(), } }) @@ -28,7 +27,7 @@ vi.mock('../../../hooks/use-workflow-variables', async (importOriginal) => { const mockUseWorkflow = vi.mocked(useWorkflow) const mockUseWorkflowVariables = vi.mocked(useWorkflowVariables) -const mockUseIsChatMode = vi.mocked(useIsChatMode) +const mockUseIsChatMode = vi.hoisted(() => vi.fn()) const createNodeData = (overrides: Partial = {}): EndNodeType => ({ title: 'End', @@ -109,3 +108,10 @@ describe('EndNode', () => { }) }) }) + +vi.mock('@/app/components/workflow/editor-resource/state', async (importOriginal) => { + const actual = + await importOriginal() + const { atom } = await import('jotai') + return { ...actual, isChatflowAtom: atom(() => mockUseIsChatMode()) } +}) diff --git a/web/app/components/workflow/nodes/human-input/__tests__/human-input.spec.tsx b/web/app/components/workflow/nodes/human-input/__tests__/human-input.spec.tsx index 0c98414f2e4..c90092a6918 100644 --- a/web/app/components/workflow/nodes/human-input/__tests__/human-input.spec.tsx +++ b/web/app/components/workflow/nodes/human-input/__tests__/human-input.spec.tsx @@ -80,7 +80,6 @@ vi.mock('../../../hooks/use-workflow', async (importOriginal) => { getNodesReadOnly: () => false, nodesReadOnly: false, }), - useIsChatMode: () => false, } }) @@ -650,3 +649,10 @@ describe('DSL Import with Human Input Node', () => { }) }) }) + +vi.mock('@/app/components/workflow/editor-resource/state', async (importOriginal) => { + const actual = + await importOriginal() + const { atom } = await import('jotai') + return { ...actual, isChatflowAtom: atom(() => false) } +}) diff --git a/web/app/components/workflow/nodes/human-input/components/delivery-method/__tests__/test-email-sender.spec.tsx b/web/app/components/workflow/nodes/human-input/components/delivery-method/__tests__/test-email-sender.spec.tsx index 4d660cca00a..4096dc88b26 100644 --- a/web/app/components/workflow/nodes/human-input/components/delivery-method/__tests__/test-email-sender.spec.tsx +++ b/web/app/components/workflow/nodes/human-input/components/delivery-method/__tests__/test-email-sender.spec.tsx @@ -10,6 +10,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { fireEvent, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { useState } from 'react' +import { EditorResourceScope } from '@/app/components/workflow/editor-resource/provider' import { HooksStoreContext } from '@/app/components/workflow/hooks-store/provider' import { createHooksStore } from '@/app/components/workflow/hooks-store/store' import { CodeLanguage } from '@/app/components/workflow/nodes/code/types' @@ -19,11 +20,6 @@ import { seedAccountProfileQuery } from '@/test/console/account-profile' import { render } from '@/test/console/render' import EmailSenderModal from '../test-email-sender' -vi.mock('@/next/navigation', async (importOriginal) => ({ - ...(await importOriginal()), - useParams: () => ({ appId: 'app-1' }), -})) - vi.mock('@/app/notifications', async (importOriginal) => ({ ...(await importOriginal()), toast: { @@ -73,7 +69,9 @@ const renderWithProviders = (ui: ReactNode) => { return render( - {ui} + + {ui} + , ) } diff --git a/web/app/components/workflow/nodes/human-input/hooks/__tests__/use-single-run-form-params.spec.ts b/web/app/components/workflow/nodes/human-input/hooks/__tests__/use-single-run-form-params.spec.ts index 7ce96ce9c7b..09d67abb13f 100644 --- a/web/app/components/workflow/nodes/human-input/hooks/__tests__/use-single-run-form-params.spec.ts +++ b/web/app/components/workflow/nodes/human-input/hooks/__tests__/use-single-run-form-params.spec.ts @@ -1,17 +1,15 @@ +import type { AppMode } from '@dify/contracts/api/console/apps/types.gen' import type { HumanInputNodeType } from '../../types' import type { FileEntity } from '@/app/components/base/file-uploader/types' import type { InputVar } from '@/app/components/workflow/types' import type { HumanInputFormData } from '@/types/workflow' import { act } from '@testing-library/react' +import { renderEditorHook as renderHookWithConsoleQuery } from '@/app/components/workflow/editor-resource/__tests__/render' import { BlockEnum, InputVarType, SupportUploadFileTypes } from '@/app/components/workflow/types' -import { - createConsoleQueryClient, - renderHookWithConsoleQuery, - seedAppDetail, -} from '@/test/console/query-data' +import { createConsoleQueryClient, seedAppDetail } from '@/test/console/query-data' import { createAppDetailFixture } from '@/test/fixtures/app' import { withSelectorKey } from '@/test/i18n-mock' -import { AppModeEnum, TransferMethod } from '@/types/app' +import { TransferMethod } from '@/types/app' import useSingleRunFormParams from '../use-single-run-form-params' const mockUseTranslation = vi.hoisted(() => vi.fn()) @@ -92,12 +90,15 @@ describe('human-input/hooks/use-single-run-form-params', () => { const mockSetRunInputData = vi.fn() const getInputVars = vi.fn() let currentInputs = createPayload() - let appDetail: { id: string; mode: AppModeEnum } | undefined + let appDetail: { id: string; mode: AppMode } | undefined const renderHook = (callback: () => Result) => { const queryClient = createConsoleQueryClient() if (appDetail) seedAppDetail(queryClient, createAppDetailFixture(appDetail)) - return renderHookWithConsoleQuery(callback, { queryClient }) + return renderHookWithConsoleQuery(callback, { + queryClient, + resource: { kind: 'app', appId: appDetail?.id ?? 'app-1', mode: 'workflow' }, + }) } beforeEach(() => { @@ -105,7 +106,7 @@ describe('human-input/hooks/use-single-run-form-params', () => { currentInputs = createPayload() appDetail = { id: 'app-1', - mode: AppModeEnum.WORKFLOW, + mode: 'workflow' as const, } mockUseTranslation.mockReturnValue({ @@ -352,13 +353,13 @@ describe('human-input/hooks/use-single-run-form-params', () => { expect(result.current.showGeneratedForm).toBe(false) }) - it('should use the advanced-chat endpoint and skip remote fetches when app detail is missing', async () => { + it('should use the advanced-chat endpoint for chatflow form previews', async () => { appDetail = { id: 'app-2', - mode: AppModeEnum.ADVANCED_CHAT, + mode: 'advanced-chat' as const, } - const { result, rerender } = renderHook(() => + const { result } = renderHook(() => useSingleRunFormParams({ id: 'node-9', payload: currentInputs, @@ -379,14 +380,6 @@ describe('human-input/hooks/use-single-run-form-params', () => { }, ) - appDetail = undefined - rerender() - - await act(async () => { - const data = await result.current.handleFetchFormContent({ topic: 'skip' }) - expect(data).toBeNull() - }) - expect(mockFetchHumanInputNodeStepRunForm).toHaveBeenCalledTimes(1) }) }) diff --git a/web/app/components/workflow/nodes/if-else/__tests__/integration.spec.tsx b/web/app/components/workflow/nodes/if-else/__tests__/integration.spec.tsx index 912cd0334a7..26f374a43f1 100644 --- a/web/app/components/workflow/nodes/if-else/__tests__/integration.spec.tsx +++ b/web/app/components/workflow/nodes/if-else/__tests__/integration.spec.tsx @@ -1,10 +1,13 @@ import type { Var } from '../../../types' import type { IfElseNodeType } from '../types' import type { PanelProps } from '@/types/workflow' -import { fireEvent, render, screen } from '@testing-library/react' +import { fireEvent, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' +import { + renderEditor as render, + renderEditor as renderWithConsoleQuery, +} from '@/app/components/workflow/editor-resource/__tests__/render' import { VarKindType } from '@/app/components/workflow/nodes/_base/types' -import { renderWithConsoleQuery } from '@/test/console/query-data' import { BlockEnum, VarType } from '../../../types' import ConditionAdd from '../components/condition-add' import ConditionFilesListValue from '../components/condition-files-list-value' @@ -219,7 +222,9 @@ describe('if-else path', () => { const user = userEvent.setup() const onSelectVariable = vi.fn() - render() + render(, { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + }) await user.click( screen.getByRole('button', { name: /workflowLogic.nodes.ifElse.addCondition/i }), @@ -253,6 +258,7 @@ describe('if-else path', () => { unit="%" />
, + { resource: { kind: 'app', appId: 'app-1', mode: 'workflow' } }, ) await user.click(screen.getByRole('button', { name: /contains/i })) @@ -341,6 +347,7 @@ describe('if-else path', () => { }} />
, + { resource: { kind: 'app', appId: 'app-1', mode: 'workflow' } }, ) expect(screen.getByText('node-1.answer')).toBeInTheDocument() @@ -386,6 +393,7 @@ describe('if-else path', () => { ], })} />, + { resource: { kind: 'app', appId: 'app-1', mode: 'workflow' } }, ) expect(screen.getByText('IF')).toBeInTheDocument() @@ -412,7 +420,9 @@ describe('if-else path', () => { }), ) - render() + render(, { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + }) await user.click(screen.getByRole('button', { name: /elif/i })) diff --git a/web/app/components/workflow/nodes/iteration-start/__tests__/index.spec.tsx b/web/app/components/workflow/nodes/iteration-start/__tests__/index.spec.tsx index dbfef5c8d13..53d3552c3fb 100644 --- a/web/app/components/workflow/nodes/iteration-start/__tests__/index.spec.tsx +++ b/web/app/components/workflow/nodes/iteration-start/__tests__/index.spec.tsx @@ -6,7 +6,7 @@ import { renderWorkflowFlowComponent } from '@/app/components/workflow/__tests__ import { BlockEnum } from '@/app/components/workflow/types' import { useAvailableBlocks } from '../../../hooks/use-available-blocks' import { useNodesInteractions } from '../../../hooks/use-nodes-interactions' -import { useIsChatMode, useNodesReadOnly } from '../../../hooks/use-workflow' +import { useNodesReadOnly } from '../../../hooks/use-workflow' import IterationStartNode, { IterationStartNodeDumb } from '../index' vi.mock('../../../hooks/use-available-blocks', async (importOriginal) => { @@ -33,14 +33,13 @@ vi.mock('../../../hooks/use-workflow', async (importOriginal) => { return { ...actual, useNodesReadOnly: vi.fn(), - useIsChatMode: vi.fn(), } }) const mockUseAvailableBlocks = vi.mocked(useAvailableBlocks) const mockUseNodesInteractions = vi.mocked(useNodesInteractions) const mockUseNodesReadOnly = vi.mocked(useNodesReadOnly) -const mockUseIsChatMode = vi.mocked(useIsChatMode) +const mockUseIsChatMode = vi.hoisted(() => vi.fn()) const createAvailableBlocksResult = (): ReturnType => ({ getAvailableBlocks: vi.fn(() => ({ @@ -106,3 +105,10 @@ describe('IterationStartNode', () => { }) }) }) + +vi.mock('@/app/components/workflow/editor-resource/state', async (importOriginal) => { + const actual = + await importOriginal() + const { atom } = await import('jotai') + return { ...actual, isChatflowAtom: atom(() => mockUseIsChatMode()) } +}) diff --git a/web/app/components/workflow/nodes/iteration/__tests__/use-config.spec.ts b/web/app/components/workflow/nodes/iteration/__tests__/use-config.spec.ts index 564ed804d75..711bda51f3e 100644 --- a/web/app/components/workflow/nodes/iteration/__tests__/use-config.spec.ts +++ b/web/app/components/workflow/nodes/iteration/__tests__/use-config.spec.ts @@ -28,7 +28,7 @@ vi.mock('../../../hooks/use-workflow', async (importOriginal) => { return { ...actual, useNodesReadOnly: () => mockUseNodesReadOnly(), - useIsChatMode: () => mockUseIsChatMode(), + useWorkflow: () => mockUseWorkflow(), } }) @@ -264,3 +264,10 @@ describe('iteration/use-config', () => { expect(mockDeleteNodeInspectorVars).toHaveBeenCalledWith('iteration-node') }) }) + +vi.mock('@/app/components/workflow/editor-resource/state', async (importOriginal) => { + const actual = + await importOriginal() + const { atom } = await import('jotai') + return { ...actual, isChatflowAtom: atom(() => mockUseIsChatMode()) } +}) diff --git a/web/app/components/workflow/nodes/knowledge-retrieval/__tests__/use-config.spec.ts b/web/app/components/workflow/nodes/knowledge-retrieval/__tests__/use-config.spec.ts index 3f97777611b..fc992727f04 100644 --- a/web/app/components/workflow/nodes/knowledge-retrieval/__tests__/use-config.spec.ts +++ b/web/app/components/workflow/nodes/knowledge-retrieval/__tests__/use-config.spec.ts @@ -14,8 +14,8 @@ import { BlockEnum, VarType } from '@/app/components/workflow/types' import { DATASET_DEFAULT } from '@/config' import { ChunkingMode, DatasetPermission, DataSourceType } from '@/models/datasets' import { fetchDatasets } from '@/service/datasets' -import { AppModeEnum, RETRIEVE_METHOD, RETRIEVE_TYPE } from '@/types/app' -import { useIsChatMode, useNodesReadOnly, useWorkflow } from '../../../hooks/use-workflow' +import { RETRIEVE_METHOD, RETRIEVE_TYPE } from '@/types/app' +import { useNodesReadOnly, useWorkflow } from '../../../hooks/use-workflow' import { ComparisonOperator, LogicalOperator, @@ -39,7 +39,7 @@ vi.mock('../../../hooks/use-workflow', async (importOriginal) => { return { ...actual, useNodesReadOnly: vi.fn(), - useIsChatMode: vi.fn(), + useWorkflow: vi.fn(), } }) @@ -68,7 +68,7 @@ vi.mock('@/service/datasets', () => ({ })) const mockUseNodesReadOnly = vi.mocked(useNodesReadOnly) -const mockUseIsChatMode = vi.mocked(useIsChatMode) +const mockUseIsChatMode = vi.hoisted(() => vi.fn()) const mockUseWorkflow = vi.mocked(useWorkflow) const mockUseModelListAndDefaultModelAndCurrentProviderAndModel = vi.mocked( useModelListAndDefaultModelAndCurrentProviderAndModel, @@ -222,7 +222,7 @@ describe('knowledge-retrieval/use-config', () => { currentModel: { model: 'rerank-model', model_properties: { - mode: AppModeEnum.CHAT, + mode: 'chat' as const, }, }, } as unknown as ReturnType @@ -235,7 +235,7 @@ describe('knowledge-retrieval/use-config', () => { currentModel: { model: 'gpt-4o-mini', model_properties: { - mode: AppModeEnum.CHAT, + mode: 'chat' as const, }, }, } as unknown as ReturnType @@ -371,7 +371,7 @@ describe('knowledge-retrieval/use-config', () => { result.current.handleModelChanged({ provider: 'anthropic', modelId: 'claude-sonnet', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, }) result.current.handleCompletionParamsChange({ temperature: 0.2 }) result.current.handleCompletionParamsChange({ temperature: 0.2 }) @@ -504,7 +504,7 @@ describe('knowledge-retrieval/use-config', () => { result.current.handleMetadataModelChange({ provider: 'openai', modelId: 'gpt-4.1-mini', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, }) result.current.handleMetadataCompletionParamsChange({ top_p: 0.3 }) }) @@ -517,7 +517,7 @@ describe('knowledge-retrieval/use-config', () => { expect(result.current.inputs.metadata_model_config).toEqual({ provider: 'openai', name: 'gpt-4.1-mini', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, completion_params: { top_p: 0.3 }, }) }) @@ -529,3 +529,10 @@ describe('knowledge-retrieval/use-config', () => { expect(result.current.filterFileVar({ type: VarType.string } as never)).toBe(false) }) }) + +vi.mock('@/app/components/workflow/editor-resource/state', async (importOriginal) => { + const actual = + await importOriginal() + const { atom } = await import('jotai') + return { ...actual, isChatflowAtom: atom(() => mockUseIsChatMode()) } +}) diff --git a/web/app/components/workflow/nodes/knowledge-retrieval/hooks/__tests__/use-knowledge-metadata-config.spec.ts b/web/app/components/workflow/nodes/knowledge-retrieval/hooks/__tests__/use-knowledge-metadata-config.spec.ts index 5fa22f43148..d4920ed834d 100644 --- a/web/app/components/workflow/nodes/knowledge-retrieval/hooks/__tests__/use-knowledge-metadata-config.spec.ts +++ b/web/app/components/workflow/nodes/knowledge-retrieval/hooks/__tests__/use-knowledge-metadata-config.spec.ts @@ -3,7 +3,6 @@ import type { KnowledgeRetrievalNodeType, MetadataFilteringCondition } from '../ import { act, renderHook } from '@testing-library/react' import useAvailableVarList from '@/app/components/workflow/nodes/_base/hooks/use-available-var-list' import { BlockEnum, VarType } from '@/app/components/workflow/types' -import { AppModeEnum } from '@/types/app' import { ComparisonOperator, LogicalOperator, @@ -132,7 +131,7 @@ describe('use-knowledge-metadata-config', () => { result.current.handleMetadataModelChange({ provider: 'openai', modelId: 'gpt-4.1-mini', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, }) result.current.handleMetadataCompletionParamsChange({ top_p: 0.3 }) }) @@ -142,7 +141,7 @@ describe('use-knowledge-metadata-config', () => { metadata_model_config: { provider: 'openai', name: 'gpt-4.1-mini', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, completion_params: { top_p: 0.3 }, }, }), @@ -202,7 +201,7 @@ describe('use-knowledge-metadata-config', () => { metadata_model_config: { provider: 'openai', name: 'gpt-4.1-mini', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, completion_params: { temperature: 0.7 }, }, }) @@ -247,7 +246,7 @@ describe('use-knowledge-metadata-config', () => { metadata_model_config: { provider: 'openai', name: 'gpt-4.1-mini', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, completion_params: { temperature: 0.9 }, }, }) @@ -274,7 +273,7 @@ describe('use-knowledge-metadata-config', () => { expect(inputRef.current.metadata_model_config).toEqual({ provider: 'anthropic', name: 'claude-sonnet', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, completion_params: { temperature: 0.9 }, }) }) diff --git a/web/app/components/workflow/nodes/knowledge-retrieval/hooks/__tests__/use-knowledge-model-config.spec.ts b/web/app/components/workflow/nodes/knowledge-retrieval/hooks/__tests__/use-knowledge-model-config.spec.ts index 1bb107d9b21..abb34e81dc8 100644 --- a/web/app/components/workflow/nodes/knowledge-retrieval/hooks/__tests__/use-knowledge-model-config.spec.ts +++ b/web/app/components/workflow/nodes/knowledge-retrieval/hooks/__tests__/use-knowledge-model-config.spec.ts @@ -5,7 +5,7 @@ import { act, renderHook, waitFor } from '@testing-library/react' import { BlockEnum } from '@/app/components/workflow/types' import { DATASET_DEFAULT } from '@/config' import { ChunkingMode, DatasetPermission, DataSourceType } from '@/models/datasets' -import { AppModeEnum, RETRIEVE_METHOD, RETRIEVE_TYPE } from '@/types/app' +import { RETRIEVE_METHOD, RETRIEVE_TYPE } from '@/types/app' import useKnowledgeModelConfig from '../use-knowledge-model-config' const createDataset = (overrides: Partial = {}): DataSet => ({ @@ -169,7 +169,7 @@ describe('use-knowledge-model-config', () => { model: { provider: 'anthropic', name: 'claude-sonnet', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, completion_params: {}, }, }, @@ -221,7 +221,7 @@ describe('use-knowledge-model-config', () => { currentModel: { model: 'gpt-4o-mini', model_properties: { - mode: AppModeEnum.CHAT, + mode: 'chat' as const, }, }, fallbackRerankModel, @@ -236,7 +236,7 @@ describe('use-knowledge-model-config', () => { model: { provider: 'openai', name: 'gpt-4o-mini', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, completion_params: {}, }, }, @@ -308,7 +308,7 @@ describe('use-knowledge-model-config', () => { model: { provider: 'openai', name: 'gpt-4o-mini', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, completion_params: {}, }, }, @@ -322,7 +322,7 @@ describe('use-knowledge-model-config', () => { model: { provider: 'openai', name: 'gpt-4o-mini', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, completion_params: {}, }, }, @@ -348,7 +348,7 @@ describe('use-knowledge-model-config', () => { currentModel: { model: 'gpt-4o-mini', model_properties: { - mode: AppModeEnum.CHAT, + mode: 'chat' as const, }, }, fallbackRerankModel, diff --git a/web/app/components/workflow/nodes/list-operator/__tests__/use-config.spec.tsx b/web/app/components/workflow/nodes/list-operator/__tests__/use-config.spec.tsx index 3c96c472cd6..3e3e61da60f 100644 --- a/web/app/components/workflow/nodes/list-operator/__tests__/use-config.spec.tsx +++ b/web/app/components/workflow/nodes/list-operator/__tests__/use-config.spec.tsx @@ -1,5 +1,5 @@ import type { ListFilterNodeType } from '../types' -import { renderHook } from '@testing-library/react' +import { renderEditorHook as renderHook } from '@/app/components/workflow/editor-resource/__tests__/render' import { BlockEnum, VarType } from '@/app/components/workflow/types' import { createNodeCrudModuleMock } from '../../__tests__/use-config-test-utils' import { ComparisonOperator } from '../../if-else/types' @@ -32,7 +32,7 @@ vi.mock('../../../hooks/use-workflow', async (importOriginal) => { return { ...actual, useNodesReadOnly: () => ({ nodesReadOnly: mockNodesReadOnly }), - useIsChatMode: () => mockIsChatMode, + useWorkflow: () => ({ getBeforeNodesInSameBranch: (...args: unknown[]) => mockGetBeforeNodesInSameBranch(...args), }), @@ -106,7 +106,9 @@ describe('list-operator/use-config', () => { }) it('should expose derived state and update inputs through the helper pipeline', () => { - const { result } = renderHook(() => useConfig('list-node', createPayload())) + const { result } = renderHook(() => useConfig('list-node', createPayload()), { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + }) expect(result.current.readOnly).toBe(false) expect(result.current.varType).toBe(VarType.arrayFile) @@ -207,24 +209,28 @@ describe('list-operator/use-config', () => { { id: 'loop-parent' }, ]) - const { result: iterationResult } = renderHook(() => - useConfig( - 'list-node', - createPayload({ - isInIteration: true, - variable: ['iteration', 'files'], - }), - ), + const { result: iterationResult } = renderHook( + () => + useConfig( + 'list-node', + createPayload({ + isInIteration: true, + variable: ['iteration', 'files'], + }), + ), + { resource: { kind: 'app', appId: 'app-1', mode: 'workflow' } }, ) - const { result: loopResult } = renderHook(() => - useConfig( - 'loop-node', - createPayload({ - isInLoop: true, - isInIteration: false, - variable: ['loop', 'names'], - }), - ), + const { result: loopResult } = renderHook( + () => + useConfig( + 'loop-node', + createPayload({ + isInLoop: true, + isInIteration: false, + variable: ['loop', 'names'], + }), + ), + { resource: { kind: 'app', appId: 'app-1', mode: 'workflow' } }, ) iterationResult.current.handleVarChanges(['iteration', 'files']) @@ -234,7 +240,7 @@ describe('list-operator/use-config', () => { expect(mockGetCurrentVariableType).toHaveBeenCalledWith( expect.objectContaining({ parentNode: expect.objectContaining({ id: 'iteration-parent' }), - isChatMode: true, + isChatflow: true, }), ) expect(mockGetCurrentVariableType).toHaveBeenCalledWith( @@ -253,3 +259,10 @@ describe('list-operator/use-config', () => { ) }) }) + +vi.mock('@/app/components/workflow/editor-resource/state', async (importOriginal) => { + const actual = + await importOriginal() + const { atom } = await import('jotai') + return { ...actual, isChatflowAtom: atom(() => mockIsChatMode) } +}) diff --git a/web/app/components/workflow/nodes/llm/__tests__/default.spec.ts b/web/app/components/workflow/nodes/llm/__tests__/default.spec.ts index 66d681bb02a..493115a6b1c 100644 --- a/web/app/components/workflow/nodes/llm/__tests__/default.spec.ts +++ b/web/app/components/workflow/nodes/llm/__tests__/default.spec.ts @@ -1,7 +1,5 @@ import type { LLMNodeType } from '../types' import { withSelectorKey } from '@/test/i18n-mock' -import { AppModeEnum } from '@/types/app' -import { FlowType } from '@/types/common' import { EditionType, PromptRole } from '../../../types' import nodeDefault from '../default' @@ -13,7 +11,7 @@ const createPayload = (overrides: Partial = {}): LLMNodeType => model: { ...nodeDefault.defaultValue.model, provider: 'langgenius/openai/gpt-4.1', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, }, prompt_template: [ { @@ -33,7 +31,7 @@ describe('llm default node validation', () => { ...nodeDefault.defaultValue.model, provider: '', name: 'gpt-4.1', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, completion_params: { temperature: 0.7, }, @@ -66,7 +64,7 @@ describe('llm default node validation', () => { value: { provider: 'langgenius/anthropic/anthropic', name: 'claude-sonnet', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, }, description: '', }, @@ -113,7 +111,7 @@ describe('llm default node validation', () => { value: { provider: 'langgenius/anthropic/anthropic', name: 'claude-sonnet', - mode: AppModeEnum.COMPLETION, + mode: 'completion' as const, }, description: '', }, @@ -137,7 +135,7 @@ describe('llm default node validation', () => { }, }), t, - { flowType: FlowType.appFlow }, + { resourceKind: 'app' }, ) expect(result.isValid).toBe(false) @@ -156,7 +154,7 @@ describe('llm default node validation', () => { }, }), t, - { flowType: FlowType.snippet }, + { resourceKind: 'snippet' }, ) expect(result.isValid).toBe(true) diff --git a/web/app/components/workflow/nodes/llm/__tests__/node.spec.tsx b/web/app/components/workflow/nodes/llm/__tests__/node.spec.tsx index 9d44b155feb..3f195f154c0 100644 --- a/web/app/components/workflow/nodes/llm/__tests__/node.spec.tsx +++ b/web/app/components/workflow/nodes/llm/__tests__/node.spec.tsx @@ -2,7 +2,6 @@ import type { LLMNodeType } from '../types' import { render, screen } from '@testing-library/react' import { useTextGenerationCurrentProviderAndModelAndModelList } from '@/app/components/header/account-setting/model-provider-page/hooks' import { BlockEnum } from '@/app/components/workflow/types' -import { AppModeEnum } from '@/types/app' import Node from '../node' vi.mock('@/app/components/header/account-setting/model-provider-page/hooks', () => ({ @@ -24,7 +23,7 @@ const createData = (overrides: Partial = {}): LLMNodeType => ({ model: { provider: 'openai', name: 'gpt-4o', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, completion_params: {}, } as LLMNodeType['model'], prompt_template: [], @@ -60,7 +59,7 @@ describe('llm/node', () => { model: { provider: '', name: '', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, completion_params: {}, } as LLMNodeType['model'], })} @@ -75,7 +74,7 @@ describe('llm/node', () => { model: { provider: 'anthropic', name: 'claude-sonnet', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, completion_params: {}, }, model_selector: ['env', 'for_summarize'], diff --git a/web/app/components/workflow/nodes/llm/__tests__/panel.spec.tsx b/web/app/components/workflow/nodes/llm/__tests__/panel.spec.tsx index 70c6e011e12..398ef53545e 100644 --- a/web/app/components/workflow/nodes/llm/__tests__/panel.spec.tsx +++ b/web/app/components/workflow/nodes/llm/__tests__/panel.spec.tsx @@ -1,6 +1,7 @@ import type { ModelProviderSummaryResponse } from '@dify/contracts/api/console/workspaces/types.gen' import type { LLMNodeType } from '../types' import type { ModelParameterModalProps } from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal' +import type { EditorResource } from '@/app/components/workflow/editor-resource/resource' import type { PanelProps } from '@/types/workflow' import { QueryClientProvider } from '@tanstack/react-query' import { screen, waitFor } from '@testing-library/react' @@ -8,8 +9,6 @@ import userEvent from '@testing-library/user-event' import { renderWorkflowFlowComponent } from '@/app/components/workflow/__tests__/workflow-test-env' import { consoleQuery } from '@/service/console' import { createConsoleQueryWrapper } from '@/test/console/query-data' -import { AppModeEnum } from '@/types/app' -import { FlowType } from '@/types/common' import { fetchAndMergeValidCompletionParams } from '@/utils/completion-params' import { BlockEnum } from '../../../types' import Panel from '../panel' @@ -99,7 +98,7 @@ const baseNodeData: LLMNodeType = { model: { provider: 'openai', name: 'gpt-4o', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, completion_params: {}, }, prompt_template: [], @@ -121,7 +120,7 @@ const buildUseConfigResult = (overrides?: Partial) => ({ environmentVariables: [], isEnvironmentModelSource: false, isChatModel: true, - isChatMode: true, + isChatflow: true, isCompletionModel: false, shouldShowContextTip: false, isVisionModel: false, @@ -163,7 +162,7 @@ const renderPanelElement = (data?: Partial) => ( ) -const renderPanel = (data?: Partial, flowType?: FlowType) => { +const renderPanel = (data?: Partial, resourceKind?: EditorResource['kind']) => { panelQueryClient = createConsoleQueryWrapper().queryClient panelQueryClient.setQueryData( consoleQuery.workspaces.current.modelProviders.summary.get.queryKey(), @@ -178,9 +177,12 @@ const renderPanel = (data?: Partial, flowType?: FlowType) => { }, ) return renderWorkflowFlowComponent(renderPanelElement(data), { - hooksStoreProps: flowType - ? { configsMap: { flowId: 'test-flow', flowType, fileSettings: {} } } - : {}, + resource: + resourceKind === 'snippet' + ? { kind: 'snippet', snippetId: 'test-snippet' } + : resourceKind === 'knowledge-pipeline' + ? { kind: 'knowledge-pipeline', pipelineId: 'test-pipeline' } + : { kind: 'app', appId: 'test-app', mode: 'workflow' }, }) } @@ -244,7 +246,7 @@ describe('LLM Panel', () => { }) it('does not offer an environment model source in snippet flows', () => { - renderPanel(undefined, FlowType.snippet) + renderPanel(undefined, 'snippet') expect( screen.queryByRole('radio', { name: 'workflow.nodes.common.typeSwitch.variable' }), @@ -265,7 +267,7 @@ describe('LLM Panel', () => { }), ) - renderPanel({ model_selector: ['env', 'shared_model'] }, FlowType.snippet) + renderPanel({ model_selector: ['env', 'shared_model'] }, 'snippet') await user.click(screen.getByRole('radio', { name: 'workflow.nodes.common.typeSwitch.input' })) expect(handleModelSourceChange).toHaveBeenCalledWith(false) @@ -289,7 +291,7 @@ describe('LLM Panel', () => { value: { provider: 'openai', name: 'gpt-4o', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, }, description: '', }, @@ -345,7 +347,7 @@ describe('LLM Panel', () => { id: 'env-llm', name: 'for_summarize', value_type: 'llm', - value: { provider: 'openai', name: 'gpt-4o', mode: AppModeEnum.CHAT }, + value: { provider: 'openai', name: 'gpt-4o', mode: 'chat' as const }, description: '', }, ], diff --git a/web/app/components/workflow/nodes/llm/__tests__/use-config.spec.ts b/web/app/components/workflow/nodes/llm/__tests__/use-config.spec.ts index d7d3080f045..0bba3420aeb 100644 --- a/web/app/components/workflow/nodes/llm/__tests__/use-config.spec.ts +++ b/web/app/components/workflow/nodes/llm/__tests__/use-config.spec.ts @@ -5,10 +5,10 @@ import { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/com import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud' import { useStore } from '@/app/components/workflow/store' import { BlockEnum } from '@/app/components/workflow/types' -import { AppModeEnum, Resolution } from '@/types/app' +import { Resolution } from '@/types/app' import useConfigVision from '../../../hooks/use-config-vision' import useInspectVarsCrud from '../../../hooks/use-inspect-vars-crud' -import { useIsChatMode, useNodesReadOnly } from '../../../hooks/use-workflow' +import { useNodesReadOnly } from '../../../hooks/use-workflow' import useAvailableVarList from '../../_base/hooks/use-available-var-list' import useLLMInputManager from '../hooks/use-llm-input-manager' import useLLMPromptConfig from '../hooks/use-llm-prompt-config' @@ -21,7 +21,6 @@ vi.mock('../../../hooks/use-workflow', async (importOriginal) => { return { ...actual, useNodesReadOnly: vi.fn(), - useIsChatMode: vi.fn(), } }) @@ -69,7 +68,7 @@ vi.mock('../hooks/use-llm-structured-output-config', () => ({ })) const mockUseNodesReadOnly = vi.mocked(useNodesReadOnly) -const mockUseIsChatMode = vi.mocked(useIsChatMode) +const mockUseIsChatMode = vi.hoisted(() => vi.fn()) const mockUseNodeCrud = vi.mocked(useNodeCrud) const mockUseInspectVarsCrud = vi.mocked(useInspectVarsCrud) const mockUseModelListAndDefaultModelAndCurrentProviderAndModel = vi.mocked( @@ -89,7 +88,7 @@ const createPayload = (overrides: Partial = {}): LLMNodeType => ({ model: { provider: 'openai', name: 'gpt-4o', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, completion_params: { temperature: 0.7, }, @@ -216,7 +215,7 @@ describe('llm/use-config', () => { const { result } = renderHook(() => useConfig('llm-node', inputRef.current)) expect(result.current.readOnly).toBe(false) - expect(result.current.isChatMode).toBe(true) + expect(result.current.isChatflow).toBe(true) expect(result.current.isChatModel).toBe(true) expect(result.current.isCompletionModel).toBe(false) expect(result.current.availableVars).toEqual([ @@ -251,7 +250,7 @@ describe('llm/use-config', () => { { provider: 'openai', modelId: 'gpt-4.1', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, }, { temperature: 0.3 }, ) @@ -283,7 +282,7 @@ describe('llm/use-config', () => { model: expect.objectContaining({ provider: 'openai', name: 'gpt-4.1', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, completion_params: { temperature: 0.3 }, }), }), @@ -296,7 +295,7 @@ describe('llm/use-config', () => { model: { provider: '', name: '', - mode: AppModeEnum.COMPLETION, + mode: 'completion' as const, completion_params: {}, }, }) @@ -311,7 +310,7 @@ describe('llm/use-config', () => { currentModel: { model: 'claude-sonnet', model_properties: { - mode: AppModeEnum.CHAT, + mode: 'chat' as const, }, }, } as unknown as ReturnType) @@ -331,7 +330,7 @@ describe('llm/use-config', () => { model: expect.objectContaining({ provider: 'anthropic', name: 'claude-sonnet', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, }), }), ) @@ -345,7 +344,7 @@ describe('llm/use-config', () => { model: { provider: 'cached-provider', name: 'cached-model', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, completion_params: { temperature: 0.2 }, }, }) @@ -363,7 +362,7 @@ describe('llm/use-config', () => { value: { provider: 'shared-provider', name: 'shared-model', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, completion_params: { temperature: 0.8 }, }, description: '', @@ -378,7 +377,7 @@ describe('llm/use-config', () => { expect(result.current.model).toEqual({ provider: 'shared-provider', name: 'shared-model', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, completion_params: { temperature: 0.8 }, }) expect(result.current.isEnvironmentModelSource).toBe(true) @@ -404,7 +403,7 @@ describe('llm/use-config', () => { value: { provider: 'shared-provider', name: 'shared-completion-model', - mode: AppModeEnum.COMPLETION, + mode: 'completion' as const, completion_params: { top_p: 0.9 }, }, description: '', @@ -439,7 +438,7 @@ describe('llm/use-config', () => { model: expect.objectContaining({ provider: 'shared-provider', name: 'shared-completion-model', - mode: AppModeEnum.COMPLETION, + mode: 'completion' as const, completion_params: { top_p: 0.9 }, }), }), @@ -457,7 +456,7 @@ describe('llm/use-config', () => { model: { provider: 'cached-provider', name: 'cached-model', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, completion_params: { temperature: 0.4 }, }, }) @@ -475,7 +474,7 @@ describe('llm/use-config', () => { value: { provider: 'latest-provider', name: 'latest-model', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, completion_params: { temperature: 0.8 }, }, description: '', @@ -496,7 +495,7 @@ describe('llm/use-config', () => { model: { provider: 'latest-provider', name: 'latest-model', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, completion_params: { temperature: 0.8 }, }, model_selector: undefined, @@ -506,3 +505,10 @@ describe('llm/use-config', () => { expect(updatedInputs).toHaveProperty('model_selector', undefined) }) }) + +vi.mock('@/app/components/workflow/editor-resource/state', async (importOriginal) => { + const actual = + await importOriginal() + const { atom } = await import('jotai') + return { ...actual, isChatflowAtom: atom(() => mockUseIsChatMode()) } +}) diff --git a/web/app/components/workflow/nodes/llm/__tests__/use-single-run-form-params.spec.ts b/web/app/components/workflow/nodes/llm/__tests__/use-single-run-form-params.spec.ts index 38c02d712f2..da3c5945ff2 100644 --- a/web/app/components/workflow/nodes/llm/__tests__/use-single-run-form-params.spec.ts +++ b/web/app/components/workflow/nodes/llm/__tests__/use-single-run-form-params.spec.ts @@ -1,6 +1,7 @@ import type { LLMNodeType } from '../types' +import type { EditorResource } from '@/app/components/workflow/editor-resource/resource' import type { InputVar, Variable } from '@/app/components/workflow/types' -import { renderHook } from '@testing-library/react' +import { renderEditorHook as renderHook } from '@/app/components/workflow/editor-resource/__tests__/render' import { BlockEnum, EditionType, @@ -8,8 +9,6 @@ import { PromptRole, VarType, } from '@/app/components/workflow/types' -import { AppModeEnum } from '@/types/app' -import { FlowType } from '@/types/common' import useConfigVision from '../../../hooks/use-config-vision' import useAvailableVarList from '../../_base/hooks/use-available-var-list' import useNodeCrud from '../../_base/hooks/use-node-crud' @@ -35,19 +34,20 @@ vi.mock('../../../hooks/use-workflow', async (importOriginal) => { return { ...actual, - useIsChatMode: () => true, } }) const mockFlowType = vi.hoisted(() => ({ - value: undefined as FlowType | undefined, + value: undefined as EditorResource['kind'] | undefined, })) vi.mock('@/app/components/workflow/hooks-store/store', () => ({ - useHooksStore: (selector: (state: { configsMap?: { flowType?: FlowType } }) => unknown) => + useHooksStore: ( + selector: (state: { configsMap?: { resourceKind?: EditorResource['kind'] } }) => unknown, + ) => selector({ configsMap: { - flowType: mockFlowType.value, + resourceKind: mockFlowType.value, }, }), })) @@ -63,7 +63,7 @@ const createData = (overrides: Partial = {}): LLMNodeType => ({ model: { provider: 'openai', name: 'gpt-4o', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, completion_params: {}, } as LLMNodeType['model'], prompt_template: [ @@ -117,7 +117,6 @@ describe('llm/use-single-run-form-params', () => { }) it('filters system variables from single-run inputs in snippet flows', () => { - mockFlowType.value = FlowType.snippet const getInputVars = vi.fn(() => [ createInputVar('#start.query#'), createInputVar('#sys.query#'), @@ -128,33 +127,35 @@ describe('llm/use-single-run-form-params', () => { createInputVar('#start.extra#'), ]) - const { result } = renderHook(() => - useSingleRunFormParams({ - id: 'llm-node', - payload: createData({ - prompt_template: [ - { - edition_type: EditionType.jinja2, - role: PromptRole.user, - text: '{{ query }}', - }, - ], - prompt_config: { - jinja2_variables: [ + const { result } = renderHook( + () => + useSingleRunFormParams({ + id: 'llm-node', + payload: createData({ + prompt_template: [ { - variable: 'extra', - value_selector: ['start', 'extra'], - value_type: VarType.string, + edition_type: EditionType.jinja2, + role: PromptRole.user, + text: '{{ query }}', }, ], - }, + prompt_config: { + jinja2_variables: [ + { + variable: 'extra', + value_selector: ['start', 'extra'], + value_type: VarType.string, + }, + ], + }, + }), + runInputData: {}, + runInputDataRef: { current: {} }, + getInputVars, + setRunInputData: vi.fn(), + toVarInputs, }), - runInputData: {}, - runInputDataRef: { current: {} }, - getInputVars, - setRunInputData: vi.fn(), - toVarInputs, - }), + { resource: { kind: 'snippet', snippetId: 'snippet-1' } }, ) expect(result.current.forms[0]!.inputs).toEqual([ @@ -167,3 +168,10 @@ describe('llm/use-single-run-form-params', () => { ]) }) }) + +vi.mock('@/app/components/workflow/editor-resource/state', async (importOriginal) => { + const actual = + await importOriginal() + const { atom } = await import('jotai') + return { ...actual, isChatflowAtom: atom(() => true) } +}) diff --git a/web/app/components/workflow/nodes/llm/__tests__/utils.spec.ts b/web/app/components/workflow/nodes/llm/__tests__/utils.spec.ts index 89bea169235..23797383438 100644 --- a/web/app/components/workflow/nodes/llm/__tests__/utils.spec.ts +++ b/web/app/components/workflow/nodes/llm/__tests__/utils.spec.ts @@ -3,7 +3,6 @@ import type { ModelConfig, ValueSelector, } from '@/app/components/workflow/types' -import { AppModeEnum } from '@/types/app' import { getLLMEnvironmentModel, getLLMModelIssue, @@ -62,7 +61,7 @@ describe('llm utils', () => { const model: ModelConfig = { provider: 'direct-provider', name: 'direct-model', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, completion_params: { temperature: 0.3 }, } const environmentVariables: EnvironmentVariable[] = [ @@ -73,7 +72,7 @@ describe('llm utils', () => { value: { provider: 'shared-provider', name: 'shared-model', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, completion_params: { temperature: 0.8 }, }, description: '', @@ -84,7 +83,7 @@ describe('llm utils', () => { expect(resolveLLMNodeModel(model, ['env', 'for_summarize'], environmentVariables)).toEqual({ provider: 'shared-provider', name: 'shared-model', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, completion_params: { temperature: 0.8 }, }) }) @@ -96,7 +95,7 @@ describe('llm utils', () => { value: { provider: 'shared-provider', name: 'shared-model', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, }, }, ] as EnvironmentVariable[] @@ -106,7 +105,7 @@ describe('llm utils', () => { ).toEqual({ provider: 'shared-provider', name: 'shared-model', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, completion_params: { temperature: 0.3 }, }) }) @@ -118,7 +117,7 @@ describe('llm utils', () => { value: { provider: 'shared-provider', name: 'shared-completion-model', - mode: AppModeEnum.COMPLETION, + mode: 'completion' as const, completion_params: { temperature: 0.8 }, }, }, @@ -127,7 +126,7 @@ describe('llm utils', () => { expect(getLLMEnvironmentModel(['env', 'for_summarize'], completionModelVariables)).toEqual({ provider: 'shared-provider', name: 'shared-completion-model', - mode: AppModeEnum.COMPLETION, + mode: 'completion' as const, completion_params: { temperature: 0.8 }, }) }) @@ -139,7 +138,7 @@ describe('llm utils', () => { value: { provider: 'shared-provider', name: 'shared-model', - mode: AppModeEnum.COMPLETION, + mode: 'completion' as const, }, }, ] as EnvironmentVariable[] diff --git a/web/app/components/workflow/nodes/llm/components/__tests__/config-prompt.spec.tsx b/web/app/components/workflow/nodes/llm/components/__tests__/config-prompt.spec.tsx index 16532c96e5d..cff77549493 100644 --- a/web/app/components/workflow/nodes/llm/components/__tests__/config-prompt.spec.tsx +++ b/web/app/components/workflow/nodes/llm/components/__tests__/config-prompt.spec.tsx @@ -94,7 +94,7 @@ function RemoteModelChangeFixture({ onChange }: { onChange: (inputs: LLMNodeType inputRef: inputManager.inputRef, setInputs: inputManager.setInputs, isChatModel, - isChatMode: true, + isChatflow: true, }) return ( diff --git a/web/app/components/workflow/nodes/llm/components/__tests__/panel-memory-section.spec.tsx b/web/app/components/workflow/nodes/llm/components/__tests__/panel-memory-section.spec.tsx index 347d11a4b88..e053c3a2f5d 100644 --- a/web/app/components/workflow/nodes/llm/components/__tests__/panel-memory-section.spec.tsx +++ b/web/app/components/workflow/nodes/llm/components/__tests__/panel-memory-section.spec.tsx @@ -1,8 +1,6 @@ import type { LLMNodeType } from '../../types' import type { Memory, Node, NodeOutPutVar } from '@/app/components/workflow/types' import { render, screen } from '@testing-library/react' -import { AppModeEnum } from '@/types/app' -import { FlowType } from '@/types/common' import PanelMemorySection from '../panel-memory-section' const mockEditor = vi.hoisted(() => vi.fn()) @@ -46,7 +44,7 @@ const createInputs = (overrides: Partial = {}): LLMNodeType => ({ model: { provider: 'openai', name: 'gpt-4o', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, completion_params: {}, } as LLMNodeType['model'], prompt_template: [], @@ -69,7 +67,7 @@ const createInputs = (overrides: Partial = {}): LLMNodeType => ({ const baseProps = { readOnly: false, - isChatMode: true, + isChatflow: true, isChatModel: true, isCompletionModel: false, inputs: createInputs(), @@ -128,7 +126,7 @@ describe('llm/panel-memory-section', () => { render( { }) it('does not default snippet memory prompts to system variables', () => { - render() + render() expect(screen.getByTestId('editor')).toHaveTextContent('') expect(mockEditor).toHaveBeenCalledWith( @@ -167,7 +165,7 @@ describe('llm/panel-memory-section', () => { render( , diff --git a/web/app/components/workflow/nodes/llm/components/__tests__/panel-output-section.spec.tsx b/web/app/components/workflow/nodes/llm/components/__tests__/panel-output-section.spec.tsx index 7ddd1aec12b..2e9c6c92d6b 100644 --- a/web/app/components/workflow/nodes/llm/components/__tests__/panel-output-section.spec.tsx +++ b/web/app/components/workflow/nodes/llm/components/__tests__/panel-output-section.spec.tsx @@ -2,7 +2,6 @@ import type { ReactNode } from 'react' import type { LLMNodeType, StructuredOutput } from '../../types' import { render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' -import { AppModeEnum } from '@/types/app' import { Type } from '../../types' import PanelOutputSection from '../panel-output-section' @@ -51,7 +50,7 @@ const createInputs = (overrides: Partial = {}): LLMNodeType => ({ model: { provider: 'openai', name: 'gpt-4o', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, completion_params: {}, } as LLMNodeType['model'], prompt_template: [], diff --git a/web/app/components/workflow/nodes/llm/components/__tests__/prompt-generator-btn.spec.tsx b/web/app/components/workflow/nodes/llm/components/__tests__/prompt-generator-btn.spec.tsx index b91a0c1c835..feb6e3dc647 100644 --- a/web/app/components/workflow/nodes/llm/components/__tests__/prompt-generator-btn.spec.tsx +++ b/web/app/components/workflow/nodes/llm/components/__tests__/prompt-generator-btn.spec.tsx @@ -1,5 +1,6 @@ -import { render, screen } from '@testing-library/react' +import { screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' +import { renderEditor as render } from '@/app/components/workflow/editor-resource/__tests__/render' import PromptGeneratorBtn from '../prompt-generator-btn' vi.mock('@/app/components/app/configuration/config/automatic/get-automatic-res', () => ({ @@ -14,7 +15,9 @@ vi.mock('@/app/components/workflow/hooks-store', () => ({ describe('PromptGeneratorBtn', () => { it('should open the prompt generator from the named action', async () => { const user = userEvent.setup() - render() + render(, { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + }) await user.click(screen.getByRole('button', { name: 'appDebug.operation.automatic' })) diff --git a/web/app/components/workflow/nodes/llm/hooks/__tests__/use-llm-input-manager.spec.ts b/web/app/components/workflow/nodes/llm/hooks/__tests__/use-llm-input-manager.spec.ts index 85d184012e5..2777752a353 100644 --- a/web/app/components/workflow/nodes/llm/hooks/__tests__/use-llm-input-manager.spec.ts +++ b/web/app/components/workflow/nodes/llm/hooks/__tests__/use-llm-input-manager.spec.ts @@ -2,7 +2,6 @@ import type { LLMNodeType } from '../../types' import type { LLMDefaultConfig } from '../use-llm-input-manager' import { act, renderHook, waitFor } from '@testing-library/react' import { BlockEnum, EditionType, PromptRole } from '@/app/components/workflow/types' -import { AppModeEnum } from '@/types/app' import useLLMInputManager from '../use-llm-input-manager' const createPayload = (overrides: Partial = {}): LLMNodeType => ({ @@ -12,7 +11,7 @@ const createPayload = (overrides: Partial = {}): LLMNodeType => ({ model: { provider: 'openai', name: 'gpt-4o', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, completion_params: {}, }, prompt_template: [ @@ -110,7 +109,7 @@ describe('use-llm-input-manager', () => { model: { provider: 'openai', name: 'gpt-4o-mini', - mode: AppModeEnum.COMPLETION, + mode: 'completion' as const, completion_params: {}, }, }), @@ -124,7 +123,7 @@ describe('use-llm-input-manager', () => { model: { provider: 'openai', name: 'gpt-4o-mini', - mode: AppModeEnum.COMPLETION, + mode: 'completion' as const, completion_params: {}, }, }) @@ -139,7 +138,7 @@ describe('use-llm-input-manager', () => { model: { provider: 'openai', name: 'gpt-4o-mini', - mode: AppModeEnum.COMPLETION, + mode: 'completion' as const, completion_params: {}, }, prompt_template: draftPayload.prompt_template, diff --git a/web/app/components/workflow/nodes/llm/hooks/__tests__/use-llm-prompt-config.spec.ts b/web/app/components/workflow/nodes/llm/hooks/__tests__/use-llm-prompt-config.spec.ts index d0dd204c4a3..66d7009c98d 100644 --- a/web/app/components/workflow/nodes/llm/hooks/__tests__/use-llm-prompt-config.spec.ts +++ b/web/app/components/workflow/nodes/llm/hooks/__tests__/use-llm-prompt-config.spec.ts @@ -7,7 +7,6 @@ import { QUERY_PLACEHOLDER_TEXT, } from '@/app/components/base/prompt-editor/constants' import { BlockEnum, EditionType, PromptRole, VarType } from '@/app/components/workflow/types' -import { AppModeEnum } from '@/types/app' import useLLMPromptConfig from '../use-llm-prompt-config' const createPayload = (overrides: Partial = {}): LLMNodeType => ({ @@ -17,7 +16,7 @@ const createPayload = (overrides: Partial = {}): LLMNodeType => ({ model: { provider: 'openai', name: 'gpt-4o', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, completion_params: {}, }, prompt_template: [ @@ -67,7 +66,7 @@ describe('use-llm-prompt-config', () => { useLLMPromptConfig({ inputs: missingConfigRef.current, inputRef: missingConfigRef, - isChatMode: true, + isChatflow: true, isChatModel: true, setInputs: handleMissingConfigInputs, }), @@ -109,7 +108,7 @@ describe('use-llm-prompt-config', () => { useLLMPromptConfig({ inputs: missingVariablesRef.current, inputRef: missingVariablesRef, - isChatMode: true, + isChatflow: true, isChatModel: true, setInputs: handleMissingVariablesInputs, }), @@ -163,7 +162,7 @@ describe('use-llm-prompt-config', () => { useLLMPromptConfig({ inputs: inputRef.current, inputRef, - isChatMode: true, + isChatflow: true, isChatModel: true, setInputs: createSetInputs(inputRef), }), @@ -211,7 +210,7 @@ describe('use-llm-prompt-config', () => { useLLMPromptConfig({ inputs: inputRef.current, inputRef, - isChatMode: true, + isChatflow: true, isChatModel: true, setInputs: handleSetInputs, }), @@ -317,7 +316,7 @@ describe('use-llm-prompt-config', () => { model: { provider: 'openai', name: 'gpt-4o-mini', - mode: AppModeEnum.COMPLETION, + mode: 'completion' as const, completion_params: {}, }, prompt_template: { @@ -333,7 +332,7 @@ describe('use-llm-prompt-config', () => { useLLMPromptConfig({ inputs: basicInputRef.current, inputRef: basicInputRef, - isChatMode: true, + isChatflow: true, isChatModel: false, setInputs: handleSetInputs, }), @@ -364,7 +363,7 @@ describe('use-llm-prompt-config', () => { model: { provider: 'openai', name: 'gpt-4o-mini', - mode: AppModeEnum.COMPLETION, + mode: 'completion' as const, completion_params: {}, }, prompt_template: { @@ -381,7 +380,7 @@ describe('use-llm-prompt-config', () => { useLLMPromptConfig({ inputs: jinjaInputRef.current, inputRef: jinjaInputRef, - isChatMode: false, + isChatflow: false, isChatModel: false, setInputs: handleJinjaInputs, }), diff --git a/web/app/components/workflow/nodes/llm/hooks/__tests__/use-llm-structured-output-config.spec.ts b/web/app/components/workflow/nodes/llm/hooks/__tests__/use-llm-structured-output-config.spec.ts index 6c3a680730a..7fc596a1176 100644 --- a/web/app/components/workflow/nodes/llm/hooks/__tests__/use-llm-structured-output-config.spec.ts +++ b/web/app/components/workflow/nodes/llm/hooks/__tests__/use-llm-structured-output-config.spec.ts @@ -4,7 +4,6 @@ import type { LLMNodeType } from '../../types' import { act, renderHook } from '@testing-library/react' import { ModelFeatureEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' import { BlockEnum } from '@/app/components/workflow/types' -import { AppModeEnum } from '@/types/app' import { Type } from '../../types' import useLLMStructuredOutputConfig from '../use-llm-structured-output-config' @@ -19,7 +18,7 @@ const createPayload = (overrides: Partial = {}): LLMNodeType => ({ model: { provider: 'openai', name: 'gpt-4o', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, completion_params: {}, }, prompt_template: [], diff --git a/web/app/components/workflow/nodes/loop-start/__tests__/index.spec.tsx b/web/app/components/workflow/nodes/loop-start/__tests__/index.spec.tsx index 25506393cc7..05edd3d07ba 100644 --- a/web/app/components/workflow/nodes/loop-start/__tests__/index.spec.tsx +++ b/web/app/components/workflow/nodes/loop-start/__tests__/index.spec.tsx @@ -6,7 +6,7 @@ import { renderWorkflowFlowComponent } from '@/app/components/workflow/__tests__ import { BlockEnum } from '@/app/components/workflow/types' import { useAvailableBlocks } from '../../../hooks/use-available-blocks' import { useNodesInteractions } from '../../../hooks/use-nodes-interactions' -import { useIsChatMode, useNodesReadOnly } from '../../../hooks/use-workflow' +import { useNodesReadOnly } from '../../../hooks/use-workflow' import LoopStartNode, { LoopStartNodeDumb } from '../index' vi.mock('../../../hooks/use-available-blocks', async (importOriginal) => { @@ -33,14 +33,13 @@ vi.mock('../../../hooks/use-workflow', async (importOriginal) => { return { ...actual, useNodesReadOnly: vi.fn(), - useIsChatMode: vi.fn(), } }) const mockUseAvailableBlocks = vi.mocked(useAvailableBlocks) const mockUseNodesInteractions = vi.mocked(useNodesInteractions) const mockUseNodesReadOnly = vi.mocked(useNodesReadOnly) -const mockUseIsChatMode = vi.mocked(useIsChatMode) +const mockUseIsChatMode = vi.hoisted(() => vi.fn()) const createAvailableBlocksResult = (): ReturnType => ({ getAvailableBlocks: vi.fn(() => ({ @@ -106,3 +105,10 @@ describe('LoopStartNode', () => { }) }) }) + +vi.mock('@/app/components/workflow/editor-resource/state', async (importOriginal) => { + const actual = + await importOriginal() + const { atom } = await import('jotai') + return { ...actual, isChatflowAtom: atom(() => mockUseIsChatMode()) } +}) diff --git a/web/app/components/workflow/nodes/loop/__tests__/use-config.spec.tsx b/web/app/components/workflow/nodes/loop/__tests__/use-config.spec.tsx index 84261702a96..883480d4d7a 100644 --- a/web/app/components/workflow/nodes/loop/__tests__/use-config.spec.tsx +++ b/web/app/components/workflow/nodes/loop/__tests__/use-config.spec.tsx @@ -40,7 +40,7 @@ vi.mock('../../../hooks/use-workflow', async (importOriginal) => { return { ...actual, useNodesReadOnly: () => ({ nodesReadOnly: false }), - useIsChatMode: () => false, + useWorkflow: () => ({ getLoopNodeChildren: (...args: unknown[]) => mockGetLoopNodeChildren(...args), }), @@ -255,3 +255,10 @@ describe('useConfig', () => { ) }) }) + +vi.mock('@/app/components/workflow/editor-resource/state', async (importOriginal) => { + const actual = + await importOriginal() + const { atom } = await import('jotai') + return { ...actual, isChatflowAtom: atom(() => false) } +}) diff --git a/web/app/components/workflow/nodes/parameter-extractor/__tests__/integration.spec.tsx b/web/app/components/workflow/nodes/parameter-extractor/__tests__/integration.spec.tsx index 2f1079cb31d..bcbb4324e0d 100644 --- a/web/app/components/workflow/nodes/parameter-extractor/__tests__/integration.spec.tsx +++ b/web/app/components/workflow/nodes/parameter-extractor/__tests__/integration.spec.tsx @@ -7,7 +7,6 @@ import userEvent from '@testing-library/user-event' import { useTextGenerationCurrentProviderAndModelAndModelList } from '@/app/components/header/account-setting/model-provider-page/hooks' import { ChangeType } from '@/app/components/workflow/types' import { toast } from '@/app/notifications' -import { AppModeEnum } from '@/types/app' import { BlockEnum } from '../../../types' import { ExtractParameterList } from '../components/extract-parameter/list' import { ParameterDialog } from '../components/extract-parameter/update' @@ -59,7 +58,7 @@ vi.mock( setModel({ provider: 'anthropic', modelId: 'claude-3-7-sonnet', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, }) } > @@ -247,7 +246,7 @@ const createData = ( model: { provider: 'openai', name: 'gpt-4o', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, completion_params: {}, }, query: ['node-1', 'query'], @@ -266,7 +265,7 @@ const createConfigResult = ( readOnly: false, handleInputVarChange: vi.fn(), filterVar: (_varPayload: Var) => true, - isChatMode: true, + isChatflow: true, inputs: createData(), isChatModel: true, isCompletionModel: false, @@ -571,7 +570,7 @@ describe('parameter-extractor path', () => { model: { provider: '', name: '', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, completion_params: {}, }, })} @@ -634,7 +633,7 @@ describe('parameter-extractor path', () => { expect(handleModelChanged).toHaveBeenCalledWith({ provider: 'anthropic', modelId: 'claude-3-7-sonnet', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, }) expect(handleCompletionParamsChange).toHaveBeenCalledWith({ temperature: 0.2 }) expect(handleInputVarChange).toHaveBeenCalledWith(['node-1', 'query']) diff --git a/web/app/components/workflow/nodes/parameter-extractor/__tests__/node.spec.tsx b/web/app/components/workflow/nodes/parameter-extractor/__tests__/node.spec.tsx index fb924bc8bd8..c25ae240402 100644 --- a/web/app/components/workflow/nodes/parameter-extractor/__tests__/node.spec.tsx +++ b/web/app/components/workflow/nodes/parameter-extractor/__tests__/node.spec.tsx @@ -2,7 +2,6 @@ import type { ParameterExtractorNodeType } from '../types' import { render, screen } from '@testing-library/react' import { useTextGenerationCurrentProviderAndModelAndModelList } from '@/app/components/header/account-setting/model-provider-page/hooks' import { BlockEnum } from '@/app/components/workflow/types' -import { AppModeEnum } from '@/types/app' import Node from '../node' import { ReasoningModeType } from '../types' @@ -27,7 +26,7 @@ const createData = ( model: { provider: 'openai', name: 'gpt-4o', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, completion_params: {}, } as ParameterExtractorNodeType['model'], query: ['node-1', 'query'], @@ -62,7 +61,7 @@ describe('parameter-extractor/node', () => { model: { provider: '', name: '', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, completion_params: {}, } as ParameterExtractorNodeType['model'], })} diff --git a/web/app/components/workflow/nodes/parameter-extractor/__tests__/panel.spec.tsx b/web/app/components/workflow/nodes/parameter-extractor/__tests__/panel.spec.tsx index 0ca37bf0d30..79dcf6065e4 100644 --- a/web/app/components/workflow/nodes/parameter-extractor/__tests__/panel.spec.tsx +++ b/web/app/components/workflow/nodes/parameter-extractor/__tests__/panel.spec.tsx @@ -3,7 +3,6 @@ import type { PanelProps } from '@/types/workflow' import { render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { BlockEnum } from '@/app/components/workflow/types' -import { AppModeEnum } from '@/types/app' import Panel from '../panel' import { ParamType, ReasoningModeType } from '../types' import useConfig from '../use-config' @@ -26,7 +25,7 @@ vi.mock( setModel({ provider: 'anthropic', modelId: 'claude-3-7-sonnet', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, }) } > @@ -137,7 +136,7 @@ const createData = ( model: { provider: 'openai', name: 'gpt-4o', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, completion_params: {}, } as ParameterExtractorNodeType['model'], query: ['node-1', 'query'], @@ -177,7 +176,7 @@ describe('parameter-extractor/panel', () => { readOnly: false, handleInputVarChange, filterVar: vi.fn(() => true), - isChatMode: true, + isChatflow: true, inputs: createData(), isChatModel: true, isCompletionModel: false, @@ -236,7 +235,7 @@ describe('parameter-extractor/panel', () => { expect(handleModelChanged).toHaveBeenCalledWith({ provider: 'anthropic', modelId: 'claude-3-7-sonnet', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, }) expect(handleCompletionParamsChange).toHaveBeenCalledWith({ temperature: 0.2 }) expect(handleInputVarChange).toHaveBeenCalledWith(['node-1', 'query']) diff --git a/web/app/components/workflow/nodes/parameter-extractor/__tests__/use-single-run-form-params.spec.ts b/web/app/components/workflow/nodes/parameter-extractor/__tests__/use-single-run-form-params.spec.ts index d731a1d67d9..cd9f5963b6e 100644 --- a/web/app/components/workflow/nodes/parameter-extractor/__tests__/use-single-run-form-params.spec.ts +++ b/web/app/components/workflow/nodes/parameter-extractor/__tests__/use-single-run-form-params.spec.ts @@ -2,7 +2,7 @@ import type { ParameterExtractorNodeType } from '../types' import type { InputVar } from '@/app/components/workflow/types' import { renderHook } from '@testing-library/react' import { BlockEnum, InputVarType, VarType } from '@/app/components/workflow/types' -import { AppModeEnum, Resolution } from '@/types/app' +import { Resolution } from '@/types/app' import useConfigVision from '../../../hooks/use-config-vision' import useAvailableVarList from '../../_base/hooks/use-available-var-list' import useNodeCrud from '../../_base/hooks/use-node-crud' @@ -37,7 +37,7 @@ const createData = ( model: { provider: 'openai', name: 'gpt-4o', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, completion_params: {}, } as ParameterExtractorNodeType['model'], query: ['node-1', 'query'], diff --git a/web/app/components/workflow/nodes/parameter-extractor/components/extract-parameter/__tests__/import-from-tool.spec.tsx b/web/app/components/workflow/nodes/parameter-extractor/components/extract-parameter/__tests__/import-from-tool.spec.tsx index a65eb3e3ba2..a04b388f3c7 100644 --- a/web/app/components/workflow/nodes/parameter-extractor/components/extract-parameter/__tests__/import-from-tool.spec.tsx +++ b/web/app/components/workflow/nodes/parameter-extractor/components/extract-parameter/__tests__/import-from-tool.spec.tsx @@ -70,10 +70,11 @@ const createToolParameter = (overrides: Partial = {}): ToolParame }) const renderImportFromTool = (ui: React.ReactElement) => { - const { wrapper: ConsoleQueryWrapper } = createConsoleQueryWrapper({ + const { queryClient } = createConsoleQueryWrapper({ systemFeatures: { enable_marketplace: false }, }) - return renderWorkflowComponent({ui}, { + return renderWorkflowComponent(ui, { + queryClient, hooksStoreProps: { availableNodesMetaData: { nodes: [] }, }, diff --git a/web/app/components/workflow/nodes/question-classifier/__tests__/integration.spec.tsx b/web/app/components/workflow/nodes/question-classifier/__tests__/integration.spec.tsx index 6d4237a992c..80c2e24b48d 100644 --- a/web/app/components/workflow/nodes/question-classifier/__tests__/integration.spec.tsx +++ b/web/app/components/workflow/nodes/question-classifier/__tests__/integration.spec.tsx @@ -181,7 +181,7 @@ const createConfigResult = ( readOnly: false, inputs: createData(), handleModelChanged: vi.fn(), - isChatMode: true, + isChatflow: true, isChatModel: true, handleCompletionParamsChange: vi.fn(), handleQueryVarChange: vi.fn(), diff --git a/web/app/components/workflow/nodes/question-classifier/__tests__/panel.spec.tsx b/web/app/components/workflow/nodes/question-classifier/__tests__/panel.spec.tsx index 9d8024a9d87..da7421aebc6 100644 --- a/web/app/components/workflow/nodes/question-classifier/__tests__/panel.spec.tsx +++ b/web/app/components/workflow/nodes/question-classifier/__tests__/panel.spec.tsx @@ -120,7 +120,7 @@ describe('question-classifier/panel', () => { readOnly: false, inputs: createData(), handleModelChanged, - isChatMode: true, + isChatflow: true, isChatModel: true, handleCompletionParamsChange, handleQueryVarChange, diff --git a/web/app/components/workflow/nodes/question-classifier/__tests__/use-config.spec.ts b/web/app/components/workflow/nodes/question-classifier/__tests__/use-config.spec.ts index c27d5c336ed..aa2f7590658 100644 --- a/web/app/components/workflow/nodes/question-classifier/__tests__/use-config.spec.ts +++ b/web/app/components/workflow/nodes/question-classifier/__tests__/use-config.spec.ts @@ -1,14 +1,14 @@ import type { QuestionClassifierNodeType } from '../types' -import { act, renderHook, waitFor } from '@testing-library/react' +import type { EditorResource } from '@/app/components/workflow/editor-resource/resource' +import { act, waitFor } from '@testing-library/react' import { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks' +import { renderEditorHook as renderHook } from '@/app/components/workflow/editor-resource/__tests__/render' import useAvailableVarList from '@/app/components/workflow/nodes/_base/hooks/use-available-var-list' import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud' import { useStore } from '@/app/components/workflow/store' import { BlockEnum } from '@/app/components/workflow/types' -import { AppModeEnum } from '@/types/app' -import { FlowType } from '@/types/common' import useConfigVision from '../../../hooks/use-config-vision' -import { useIsChatMode, useNodesReadOnly, useWorkflow } from '../../../hooks/use-workflow' +import { useNodesReadOnly, useWorkflow } from '../../../hooks/use-workflow' import useConfig from '../use-config' vi.mock('../../../hooks/use-workflow', async (importOriginal) => { @@ -17,7 +17,7 @@ vi.mock('../../../hooks/use-workflow', async (importOriginal) => { return { ...actual, useNodesReadOnly: vi.fn(), - useIsChatMode: vi.fn(), + useWorkflow: vi.fn(), } }) @@ -40,14 +40,16 @@ vi.mock('@/app/components/workflow/store', () => ({ })) const mockFlowType = vi.hoisted(() => ({ - value: undefined as FlowType | undefined, + value: undefined as EditorResource['kind'] | undefined, })) vi.mock('@/app/components/workflow/hooks-store/store', () => ({ - useHooksStore: (selector: (state: { configsMap?: { flowType?: FlowType } }) => unknown) => + useHooksStore: ( + selector: (state: { configsMap?: { resourceKind?: EditorResource['kind'] } }) => unknown, + ) => selector({ configsMap: { - flowType: mockFlowType.value, + resourceKind: mockFlowType.value, }, }), })) @@ -63,7 +65,7 @@ vi.mock('@/app/components/workflow/nodes/_base/hooks/use-available-var-list', () })) const mockUseNodesReadOnly = vi.mocked(useNodesReadOnly) -const mockUseIsChatMode = vi.mocked(useIsChatMode) +const mockUseIsChatMode = vi.hoisted(() => vi.fn()) const mockUseWorkflow = vi.mocked(useWorkflow) const mockUseNodeCrud = vi.mocked(useNodeCrud) const mockUseModelListAndDefaultModelAndCurrentProviderAndModel = vi.mocked( @@ -82,7 +84,7 @@ const createPayload = ( model: { provider: '', name: '', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, completion_params: {}, }, classes: [{ id: 'topic-1', name: 'Billing questions', label: 'CLASS 1' }], @@ -141,13 +143,15 @@ describe('question-classifier/use-config', () => { }) it('preserves the selected model when the vision follow-up updates after model selection', async () => { - const { result } = renderHook(() => useConfig('question-classifier-node', createPayload())) + const { result } = renderHook(() => useConfig('question-classifier-node', createPayload()), { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + }) act(() => { result.current.handleModelChanged({ provider: 'openai', modelId: 'gpt-4o', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, }) }) @@ -157,7 +161,7 @@ describe('question-classifier/use-config', () => { model: expect.objectContaining({ provider: 'openai', name: 'gpt-4o', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, }), vision: { enabled: false, @@ -168,7 +172,6 @@ describe('question-classifier/use-config', () => { }) it('does not default the query selector to sys.query in snippet flows', async () => { - mockFlowType.value = FlowType.snippet mockUseWorkflow.mockReturnValue({ getBeforeNodesInSameBranch: vi.fn(() => [ { @@ -198,13 +201,15 @@ describe('question-classifier/use-config', () => { setInputs, }) - renderHook(() => - useConfig( - 'question-classifier-node', - createPayload({ - query_variable_selector: [], - }), - ), + renderHook( + () => + useConfig( + 'question-classifier-node', + createPayload({ + query_variable_selector: [], + }), + ), + { resource: { kind: 'snippet', snippetId: 'snippet-1' } }, ) await waitFor(() => { @@ -212,3 +217,10 @@ describe('question-classifier/use-config', () => { }) }) }) + +vi.mock('@/app/components/workflow/editor-resource/state', async (importOriginal) => { + const actual = + await importOriginal() + const { atom } = await import('jotai') + return { ...actual, isChatflowAtom: atom(() => mockUseIsChatMode()) } +}) diff --git a/web/app/components/workflow/nodes/start/__tests__/panel.spec.tsx b/web/app/components/workflow/nodes/start/__tests__/panel.spec.tsx index a65474e6ea9..811b1363178 100644 --- a/web/app/components/workflow/nodes/start/__tests__/panel.spec.tsx +++ b/web/app/components/workflow/nodes/start/__tests__/panel.spec.tsx @@ -69,7 +69,7 @@ describe('StartPanel', () => { handleAddVariable.mockReturnValue(true) mockUseConfig.mockReturnValue({ readOnly: false, - isChatMode: true, + isChatflow: true, inputs: createData(), isShowAddVarModal: false, showAddVarModal, @@ -99,7 +99,7 @@ describe('StartPanel', () => { it('should render the add modal and hide it after a successful confirm', () => { mockUseConfig.mockReturnValue({ readOnly: false, - isChatMode: false, + isChatflow: false, inputs: createData(), isShowAddVarModal: true, showAddVarModal, @@ -131,7 +131,7 @@ describe('StartPanel', () => { handleAddVariable.mockReturnValue(false) mockUseConfig.mockReturnValue({ readOnly: false, - isChatMode: false, + isChatflow: false, inputs: createData({ variables: [ { diff --git a/web/app/components/workflow/nodes/start/__tests__/use-config.spec.ts b/web/app/components/workflow/nodes/start/__tests__/use-config.spec.ts index 4854693d88a..6a3e921f1b4 100644 --- a/web/app/components/workflow/nodes/start/__tests__/use-config.spec.ts +++ b/web/app/components/workflow/nodes/start/__tests__/use-config.spec.ts @@ -24,7 +24,6 @@ vi.mock('../../../hooks/use-workflow', async (importOriginal) => { ...actual, useNodesReadOnly: () => mockUseNodesReadOnly(), useWorkflow: () => mockUseWorkflow(), - useIsChatMode: () => mockUseIsChatMode(), } }) @@ -151,7 +150,7 @@ describe('start/use-config', () => { ) expect(mockRenameInspectVarName).toHaveBeenCalledWith('start-node', 'query', 'prompt') expect(result.current.readOnly).toBe(false) - expect(result.current.isChatMode).toBe(false) + expect(result.current.isChatflow).toBe(false) }) it('should block removal when the variable is still in use and confirm the deletion later', () => { @@ -271,3 +270,10 @@ describe('start/use-config', () => { ) }) }) + +vi.mock('@/app/components/workflow/editor-resource/state', async (importOriginal) => { + const actual = + await importOriginal() + const { atom } = await import('jotai') + return { ...actual, isChatflowAtom: atom(() => mockUseIsChatMode()) } +}) diff --git a/web/app/components/workflow/nodes/start/__tests__/use-single-run-form-params.spec.ts b/web/app/components/workflow/nodes/start/__tests__/use-single-run-form-params.spec.ts index b3f3d783c56..3ad4db8f256 100644 --- a/web/app/components/workflow/nodes/start/__tests__/use-single-run-form-params.spec.ts +++ b/web/app/components/workflow/nodes/start/__tests__/use-single-run-form-params.spec.ts @@ -1,5 +1,5 @@ import type { StartNodeType } from '../types' -import { renderHook } from '@testing-library/react' +import { renderEditorHook as renderHook } from '@/app/components/workflow/editor-resource/__tests__/render' import { BlockEnum, InputVarType } from '@/app/components/workflow/types' import { withSelectorKey } from '@/test/i18n-mock' import useSingleRunFormParams from '../use-single-run-form-params' @@ -16,7 +16,6 @@ vi.mock('../../../hooks/use-workflow', async (importOriginal) => { return { ...actual, - useIsChatMode: () => mockUseIsChatMode(), } }) @@ -48,16 +47,18 @@ describe('start/use-single-run-form-params', () => { it('should include sys.query and sys.files dependencies for chat mode', () => { mockUseIsChatMode.mockReturnValue(true) - const { result } = renderHook(() => - useSingleRunFormParams({ - id: 'start-node', - payload: createPayload(), - runInputData: { query: 'hello' }, - runInputDataRef: { current: {} }, - getInputVars: () => [], - setRunInputData, - toVarInputs: () => [], - }), + const { result } = renderHook( + () => + useSingleRunFormParams({ + id: 'start-node', + payload: createPayload(), + runInputData: { query: 'hello' }, + runInputDataRef: { current: {} }, + getInputVars: () => [], + setRunInputData, + toVarInputs: () => [], + }), + { resource: { kind: 'app', appId: 'app-1', mode: 'workflow' } }, ) expect(result.current.forms).toHaveLength(1) @@ -84,16 +85,18 @@ describe('start/use-single-run-form-params', () => { it('should omit sys.query when the workflow is not in chat mode', () => { mockUseIsChatMode.mockReturnValue(false) - const { result } = renderHook(() => - useSingleRunFormParams({ - id: 'start-node', - payload: createPayload(), - runInputData: {}, - runInputDataRef: { current: {} }, - getInputVars: () => [], - setRunInputData, - toVarInputs: () => [], - }), + const { result } = renderHook( + () => + useSingleRunFormParams({ + id: 'start-node', + payload: createPayload(), + runInputData: {}, + runInputDataRef: { current: {} }, + getInputVars: () => [], + setRunInputData, + toVarInputs: () => [], + }), + { resource: { kind: 'app', appId: 'app-1', mode: 'workflow' } }, ) expect(result.current.forms[0]!.inputs).toEqual( @@ -105,3 +108,10 @@ describe('start/use-single-run-form-params', () => { ]) }) }) + +vi.mock('@/app/components/workflow/editor-resource/state', async (importOriginal) => { + const actual = + await importOriginal() + const { atom } = await import('jotai') + return { ...actual, isChatflowAtom: atom(() => mockUseIsChatMode()) } +}) diff --git a/web/app/components/workflow/nodes/tool/__tests__/panel.spec.tsx b/web/app/components/workflow/nodes/tool/__tests__/panel.spec.tsx index 6ea8227d6c7..f385892c379 100644 --- a/web/app/components/workflow/nodes/tool/__tests__/panel.spec.tsx +++ b/web/app/components/workflow/nodes/tool/__tests__/panel.spec.tsx @@ -2,9 +2,10 @@ import type { ReactNode } from 'react' import type { ToolNodeType } from '../types' import type { CredentialFormSchema } from '@/app/components/header/account-setting/model-provider-page/declarations' import type { NodePanelProps } from '@/app/components/workflow/types' -import { fireEvent, render, screen } from '@testing-library/react' +import { fireEvent, screen } from '@testing-library/react' import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' import { CollectionType } from '@/app/components/tools/types' +import { renderEditor as render } from '@/app/components/workflow/editor-resource/__tests__/render' import { BlockEnum } from '@/app/components/workflow/types' import Panel from '../panel' @@ -146,7 +147,9 @@ const renderPanel = (data: ToolNodeType = createNodeData()) => { }, } - return render() + return render(, { + resource: { kind: 'knowledge-pipeline', pipelineId: 'pipeline-1' }, + }) } const createConfigResult = (overrides: Record = {}) => ({ diff --git a/web/app/components/workflow/nodes/trigger-webhook/__tests__/use-config.spec.tsx b/web/app/components/workflow/nodes/trigger-webhook/__tests__/use-config.spec.tsx index 0cbe43f7829..888ab428b05 100644 --- a/web/app/components/workflow/nodes/trigger-webhook/__tests__/use-config.spec.tsx +++ b/web/app/components/workflow/nodes/trigger-webhook/__tests__/use-config.spec.tsx @@ -1,7 +1,7 @@ import type { WebhookTriggerNodeType } from '../types' +import { renderEditorHook as renderHookWithConsoleQuery } from '@/app/components/workflow/editor-resource/__tests__/render' import { BlockEnum, VarType } from '@/app/components/workflow/types' import { toast } from '@/app/notifications' -import { renderHookWithConsoleQuery } from '@/test/console/query-data' import { createNodeCrudModuleMock } from '../../__tests__/use-config-test-utils' import { useConfig } from '../use-config' @@ -43,14 +43,8 @@ vi.mock('@/app/components/workflow/nodes/_base/hooks/use-node-crud', () => ({ ...createNodeCrudModuleMock(mockSetInputs), })) -const routeParams = vi.hoisted(() => ({ appId: 'app-1' as string | undefined })) const mockRequest = vi.hoisted(() => vi.fn()) -vi.mock('@/next/navigation', async (importOriginal) => ({ - ...(await importOriginal()), - useParams: () => routeParams, -})) - vi.mock('@/service/console/browser', () => ({ consoleBrowserLink: { call: mockRequest }, })) @@ -77,7 +71,6 @@ const createPayload = ( describe('useConfig', () => { beforeEach(() => { vi.clearAllMocks() - routeParams.appId = 'app-1' mockRequest.mockReset() mockUseNodesReadOnly.mockReturnValue({ nodesReadOnly: false }) mockIsVarUsedInNodes.mockReturnValue(false) @@ -240,11 +233,15 @@ describe('useConfig', () => { expect(mockRequest).toHaveBeenCalledTimes(2) }) - it('should expose readonly state and skip url generation without app id', async () => { + it('should expose readonly state and skip url generation in a snippet', async () => { mockUseNodesReadOnly.mockReturnValue({ nodesReadOnly: true }) - routeParams.appId = undefined - const { result } = renderHookWithConsoleQuery(() => useConfig('webhook-node', createPayload())) + const { result } = renderHookWithConsoleQuery( + () => useConfig('webhook-node', createPayload()), + { + resource: { kind: 'snippet', snippetId: 'snippet-1' }, + }, + ) expect(result.current.readOnly).toBe(true) diff --git a/web/app/components/workflow/nodes/variable-assigner/__tests__/hooks.spec.ts b/web/app/components/workflow/nodes/variable-assigner/__tests__/hooks.spec.ts index 2a2cd77df0f..9bb2ed5f1f8 100644 --- a/web/app/components/workflow/nodes/variable-assigner/__tests__/hooks.spec.ts +++ b/web/app/components/workflow/nodes/variable-assigner/__tests__/hooks.spec.ts @@ -1,5 +1,6 @@ -import { act, renderHook } from '@testing-library/react' -import { FlowType } from '@/types/common' +import type { EditorResource } from '@/app/components/workflow/editor-resource/resource' +import { act } from '@testing-library/react' +import { renderEditorHook as renderHook } from '@/app/components/workflow/editor-resource/__tests__/render' import { VarType } from '../../../types' import { useGetAvailableVars, useVariableAssigner } from '../hooks' @@ -11,7 +12,7 @@ const mockUseWorkflowVariables = vi.hoisted(() => vi.fn()) const mockUseIsChatMode = vi.hoisted(() => vi.fn()) const mockUseWorkflowStore = vi.hoisted(() => vi.fn()) const mockFlowType = vi.hoisted(() => ({ - value: undefined as FlowType | undefined, + value: undefined as EditorResource['kind'] | undefined, })) vi.mock('reactflow', () => ({ @@ -34,7 +35,6 @@ vi.mock('../../../hooks/use-workflow', async (importOriginal) => { return { ...actual, useWorkflow: () => mockUseWorkflow(), - useIsChatMode: () => mockUseIsChatMode(), } }) @@ -52,10 +52,12 @@ vi.mock('@/app/components/workflow/store', () => ({ })) vi.mock('@/app/components/workflow/hooks-store/store', () => ({ - useHooksStore: (selector: (state: { configsMap?: { flowType?: FlowType } }) => unknown) => + useHooksStore: ( + selector: (state: { configsMap?: { resourceKind?: EditorResource['kind'] } }) => unknown, + ) => selector({ configsMap: { - flowType: mockFlowType.value, + resourceKind: mockFlowType.value, }, }), })) @@ -115,7 +117,9 @@ describe('variable-assigner/hooks', () => { }) it('should append target variables, ignore duplicates, and update grouped variables', () => { - const { result } = renderHook(() => useVariableAssigner()) + const { result } = renderHook(() => useVariableAssigner(), { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + }) act(() => { result.current.handleAssignVariableValueChange('assigner-1', ['start', 'bar'], { @@ -186,7 +190,9 @@ describe('variable-assigner/hooks', () => { }, ]) - const { result } = renderHook(() => useVariableAssigner()) + const { result } = renderHook(() => useVariableAssigner(), { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + }) act(() => { result.current.handleAddVariableInAddVariablePopupWithPosition( @@ -232,7 +238,9 @@ describe('variable-assigner/hooks', () => { }) it('should update the hovered group state on enter and leave', () => { - const { result } = renderHook(() => useVariableAssigner()) + const { result } = renderHook(() => useVariableAssigner(), { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + }) act(() => { result.current.handleGroupItemMouseEnter('group-1') @@ -286,7 +294,9 @@ describe('variable-assigner/hooks', () => { getNodeAvailableVars, }) - const { result } = renderHook(() => useGetAvailableVars()) + const { result } = renderHook(() => useGetAvailableVars(), { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + }) expect(result.current('current-node', 'target', () => true, true)).toEqual([ { @@ -304,7 +314,6 @@ describe('variable-assigner/hooks', () => { }) it('should filter system variables when the current flow is a snippet', () => { - mockFlowType.value = FlowType.snippet mockUseNodes.mockReturnValue([ { id: 'current-node', @@ -328,7 +337,9 @@ describe('variable-assigner/hooks', () => { getNodeAvailableVars, }) - const { result } = renderHook(() => useGetAvailableVars()) + const { result } = renderHook(() => useGetAvailableVars(), { + resource: { kind: 'snippet', snippetId: 'snippet-1' }, + }) expect(result.current('current-node', 'target', () => true, false)).toEqual([ { @@ -338,3 +349,10 @@ describe('variable-assigner/hooks', () => { ]) }) }) + +vi.mock('@/app/components/workflow/editor-resource/state', async (importOriginal) => { + const actual = + await importOriginal() + const { atom } = await import('jotai') + return { ...actual, isChatflowAtom: atom(() => mockUseIsChatMode()) } +}) diff --git a/web/app/components/workflow/operator/__tests__/add-block.spec.tsx b/web/app/components/workflow/operator/__tests__/add-block.spec.tsx index 6a08533e79e..c2b71d70919 100644 --- a/web/app/components/workflow/operator/__tests__/add-block.spec.tsx +++ b/web/app/components/workflow/operator/__tests__/add-block.spec.tsx @@ -1,7 +1,7 @@ import type { NodeDefault } from '../../types' +import type { EditorResource } from '@/app/components/workflow/editor-resource/resource' import { screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' -import { FlowType } from '@/types/common' import { createNode } from '../../__tests__/fixtures' import { renderWorkflowFlowComponent } from '../../__tests__/workflow-test-env' import { BlockClassification } from '../../block-selector/types' @@ -29,7 +29,7 @@ const { let mockNodesReadOnly = false let mockIsChatMode = false -let mockFlowType: FlowType = FlowType.appFlow +let mockFlowType: EditorResource['kind'] = 'app' const answerBlock: NodeDefault = { metaData: { @@ -55,7 +55,6 @@ vi.mock('../../hooks/use-available-blocks', () => ({ })) vi.mock('../../hooks/use-workflow', () => ({ - useIsChatMode: () => mockIsChatMode, useNodesReadOnly: () => ({ nodesReadOnly: mockNodesReadOnly }), })) @@ -74,7 +73,7 @@ vi.mock('../../hooks/use-panel-interactions', () => ({ vi.mock('../../hooks-store', () => ({ useHooksStore: (selector: (state: Record) => unknown) => selector({ - configsMap: { flowType: mockFlowType }, + configsMap: { resourceKind: mockFlowType }, availableNodesMetaData: { nodes: [answerBlock] }, }), })) @@ -125,7 +124,7 @@ describe('AddBlock', () => { vi.clearAllMocks() mockNodesReadOnly = false mockIsChatMode = false - mockFlowType = FlowType.appFlow + mockFlowType = 'app' }) it('opens the real selector and exposes its open state', async () => { @@ -198,3 +197,10 @@ describe('AddBlock', () => { }) }) }) + +vi.mock('@/app/components/workflow/editor-resource/state', async (importOriginal) => { + const actual = + await importOriginal() + const { atom } = await import('jotai') + return { ...actual, isChatflowAtom: atom(() => mockIsChatMode) } +}) diff --git a/web/app/components/workflow/operator/__tests__/export-image.spec.tsx b/web/app/components/workflow/operator/__tests__/export-image.spec.tsx index 24d6dc1d806..7560361282d 100644 --- a/web/app/components/workflow/operator/__tests__/export-image.spec.tsx +++ b/web/app/components/workflow/operator/__tests__/export-image.spec.tsx @@ -1,6 +1,9 @@ -import { fireEvent, render, screen, waitFor, within } from '@testing-library/react' +import { fireEvent, screen, waitFor, within } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { act } from 'react' +import { renderEditor as render } from '@/app/components/workflow/editor-resource/__tests__/render' +import { createConsoleQueryClient, seedAppDetail } from '@/test/console/query-data' +import { createAppDetailFixture } from '@/test/fixtures/app' import { ExportImage } from '../export-image' const mockToPng = vi.fn() @@ -61,6 +64,15 @@ vi.mock('@/app/components/base/image-uploader/image-preview', () => ({ ), })) +function createExportQueryClient() { + const queryClient = createConsoleQueryClient() + seedAppDetail( + queryClient, + createAppDetailFixture({ id: 'app-1', mode: 'workflow', name: mockWorkflowState.appName }), + ) + return queryClient +} + describe('ExportImage', () => { beforeEach(() => { vi.clearAllMocks() @@ -81,7 +93,10 @@ describe('ExportImage', () => { it('opens the menu and exports the current view as png', async () => { const user = userEvent.setup() - render() + render(, { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + queryClient: createExportQueryClient(), + }) await user.click(screen.getByRole('button', { name: 'workflow.common.exportImage' })) const menu = screen.getByRole('menu', { name: 'workflow.common.exportImage' }) @@ -114,7 +129,10 @@ describe('ExportImage', () => { const user = userEvent.setup() mockGetNodesReadOnly.mockReturnValue(true) - render() + render(, { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + queryClient: createExportQueryClient(), + }) const trigger = screen.getByRole('button', { name: 'workflow.common.exportImage' }) expect(trigger).toHaveAttribute('aria-disabled', 'true') @@ -129,7 +147,10 @@ describe('ExportImage', () => { it('shows a preview when exporting the whole workflow', async () => { vi.useFakeTimers() - render() + render(, { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + queryClient: createExportQueryClient(), + }) fireEvent.click(screen.getByRole('button', { name: 'workflow.common.exportImage' })) fireEvent.click(screen.getAllByText('workflow.common.exportPNG')[1]!) @@ -154,7 +175,10 @@ describe('ExportImage', () => { ])('exports the current view with %s', async (label, exporter, fileName) => { const user = userEvent.setup() - render() + render(, { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + queryClient: createExportQueryClient(), + }) await user.click(screen.getByRole('button', { name: 'workflow.common.exportImage' })) await user.click(screen.getAllByText(label)[0]!) @@ -171,7 +195,10 @@ describe('ExportImage', () => { it('exports the whole workflow as svg', async () => { vi.useFakeTimers() - render() + render(, { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + queryClient: createExportQueryClient(), + }) fireEvent.click(screen.getByRole('button', { name: 'workflow.common.exportImage' })) fireEvent.click(screen.getAllByText('workflow.common.exportSVG')[1]!) @@ -192,7 +219,10 @@ describe('ExportImage', () => { mockWorkflowState.appName = '' mockWorkflowState.knowledgeName = '' - render() + render(, { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + queryClient: createExportQueryClient(), + }) await user.click(screen.getByRole('button', { name: 'workflow.common.exportImage' })) await user.click(screen.getAllByText('workflow.common.exportPNG')[0]!) @@ -205,7 +235,10 @@ describe('ExportImage', () => { const user = userEvent.setup() document.querySelector('.react-flow__viewport')?.remove() - render() + render(, { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + queryClient: createExportQueryClient(), + }) await user.click(screen.getByRole('button', { name: 'workflow.common.exportImage' })) await user.click(screen.getAllByText('workflow.common.exportPNG')[0]!) @@ -217,7 +250,10 @@ describe('ExportImage', () => { it('returns early when the workflow becomes read only before exporting', async () => { const user = userEvent.setup() - render() + render(, { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + queryClient: createExportQueryClient(), + }) await user.click(screen.getByRole('button', { name: 'workflow.common.exportImage' })) mockGetNodesReadOnly.mockReturnValue(true) @@ -232,7 +268,10 @@ describe('ExportImage', () => { const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) mockToJpeg.mockRejectedValueOnce(new Error('boom')) - render() + render(, { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + queryClient: createExportQueryClient(), + }) await user.click(screen.getByRole('button', { name: 'workflow.common.exportImage' })) await user.click(screen.getAllByText('workflow.common.exportJPEG')[0]!) diff --git a/web/app/components/workflow/panel/__tests__/inputs-panel.spec.tsx b/web/app/components/workflow/panel/__tests__/inputs-panel.spec.tsx index d9f340afe1d..41145508890 100644 --- a/web/app/components/workflow/panel/__tests__/inputs-panel.spec.tsx +++ b/web/app/components/workflow/panel/__tests__/inputs-panel.spec.tsx @@ -4,7 +4,6 @@ import type { FileUpload } from '@/app/components/base/features/types' import { screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { TransferMethod } from '@/types/app' -import { FlowType } from '@/types/common' import { createStartNode } from '../../__tests__/fixtures' import { renderWorkflowFlowComponent } from '../../__tests__/workflow-test-env' import { InputVarType, WorkflowRunningStatus } from '../../types' @@ -56,11 +55,7 @@ const createHooksStoreProps = ( overrides: Partial = {}, ): Partial => ({ handleRun: vi.fn(), - configsMap: { - flowId: 'flow-1', - flowType: FlowType.appFlow, - fileSettings: fileSettingsWithImage, - }, + fileSettings: fileSettingsWithImage, ...overrides, }) @@ -236,12 +231,8 @@ describe('InputsPanel', () => { }, hooksStoreProps: createHooksStoreProps({ handleRun, - configsMap: { - flowId: 'flow-1', - flowType: FlowType.appFlow, - fileSettings: { - enabled: false, - }, + fileSettings: { + enabled: false, }, }), }, @@ -268,12 +259,8 @@ describe('InputsPanel', () => { files: [uploadingRunFile], }, hooksStoreProps: createHooksStoreProps({ - configsMap: { - flowId: 'flow-1', - flowType: FlowType.appFlow, - fileSettings: { - enabled: false, - }, + fileSettings: { + enabled: false, }, }), }) diff --git a/web/app/components/workflow/panel/chat-record/__tests__/index.spec.tsx b/web/app/components/workflow/panel/chat-record/__tests__/index.spec.tsx index 3ddb5175827..0d2192915d1 100644 --- a/web/app/components/workflow/panel/chat-record/__tests__/index.spec.tsx +++ b/web/app/components/workflow/panel/chat-record/__tests__/index.spec.tsx @@ -2,16 +2,11 @@ import type { HistoryWorkflowData } from '@/app/components/workflow/types' import { act, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { renderWorkflowComponent } from '@/app/components/workflow/__tests__/workflow-test-env' +import { EditorResourceScope } from '@/app/components/workflow/editor-resource/provider' import ChatRecord from '../index' -const routeParams = vi.hoisted(() => ({ appId: 'app-1' })) const mockRequest = vi.hoisted(() => vi.fn()) -vi.mock('@/next/navigation', async (importOriginal) => ({ - ...(await importOriginal()), - useParams: () => routeParams, -})) - vi.mock('@/service/console/browser', () => ({ consoleBrowserLink: { call: mockRequest }, })) @@ -25,7 +20,6 @@ const historyWorkflowData: HistoryWorkflowData = { describe('ChatRecord', () => { beforeEach(() => { - routeParams.appId = 'app-1' mockRequest.mockReset() }) @@ -116,17 +110,22 @@ describe('ChatRecord', () => { has_more: false, limit: 20, }) - const { store } = renderWorkflowComponent(, { + const view = (appId: string) => ( + + + + ) + const { store, rerender } = renderWorkflowComponent(view('app-1'), { initialStoreState: { historyWorkflowData }, }) await waitFor(() => expect(mockRequest).toHaveBeenCalledTimes(1)) - routeParams.appId = 'app-2' act(() => { store.setState({ historyWorkflowData: { ...historyWorkflowData, conversation_id: 'conversation-2' }, }) }) + rerender(view('app-2')) expect(await screen.findByText('Current answer')).toBeInTheDocument() expect(mockRequest).toHaveBeenLastCalledWith( ['apps', 'byAppId', 'chatMessages', 'get'], diff --git a/web/app/components/workflow/panel/chat-variable-panel/__tests__/index.spec.tsx b/web/app/components/workflow/panel/chat-variable-panel/__tests__/index.spec.tsx index 51979764356..d3081ebeb95 100644 --- a/web/app/components/workflow/panel/chat-variable-panel/__tests__/index.spec.tsx +++ b/web/app/components/workflow/panel/chat-variable-panel/__tests__/index.spec.tsx @@ -1,6 +1,7 @@ import type { ConversationVariable, Node } from '@/app/components/workflow/types' -import { render, screen, waitFor } from '@testing-library/react' +import { screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' +import { renderEditor as render } from '@/app/components/workflow/editor-resource/__tests__/render' import ChatVariablePanel from '../index' import { ChatVarType } from '../type' @@ -197,7 +198,7 @@ describe('ChatVariablePanel', () => { it('should toggle the tips area and close the panel', async () => { const user = userEvent.setup() - render() + render(, { resource: { kind: 'app', appId: 'app-1', mode: 'workflow' } }) expect(screen.getByText('workflow.chatVariable.panelDescription')).toBeInTheDocument() @@ -222,7 +223,7 @@ describe('ChatVariablePanel', () => { it('should prepend newly added variables and sync the workflow draft', async () => { const user = userEvent.setup() - render() + render(, { resource: { kind: 'app', appId: 'app-1', mode: 'workflow' } }) await user.click(screen.getByRole('button', { name: 'save-add' })) @@ -250,7 +251,7 @@ describe('ChatVariablePanel', () => { mockFindUsedVarNodes.mockReturnValue([effectedNode]) mockUpdateNodeVars.mockReturnValue(updatedNode) - render() + render(, { resource: { kind: 'app', appId: 'app-1', mode: 'workflow' } }) await user.click(screen.getByRole('button', { name: 'edit-conversation_var' })) await user.click(screen.getByRole('button', { name: 'save-edit' })) @@ -275,7 +276,7 @@ describe('ChatVariablePanel', () => { mockFindUsedVarNodes.mockReturnValue([effectedNode]) mockUpdateNodeVars.mockReturnValue(prunedNode) - render() + render(, { resource: { kind: 'app', appId: 'app-1', mode: 'workflow' } }) await user.click(screen.getByRole('button', { name: 'delete-conversation_var' })) expect(screen.getByTestId('remove-effect-var-confirm')).toBeInTheDocument() diff --git a/web/app/components/workflow/panel/debug-and-preview/__tests__/chat-wrapper.spec.tsx b/web/app/components/workflow/panel/debug-and-preview/__tests__/chat-wrapper.spec.tsx index 7e769acb35d..85a0c94fe49 100644 --- a/web/app/components/workflow/panel/debug-and-preview/__tests__/chat-wrapper.spec.tsx +++ b/web/app/components/workflow/panel/debug-and-preview/__tests__/chat-wrapper.spec.tsx @@ -13,14 +13,8 @@ import ChatWrapper from '../chat-wrapper' const mockUseChat = vi.hoisted(() => vi.fn()) const mockUseSubscription = vi.hoisted(() => vi.fn()) -const routeParams = vi.hoisted(() => ({ appId: 'app-1' })) const mockRequest = vi.hoisted(() => vi.fn()) -vi.mock('@/next/navigation', async (importOriginal) => ({ - ...(await importOriginal()), - useParams: () => routeParams, -})) - vi.mock('@/service/console/browser', () => ({ consoleBrowserLink: { call: mockRequest }, })) @@ -164,7 +158,6 @@ const createChatWrapperRef = () => describe('ChatWrapper', () => { beforeEach(() => { vi.clearAllMocks() - routeParams.appId = 'app-1' mockRequest.mockReset() mockRequest.mockResolvedValue({ data: [] }) mockUseChat.mockReturnValue(createChatState()) diff --git a/web/app/components/workflow/panel/debug-and-preview/__tests__/components.spec.tsx b/web/app/components/workflow/panel/debug-and-preview/__tests__/components.spec.tsx index c7ad62eb3d2..2d307d2a43f 100644 --- a/web/app/components/workflow/panel/debug-and-preview/__tests__/components.spec.tsx +++ b/web/app/components/workflow/panel/debug-and-preview/__tests__/components.spec.tsx @@ -247,8 +247,8 @@ describe('debug-and-preview components', () => { renderWorkflowComponent( , { + resource: { kind: 'app' as const, appId: 'app-1', mode: 'workflow' as const }, initialStoreState: { - appId: 'app-1', conversationVariables: [createConversationVariable()], }, }, @@ -428,8 +428,8 @@ describe('debug-and-preview components', () => { }), ], edges: [], + resource: { kind: 'app' as const, appId: 'app-1', mode: 'workflow' as const }, initialStoreState: { - appId: 'app-1', conversationVariables: [createConversationVariable()], }, }, diff --git a/web/app/components/workflow/panel/debug-and-preview/__tests__/conversation-variable-modal.spec.tsx b/web/app/components/workflow/panel/debug-and-preview/__tests__/conversation-variable-modal.spec.tsx index 96cd5dca1d9..3f1313bc74e 100644 --- a/web/app/components/workflow/panel/debug-and-preview/__tests__/conversation-variable-modal.spec.tsx +++ b/web/app/components/workflow/panel/debug-and-preview/__tests__/conversation-variable-modal.spec.tsx @@ -96,8 +96,8 @@ describe('ConversationVariableModal', () => { renderWorkflowComponent( , { + resource: { kind: 'app' as const, appId: 'app-1', mode: 'workflow' as const }, initialStoreState: { - appId: 'app-1', conversationVariables: [ createConversationVariable(), createConversationVariable({ @@ -139,8 +139,8 @@ describe('ConversationVariableModal', () => { renderWorkflowComponent( , { + resource: { kind: 'app' as const, appId: 'app-1', mode: 'workflow' as const }, initialStoreState: { - appId: 'app-1', conversationVariables: [createConversationVariable()], }, }, diff --git a/web/app/components/workflow/panel/debug-and-preview/__tests__/hooks.spec.ts b/web/app/components/workflow/panel/debug-and-preview/__tests__/hooks.spec.ts index dad3dd59f05..9ce2dbd0098 100644 --- a/web/app/components/workflow/panel/debug-and-preview/__tests__/hooks.spec.ts +++ b/web/app/components/workflow/panel/debug-and-preview/__tests__/hooks.spec.ts @@ -1,4 +1,5 @@ -import { act, renderHook } from '@testing-library/react' +import { act } from '@testing-library/react' +import { renderEditorHook as renderHook } from '@/app/components/workflow/editor-resource/__tests__/render' import { sseGet } from '@/service/base' import { submitHumanInputForm } from '@/service/workflow' import { useChat } from '../hooks' @@ -21,7 +22,7 @@ vi.mock('@/service/base', () => ({ })) vi.mock('@/service/use-workflow', () => ({ - useInvalidAllLastRun: () => mockInvalidAllLastRun, + useInvalidateAllLastRun: () => mockInvalidAllLastRun, })) vi.mock('@/service/workflow', () => ({ @@ -56,7 +57,7 @@ vi.mock('../../../hooks/use-set-workflow-vars-with-value', () => ({ vi.mock('../../../hooks-store', () => ({ useHooksStore: () => ({ configsMap: { - flowType: 'appFlow', + resourceKind: 'appFlow', flowId: 'flow-1', }, }), @@ -99,7 +100,9 @@ describe('useChat', () => { sendCallbacks = callbacks }) - const { result } = renderHook(() => useChat({})) + const { result } = renderHook(() => useChat({}), { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + }) act(() => { result.current.handleSend({ query: 'test' }, {}) @@ -133,7 +136,9 @@ describe('useChat', () => { }) it('submits human input forms through the workflow service', async () => { - const { result } = renderHook(() => useChat({})) + const { result } = renderHook(() => useChat({}), { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + }) await act(async () => { await result.current.handleSubmitHumanInputForm('token-123', { @@ -153,7 +158,9 @@ describe('useChat', () => { const node = { id: 'node-1', type: 'custom', data: { title: 'Human Input' } } mockGetNodes.mockReturnValue([node, { id: 'node-2', type: 'custom', data: { title: 'Other' } }]) - const { result } = renderHook(() => useChat({})) + const { result } = renderHook(() => useChat({}), { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + }) expect(result.current.getHumanInputNodeData('node-1')).toEqual(node) expect(result.current.getHumanInputNodeData('missing')).toBeUndefined() diff --git a/web/app/components/workflow/panel/debug-and-preview/__tests__/hooks/handle-resume.spec.ts b/web/app/components/workflow/panel/debug-and-preview/__tests__/hooks/handle-resume.spec.ts index 7974b447d6a..48925586011 100644 --- a/web/app/components/workflow/panel/debug-and-preview/__tests__/hooks/handle-resume.spec.ts +++ b/web/app/components/workflow/panel/debug-and-preview/__tests__/hooks/handle-resume.spec.ts @@ -1,5 +1,6 @@ import type { ChatItemInTree } from '@/app/components/base/chat/types' -import { act, renderHook } from '@testing-library/react' +import { act } from '@testing-library/react' +import { renderEditorHook as renderHook } from '@/app/components/workflow/editor-resource/__tests__/render' import { useChat } from '../../hooks' const mockHandleRun = vi.fn() @@ -19,7 +20,7 @@ vi.mock('@/service/base', () => ({ })) vi.mock('@/service/use-workflow', () => ({ - useInvalidAllLastRun: () => mockInvalidAllLastRun, + useInvalidateAllLastRun: () => mockInvalidAllLastRun, })) vi.mock('@/service/workflow', () => ({ @@ -90,7 +91,9 @@ describe('useChat – handleResume', () => { capturedResumeOptions = options }) - const hook = renderHook(() => useChat({})) + const hook = renderHook(() => useChat({}), { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + }) act(() => { hook.result.current.handleSend({ query: 'test' }, {}) @@ -117,7 +120,9 @@ describe('useChat – handleResume', () => { } it('should call sseGet with the correct URL', () => { - const { result } = renderHook(() => useChat({})) + const { result } = renderHook(() => useChat({}), { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + }) act(() => { result.current.handleResume('msg-1', 'wfr-1', {}) @@ -136,7 +141,9 @@ describe('useChat – handleResume', () => { options.getAbortController(mockAbortCtrl) }) - const { result } = renderHook(() => useChat({})) + const { result } = renderHook(() => useChat({}), { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + }) act(() => { result.current.handleResume('msg-1', 'wfr-1', {}) @@ -161,7 +168,9 @@ describe('useChat – handleResume', () => { sendCallbacks = callbacks }) - const { result } = renderHook(() => useChat({})) + const { result } = renderHook(() => useChat({}), { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + }) act(() => { result.current.handleSend({ query: 'test' }, {}) @@ -203,7 +212,9 @@ describe('useChat – handleResume', () => { capturedResumeOptions = options }) - const hook = renderHook(() => useChat({})) + const hook = renderHook(() => useChat({}), { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + }) act(() => { hook.result.current.handleSend({ query: 'test' }, {}) @@ -380,8 +391,9 @@ describe('useChat – handleResume', () => { capturedResumeOptions = options }) - const hook = renderHook(() => - useChat({ suggested_questions_after_answer: { enabled: true } }), + const hook = renderHook( + () => useChat({ suggested_questions_after_answer: { enabled: true } }), + { resource: { kind: 'app', appId: 'app-1', mode: 'workflow' } }, ) act(() => { @@ -430,8 +442,9 @@ describe('useChat – handleResume', () => { capturedResumeOptions = options }) - const hook = renderHook(() => - useChat({ suggested_questions_after_answer: { enabled: true } }), + const hook = renderHook( + () => useChat({ suggested_questions_after_answer: { enabled: true } }), + { resource: { kind: 'app', appId: 'app-1', mode: 'workflow' } }, ) act(() => { @@ -474,8 +487,9 @@ describe('useChat – handleResume', () => { capturedResumeOptions = options }) - const hook = renderHook(() => - useChat({ suggested_questions_after_answer: { enabled: true } }), + const hook = renderHook( + () => useChat({ suggested_questions_after_answer: { enabled: true } }), + { resource: { kind: 'app', appId: 'app-1', mode: 'workflow' } }, ) act(() => { @@ -941,7 +955,9 @@ describe('useChat – handleResume with bare prevChatTree (no humanInputFormData }, ] - const hook = renderHook(() => useChat({}, undefined, prevChatTree)) + const hook = renderHook(() => useChat({}, undefined, prevChatTree), { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + }) act(() => { hook.result.current.handleResume('bare-msg', 'wfr-bare', {}) @@ -972,7 +988,9 @@ describe('useChat – handleResume with bare prevChatTree (no humanInputFormData }, ] - const hook = renderHook(() => useChat({}, undefined, prevChatTree)) + const hook = renderHook(() => useChat({}, undefined, prevChatTree), { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + }) act(() => { hook.result.current.handleResume('bare-msg-nt', 'wfr-bare-nt', {}) @@ -1102,7 +1120,9 @@ describe('useChat – handleResume with bare prevChatTree (no humanInputFormData }, ] - const hook = renderHook(() => useChat({}, undefined, prevChatTreeNoWP)) + const hook = renderHook(() => useChat({}, undefined, prevChatTreeNoWP), { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + }) let opts: any mockSseGet.mockImplementation((_url: any, _opts: any, options: any) => { opts = options diff --git a/web/app/components/workflow/panel/debug-and-preview/__tests__/hooks/handle-send.spec.ts b/web/app/components/workflow/panel/debug-and-preview/__tests__/hooks/handle-send.spec.ts index 1fed2813aa6..23d075f5647 100644 --- a/web/app/components/workflow/panel/debug-and-preview/__tests__/hooks/handle-send.spec.ts +++ b/web/app/components/workflow/panel/debug-and-preview/__tests__/hooks/handle-send.spec.ts @@ -1,4 +1,5 @@ -import { act, renderHook } from '@testing-library/react' +import { act } from '@testing-library/react' +import { renderEditorHook as renderHook } from '@/app/components/workflow/editor-resource/__tests__/render' import { useChat } from '../../hooks' const mockHandleRun = vi.fn() @@ -21,7 +22,7 @@ vi.mock('@/service/base', () => ({ })) vi.mock('@/service/use-workflow', () => ({ - useInvalidAllLastRun: () => mockInvalidAllLastRun, + useInvalidateAllLastRun: () => mockInvalidAllLastRun, })) vi.mock('@/service/workflow', () => ({ @@ -90,7 +91,9 @@ describe('useChat – handleSend', () => { }) it('should call handleRun with processed params', () => { - const { result } = renderHook(() => useChat({})) + const { result } = renderHook(() => useChat({}), { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + }) act(() => { result.current.handleSend({ query: 'hello', inputs: {} }, {}) @@ -103,7 +106,9 @@ describe('useChat – handleSend', () => { it('should return false and skip handleRun when test/run permission is missing', () => { mockHooksStoreState.canRun = false - const { result } = renderHook(() => useChat({})) + const { result } = renderHook(() => useChat({}), { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + }) let returned: unknown act(() => { @@ -118,7 +123,9 @@ describe('useChat – handleSend', () => { it('should show notification and return false when already responding', () => { mockHandleRun.mockImplementation(() => {}) - const { result } = renderHook(() => useChat({})) + const { result } = renderHook(() => useChat({}), { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + }) act(() => { result.current.handleSend({ query: 'first' }, {}) @@ -133,7 +140,9 @@ describe('useChat – handleSend', () => { }) it('should set isResponding to true after sending', () => { - const { result } = renderHook(() => useChat({})) + const { result } = renderHook(() => useChat({}), { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + }) act(() => { result.current.handleSend({ query: 'hello' }, {}) }) @@ -141,7 +150,9 @@ describe('useChat – handleSend', () => { }) it('should add placeholder question and answer to chatList', () => { - const { result } = renderHook(() => useChat({})) + const { result } = renderHook(() => useChat({}), { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + }) act(() => { result.current.handleSend({ query: 'test question' }, {}) @@ -158,7 +169,9 @@ describe('useChat – handleSend', () => { }) it('should strip url from local_file transfer method files', () => { - const { result } = renderHook(() => useChat({})) + const { result } = renderHook(() => useChat({}), { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + }) act(() => { result.current.handleSend( @@ -208,7 +221,9 @@ describe('useChat – handleSend', () => { callbacks.onCompleted(false) }) - const { result } = renderHook(() => useChat({})) + const { result } = renderHook(() => useChat({}), { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + }) act(() => { result.current.handleSend({ query: 'first' }, {}) diff --git a/web/app/components/workflow/panel/debug-and-preview/__tests__/hooks/handle-stop-restart.spec.ts b/web/app/components/workflow/panel/debug-and-preview/__tests__/hooks/handle-stop-restart.spec.ts index e5d5c58e010..70cbc2383aa 100644 --- a/web/app/components/workflow/panel/debug-and-preview/__tests__/hooks/handle-stop-restart.spec.ts +++ b/web/app/components/workflow/panel/debug-and-preview/__tests__/hooks/handle-stop-restart.spec.ts @@ -1,4 +1,5 @@ -import { act, renderHook } from '@testing-library/react' +import { act } from '@testing-library/react' +import { renderEditorHook as renderHook } from '@/app/components/workflow/editor-resource/__tests__/render' import { useChat } from '../../hooks' const mockHandleRun = vi.fn() @@ -19,7 +20,7 @@ vi.mock('@/service/base', () => ({ })) vi.mock('@/service/use-workflow', () => ({ - useInvalidAllLastRun: () => mockInvalidAllLastRun, + useInvalidateAllLastRun: () => mockInvalidAllLastRun, })) vi.mock('@/service/workflow', () => ({ @@ -78,7 +79,9 @@ describe('useChat – handleStop', () => { }) it('should set isResponding to false', () => { - const { result } = renderHook(() => useChat({})) + const { result } = renderHook(() => useChat({}), { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + }) act(() => { result.current.handleStop() }) @@ -86,7 +89,9 @@ describe('useChat – handleStop', () => { }) it('should not call stopChat when taskId is empty even if stopChat is provided', () => { - const { result } = renderHook(() => useChat({}, undefined, undefined, mockStopChat)) + const { result } = renderHook(() => useChat({}, undefined, undefined, mockStopChat), { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + }) act(() => { result.current.handleStop() }) @@ -94,7 +99,9 @@ describe('useChat – handleStop', () => { }) it('should reset iter/loop times to defaults', () => { - const { result } = renderHook(() => useChat({})) + const { result } = renderHook(() => useChat({}), { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + }) act(() => { result.current.handleStop() }) @@ -108,7 +115,9 @@ describe('useChat – handleStop', () => { callbacks.getAbortController({ abort: mockWfAbort } as any) }) - const { result } = renderHook(() => useChat({})) + const { result } = renderHook(() => useChat({}), { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + }) act(() => { result.current.handleSend({ query: 'test' }, {}) @@ -136,8 +145,9 @@ describe('useChat – handleStop', () => { return Promise.resolve({ data: ['s'] }) }) - const { result } = renderHook(() => - useChat({ suggested_questions_after_answer: { enabled: true } }), + const { result } = renderHook( + () => useChat({ suggested_questions_after_answer: { enabled: true } }), + { resource: { kind: 'app', appId: 'app-1', mode: 'workflow' } }, ) act(() => { @@ -169,7 +179,9 @@ describe('useChat – handleStop', () => { }) }) - const { result } = renderHook(() => useChat({}, undefined, undefined, mockStopChat)) + const { result } = renderHook(() => useChat({}, undefined, undefined, mockStopChat), { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + }) act(() => { result.current.handleSend({ query: 'test' }, {}) @@ -190,7 +202,9 @@ describe('useChat – handleRestart', () => { it('should clear suggestedQuestions and set isResponding to false', () => { const config = { opening_statement: 'Hello' } - const { result } = renderHook(() => useChat(config)) + const { result } = renderHook(() => useChat(config), { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + }) act(() => { result.current.handleRestart() @@ -201,7 +215,9 @@ describe('useChat – handleRestart', () => { }) it('should reset iter/loop times to defaults', () => { - const { result } = renderHook(() => useChat({})) + const { result } = renderHook(() => useChat({}), { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + }) act(() => { result.current.handleRestart() }) diff --git a/web/app/components/workflow/panel/debug-and-preview/__tests__/hooks/misc.spec.ts b/web/app/components/workflow/panel/debug-and-preview/__tests__/hooks/misc.spec.ts index b35764f3b43..e71f43c5afd 100644 --- a/web/app/components/workflow/panel/debug-and-preview/__tests__/hooks/misc.spec.ts +++ b/web/app/components/workflow/panel/debug-and-preview/__tests__/hooks/misc.spec.ts @@ -1,5 +1,6 @@ import type { ChatItemInTree } from '@/app/components/base/chat/types' -import { act, renderHook } from '@testing-library/react' +import { act } from '@testing-library/react' +import { renderEditorHook as renderHook } from '@/app/components/workflow/editor-resource/__tests__/render' import { useChat } from '../../hooks' const mockHandleRun = vi.fn() @@ -19,7 +20,7 @@ vi.mock('@/service/base', () => ({ })) vi.mock('@/service/use-workflow', () => ({ - useInvalidAllLastRun: () => mockInvalidAllLastRun, + useInvalidateAllLastRun: () => mockInvalidAllLastRun, })) vi.mock('@/service/workflow', () => ({ @@ -86,7 +87,9 @@ describe('useChat – handleSwitchSibling', () => { }) mockSseGet.mockImplementation(() => {}) - const { result } = renderHook(() => useChat({})) + const { result } = renderHook(() => useChat({}), { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + }) act(() => { result.current.handleSend({ query: 'test' }, {}) @@ -124,7 +127,9 @@ describe('useChat – handleSwitchSibling', () => { sendCallbacks = callbacks }) - const { result } = renderHook(() => useChat({})) + const { result } = renderHook(() => useChat({}), { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + }) act(() => { result.current.handleSend({ query: 'test' }, {}) @@ -151,7 +156,9 @@ describe('useChat – handleSwitchSibling', () => { }) it('should return undefined from findMessageInTree when not found', () => { - const { result } = renderHook(() => useChat({})) + const { result } = renderHook(() => useChat({}), { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + }) act(() => { result.current.handleSwitchSibling('nonexistent-id', {}) @@ -167,7 +174,9 @@ describe('useChat – handleSwitchSibling', () => { }) mockSseGet.mockImplementation(() => {}) - const { result } = renderHook(() => useChat({})) + const { result } = renderHook(() => useChat({}), { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + }) act(() => { result.current.handleSend({ query: 'parent' }, {}) @@ -230,7 +239,9 @@ describe('useChat – handleSubmitHumanInputForm', () => { }) it('should call submitHumanInputForm with token and data', async () => { - const { result } = renderHook(() => useChat({})) + const { result } = renderHook(() => useChat({}), { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + }) await act(async () => { await result.current.handleSubmitHumanInputForm('token-123', { @@ -259,7 +270,9 @@ describe('useChat – getHumanInputNodeData', () => { { id: 'node-2', type: 'custom', data: { title: 'Other' } }, ]) - const { result } = renderHook(() => useChat({})) + const { result } = renderHook(() => useChat({}), { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + }) const node = result.current.getHumanInputNodeData('node-1') expect(node).toEqual(mockNode) }) @@ -267,7 +280,9 @@ describe('useChat – getHumanInputNodeData', () => { it('should return undefined when no matching node', () => { mockGetNodes.mockReturnValue([{ id: 'node-2', type: 'custom', data: {} }]) - const { result } = renderHook(() => useChat({})) + const { result } = renderHook(() => useChat({}), { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + }) const node = result.current.getHumanInputNodeData('nonexistent') expect(node).toBeUndefined() }) @@ -278,7 +293,9 @@ describe('useChat – getHumanInputNodeData', () => { { id: 'node-1', type: 'custom', data: { found: true } }, ]) - const { result } = renderHook(() => useChat({})) + const { result } = renderHook(() => useChat({}), { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + }) const node = result.current.getHumanInputNodeData('node-1') expect(node).toEqual({ id: 'node-1', type: 'custom', data: { found: true } }) }) @@ -290,7 +307,9 @@ describe('useChat – conversationId and setTargetMessageId', () => { }) it('should initially be an empty string', () => { - const { result } = renderHook(() => useChat({})) + const { result } = renderHook(() => useChat({}), { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + }) expect(result.current.conversationId).toBe('') }) @@ -328,7 +347,9 @@ describe('useChat – conversationId and setTargetMessageId', () => { }, ] - const { result } = renderHook(() => useChat({}, undefined, prevChatTree)) + const { result } = renderHook(() => useChat({}, undefined, prevChatTree), { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + }) const defaultList = result.current.chatList expect(defaultList.some((item) => item.id === 'a1')).toBe(true) @@ -363,7 +384,9 @@ describe('useChat – updateCurrentQAOnTree with parent_message_id', () => { }) it('should handle follow-up message with parent_message_id', async () => { - const { result } = renderHook(() => useChat({})) + const { result } = renderHook(() => useChat({}), { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + }) act(() => { result.current.handleSend({ query: 'first' }, {}) diff --git a/web/app/components/workflow/panel/debug-and-preview/__tests__/hooks/opening-statement.spec.ts b/web/app/components/workflow/panel/debug-and-preview/__tests__/hooks/opening-statement.spec.ts index 84289e0b540..d5e786f029d 100644 --- a/web/app/components/workflow/panel/debug-and-preview/__tests__/hooks/opening-statement.spec.ts +++ b/web/app/components/workflow/panel/debug-and-preview/__tests__/hooks/opening-statement.spec.ts @@ -1,5 +1,5 @@ import type { ChatItemInTree } from '@/app/components/base/chat/types' -import { renderHook } from '@testing-library/react' +import { renderEditorHook as renderHook } from '@/app/components/workflow/editor-resource/__tests__/render' import { useChat } from '../../hooks' const mockHandleRun = vi.fn() @@ -19,7 +19,7 @@ vi.mock('@/service/base', () => ({ })) vi.mock('@/service/use-workflow', () => ({ - useInvalidAllLastRun: () => mockInvalidAllLastRun, + useInvalidateAllLastRun: () => mockInvalidAllLastRun, })) vi.mock('@/service/workflow', () => ({ @@ -78,18 +78,24 @@ describe('workflow debug useChat – opening statement stability', () => { }) it('should return empty chatList when config has no opening_statement', () => { - const { result } = renderHook(() => useChat({})) + const { result } = renderHook(() => useChat({}), { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + }) expect(result.current.chatList).toEqual([]) }) it('should return empty chatList when opening_statement is an empty string', () => { - const { result } = renderHook(() => useChat({ opening_statement: '' })) + const { result } = renderHook(() => useChat({ opening_statement: '' }), { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + }) expect(result.current.chatList).toEqual([]) }) it('should use stable id "opening-statement" instead of Date.now()', () => { const config = { opening_statement: 'Welcome!' } - const { result } = renderHook(() => useChat(config)) + const { result } = renderHook(() => useChat(config), { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + }) expect(result.current.chatList[0]!.id).toBe('opening-statement') }) @@ -101,6 +107,7 @@ describe('workflow debug useChat – opening statement stability', () => { const formSettings = { inputs: { name: 'Alice' }, inputsForm: [] } const { result, rerender } = renderHook(({ fs }) => useChat(config, fs), { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, initialProps: { fs: formSettings }, }) @@ -120,6 +127,7 @@ describe('workflow debug useChat – opening statement stability', () => { } const { result, rerender } = renderHook(({ fs }) => useChat(config, fs), { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, initialProps: { fs: { inputs: { name: 'Alice' }, inputsForm: [] } }, }) @@ -150,7 +158,10 @@ describe('workflow debug useChat – opening statement stability', () => { const { result, rerender } = renderHook( ({ cfg }) => useChat(cfg, undefined, prevChatTree as ChatItemInTree[]), - { initialProps: { cfg: config } }, + { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + initialProps: { cfg: config }, + }, ) const openerBefore = result.current.chatList[0] @@ -167,14 +178,18 @@ describe('workflow debug useChat – opening statement stability', () => { opening_statement: 'Welcome!', suggested_questions: ['How are you?', 'What can you do?'], } - const { result } = renderHook(() => useChat(config)) + const { result } = renderHook(() => useChat(config), { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + }) const opener = result.current.chatList[0] expect(opener!.suggestedQuestions).toEqual(['How are you?', 'What can you do?']) }) it('should not include suggestedQuestions when config has none', () => { const config = { opening_statement: 'Welcome!' } - const { result } = renderHook(() => useChat(config)) + const { result } = renderHook(() => useChat(config), { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + }) const opener = result.current.chatList[0] expect(opener!.suggestedQuestions).toBeUndefined() }) diff --git a/web/app/components/workflow/panel/debug-and-preview/__tests__/hooks/sse-callbacks.spec.ts b/web/app/components/workflow/panel/debug-and-preview/__tests__/hooks/sse-callbacks.spec.ts index 470915484cc..4753a39e3e9 100644 --- a/web/app/components/workflow/panel/debug-and-preview/__tests__/hooks/sse-callbacks.spec.ts +++ b/web/app/components/workflow/panel/debug-and-preview/__tests__/hooks/sse-callbacks.spec.ts @@ -1,4 +1,5 @@ -import { act, renderHook } from '@testing-library/react' +import { act } from '@testing-library/react' +import { renderEditorHook as renderHook } from '@/app/components/workflow/editor-resource/__tests__/render' import { useChat } from '../../hooks' const mockHandleRun = vi.fn() @@ -18,7 +19,7 @@ vi.mock('@/service/base', () => ({ })) vi.mock('@/service/use-workflow', () => ({ - useInvalidAllLastRun: () => mockInvalidAllLastRun, + useInvalidateAllLastRun: () => mockInvalidAllLastRun, })) vi.mock('@/service/workflow', () => ({ @@ -83,7 +84,9 @@ describe('useChat – handleSend SSE callbacks', () => { }) function setupAndSend(config: any = {}) { - const hook = renderHook(() => useChat(config)) + const hook = renderHook(() => useChat(config), { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, + }) act(() => { hook.result.current.handleSend( { query: 'test' }, @@ -339,8 +342,9 @@ describe('useChat – handleSend SSE callbacks', () => { getAbortCtrl(new AbortController()) return Promise.resolve({ data: ['suggestion1'] }) }) - const hook = renderHook(() => - useChat({ suggested_questions_after_answer: { enabled: true } }), + const hook = renderHook( + () => useChat({ suggested_questions_after_answer: { enabled: true } }), + { resource: { kind: 'app', appId: 'app-1', mode: 'workflow' } }, ) mockHandleRun.mockImplementation((_params: any, callbacks: any) => { @@ -365,8 +369,9 @@ describe('useChat – handleSend SSE callbacks', () => { it('should set suggestedQuestions to empty array when fetch fails', async () => { const mockGetSuggested = vi.fn().mockRejectedValue(new Error('fail')) - const hook = renderHook(() => - useChat({ suggested_questions_after_answer: { enabled: true } }), + const hook = renderHook( + () => useChat({ suggested_questions_after_answer: { enabled: true } }), + { resource: { kind: 'app', appId: 'app-1', mode: 'workflow' } }, ) mockHandleRun.mockImplementation((_params: any, callbacks: any) => { diff --git a/web/app/components/workflow/panel/env-panel/__tests__/index.spec.tsx b/web/app/components/workflow/panel/env-panel/__tests__/index.spec.tsx index d184ab2e7d1..365c241610d 100644 --- a/web/app/components/workflow/panel/env-panel/__tests__/index.spec.tsx +++ b/web/app/components/workflow/panel/env-panel/__tests__/index.spec.tsx @@ -1,12 +1,14 @@ import type { ReactElement } from 'react' import type { ModelParameterRule } from '@/app/components/header/account-setting/model-provider-page/declarations' +import type { EditorResource } from '@/app/components/workflow/editor-resource/resource' import type { SyncDraftCallback } from '@/app/components/workflow/hooks-store' import type { Shape } from '@/app/components/workflow/store/workflow' import type { EnvironmentVariable } from '@/app/components/workflow/types' -import { act, render, screen, waitFor } from '@testing-library/react' +import { act, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { collaborationManager } from '@/app/components/workflow/collaboration/core/collaboration-manager' import { WorkflowContext } from '@/app/components/workflow/context' +import { renderEditor as render } from '@/app/components/workflow/editor-resource/__tests__/render' import { createWorkflowStore } from '@/app/components/workflow/store/workflow' import EnvPanel from '../index' @@ -120,13 +122,16 @@ vi.mock('@/app/components/workflow/nodes/_base/components/variable/utils', () => updateNodeVars: mockUpdateNodeVars, })) +vi.mock('../requests', () => ({ + fetchEnvironmentVariables: async () => (await mockFetchWorkflowDraft()).environment_variables, +})) + vi.mock('@/service/workflow', () => ({ updateEnvironmentVariables: (payload: { appId: string environmentVariables: EnvironmentVariable[] deletedEnvironmentVariableIds: string[] }) => mockUpdateEnvironmentVariables(payload), - fetchWorkflowDraft: () => mockFetchWorkflowDraft(), })) vi.mock('@/app/components/workflow/collaboration/core/websocket-manager', () => ({ @@ -221,13 +226,19 @@ const createEnv = (overrides: Partial = {}): EnvironmentVar ...overrides, }) -const renderWithProviders = (ui: ReactElement, storeState: Partial = {}) => { +const renderWithProviders = ( + ui: ReactElement, + storeState: Partial = {}, + resource: EditorResource = { kind: 'app', appId: 'app-1', mode: 'workflow' }, +) => { const store = createWorkflowStore({}) store.setState(storeState) return { store, - ...render({ui}), + ...render({ui}, { + resource, + }), } } @@ -256,7 +267,6 @@ describe('EnvPanel container', () => { it('should add variables and normalize secret values after syncing', async () => { const user = userEvent.setup() const { store } = renderWithProviders(, { - appId: 'app-1', environmentVariables: [], envSecrets: {}, }) @@ -280,11 +290,109 @@ describe('EnvPanel container', () => { expect(mockDoSyncWorkflowDraft).not.toHaveBeenCalled() }) + it('persists new pipeline variables through draft sync without app requests or socket events', async () => { + const user = userEvent.setup() + const existingEnv = createEnv({ + name: 'existing_variable', + value_type: 'string', + value: 'existing', + }) + const emit = vi.fn() + mockGetSocket.mockReturnValue({ emit }) + const { store } = renderWithProviders( + , + { + environmentVariables: [existingEnv], + envSecrets: {}, + }, + { kind: 'knowledge-pipeline', pipelineId: 'pipeline-1' }, + ) + let completeSync!: () => void + let variablesSentToDraft: EnvironmentVariable[] = [] + mockDoSyncWorkflowDraft.mockImplementationOnce((_notRefresh, callback) => { + variablesSentToDraft = store.getState().environmentVariables + return new Promise((resolve) => { + completeSync = () => { + callback?.onSuccess?.() + resolve() + } + }) + }) + + await user.click(screen.getByRole('button', { name: 'Save variable' })) + expect(await screen.findByText('created_name')).toBeInTheDocument() + expect(variablesSentToDraft).toEqual([ + expect.objectContaining({ id: 'env-created', name: 'created_name', value: 'created-value' }), + existingEnv, + ]) + await act(async () => completeSync()) + + expect(screen.getByText('created_name')).toBeInTheDocument() + expect(screen.getByText('existing_variable')).toBeInTheDocument() + expect(mockUpdateEnvironmentVariables).not.toHaveBeenCalled() + expect(mockGetSocket).not.toHaveBeenCalled() + expect(emit).not.toHaveBeenCalled() + }) + + it('restores the persisted pipeline variable after draft sync fails and preserves other variables', async () => { + const user = userEvent.setup() + const existingEnv = createEnv({ + name: 'existing_variable', + value_type: 'string', + value: 'existing', + }) + const untouchedEnv = createEnv({ + id: 'env-untouched', + name: 'untouched_variable', + value_type: 'string', + value: 'untouched', + }) + const emit = vi.fn() + mockGetSocket.mockReturnValue({ emit }) + mockVariableTriggerState.savePayload = { ...existingEnv, name: 'rejected_name' } + mockFetchWorkflowDraft.mockResolvedValue({ environment_variables: [existingEnv, untouchedEnv] }) + let failSync!: () => void + mockDoSyncWorkflowDraft.mockImplementation(async (_notRefresh, callback) => { + callback?.onError?.() + }) + mockDoSyncWorkflowDraft.mockImplementationOnce( + (_notRefresh, callback) => + new Promise((resolve) => { + failSync = () => { + callback?.onError?.() + resolve() + } + }), + ) + renderWithProviders( + , + { + environmentVariables: [existingEnv, untouchedEnv], + envSecrets: {}, + }, + { kind: 'knowledge-pipeline', pipelineId: 'pipeline-1' }, + ) + + await user.click(screen.getByRole('button', { name: 'Edit existing_variable' })) + await user.click(screen.getByRole('button', { name: 'Save variable' })) + expect(await screen.findByRole('button', { name: 'Edit rejected_name' })).toBeInTheDocument() + await act(async () => failSync()) + + expect( + await screen.findByRole('button', { name: 'Edit existing_variable' }), + ).toBeInTheDocument() + expect(screen.queryByRole('button', { name: 'Edit rejected_name' })).not.toBeInTheDocument() + expect(screen.getByText('untouched_variable')).toBeInTheDocument() + expect(mockFetchWorkflowDraft).toHaveBeenCalled() + expect(mockUpdateEnvironmentVariables).not.toHaveBeenCalled() + expect(mockGetSocket).not.toHaveBeenCalled() + expect(emit).not.toHaveBeenCalled() + }) + it('should delete unused variables and sync draft changes', async () => { const user = userEvent.setup() const env = createEnv({ value_type: 'string', value: 'plain-text' }) const { store } = renderWithProviders(, { - appId: 'app-1', environmentVariables: [env], envSecrets: {}, }) @@ -497,7 +605,6 @@ describe('EnvPanel container', () => { }) const { store } = renderWithProviders(, { - appId: 'app-1', environmentVariables: [env], envSecrets: {}, }) @@ -630,7 +737,6 @@ describe('EnvPanel container', () => { mockVariableTriggerState.savePayload = nextEnv const { store } = renderWithProviders(, { - appId: 'app-1', environmentVariables: [env], envSecrets: {}, }) @@ -684,7 +790,6 @@ describe('EnvPanel container', () => { mockGetNodes.mockReturnValue([llmNode]) const { store } = renderWithProviders(, { - appId: 'app-1', environmentVariables: [env], envSecrets: {}, }) diff --git a/web/app/components/workflow/panel/global-variable-panel/__tests__/index.spec.tsx b/web/app/components/workflow/panel/global-variable-panel/__tests__/index.spec.tsx index be6e1f92bb4..6635ff0ed6f 100644 --- a/web/app/components/workflow/panel/global-variable-panel/__tests__/index.spec.tsx +++ b/web/app/components/workflow/panel/global-variable-panel/__tests__/index.spec.tsx @@ -18,9 +18,7 @@ vi.mock('../../../constants', () => ({ isInWorkflowPage: () => mockIsWorkflowPage, })) -vi.mock('../../../hooks/use-workflow', () => ({ - useIsChatMode: () => mockIsChatMode, -})) +vi.mock('../../../hooks/use-workflow', () => ({})) describe('global-variable-panel path', () => { beforeEach(() => { @@ -43,3 +41,10 @@ describe('global-variable-panel path', () => { ).toBeInTheDocument() }) }) + +vi.mock('@/app/components/workflow/editor-resource/state', async (importOriginal) => { + const actual = + await importOriginal() + const { atom } = await import('jotai') + return { ...actual, isChatflowAtom: atom(() => mockIsChatMode) } +}) diff --git a/web/app/components/workflow/panel/version-history-panel/__tests__/index.spec.tsx b/web/app/components/workflow/panel/version-history-panel/__tests__/index.spec.tsx index 9666067fa35..195c28b35f4 100644 --- a/web/app/components/workflow/panel/version-history-panel/__tests__/index.spec.tsx +++ b/web/app/components/workflow/panel/version-history-panel/__tests__/index.spec.tsx @@ -6,9 +6,8 @@ import { fireEvent, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { NuqsTestingAdapter } from 'nuqs/adapters/testing' import { useEffect, useRef } from 'react' +import { renderEditor as renderWithConsoleState } from '@/app/components/workflow/editor-resource/__tests__/render' import { createConsoleQueryWrapper } from '@/test/console/query-data' -import { render as renderWithConsoleState } from '@/test/console/render' -import { AppModeEnum } from '@/types/app' import { VersionHistoryContextMenuOptions, WorkflowVersion } from '../../../types' const mockHandleRestoreFromPublishedWorkflow = vi.fn() @@ -86,7 +85,7 @@ vi.mock('@/app/notifications', () => ({ toast: mockToast })) vi.mock('@/service/use-workflow', () => ({ useDeleteWorkflow: () => ({ mutateAsync: vi.fn() }), useInvalidateAppWorkflow: () => mockInvalidateAppWorkflow, - useInvalidAllLastRun: () => vi.fn(), + useInvalidateAllLastRun: () => vi.fn(), useResetWorkflowVersionHistory: () => vi.fn(), useRestoreWorkflow: () => ({ mutateAsync: mockRestoreWorkflow }), useUpdateWorkflow: () => ({ mutateAsync: mockUpdateWorkflow }), @@ -135,14 +134,14 @@ vi.mock('../../../hooks-store', () => ({ useHooksStore: ( selector: (state: { accessControl: { canImportExportDSL: boolean } - configsMap: { flowId: string; flowType: string } + configsMap: { flowId: string; resourceKind: string } }) => unknown, ) => selector({ accessControl: { canImportExportDSL: true }, configsMap: { flowId: 'app-1', - flowType: 'appFlow', + resourceKind: 'appFlow', }, }), })) @@ -505,7 +504,7 @@ describe('VersionHistoryPanel', () => { render( `/apps/app-1/workflows/${versionId}/restore`} updateVersionUrl={(versionId) => `/apps/app-1/workflows/${versionId}`} @@ -519,7 +518,7 @@ describe('VersionHistoryPanel', () => { expect(mockUpdateWorkflow).toHaveBeenCalledWith( { appId: 'app-1', - appMode: AppModeEnum.WORKFLOW, + appMode: 'workflow' as const, releaseNotes: 'Updated notes', title: 'Updated release', url: '/apps/app-1/workflows/published-version-id', @@ -537,6 +536,7 @@ function render(ui: ReactElement) { features: { billing: { subscription: { plan: mockPlanType } } }, }) return renderWithConsoleState(ui, { + resource: { kind: 'app', appId: 'app-1', mode: 'workflow' }, wrapper: ({ children }) => ( {children} diff --git a/web/app/components/workflow/panel/version-history-panel/__tests__/version-history-item.spec.tsx b/web/app/components/workflow/panel/version-history-panel/__tests__/version-history-item.spec.tsx index 7babfd7f96b..9bcc932db30 100644 --- a/web/app/components/workflow/panel/version-history-panel/__tests__/version-history-item.spec.tsx +++ b/web/app/components/workflow/panel/version-history-panel/__tests__/version-history-item.spec.tsx @@ -1,7 +1,7 @@ import type { VersionHistory } from '@/types/workflow' import { fireEvent, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' -import { renderWithConsoleQuery as render } from '@/test/console/query-data' +import { renderEditor as render } from '@/app/components/workflow/editor-resource/__tests__/render' import { VersionHistoryContextMenuOptions, WorkflowVersion } from '../../../types' import VersionHistoryItem from '../version-history-item' diff --git a/web/app/components/workflow/panel/version-history-panel/action-menu/__tests__/index.spec.tsx b/web/app/components/workflow/panel/version-history-panel/action-menu/__tests__/index.spec.tsx index b5b79b87366..0f3b0478b9a 100644 --- a/web/app/components/workflow/panel/version-history-panel/action-menu/__tests__/index.spec.tsx +++ b/web/app/components/workflow/panel/version-history-panel/action-menu/__tests__/index.spec.tsx @@ -20,7 +20,9 @@ const renderActionMenu = (ui: React.ReactElement, pipelineId?: string) => { seedSystemFeatures(queryClient, { deployment_edition: deploymentEdition }) return renderWorkflowComponent({ui}, { queryClient, - initialStoreState: { pipelineId }, + resource: pipelineId + ? { kind: 'knowledge-pipeline', pipelineId } + : { kind: 'app', appId: 'app-1', mode: 'workflow' }, }) } diff --git a/web/app/components/workflow/variable-inspect/__tests__/resource-requests.spec.tsx b/web/app/components/workflow/variable-inspect/__tests__/resource-requests.spec.tsx new file mode 100644 index 00000000000..21cffefcf75 --- /dev/null +++ b/web/app/components/workflow/variable-inspect/__tests__/resource-requests.spec.tsx @@ -0,0 +1,243 @@ +import type { + WorkflowDraftVariableResponse, + WorkflowDraftVariableWithoutValueResponse, +} from '@dify/contracts/api/console/apps/types.gen' +import type { EditorResource } from '../../editor-resource/resource' +import { act, renderHook, waitFor } from '@testing-library/react' +import { consoleQuery } from '@/service/console' +import { + useConversationVarValues, + useDeleteAllInspectorVars, + useDeleteInspectVar, + useDeleteNodeInspectorVars, + useEditInspectorVar, + useInvalidateAllLastRun, + useInvalidateConversationVarValues, + useInvalidateLastRun, + useInvalidateSysVarValues, + useResetInspectorVar, + useSysVarValues, +} from '@/service/use-workflow' +import { fetchAllInspectVars, fetchNodeInspectVars } from '@/service/workflow' +import { createQueryClientWrapper } from '@/test/console/query-client' +import { createTestQueryClient } from '@/test/query-client' + +const request = vi.hoisted(() => + vi.fn< + (url: string, init: RequestInit | undefined, options: { request: Request }) => Promise + >(), +) +vi.mock('@/service/base', () => ({ request })) + +const summary: WorkflowDraftVariableWithoutValueResponse = { + id: 'variable-1', + type: 'node', + name: 'answer', + description: '', + selector: ['node-1', 'answer'], + value_type: 'string', + edited: false, + visible: true, + is_truncated: false, +} +const variable: WorkflowDraftVariableResponse = { ...summary, value: 'result', full_content: null } +const hosts: { resource: EditorResource; prefix: string }[] = [ + { resource: { kind: 'app', appId: 'workflow-1', mode: 'workflow' }, prefix: '/apps/workflow-1' }, + { + resource: { kind: 'app', appId: 'chatflow-1', mode: 'advanced-chat' }, + prefix: '/apps/chatflow-1', + }, + { + resource: { kind: 'knowledge-pipeline', pipelineId: 'pipeline-1' }, + prefix: '/rag/pipelines/pipeline-1', + }, + { resource: { kind: 'snippet', snippetId: 'snippet-1' }, prefix: '/snippets/snippet-1' }, +] + +beforeEach(() => { + request.mockReset() +}) + +describe.each(hosts)('inspector requests for $prefix', ({ resource, prefix }) => { + it('loads every summary page, then loads values for the selected node through the same resource', async () => { + request.mockImplementation(async (url) => { + const path = new URL(url) + if (path.pathname.endsWith('/nodes/node-1/variables')) + return Response.json({ items: [variable] }) + return Response.json({ + total: 101, + items: + path.searchParams.get('page') === '1' + ? Array.from({ length: 100 }, (_, index) => ({ ...summary, id: `variable-${index}` })) + : [{ ...summary, id: 'variable-last' }], + }) + }) + const summaries = await fetchAllInspectVars(resource) + expect(summaries).toHaveLength(101) + expect(summaries[0]).not.toHaveProperty('value') + expect(await fetchNodeInspectVars(resource, 'node-1')).toEqual([variable]) + expect(request.mock.calls.map(([url]) => new URL(url).pathname)).toEqual([ + `/console/api${prefix}/workflows/draft/variables`, + `/console/api${prefix}/workflows/draft/variables`, + `/console/api${prefix}/workflows/draft/nodes/node-1/variables`, + ]) + }) + + it('routes edits, reset and deletions without treating another resource ID as an app ID', async () => { + request.mockImplementation(async (_url, _init, { request }) => + request.method === 'DELETE' || request.method === 'PUT' + ? new Response(null, { status: 204 }) + : Response.json(variable), + ) + const client = createTestQueryClient() + const { result } = renderHook( + () => ({ + edit: useEditInspectorVar(resource), + reset: useResetInspectorVar(resource), + remove: useDeleteInspectVar(resource), + removeNode: useDeleteNodeInspectorVars(resource), + removeAll: useDeleteAllInspectorVars(resource), + }), + { wrapper: createQueryClientWrapper(client) }, + ) + await act(async () => { + await result.current.edit.mutateAsync({ varId: 'variable-1', value: 'changed' }) + await result.current.reset.mutateAsync('variable-1') + await result.current.remove.mutateAsync('variable-1') + await result.current.removeNode.mutateAsync('node-1') + await result.current.removeAll.mutateAsync() + }) + const requests = request.mock.calls.map(([, , options]) => options.request) + expect(requests.map((request) => request.method)).toEqual([ + 'PATCH', + 'PUT', + 'DELETE', + 'DELETE', + 'DELETE', + ]) + expect(requests.map((request) => new URL(request.url).pathname)).toEqual([ + `/console/api${prefix}/workflows/draft/variables/variable-1`, + `/console/api${prefix}/workflows/draft/variables/variable-1/reset`, + `/console/api${prefix}/workflows/draft/variables/variable-1`, + `/console/api${prefix}/workflows/draft/nodes/node-1/variables`, + `/console/api${prefix}/workflows/draft/variables`, + ]) + expect(await requests[0]!.json()).toEqual({ value: 'changed' }) + }) +}) + +it('invalidates only the current app variable queries and leaves other editors cached', async () => { + request.mockResolvedValue(Response.json({ items: [variable] })) + const client = createTestQueryClient() + const current: EditorResource = { kind: 'app', appId: 'app-1', mode: 'advanced-chat' } + const input = (appId: string) => ({ input: { params: { app_id: appId } } }) + const conversation = consoleQuery.apps.byAppId.workflows.draft.conversationVariables.get + const system = consoleQuery.apps.byAppId.workflows.draft.systemVariables.get + for (const appId of ['app-1', 'app-2']) { + client.setQueryData(conversation.queryKey(input(appId)), { items: [] }) + client.setQueryData(system.queryKey(input(appId)), { items: [] }) + } + const { result } = renderHook( + () => ({ + conversation: useInvalidateConversationVarValues(current), + system: useInvalidateSysVarValues(current), + }), + { wrapper: createQueryClientWrapper(client) }, + ) + await act(async () => { + await result.current.conversation() + await result.current.system() + }) + expect(client.getQueryState(conversation.queryKey(input('app-1')))?.isInvalidated).toBe(true) + expect(client.getQueryState(system.queryKey(input('app-1')))?.isInvalidated).toBe(true) + expect(client.getQueryState(conversation.queryKey(input('app-2')))?.isInvalidated).toBe(false) + expect(client.getQueryState(system.queryKey(input('app-2')))?.isInvalidated).toBe(false) + expect(request).not.toHaveBeenCalled() +}) + +it.each(hosts.filter(({ resource }) => resource.kind !== 'app'))( + 'does not request app-only variables for $prefix', + async ({ resource }) => { + const { result } = renderHook( + () => ({ + conversation: useConversationVarValues(resource), + system: useSysVarValues(resource), + }), + { wrapper: createQueryClientWrapper(createTestQueryClient()) }, + ) + await waitFor(() => expect(result.current.conversation.fetchStatus).toBe('idle')) + expect(result.current.system.fetchStatus).toBe('idle') + expect(request).not.toHaveBeenCalled() + }, +) + +const lastRunTargets = [ + { + resource: { kind: 'app', appId: 'shared-id', mode: 'workflow' }, + keyForNode: (nodeId: string) => + consoleQuery.apps.byAppId.workflows.draft.nodes.byNodeId.lastRun.get.queryKey({ + input: { params: { app_id: 'shared-id', node_id: nodeId } }, + }), + }, + { + resource: { kind: 'knowledge-pipeline', pipelineId: 'shared-id' }, + keyForNode: (nodeId: string) => + consoleQuery.rag.pipelines.byPipelineId.workflows.draft.nodes.byNodeId.lastRun.get.queryKey({ + input: { params: { pipeline_id: 'shared-id', node_id: nodeId } }, + }), + }, + { + resource: { kind: 'snippet', snippetId: 'shared-id' }, + keyForNode: (nodeId: string) => + consoleQuery.snippets.bySnippetId.workflows.draft.nodes.byNodeId.lastRun.get.queryKey({ + input: { params: { snippet_id: 'shared-id', node_id: nodeId } }, + }), + }, + { + resource: { kind: 'app', appId: 'other-app', mode: 'advanced-chat' }, + keyForNode: (nodeId: string) => + consoleQuery.apps.byAppId.workflows.draft.nodes.byNodeId.lastRun.get.queryKey({ + input: { params: { app_id: 'other-app', node_id: nodeId } }, + }), + }, +] satisfies { resource: EditorResource; keyForNode: (nodeId: string) => readonly unknown[] }[] + +it.each(lastRunTargets)( + 'isolates node and whole-resource last-run invalidation for $resource', + async (target) => { + const client = createTestQueryClient() + const cachedRuns = lastRunTargets.flatMap((entry) => + ['node-1', 'node-2'].map((nodeId) => ({ + target: entry, + nodeId, + queryKey: entry.keyForNode(nodeId), + })), + ) + for (const { queryKey, nodeId } of cachedRuns) + client.setQueryData(queryKey, { id: `run-${nodeId}`, node_id: nodeId, status: 'succeeded' }) + + const { result } = renderHook( + () => ({ + invalidateNode: useInvalidateLastRun(target.resource, 'node-1'), + invalidateResource: useInvalidateAllLastRun(target.resource), + }), + { wrapper: createQueryClientWrapper(client) }, + ) + + await act(async () => { + await result.current.invalidateNode() + }) + for (const entry of cachedRuns) + expect(client.getQueryState(entry.queryKey)?.isInvalidated).toBe( + entry.target === target && entry.nodeId === 'node-1', + ) + + await act(async () => { + await result.current.invalidateResource() + }) + for (const entry of cachedRuns) + expect(client.getQueryState(entry.queryKey)?.isInvalidated).toBe(entry.target === target) + + expect(request).not.toHaveBeenCalled() + }, +) diff --git a/web/app/components/workflow/variable-inspect/__tests__/value-content.helpers.spec.ts b/web/app/components/workflow/variable-inspect/__tests__/value-content.helpers.spec.ts index a2bf049c554..b6beb6df553 100644 --- a/web/app/components/workflow/variable-inspect/__tests__/value-content.helpers.spec.ts +++ b/web/app/components/workflow/variable-inspect/__tests__/value-content.helpers.spec.ts @@ -71,7 +71,16 @@ describe('value-content helpers', () => { createVar({ name: 'file', value_type: VarType.file, - value: { id: 'file-1' }, + value: { + id: 'file-1', + filename: 'report.pdf', + mime_type: 'application/pdf', + type: 'document', + transfer_method: 'remote_url', + related_id: null, + remote_url: 'https://example.com/report.pdf', + size: 12, + }, }), ), ).toHaveLength(1) @@ -84,7 +93,16 @@ describe('value-content helpers', () => { type: VarInInspectType.system, name: 'files', value_type: VarType.arrayFile, - value: [{ id: 'file-2' }], + value: [ + { + id: 'file-2', + filename: 'photo.png', + type: 'image', + transfer_method: 'local_file', + related_id: 'upload-2', + size: 20, + }, + ], }), ), ).toHaveLength(1) diff --git a/web/app/components/workflow/workflow-generator/__tests__/apply.spec.ts b/web/app/components/workflow/workflow-generator/__tests__/apply.spec.ts index f73be396cd8..e319a3b6122 100644 --- a/web/app/components/workflow/workflow-generator/__tests__/apply.spec.ts +++ b/web/app/components/workflow/workflow-generator/__tests__/apply.spec.ts @@ -2,7 +2,6 @@ import type { GeneratedGraph } from '../types' import { BlockEnum } from '@/app/components/workflow/types' import { createHttpError } from '@/service/http-error' import { createAppDetailFixture } from '@/test/fixtures/app' -import { AppModeEnum } from '@/types/app' import { applyToCurrentApp, applyToNewApp, @@ -69,7 +68,7 @@ describe('applyToNewApp', () => { beforeEach(() => { vi.clearAllMocks() mockCreateApp.mockResolvedValue( - createAppDetailFixture({ id: 'new-app-1', mode: AppModeEnum.WORKFLOW }), + createAppDetailFixture({ id: 'new-app-1', mode: 'workflow' as const }), ) mockSyncWorkflowDraft.mockResolvedValue({}) }) @@ -78,11 +77,15 @@ describe('applyToNewApp', () => { // its draft and return the routing context the caller uses to navigate. it('should create the app, sync the draft and return the new app id and mode', async () => { const graph = makeGraph() - const result = await applyToNewApp({ mode: 'workflow', graph, instruction: 'Summarize a URL' }) + const result = await applyToNewApp({ + mode: 'workflow', + graph, + instruction: 'Summarize a URL', + }) expect(mockCreateApp).toHaveBeenCalledWith( expect.objectContaining({ - mode: AppModeEnum.WORKFLOW, + mode: 'workflow', icon_type: 'emoji', }), ) @@ -96,16 +99,16 @@ describe('applyToNewApp', () => { }) expect(result).toEqual({ appId: 'new-app-1', - appMode: AppModeEnum.WORKFLOW, + appMode: 'workflow' as const, permissionKeys: [], }) }) - // Mode → AppModeEnum must round-trip for chatflow; the type-level guarantee + // Mode → AppMode must round-trip for chatflow; the type-level guarantee // is verified at runtime so a regression here is caught before users hit it. - it('should map advanced-chat mode to AppModeEnum.ADVANCED_CHAT', async () => { + it('should map advanced-chat mode to Chatflow', async () => { mockCreateApp.mockResolvedValueOnce( - createAppDetailFixture({ id: 'cf-1', mode: AppModeEnum.ADVANCED_CHAT }), + createAppDetailFixture({ id: 'cf-1', mode: 'advanced-chat' as const }), ) const result = await applyToNewApp({ @@ -115,9 +118,9 @@ describe('applyToNewApp', () => { }) expect(mockCreateApp).toHaveBeenCalledWith( - expect.objectContaining({ mode: AppModeEnum.ADVANCED_CHAT }), + expect.objectContaining({ mode: 'advanced-chat' as const }), ) - expect(result.appMode).toBe(AppModeEnum.ADVANCED_CHAT) + expect(result.appMode).toBe('advanced-chat') }) it('should remove LLM memory before saving a generated Workflow draft', async () => { @@ -214,7 +217,7 @@ describe('applyToNewApp', () => { // and the original sync error is re-thrown so the caller can toast it. it('should delete the new app when syncWorkflowDraft fails', async () => { mockCreateApp.mockResolvedValueOnce( - createAppDetailFixture({ id: 'doomed', mode: AppModeEnum.WORKFLOW }), + createAppDetailFixture({ id: 'doomed', mode: 'workflow' as const }), ) const syncErr = new Error('sync exploded') mockSyncWorkflowDraft.mockRejectedValueOnce(syncErr) @@ -237,7 +240,7 @@ describe('applyToNewApp', () => { // carries the orphan app id so the toast can name it. it('should throw WorkflowApplyOrphanError when both sync and rollback fail', async () => { mockCreateApp.mockResolvedValueOnce( - createAppDetailFixture({ id: 'orphan-7', mode: AppModeEnum.WORKFLOW }), + createAppDetailFixture({ id: 'orphan-7', mode: 'workflow' as const }), ) mockSyncWorkflowDraft.mockRejectedValueOnce(new Error('sync exploded')) mockDeleteApp.mockRejectedValueOnce(new Error('delete also exploded')) @@ -262,7 +265,7 @@ describe('applyToCurrentApp', () => { it('should remove LLM memory before replacing the current Workflow draft', async () => { await applyToCurrentApp({ appId: 'app-42', - appMode: AppModeEnum.WORKFLOW, + appMode: 'workflow' as const, graph: makeGraphWithMemory(), }) @@ -270,7 +273,7 @@ describe('applyToCurrentApp', () => { expect(savedGraph.nodes[1].data).not.toHaveProperty('memory') }) - it.each([AppModeEnum.ADVANCED_CHAT, undefined])( + it.each(['advanced-chat', undefined] as const)( 'should preserve LLM memory when the current app mode is %s', async (appMode) => { const graph = makeGraphWithMemory() diff --git a/web/features/home/__tests__/template-card.spec.tsx b/web/features/home/__tests__/template-card.spec.tsx index 10e9cf6e2fb..74611d65d34 100644 --- a/web/features/home/__tests__/template-card.spec.tsx +++ b/web/features/home/__tests__/template-card.spec.tsx @@ -8,7 +8,6 @@ import userEvent from '@testing-library/user-event' import * as React from 'react' import { trackEvent } from '@/app/components/base/amplitude' import { renderWithConsoleQuery } from '@/test/console/query-data' -import { AppModeEnum } from '@/types/app' import { TemplateCard } from '../template-card' vi.mock('@/app/components/app/type-selector', () => ({ @@ -37,7 +36,7 @@ const createApp = (overrides: TemplateFixtureOverrides = {}): TemplateFixture => ...overrides, app: { id: 'id-1', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, icon_type: null, icon: '🤖', icon_background: '#fff', @@ -80,13 +79,13 @@ describe('TemplateCard', () => { }) it.each([ - [AppModeEnum.CHAT, 'APP.TYPES.CHATBOT'], - [AppModeEnum.ADVANCED_CHAT, 'APP.TYPES.ADVANCED'], - [AppModeEnum.AGENT_CHAT, 'APP.TYPES.AGENT'], - [AppModeEnum.AGENT, 'APP.TYPES.AGENT'], - [AppModeEnum.WORKFLOW, 'APP.TYPES.WORKFLOW'], - [AppModeEnum.COMPLETION, 'APP.TYPES.COMPLETION'], - ])('should render correct mode label for %s mode', (mode, label) => { + ['chat', 'APP.TYPES.CHATBOT'], + ['advanced-chat', 'APP.TYPES.ADVANCED'], + ['agent-chat', 'APP.TYPES.AGENT'], + ['agent', 'APP.TYPES.AGENT'], + ['workflow', 'APP.TYPES.WORKFLOW'], + ['completion', 'APP.TYPES.COMPLETION'], + ] as const)('should render correct mode label for %s mode', (mode, label) => { renderComponent({ app: createApp({ app: { ...createApp().app, mode } }) }) expect(screen.getByText(label)).toBeInTheDocument() @@ -120,7 +119,7 @@ describe('TemplateCard', () => { deploymentEdition = edition const templateName = 'An Agent template with a full descriptive name' const app = createApp({ - app: { mode: AppModeEnum.AGENT, name: templateName }, + app: { mode: 'agent' as const, name: templateName }, description: 'A full template description that remains available when visually truncated.', }) @@ -144,7 +143,7 @@ describe('TemplateCard', () => { it('should make the app card clickable on cloud edition', () => { renderComponent({ - app: createApp({ app: { ...createApp().app, mode: AppModeEnum.WORKFLOW } }), + app: createApp({ app: { ...createApp().app, mode: 'workflow' as const } }), canCreate: true, }) @@ -192,7 +191,7 @@ describe('TemplateCard', () => { (edition, action) => { deploymentEdition = edition renderComponent({ - app: createApp({ app: { mode: AppModeEnum.AGENT }, description: '' }), + app: createApp({ app: { mode: 'agent' as const }, description: '' }), canCreate: true, }) @@ -242,7 +241,7 @@ describe('TemplateCard', () => { it.each(['{Enter}', ' '])('opens an Agent template with %s from the keyboard', async (key) => { const user = userEvent.setup() - const app = createApp({ app: { mode: AppModeEnum.AGENT } }) + const app = createApp({ app: { mode: 'agent' as const } }) renderComponent({ app, canCreate: true }) diff --git a/web/features/home/home-content/__tests__/home-content.spec.tsx b/web/features/home/home-content/__tests__/home-content.spec.tsx index 05ac7f68c6c..e524ed3cf7d 100644 --- a/web/features/home/home-content/__tests__/home-content.spec.tsx +++ b/web/features/home/home-content/__tests__/home-content.spec.tsx @@ -1,4 +1,4 @@ -import type { RecentAppResponse } from '@dify/contracts/api/console/apps/types.gen' +import type { AppMode, RecentAppResponse } from '@dify/contracts/api/console/apps/types.gen' import type { BannerResponse, RecommendedAppDetailResponse, @@ -28,7 +28,6 @@ import { consoleQuery } from '@/service/console' import { createConsoleQueryWrapper } from '@/test/console/query-data' import { seedRegisteredConsoleStateFixture } from '@/test/console/state-fixture' import { renderWithNuqs } from '@/test/nuqs-testing' -import { AppModeEnum } from '@/types/app' import { AppACLPermission } from '@/utils/permission' import { HomeContent } from '../home-content' @@ -473,7 +472,7 @@ type AppFixtureOverrides = Omit, 'app'> & { const createApp = (overrides: AppFixtureOverrides = {}): AppFixture => ({ app: { id: overrides.app?.id ?? 'app-basic-id', - mode: overrides.app?.mode ?? AppModeEnum.CHAT, + mode: overrides.app?.mode ?? 'chat', icon_type: overrides.app?.icon_type ?? 'emoji', icon: overrides.app?.icon ?? '😀', icon_background: overrides.app?.icon_background ?? '#fff', @@ -1028,7 +1027,7 @@ describe('HomeContent', () => { name: 'Alpha', can_trial: true, export_data: 'yaml-content', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, }) mockHandleImportDSL.mockImplementation( async (_payload: unknown, options: { onSuccess?: () => void; onPending?: () => void }) => { @@ -1036,8 +1035,8 @@ describe('HomeContent', () => { }, ) mockHandleImportDSLConfirm.mockImplementation( - async (options: { onSuccess?: (payload: { app_mode: AppModeEnum }) => void }) => { - options.onSuccess?.({ app_mode: AppModeEnum.CHAT }) + async (options: { onSuccess?: (payload: { app_mode: AppMode }) => void }) => { + options.onSuccess?.({ app_mode: 'chat' }) }, ) @@ -1056,7 +1055,7 @@ describe('HomeContent', () => { expect(mockHandleImportDSLConfirm).toHaveBeenCalledTimes(1) expect(mockTrackCreateApp).toHaveBeenCalledWith({ source: 'explore_template_list', - appMode: AppModeEnum.CHAT, + appMode: 'chat' as const, templateId: 'app-1', }) }) @@ -1073,7 +1072,7 @@ describe('HomeContent', () => { name: 'Alpha', can_trial: true, export_data: 'latest-yaml', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, }) mockHandleImportDSL.mockResolvedValue(undefined) const { queryClient } = renderHomeContent({ hasEditPermission: true }) @@ -1082,7 +1081,7 @@ describe('HomeContent', () => { name: 'Alpha', can_trial: true, export_data: 'cached-yaml', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, }) await queryClient.invalidateQueries({ queryKey: recommendedAppQueryKey('app-1'), @@ -1131,14 +1130,14 @@ describe('HomeContent', () => { name: 'Alpha', can_trial: true, export_data: 'yaml-content', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, }) mockHandleImportDSL.mockImplementation( async ( _payload: unknown, - options: { onSuccess?: (payload: { app_mode: AppModeEnum }) => void }, + options: { onSuccess?: (payload: { app_mode: AppMode }) => void }, ) => { - options.onSuccess?.({ app_mode: AppModeEnum.CHAT }) + options.onSuccess?.({ app_mode: 'chat' }) }, ) @@ -1272,14 +1271,14 @@ describe('HomeContent', () => { name: 'Alpha', can_trial: true, export_data: 'yaml-content', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, }) mockHandleImportDSL.mockImplementation( async ( _payload: unknown, - options: { onSuccess?: (payload: { app_mode: AppModeEnum }) => void }, + options: { onSuccess?: (payload: { app_mode: AppMode }) => void }, ) => { - options.onSuccess?.({ app_mode: AppModeEnum.CHAT }) + options.onSuccess?.({ app_mode: 'chat' }) }, ) @@ -1332,14 +1331,14 @@ describe('HomeContent', () => { name: 'Alpha', can_trial: true, export_data: 'yaml-content', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, }) mockHandleImportDSL.mockImplementation( async ( _payload: unknown, - options: { onSuccess?: (payload: { app_mode: AppModeEnum }) => void }, + options: { onSuccess?: (payload: { app_mode: AppMode }) => void }, ) => { - options.onSuccess?.({ app_mode: AppModeEnum.CHAT }) + options.onSuccess?.({ app_mode: 'chat' }) }, ) @@ -1400,7 +1399,7 @@ describe('HomeContent', () => { name: 'Alpha', can_trial: true, export_data: 'yaml-content', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, }) mockHandleImportDSL.mockImplementation( async (_payload: unknown, options: { onPending?: () => void }) => { @@ -1408,8 +1407,8 @@ describe('HomeContent', () => { }, ) mockHandleImportDSLConfirm.mockImplementation( - async (options: { onSuccess?: (payload: { app_mode: AppModeEnum }) => void }) => { - options.onSuccess?.({ app_mode: AppModeEnum.CHAT }) + async (options: { onSuccess?: (payload: { app_mode: AppMode }) => void }) => { + options.onSuccess?.({ app_mode: 'chat' }) }, ) @@ -1500,7 +1499,7 @@ describe('HomeContent', () => { name: 'Alpha', can_trial: true, export_data: 'yaml', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, }) renderHomeContent({ hasEditPermission: true }) @@ -1525,14 +1524,14 @@ describe('HomeContent', () => { name: 'Alpha', can_trial: true, export_data: 'yaml', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, }) mockHandleImportDSL.mockImplementation( async ( _payload: unknown, - options: { onSuccess?: (payload: { app_mode: AppModeEnum }) => void }, + options: { onSuccess?: (payload: { app_mode: AppMode }) => void }, ) => { - options.onSuccess?.({ app_mode: AppModeEnum.CHAT }) + options.onSuccess?.({ app_mode: 'chat' }) }, ) @@ -1556,7 +1555,7 @@ describe('HomeContent', () => { name: 'Alpha', can_trial: true, export_data: 'yaml', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, }) mockHandleImportDSL.mockImplementation( async (_payload: unknown, options: { onPending?: () => void }) => { @@ -1610,14 +1609,14 @@ describe('HomeContent', () => { name: 'Alpha', can_trial: true, export_data: 'yaml', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, }) mockHandleImportDSL.mockImplementation( async ( _payload: unknown, - options: { onSuccess?: (payload: { app_mode: AppModeEnum }) => void }, + options: { onSuccess?: (payload: { app_mode: AppMode }) => void }, ) => { - options.onSuccess?.({ app_mode: AppModeEnum.CHAT }) + options.onSuccess?.({ app_mode: 'chat' }) }, ) @@ -1631,7 +1630,7 @@ describe('HomeContent', () => { await waitFor(() => { expect(mockTrackCreateApp).toHaveBeenCalledWith({ source: 'explore_template_preview', - appMode: AppModeEnum.CHAT, + appMode: 'chat' as const, templateId: 'app-1', }) }) diff --git a/web/hooks/use-import-dsl.spec.tsx b/web/hooks/use-import-dsl.spec.tsx index 9ed470e595d..b917415acce 100644 --- a/web/hooks/use-import-dsl.spec.tsx +++ b/web/hooks/use-import-dsl.spec.tsx @@ -2,7 +2,6 @@ import { act, render, screen, waitFor } from '@testing-library/react' import { DSLImportMode, DSLImportStatus } from '@/models/app' import { httpErrorFromResponse } from '@/service/http-error' import { renderHookWithConsoleQuery } from '@/test/console/query-data' -import { AppModeEnum } from '@/types/app' import { useImportDSL } from './use-import-dsl' const mockPush = vi.hoisted(() => vi.fn()) @@ -121,7 +120,7 @@ describe('useImportDSL', () => { id: 'import-1', status: DSLImportStatus.COMPLETED_WITH_WARNINGS, app_id: 'app-1', - app_mode: AppModeEnum.WORKFLOW, + app_mode: 'workflow', permission_keys: [], warnings: [ { @@ -161,7 +160,7 @@ describe('useImportDSL', () => { const pendingResponse = { id: 'import-1', status: DSLImportStatus.PENDING, - app_mode: AppModeEnum.AGENT, + app_mode: 'agent', imported_dsl_version: '0.2.0', current_dsl_version: '0.1.0', permission_keys: [], @@ -170,7 +169,7 @@ describe('useImportDSL', () => { id: 'import-1', status: DSLImportStatus.COMPLETED_WITH_WARNINGS, app_id: 'app-1', - app_mode: AppModeEnum.AGENT, + app_mode: 'agent', permission_keys: ['app.acl.view_layout'], warnings: [ { @@ -235,7 +234,7 @@ describe('useImportDSL', () => { expect(mockHandleCheckPluginDependencies).toHaveBeenCalledWith('app-1') expect(mockResolveImportedAppRedirectionTarget).toHaveBeenCalledWith({ id: 'app-1', - mode: AppModeEnum.AGENT, + mode: 'agent' as const, permission_keys: ['app.acl.view_layout'], }) expect(mockGetRedirection).toHaveBeenCalledTimes(1) diff --git a/web/service/__tests__/use-workflow.spec.tsx b/web/service/__tests__/use-workflow.spec.tsx index 447326d53b9..2e5cf53f2f9 100644 --- a/web/service/__tests__/use-workflow.spec.tsx +++ b/web/service/__tests__/use-workflow.spec.tsx @@ -21,7 +21,6 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { act, renderHook } from '@testing-library/react' import { beforeEach, describe, expect, it, vi } from 'vite-plus/test' import { consoleQuery } from '@/service/console' -import { AppModeEnum } from '@/types/app' import { usePublishWorkflow, useUpdateWorkflow } from '../use-workflow' import { appWorkflowQueryOptions, @@ -175,7 +174,7 @@ describe('useUpdateWorkflow', () => { }) }) - it.each([AppModeEnum.WORKFLOW, AppModeEnum.ADVANCED_CHAT])( + it.each(['workflow', 'advanced-chat'] as const)( 'should synchronize every cached app deployment reference to the updated workflow version (%s)', async (appMode) => { const queryClient = createQueryClient() @@ -384,7 +383,7 @@ describe('useUpdateWorkflow', () => { await act(async () => { await result.current.mutateAsync({ appId: 'app-1', - appMode: AppModeEnum.CHAT, + appMode: 'chat' as const, url: '/apps/app-1/workflows/workflow-1', title: 'New release', releaseNotes: 'New notes', diff --git a/web/service/workflow.spec.ts b/web/service/workflow.spec.ts index 4388e143171..2bc392aaf33 100644 --- a/web/service/workflow.spec.ts +++ b/web/service/workflow.spec.ts @@ -2,15 +2,10 @@ import type { EnvironmentVariable } from '@/app/components/workflow/types' import { beforeEach, describe, expect, it, vi } from 'vite-plus/test' import { updateEnvironmentVariables } from './workflow' -const mockPost = vi.hoisted(() => vi.fn()) +const mockRequest = vi.hoisted(() => vi.fn()) -vi.mock('./base', () => ({ - get: vi.fn(), - post: mockPost, -})) - -vi.mock('@/service/console', () => ({ - consoleClient: {}, +vi.mock('@/service/console/browser', () => ({ + consoleBrowserLink: { call: mockRequest }, })) describe('updateEnvironmentVariables', () => { @@ -32,7 +27,7 @@ describe('updateEnvironmentVariables', () => { }, }, ] satisfies EnvironmentVariable[] - mockPost.mockResolvedValue({ result: 'success' }) + mockRequest.mockResolvedValue({ result: 'success' }) await updateEnvironmentVariables({ appId: 'app-1', @@ -40,12 +35,17 @@ describe('updateEnvironmentVariables', () => { deletedEnvironmentVariableIds: ['env-2'], }) - expect(mockPost).toHaveBeenCalledWith('apps/app-1/workflows/draft/environment-variables', { - body: { - environment_variables: environmentVariables, - patch: true, - deleted_environment_variable_ids: ['env-2'], + expect(mockRequest).toHaveBeenCalledWith( + ['apps', 'byAppId', 'workflows', 'draft', 'environmentVariables', 'post'], + { + params: { app_id: 'app-1' }, + body: { + environment_variables: environmentVariables, + patch: true, + deleted_environment_variable_ids: ['env-2'], + }, }, - }) + expect.any(Object), + ) }) }) diff --git a/web/utils/__tests__/create-app-tracking.spec.ts b/web/utils/__tests__/create-app-tracking.spec.ts index bf0d22d057b..ce441e7ed5f 100644 --- a/web/utils/__tests__/create-app-tracking.spec.ts +++ b/web/utils/__tests__/create-app-tracking.spec.ts @@ -2,7 +2,6 @@ import Cookies from 'js-cookie' import { beforeEach, describe, expect, it, vi } from 'vite-plus/test' import * as amplitude from '@/app/components/base/amplitude/utils' import { AgentScope } from '@/features/agent-v2/analytics' -import { AppModeEnum } from '@/types/app' import { buildCreateAppEventPayload, extractExternalCreateAppAttribution, @@ -96,7 +95,7 @@ describe('create-app-tracking', () => { buildCreateAppEventPayload( { source: 'studio_blank', - appMode: AppModeEnum.ADVANCED_CHAT, + appMode: 'advanced-chat' as const, }, null, new Date(2026, 3, 13, 14, 5, 9), @@ -113,7 +112,7 @@ describe('create-app-tracking', () => { buildCreateAppEventPayload( { source: 'studio_blank', - appMode: AppModeEnum.AGENT_CHAT, + appMode: 'agent-chat' as const, }, null, new Date(2026, 3, 13, 9, 8, 7), @@ -166,7 +165,7 @@ describe('create-app-tracking', () => { buildCreateAppEventPayload( { source: 'studio_blank', - appMode: AppModeEnum.CHAT, + appMode: 'chat' as const, }, null, new Date(2026, 3, 13, 8, 0, 1), @@ -181,7 +180,7 @@ describe('create-app-tracking', () => { buildCreateAppEventPayload( { source: 'studio_blank', - appMode: AppModeEnum.COMPLETION, + appMode: 'completion' as const, }, null, new Date(2026, 3, 13, 8, 0, 2), @@ -198,7 +197,7 @@ describe('create-app-tracking', () => { buildCreateAppEventPayload( { source: 'studio_blank', - appMode: AppModeEnum.WORKFLOW, + appMode: 'workflow' as const, }, null, new Date(2026, 3, 13, 7, 6, 5), @@ -215,7 +214,7 @@ describe('create-app-tracking', () => { buildCreateAppEventPayload( { source: 'studio_template_list', - appMode: AppModeEnum.CHAT, + appMode: 'chat' as const, templateId: 'template-1', }, null, @@ -234,7 +233,7 @@ describe('create-app-tracking', () => { buildCreateAppEventPayload( { source: 'studio_template_list', - appMode: AppModeEnum.WORKFLOW, + appMode: 'workflow' as const, templateId: 'template-1', }, { @@ -258,7 +257,7 @@ describe('create-app-tracking', () => { buildCreateAppEventPayload( { source: 'external', - appMode: AppModeEnum.WORKFLOW, + appMode: 'workflow' as const, }, null, new Date(2026, 3, 13, 7, 6, 5), @@ -277,7 +276,7 @@ describe('create-app-tracking', () => { } as ReturnType) await expect( - trackCreateApp({ source: 'studio_blank', appMode: AppModeEnum.ADVANCED_CHAT }), + trackCreateApp({ source: 'studio_blank', appMode: 'advanced-chat' as const }), ).resolves.toBeUndefined() expect(amplitude.trackEvent).toHaveBeenCalledWith('create_app', { @@ -295,7 +294,7 @@ describe('create-app-tracking', () => { trackCreateApp({ source: 'studio_template_list', - appMode: AppModeEnum.WORKFLOW, + appMode: 'workflow' as const, templateId: 'template-1', }) @@ -310,7 +309,7 @@ describe('create-app-tracking', () => { trackCreateApp({ source: 'studio_template_list', - appMode: AppModeEnum.WORKFLOW, + appMode: 'workflow' as const, templateId: 'template-1', }) @@ -357,7 +356,7 @@ describe('create-app-tracking', () => { trackCreateApp({ source: 'explore_template_preview', - appMode: AppModeEnum.CHAT, + appMode: 'chat' as const, templateId: 'template-2', }) @@ -380,7 +379,7 @@ describe('create-app-tracking', () => { value: undefined, }) - trackCreateApp({ source: 'studio_blank', appMode: AppModeEnum.AGENT_CHAT }) + trackCreateApp({ source: 'studio_blank', appMode: 'agent-chat' as const }) expect(amplitude.trackEvent).toHaveBeenCalledWith('create_app', { source: 'studio_blank', @@ -404,7 +403,7 @@ describe('create-app-tracking', () => { }), ) - trackCreateApp({ source: 'studio_blank', appMode: AppModeEnum.CHAT }) + trackCreateApp({ source: 'studio_blank', appMode: 'chat' as const }) expect(amplitude.trackEvent).toHaveBeenCalledWith('create_app', { source: 'external', @@ -419,7 +418,7 @@ describe('create-app-tracking', () => { it('should not track external source without remembered attribution', () => { trackCreateApp({ source: 'external', - appMode: AppModeEnum.WORKFLOW, + appMode: 'workflow' as const, templateId: 'template-1', }) diff --git a/web/utils/app-redirection.spec.ts b/web/utils/app-redirection.spec.ts index a53955d4fa6..b370e45f61e 100644 --- a/web/utils/app-redirection.spec.ts +++ b/web/utils/app-redirection.spec.ts @@ -2,7 +2,7 @@ * Test suite for app redirection utility functions * Tests navigation path generation based on user permissions and app modes */ -import { AppModeEnum } from '@/types/app' + import { AppACLPermission } from '@/utils/permission' import { getRedirection, getRedirectionPath } from './app-redirection' @@ -17,7 +17,7 @@ describe('app-redirection', () => { it('returns access point path when it is the only accessible app page', () => { const app = { id: 'app-123', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, permission_keys: [AppACLPermission.AccessPointView], } const result = getRedirectionPath(app) @@ -25,7 +25,7 @@ describe('app-redirection', () => { }) it('returns the app list when no app page is accessible', () => { - const app = { id: 'app-123', mode: AppModeEnum.CHAT, permission_keys: [] } + const app = { id: 'app-123', mode: 'chat' as const, permission_keys: [] } expect(getRedirectionPath(app)).toBe('/apps') }) @@ -33,7 +33,7 @@ describe('app-redirection', () => { it('does not redirect Access Point managers to a page they cannot view', () => { const app = { id: 'app-123', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, permission_keys: [AppACLPermission.AccessPointManage], } @@ -43,7 +43,7 @@ describe('app-redirection', () => { it('returns workflow path for workflow mode when app ACL can access layout', () => { const app = { id: 'app-123', - mode: AppModeEnum.WORKFLOW, + mode: 'workflow' as const, permission_keys: [AppACLPermission.ViewLayout], } const result = getRedirectionPath(app) @@ -53,7 +53,7 @@ describe('app-redirection', () => { it('returns workflow path for advanced-chat mode when app ACL can access layout', () => { const app = { id: 'app-123', - mode: AppModeEnum.ADVANCED_CHAT, + mode: 'advanced-chat' as const, permission_keys: [AppACLPermission.TestAndRun], } const result = getRedirectionPath(app) @@ -63,7 +63,7 @@ describe('app-redirection', () => { it('returns configuration path for chat mode when app ACL can access layout', () => { const app = { id: 'app-123', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, permission_keys: [AppACLPermission.Edit], } const result = getRedirectionPath(app) @@ -73,7 +73,7 @@ describe('app-redirection', () => { it('returns configuration path for completion mode when app ACL can access layout', () => { const app = { id: 'app-123', - mode: AppModeEnum.COMPLETION, + mode: 'completion' as const, permission_keys: [AppACLPermission.ViewLayout], } const result = getRedirectionPath(app) @@ -83,7 +83,7 @@ describe('app-redirection', () => { it('returns configuration path for agent-chat mode when app ACL can access layout', () => { const app = { id: 'app-456', - mode: AppModeEnum.AGENT_CHAT, + mode: 'agent-chat' as const, permission_keys: [AppACLPermission.ViewLayout], } const result = getRedirectionPath(app) @@ -93,7 +93,7 @@ describe('app-redirection', () => { it('returns the Agent configure path when the backing Agent ID is available', () => { const app = { id: 'app-1', - mode: AppModeEnum.AGENT, + mode: 'agent' as const, permission_keys: [AppACLPermission.ViewLayout], bound_agent_id: 'agent-1', } @@ -104,7 +104,7 @@ describe('app-redirection', () => { it('falls back to the Agent roster when the backing Agent ID is unavailable', () => { const app = { id: 'app-1', - mode: AppModeEnum.AGENT, + mode: 'agent' as const, permission_keys: [AppACLPermission.ViewLayout], } @@ -114,12 +114,12 @@ describe('app-redirection', () => { it('handles different app IDs', () => { const app1 = { id: 'abc-123', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, permission_keys: [AppACLPermission.AccessPointView], } const app2 = { id: 'xyz-789', - mode: AppModeEnum.WORKFLOW, + mode: 'workflow' as const, permission_keys: [AppACLPermission.ViewLayout], } @@ -128,7 +128,7 @@ describe('app-redirection', () => { }) it('returns layout path when the app maintainer has app.create_and_management permission without app ACL keys', () => { - const app = { id: 'app-123', mode: AppModeEnum.CHAT, permission_keys: [] } + const app = { id: 'app-123', mode: 'chat' as const, permission_keys: [] } expect( getRedirectionPath(app, { @@ -142,7 +142,7 @@ describe('app-redirection', () => { it('returns access config path when app ACL can only configure access', () => { const app = { id: 'app-123', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, permission_keys: [AppACLPermission.AccessConfig], } @@ -152,7 +152,7 @@ describe('app-redirection', () => { it('returns access point path when RBAC is disabled and Access Point remains accessible', () => { const app = { id: 'app-123', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, permission_keys: [AppACLPermission.AccessConfig, AppACLPermission.AccessPointView], } @@ -162,7 +162,7 @@ describe('app-redirection', () => { it('returns overview path when app ACL can only monitor the app', () => { const app = { id: 'app-123', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, permission_keys: [AppACLPermission.Monitor], } @@ -172,7 +172,7 @@ describe('app-redirection', () => { it('returns logs path when app ACL can only access logs and annotations', () => { const app = { id: 'app-123', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, permission_keys: [AppACLPermission.LogAndAnnotation], } @@ -182,7 +182,7 @@ describe('app-redirection', () => { it('returns deploy path when a workflow app ACL can only deploy', () => { const app = { id: 'app-123', - mode: AppModeEnum.WORKFLOW, + mode: 'workflow' as const, permission_keys: [AppACLPermission.Deploy], } @@ -200,7 +200,7 @@ describe('app-redirection', () => { it('calls redirection function with access point path when it is accessible', () => { const app = { id: 'app-123', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, permission_keys: [AppACLPermission.AccessPointView], } const mockRedirect = vi.fn() @@ -214,7 +214,7 @@ describe('app-redirection', () => { it('calls redirection function with workflow path when app ACL can access layout', () => { const app = { id: 'app-123', - mode: AppModeEnum.WORKFLOW, + mode: 'workflow' as const, permission_keys: [AppACLPermission.ViewLayout], } const mockRedirect = vi.fn() @@ -228,7 +228,7 @@ describe('app-redirection', () => { it('calls redirection function with configuration path for chat mode with layout access', () => { const app = { id: 'app-123', - mode: AppModeEnum.CHAT, + mode: 'chat' as const, permission_keys: [AppACLPermission.ViewLayout], } const mockRedirect = vi.fn() @@ -242,7 +242,7 @@ describe('app-redirection', () => { it('works with different redirection functions', () => { const app = { id: 'app-123', - mode: AppModeEnum.WORKFLOW, + mode: 'workflow' as const, permission_keys: [AppACLPermission.ViewLayout], } const paths: string[] = [] diff --git a/web/utils/imported-app-redirection.spec.ts b/web/utils/imported-app-redirection.spec.ts index 9f9ed5d166f..14d61694b02 100644 --- a/web/utils/imported-app-redirection.spec.ts +++ b/web/utils/imported-app-redirection.spec.ts @@ -1,4 +1,3 @@ -import { AppModeEnum } from '@/types/app' import { resolveImportedAppRedirectionTarget } from './imported-app-redirection' const mockFetchAppDetail = vi.hoisted(() => vi.fn()) @@ -24,12 +23,12 @@ describe('resolveImportedAppRedirectionTarget', () => { await expect( resolveImportedAppRedirectionTarget({ id: 'app-1', - mode: AppModeEnum.AGENT, + mode: 'agent' as const, permission_keys: ['app.acl.view_layout'], }), ).resolves.toEqual({ id: 'app-1', - mode: AppModeEnum.AGENT, + mode: 'agent' as const, permission_keys: ['app.acl.view_layout'], bound_agent_id: 'agent-1', }) @@ -42,7 +41,7 @@ describe('resolveImportedAppRedirectionTarget', () => { mockFetchAppDetail.mockRejectedValue(new Error('Failed to fetch App detail')) const target = { id: 'app-1', - mode: AppModeEnum.AGENT, + mode: 'agent' as const, } await expect(resolveImportedAppRedirectionTarget(target)).resolves.toEqual(target)