From e79eab2b6067a4a632496c25287677d2070a0bcc Mon Sep 17 00:00:00 2001 From: aalises Date: Mon, 20 Apr 2026 18:56:30 +0200 Subject: [PATCH] fix(core): Hide pre-resolved setup requests from Instance AI wizard analyzeWorkflow emitted a request for every credential-bearing node, including ones whose credential was already set and had tested successfully. The wizard rendered those as empty "done" steps with nothing for the user to do. Filter the request list so only items that still need user work remain: requests with needsAction set, or triggers that are still testable. Pre-valid credential-only cards drop out; trigger steps survive regardless of credential state, and parameter-issue requests (which always flip needsAction) are unaffected. Ref: https://linear.app/n8n/issue/AI-2395 --- .../__tests__/setup-workflow.service.test.ts | 83 ++++++++++++++++++- .../tools/workflows/setup-workflow.service.ts | 7 +- 2 files changed, 88 insertions(+), 2 deletions(-) diff --git a/packages/@n8n/instance-ai/src/tools/workflows/__tests__/setup-workflow.service.test.ts b/packages/@n8n/instance-ai/src/tools/workflows/__tests__/setup-workflow.service.test.ts index 567f13a71133f..126f39894a206 100644 --- a/packages/@n8n/instance-ai/src/tools/workflows/__tests__/setup-workflow.service.test.ts +++ b/packages/@n8n/instance-ai/src/tools/workflows/__tests__/setup-workflow.service.test.ts @@ -539,7 +539,7 @@ describe('analyzeWorkflow', () => { expect(result[0].credentialType).toBe('slackApi'); }); - it('marks needsAction correctly after credentials are applied', async () => { + it('hides credential-only requests whose credential is already set and tests OK', async () => { const node = makeNode({ credentials: { slackApi: { id: 'cred-1', name: 'My Slack' } }, }); @@ -557,10 +557,91 @@ describe('analyzeWorkflow', () => { const result = await analyzeWorkflow(context, 'wf-1'); + expect(result).toHaveLength(0); + }); + + it('keeps credential-only requests whose credential test fails', async () => { + const node = makeNode({ + credentials: { slackApi: { id: 'cred-1', name: 'My Slack' } }, + }); + (context.workflowService.getAsWorkflowJSON as jest.Mock).mockResolvedValue( + makeWorkflowJSON([node]), + ); + (context.nodeService.getDescription as jest.Mock).mockResolvedValue({ + group: [], + credentials: [{ name: 'slackApi' }], + }); + (context.credentialService.list as jest.Mock).mockResolvedValue([ + { id: 'cred-1', name: 'My Slack', updatedAt: '2025-01-01T00:00:00.000Z' }, + ]); + (context.credentialService.test as jest.Mock).mockResolvedValue({ + success: false, + message: 'Invalid token', + }); + + const result = await analyzeWorkflow(context, 'wf-1'); + + expect(result).toHaveLength(1); + expect(result[0].needsAction).toBe(true); + }); + + it('keeps testable trigger requests even when their credential is already valid', async () => { + const trigger = makeNode({ + name: 'Webhook', + type: 'n8n-nodes-base.webhook', + id: 'n-trigger', + credentials: { httpHeaderAuth: { id: 'cred-1', name: 'My Auth' } }, + }); + (context.workflowService.getAsWorkflowJSON as jest.Mock).mockResolvedValue( + makeWorkflowJSON([trigger]), + ); + (context.nodeService.getDescription as jest.Mock).mockResolvedValue({ + group: ['trigger'], + credentials: [{ name: 'httpHeaderAuth' }], + webhooks: [{}], + }); + (context.credentialService.list as jest.Mock).mockResolvedValue([ + { id: 'cred-1', name: 'My Auth', updatedAt: '2025-01-01T00:00:00.000Z' }, + ]); + (context.credentialService.test as jest.Mock).mockResolvedValue({ success: true }); + + const result = await analyzeWorkflow(context, 'wf-1'); + expect(result).toHaveLength(1); + expect(result[0].isTrigger).toBe(true); + expect(result[0].isTestable).toBe(true); expect(result[0].needsAction).toBe(false); }); + it('keeps requests with parameter issues regardless of credential validity', async () => { + const node = makeNode({ + credentials: { slackApi: { id: 'cred-1', name: 'My Slack' } }, + }); + (context.workflowService.getAsWorkflowJSON as jest.Mock).mockResolvedValue( + makeWorkflowJSON([node]), + ); + (context.nodeService.getDescription as jest.Mock).mockResolvedValue({ + group: [], + credentials: [{ name: 'slackApi' }], + properties: [{ name: 'resource', displayName: 'Resource', type: 'string' }], + }); + (context.nodeService as unknown as Record).getParameterIssues = jest + .fn() + .mockResolvedValue({ + resource: ['Parameter "resource" is required'], + }); + (context.credentialService.list as jest.Mock).mockResolvedValue([ + { id: 'cred-1', name: 'My Slack', updatedAt: '2025-01-01T00:00:00.000Z' }, + ]); + (context.credentialService.test as jest.Mock).mockResolvedValue({ success: true }); + + const result = await analyzeWorkflow(context, 'wf-1'); + + expect(result).toHaveLength(1); + expect(result[0].needsAction).toBe(true); + expect(result[0].parameterIssues).toBeDefined(); + }); + it('sorts by execution order with triggers first', async () => { const trigger = makeNode({ name: 'Webhook', diff --git a/packages/@n8n/instance-ai/src/tools/workflows/setup-workflow.service.ts b/packages/@n8n/instance-ai/src/tools/workflows/setup-workflow.service.ts index af061c27f4af4..816a0ac984046 100644 --- a/packages/@n8n/instance-ai/src/tools/workflows/setup-workflow.service.ts +++ b/packages/@n8n/instance-ai/src/tools/workflows/setup-workflow.service.ts @@ -770,7 +770,12 @@ export async function analyzeWorkflow( req.credentialType !== undefined || req.isTrigger || (req.parameterIssues && Object.keys(req.parameterIssues).length > 0), - ); + ) + // Hide cards the user has nothing to do on: credentials already set and + // tested, no parameter issues, not a trigger awaiting testing. Trigger + // steps are always kept — triggers require user testing regardless of + // credential state. + .filter((req) => !!req.needsAction || (req.isTrigger && !!req.isTestable)); sortByExecutionOrder( setupRequests,