From 4ea39559c029084a83d4e142567fbd1ce7d5fc8c Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Thu, 6 Aug 2026 12:13:44 +0100 Subject: [PATCH] Fix tools that could not run in Automate (cherry picked from commit e7b48dbea3953ffc9b24c45182d4aa0e85ba3a66) --- .../tools/automate/ToolSelector.tsx | 8 +++-- ...tomatableToolsHaveOperationConfig.test.tsx | 33 +++++++++++++++++++ .../core/data/useTranslatedToolRegistry.tsx | 11 ++++++- 3 files changed, 49 insertions(+), 3 deletions(-) create mode 100644 frontend/editor/src/core/data/automatableToolsHaveOperationConfig.test.tsx diff --git a/frontend/editor/src/core/components/tools/automate/ToolSelector.tsx b/frontend/editor/src/core/components/tools/automate/ToolSelector.tsx index c5c3998bc0..ad7bb33a6e 100644 --- a/frontend/editor/src/core/components/tools/automate/ToolSelector.tsx +++ b/frontend/editor/src/core/components/tools/automate/ToolSelector.tsx @@ -34,13 +34,17 @@ export default function ToolSelector({ const [shouldAutoFocus, setShouldAutoFocus] = useState(false); const containerRef = useRef(null); - // Filter out excluded tools (like 'automate' itself) and tools that don't support automation + // Filter out excluded tools (like 'automate' itself), tools that don't support + // automation, and tools with no operationConfig - the executor resolves a step + // through operationConfig, so offering one without it fails only at run time. const baseFilteredTools = useMemo(() => { return ( Object.entries(toolRegistry) as [ToolId, ToolRegistryEntry][] ).filter( ([key, tool]) => - !excludeTools.includes(key) && getToolSupportsAutomate(tool), + !excludeTools.includes(key) && + getToolSupportsAutomate(tool) && + Boolean(tool.operationConfig), ); }, [toolRegistry, excludeTools]); diff --git a/frontend/editor/src/core/data/automatableToolsHaveOperationConfig.test.tsx b/frontend/editor/src/core/data/automatableToolsHaveOperationConfig.test.tsx new file mode 100644 index 0000000000..461c07ebdf --- /dev/null +++ b/frontend/editor/src/core/data/automatableToolsHaveOperationConfig.test.tsx @@ -0,0 +1,33 @@ +/** + * Registry invariant: the Automate picker offers a tool whenever it doesn't opt out via + * `supportsAutomate: false`, but automationExecutor resolves each step through the tool's + * `operationConfig`. A tool that is offered without one is selectable in the builder and + * only fails when the automation runs, with "Tool operation not supported: ". + * + * So a tool must either carry an operationConfig or declare supportsAutomate: false. + */ +import { describe, expect, test, vi } from "vitest"; +import { renderHook } from "@testing-library/react"; +import { useTranslatedToolCatalog } from "@app/data/useTranslatedToolRegistry"; +import { getToolSupportsAutomate } from "@app/data/toolsTaxonomy"; + +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ + t: (key: string, fallback?: string) => fallback ?? key, + i18n: { changeLanguage: vi.fn(), language: "en-US" }, + }), + Trans: ({ children }: { children?: unknown }) => children, +})); + +describe("automatable tools", () => { + test("every tool offered to Automate can be executed as a step", () => { + const { result } = renderHook(() => useTranslatedToolCatalog()); + + const offeredWithoutConfig = Object.entries(result.current.regularTools) + .filter(([, entry]) => entry && getToolSupportsAutomate(entry)) + .filter(([, entry]) => !entry.operationConfig) + .map(([id]) => id); + + expect(offeredWithoutConfig).toEqual([]); + }); +}); diff --git a/frontend/editor/src/core/data/useTranslatedToolRegistry.tsx b/frontend/editor/src/core/data/useTranslatedToolRegistry.tsx index 58b61c71be..027d017bd6 100644 --- a/frontend/editor/src/core/data/useTranslatedToolRegistry.tsx +++ b/frontend/editor/src/core/data/useTranslatedToolRegistry.tsx @@ -49,6 +49,8 @@ import { changeMetadataOperationConfig } from "@app/hooks/tools/changeMetadata/u import { signOperationConfig } from "@app/hooks/tools/sign/useSignOperation"; import { cropOperationConfig } from "@app/hooks/tools/crop/useCropOperation"; import { removeAnnotationsOperationConfig } from "@app/hooks/tools/removeAnnotations/useRemoveAnnotationsOperation"; +import { removeImageOperationConfig } from "@app/hooks/tools/removeImage/useRemoveImageOperation"; +import { pageLayoutOperationConfig } from "@app/hooks/tools/pageLayout/usePageLayoutOperation"; import { extractImagesOperationConfig } from "@app/hooks/tools/extractImages/useExtractImagesOperation"; import { replaceColorOperationConfig } from "@app/hooks/tools/replaceColor/useReplaceColorOperation"; import { removePagesOperationConfig } from "@app/hooks/tools/removePages/useRemovePagesOperation"; @@ -525,6 +527,9 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { maxFiles: -1, endpoints: ["validate-signature"], synonyms: getSynonyms(t, "validateSignature"), + // Reports on signatures rather than transforming the PDF, and its hook is + // not on the operationConfig seam, so it cannot run as an automation step. + supportsAutomate: false, automationSettings: null, }, @@ -729,6 +734,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { subcategoryId: SubcategoryId.PAGE_FORMATTING, maxFiles: -1, endpoints: ["multi-page-layout"], + operationConfig: asRegistryConfig(pageLayoutOperationConfig), automationSettings: lazySettings( () => import("@app/components/tools/pageLayout/PageLayoutSettings"), ), @@ -941,7 +947,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { subcategoryId: SubcategoryId.REMOVAL, maxFiles: -1, endpoints: ["remove-image-pdf"], - operationConfig: undefined, + operationConfig: asRegistryConfig(removeImageOperationConfig), synonyms: getSynonyms(t, "removeImage"), automationSettings: null, }, @@ -1170,6 +1176,9 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { subcategoryId: SubcategoryId.ADVANCED_FORMATTING, endpoints: ["scanner-effect"], synonyms: getSynonyms(t, "scannerEffect"), + // No frontend implementation yet (component is null), so it has no + // operationConfig to execute as an automation step. + supportsAutomate: false, automationSettings: null, },