From 29c6bc9a15ec6e29cb0359a35e9a00654a7401b7 Mon Sep 17 00:00:00 2001 From: ogp-weeloong Date: Thu, 4 Jun 2026 00:55:22 +0800 Subject: [PATCH] feat(test-step): forward testRunMetadata to actions through metadata MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit processAction already passes its `metadata` option to both actionCommand.run and actionCommand.testRun, but test-step.ts never populated that field for the action path — so testRun handlers always received {}. Forward options.testRunMetadata as `metadata` so actions can read it. Safe because metadata's two callers are mutually exclusive: for-each iteration state is only set during non-test runs (enqueueFirstForEachStep is gated on !testRun), so the field is free during test runs. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../__tests__/mutations/execute-step.itest.ts | 89 ++++++++++++++++++- packages/backend/src/services/test-step.ts | 1 + 2 files changed, 89 insertions(+), 1 deletion(-) diff --git a/packages/backend/src/graphql/__tests__/mutations/execute-step.itest.ts b/packages/backend/src/graphql/__tests__/mutations/execute-step.itest.ts index 09d95dbdd7..9d327ccf91 100644 --- a/packages/backend/src/graphql/__tests__/mutations/execute-step.itest.ts +++ b/packages/backend/src/graphql/__tests__/mutations/execute-step.itest.ts @@ -1,9 +1,20 @@ import { randomUUID } from 'crypto' -import { beforeEach, describe, expect, it, MockInstance, vi } from 'vitest' +import { + afterEach, + beforeEach, + describe, + expect, + it, + MockInstance, + vi, +} from 'vitest' import { ForbiddenError } from '@/errors/graphql-errors' import { NotFoundError } from '@/errors/graphql-errors/not-found' import executeStep from '@/graphql/mutations/execute-step' +import Execution from '@/models/execution' +import Flow from '@/models/flow' +import Step from '@/models/step' import User from '@/models/user' import { TestStepOptions, TestStepResult } from '@/services/test-step' import Context from '@/types/express/context' @@ -305,3 +316,79 @@ describe('executeStep mutation - access control', () => { ) }) }) + +describe('executeStep mutation - testRunMetadata propagation to actions', () => { + let context: Context + let flow: Flow + let actionStep: Step + let testRunSpy: ReturnType + + beforeEach(async () => { + vi.resetAllMocks() + + context = await generateMockContext() + + flow = await Flow.query().insertGraphAndFetch({ + userId: context.currentUser.id, + name: 'testRunMetadata propagation flow', + steps: [ + { + key: 'mock-trigger', + appKey: 'mock-app', + type: 'trigger', + position: 1, + status: 'completed', + }, + { + key: 'mock-action', + appKey: 'mock-app', + type: 'action', + position: 2, + status: 'completed', + }, + ], + }) + actionStep = flow.steps[1] + + const execution = await Execution.query().insertAndFetch({ + flowId: flow.id, + testRun: true, + }) + await flow.$query().patch({ testExecutionId: execution.id }) + + testRunSpy = vi.fn().mockResolvedValue({}) + + vi.spyOn(Step.prototype, 'getApp').mockResolvedValue({ + key: 'mock-app', + apiBaseUrl: null, + beforeRequest: [], + requestErrorHandler: null, + } as any) + vi.spyOn(Step.prototype, 'getActionCommand').mockResolvedValue({ + run: vi.fn(), + testRun: testRunSpy, + preprocessVariable: undefined, + } as any) + vi.spyOn(Step.prototype, 'getNextStep').mockResolvedValue(null) + }) + + afterEach(() => { + vi.restoreAllMocks() + }) + + it("forwards testRunMetadata to the action's testRun handler", async () => { + const { default: realTestStep } = await vi.importActual< + typeof import('@/services/test-step') + >('@/services/test-step') + + const testRunMetadata = { 'fake:key': { hello: 'world' } } + + await realTestStep({ + stepId: actionStep.id, + testRunMetadata, + }) + + expect(testRunSpy).toHaveBeenCalledTimes(1) + expect(testRunSpy.mock.calls[0][1]).toEqual(testRunMetadata) + }) +}) diff --git a/packages/backend/src/services/test-step.ts b/packages/backend/src/services/test-step.ts index 45325f8bda..c6fec56545 100644 --- a/packages/backend/src/services/test-step.ts +++ b/packages/backend/src/services/test-step.ts @@ -91,6 +91,7 @@ const testStep = async (options: TestStepOptions): Promise => { stepId: stepToTest.id, executionId: flow.testExecutionId, testRun: true, + metadata: options.testRunMetadata, }) // Delete old execution steps of the same step