diff --git a/packages/cli/src/modules/instance-ai/eval/__tests__/eval-mocked-credentials-helper.test.ts b/packages/cli/src/modules/instance-ai/eval/__tests__/eval-mocked-credentials-helper.test.ts index 356d13b6fa7..7f96c592917 100644 --- a/packages/cli/src/modules/instance-ai/eval/__tests__/eval-mocked-credentials-helper.test.ts +++ b/packages/cli/src/modules/instance-ai/eval/__tests__/eval-mocked-credentials-helper.test.ts @@ -415,11 +415,7 @@ describe('EvalMockedCredentialsHelper', () => { }); describe('getDecrypted — schema synthesis when id is null', () => { - // Core's eval-mode bypass passes `{ id: null, name: type }` when a node - // has no credentials configured at all. The inner helper throws - // CredentialNotFoundError on a null id; the catch below schema-synthesizes - // (and applies the URL rewrite) so vendor SDK traffic stays inside the - // wire server instead of escaping to the real provider with 401. + // `{ id: null }` short-circuits to schema synthesis without delegating to the inner helper. const propsSchema = [ { name: 'apiKey', @@ -441,7 +437,7 @@ describe('EvalMockedCredentialsHelper', () => { function makeSynthesizingInner(): ICredentialsHelper { return makeInner({ getCredentialsProperties: jest.fn().mockReturnValue(propsSchema), - // Inner throws on a null-id lookup → catch fires → schema synthesis. + // Not reached for a null id (short-circuits first); left rejecting so a regression fails loudly. getDecrypted: jest.fn().mockRejectedValue(new CredentialNotFoundError('null', 'openAiApi')), }); } @@ -569,6 +565,65 @@ describe('EvalMockedCredentialsHelper', () => { }); }); + describe('no-id credential references — regression for "Found credential with no ID."', () => { + // Id-less refs (builder "set up later" placeholders, core's `{ id: null }`) make the real + // inner throw UnexpectedError, not CredentialNotFoundError — so the helper must synthesize, not delegate. + const propsSchema = [ + { + name: 'apiKey', + displayName: 'API Key', + type: 'string' as const, + default: '', + typeOptions: { password: true }, + }, + ]; + + it.each([ + ['null id (fully-unconfigured bypass)', { id: null, name: 'telegramApi' }], + ['empty-string id (placeholder)', { id: '', name: 'Telegram cred' }], + ['missing id (placeholder)', { name: 'Telegram cred' }], + ])('synthesizes without delegating to inner — %s', async (_label, creds) => { + const inner = makeInner({ + getCredentialsProperties: jest.fn().mockReturnValue(propsSchema), + // Stands in for core's UnexpectedError on a falsy id — fails loudly if the short-circuit regresses. + getDecrypted: jest.fn().mockRejectedValue(new Error('Found credential with no ID.')), + }); + const helper = new EvalMockedCredentialsHelper(inner); + + const result = await helper.getDecrypted( + fakeAdditionalData, + creds as INodeCredentialsDetails, + 'telegramApi', + 'manual', + { node: fakeNode } as IExecuteData, + ); + + expect(result.__evalMockedCredential).toBe(true); + expect(inner.getDecrypted).not.toHaveBeenCalled(); + expect(helper.mockedCredentials).toEqual([ + { nodeName: 'Telegram', credentialType: 'telegramApi', credentialId: undefined }, + ]); + }); + + it('still delegates (and surfaces the throw) when an id IS present', async () => { + // A present id whose lookup fails with a non-CredentialNotFoundError must still propagate. + const inner = makeInner({ + getDecrypted: jest.fn().mockRejectedValue(new Error('database is down')), + }); + const helper = new EvalMockedCredentialsHelper(inner); + + await expect( + helper.getDecrypted( + fakeAdditionalData, + { id: 'real-id', name: 'Telegram cred' }, + 'telegramApi', + 'manual', + ), + ).rejects.toThrow('database is down'); + expect(inner.getDecrypted).toHaveBeenCalled(); + }); + }); + describe('authenticate', () => { it('passes the request through unchanged for marker payloads', async () => { const inner = makeInner(); diff --git a/packages/cli/src/modules/instance-ai/eval/eval-mocked-credentials-helper.ts b/packages/cli/src/modules/instance-ai/eval/eval-mocked-credentials-helper.ts index bab3f96a7d8..827cf42ce7f 100644 --- a/packages/cli/src/modules/instance-ai/eval/eval-mocked-credentials-helper.ts +++ b/packages/cli/src/modules/instance-ai/eval/eval-mocked-credentials-helper.ts @@ -104,6 +104,21 @@ export class EvalMockedCredentialsHelper extends ICredentialsHelper { raw?: boolean, expressionResolveValues?: ICredentialsExpressionResolveValues, ): Promise { + // Id-less refs make the inner helper throw UnexpectedError (not CredentialNotFoundError), + // which the catch below won't handle — synthesize a mock here instead of delegating. + if (!nodeCredentials.id) { + this.mockedCredentials.push({ + nodeName: executeData?.node?.name ?? 'unknown', + credentialType: type, + credentialId: undefined, + }); + const synthesized = { + ...buildEvalMockCredentials(this.inner.getCredentialsProperties(type)), + [MOCK_MARKER]: true, + } as ICredentialDataDecryptedObject; + return this.applyServerUrlRewrite(synthesized, type, nodeCredentials, executeData); + } + let credentials: ICredentialDataDecryptedObject; try { credentials = await this.inner.getDecrypted( @@ -118,28 +133,13 @@ export class EvalMockedCredentialsHelper extends ICredentialsHelper { } catch (error) { if (!(error instanceof CredentialNotFoundError)) throw error; + // id present but absent from the DB — a bare marker stub is enough; URL rewrite still runs below. this.mockedCredentials.push({ nodeName: executeData?.node?.name ?? 'unknown', credentialType: type, credentialId: nodeCredentials.id ?? undefined, }); - - // When called with no credential id (eval-mode bypass for nodes - // with no credentials of any type configured), schema-synthesize - // so the wire-server URL rewrite below has a real `url` field to - // augment. Otherwise vendor SDK traffic would escape to the real - // provider with placeholder values and 401 at the wire layer. - // `buildEvalMockCredentials` is typed `Record` — - // schema defaults can be richer than `CredentialInformation`, but - // at runtime emits only JSON-shaped values, which is what the - // rewrite path consumes. - credentials = - nodeCredentials.id === null - ? ({ - ...buildEvalMockCredentials(this.inner.getCredentialsProperties(type)), - [MOCK_MARKER]: true, - } as ICredentialDataDecryptedObject) - : { [MOCK_MARKER]: true }; + credentials = { [MOCK_MARKER]: true }; } return this.applyServerUrlRewrite(credentials, type, nodeCredentials, executeData);