From 8c4d9f2efcf350e24ac8fc4be12cc2a79c6d0806 Mon Sep 17 00:00:00 2001 From: alban bertolini Date: Thu, 26 Mar 2026 15:42:47 +0100 Subject: [PATCH 1/4] feat(workflow-executor): add info logging around step execution Add info level to Logger interface. Log step execution start (with context: runId, stepId, stepType, collection) and completion (with status) around doExecute in BaseStepExecutor. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/adapters/console-logger.ts | 11 +++++++++- .../src/executors/base-step-executor.ts | 21 ++++++++++++++++++- .../src/ports/logger-port.ts | 1 + .../test/executors/base-step-executor.test.ts | 2 +- .../executors/condition-step-executor.test.ts | 2 +- .../load-related-record-step-executor.test.ts | 4 ++-- .../executors/mcp-task-step-executor.test.ts | 12 +++++------ .../read-record-step-executor.test.ts | 4 ++-- ...rigger-record-action-step-executor.test.ts | 4 ++-- .../update-record-step-executor.test.ts | 4 ++-- .../test/http/executor-http-server.test.ts | 2 +- .../workflow-executor/test/runner.test.ts | 8 +++---- 12 files changed, 52 insertions(+), 23 deletions(-) diff --git a/packages/workflow-executor/src/adapters/console-logger.ts b/packages/workflow-executor/src/adapters/console-logger.ts index cb9ca735a1..6c07d21f7b 100644 --- a/packages/workflow-executor/src/adapters/console-logger.ts +++ b/packages/workflow-executor/src/adapters/console-logger.ts @@ -1,8 +1,17 @@ import type { Logger } from '../ports/logger-port'; export default class ConsoleLogger implements Logger { + info(message: string, context: Record): void { + // eslint-disable-next-line no-console + console.log( + JSON.stringify({ level: 'info', message, timestamp: new Date().toISOString(), ...context }), + ); + } + error(message: string, context: Record): void { - console.error(JSON.stringify({ message, timestamp: new Date().toISOString(), ...context })); + console.error( + JSON.stringify({ level: 'error', message, timestamp: new Date().toISOString(), ...context }), + ); } info(message: string, context: Record): void { diff --git a/packages/workflow-executor/src/executors/base-step-executor.ts b/packages/workflow-executor/src/executors/base-step-executor.ts index 59ba338f25..4c7bc54814 100644 --- a/packages/workflow-executor/src/executors/base-step-executor.ts +++ b/packages/workflow-executor/src/executors/base-step-executor.ts @@ -35,8 +35,27 @@ export default abstract class BaseStepExecutor { + const { runId, stepId, stepIndex, stepDefinition, baseRecordRef } = this.context; + + this.context.logger.info('Step execution started', { + runId, + stepId, + stepIndex, + stepType: stepDefinition.type, + collection: baseRecordRef.collectionName, + }); + try { - return await this.doExecute(); + const result = await this.doExecute(); + + this.context.logger.info('Step execution completed', { + runId, + stepId, + stepIndex, + status: result.stepOutcome.status, + }); + + return result; } catch (error) { if (error instanceof WorkflowExecutorError) { if (error.cause !== undefined) { diff --git a/packages/workflow-executor/src/ports/logger-port.ts b/packages/workflow-executor/src/ports/logger-port.ts index ed2acd3930..406cf1ec8f 100644 --- a/packages/workflow-executor/src/ports/logger-port.ts +++ b/packages/workflow-executor/src/ports/logger-port.ts @@ -1,4 +1,5 @@ export interface Logger { + info(message: string, context: Record): void; error(message: string, context: Record): void; info?(message: string, context: Record): void; } diff --git a/packages/workflow-executor/test/executors/base-step-executor.test.ts b/packages/workflow-executor/test/executors/base-step-executor.test.ts index 45a4582c40..5825d13da6 100644 --- a/packages/workflow-executor/test/executors/base-step-executor.test.ts +++ b/packages/workflow-executor/test/executors/base-step-executor.test.ts @@ -87,7 +87,7 @@ function makeMockRunStore(stepExecutions: StepExecutionData[] = []): RunStore { } function makeMockLogger(): Logger { - return { error: jest.fn() }; + return { info: jest.fn(), error: jest.fn() }; } function makeContext(overrides: Partial = {}): ExecutionContext { diff --git a/packages/workflow-executor/test/executors/condition-step-executor.test.ts b/packages/workflow-executor/test/executors/condition-step-executor.test.ts index a8ecbabd60..3a9e13405b 100644 --- a/packages/workflow-executor/test/executors/condition-step-executor.test.ts +++ b/packages/workflow-executor/test/executors/condition-step-executor.test.ts @@ -69,7 +69,7 @@ function makeContext( }, schemaCache: new SchemaCache(), previousSteps: [], - logger: { error: jest.fn() }, + logger: { info: jest.fn(), error: jest.fn() }, ...overrides, }; } diff --git a/packages/workflow-executor/test/executors/load-related-record-step-executor.test.ts b/packages/workflow-executor/test/executors/load-related-record-step-executor.test.ts index 7d2f7ec461..e42757ca38 100644 --- a/packages/workflow-executor/test/executors/load-related-record-step-executor.test.ts +++ b/packages/workflow-executor/test/executors/load-related-record-step-executor.test.ts @@ -140,7 +140,7 @@ function makeContext( }, schemaCache: new SchemaCache(), previousSteps: [], - logger: { error: jest.fn() }, + logger: { info: jest.fn(), error: jest.fn() }, ...overrides, }; } @@ -1295,7 +1295,7 @@ describe('LoadRelatedRecordStepExecutor', () => { }); it('returns user message and logs cause when agentPort.getRelatedData throws an infra error', async () => { - const logger = { error: jest.fn() }; + const logger = { info: jest.fn(), error: jest.fn() }; const agentPort = makeMockAgentPort(); (agentPort.getRelatedData as jest.Mock).mockRejectedValue(new Error('DB connection lost')); const mockModel = makeMockModel({ relationName: 'Order', reasoning: 'test' }); diff --git a/packages/workflow-executor/test/executors/mcp-task-step-executor.test.ts b/packages/workflow-executor/test/executors/mcp-task-step-executor.test.ts index f23b45bd48..ff97f87a69 100644 --- a/packages/workflow-executor/test/executors/mcp-task-step-executor.test.ts +++ b/packages/workflow-executor/test/executors/mcp-task-step-executor.test.ts @@ -107,7 +107,7 @@ function makeContext( }, schemaCache: new SchemaCache(), previousSteps: [], - logger: { error: jest.fn() }, + logger: { info: jest.fn(), error: jest.fn() }, ...overrides, }; } @@ -218,7 +218,7 @@ describe('McpTaskStepExecutor', () => { tool_calls: [{ name: 'send_notification', args: { message: 'Hi' }, id: 'call_1' }], }) .mockResolvedValueOnce({ tool_calls: [] }); - const logger = { error: jest.fn() }; + const logger = { info: jest.fn(), error: jest.fn() }; const runStore = makeMockRunStore(); const context = makeContext({ model, @@ -299,7 +299,7 @@ describe('McpTaskStepExecutor', () => { it('returns error when saveStepExecution fails (Branch C)', async () => { const { model } = makeMockModel('send_notification', { message: 'Hello' }); - const logger = { error: jest.fn() }; + const logger = { info: jest.fn(), error: jest.fn() }; const runStore = makeMockRunStore({ saveStepExecution: jest.fn().mockRejectedValue(new Error('DB unavailable')), }); @@ -491,7 +491,7 @@ describe('McpTaskStepExecutor', () => { invoke: invokeFn, }); const { model } = makeMockModel('send_notification', { message: 'Hello' }); - const logger = { error: jest.fn() }; + const logger = { info: jest.fn(), error: jest.fn() }; const runStore = makeMockRunStore({ saveStepExecution: jest.fn().mockRejectedValue(new Error('Disk full')), }); @@ -529,7 +529,7 @@ describe('McpTaskStepExecutor', () => { userConfirmed: true, }, }; - const logger = { error: jest.fn() }; + const logger = { info: jest.fn(), error: jest.fn() }; const runStore = makeMockRunStore({ getStepExecutions: jest.fn().mockResolvedValue([execution]), saveStepExecution: jest.fn().mockRejectedValue(new Error('Disk full')), @@ -638,7 +638,7 @@ describe('McpTaskStepExecutor', () => { invoke: invokeFn, }); const { model } = makeMockModel('send_notification', {}); - const logger = { error: jest.fn() }; + const logger = { info: jest.fn(), error: jest.fn() }; const context = makeContext({ model, stepDefinition: makeStep({ automaticExecution: true }), diff --git a/packages/workflow-executor/test/executors/read-record-step-executor.test.ts b/packages/workflow-executor/test/executors/read-record-step-executor.test.ts index 2578b856d1..218784679f 100644 --- a/packages/workflow-executor/test/executors/read-record-step-executor.test.ts +++ b/packages/workflow-executor/test/executors/read-record-step-executor.test.ts @@ -129,7 +129,7 @@ function makeContext( }, schemaCache: new SchemaCache(), previousSteps: [], - logger: { error: jest.fn() }, + logger: { info: jest.fn(), error: jest.fn() }, ...overrides, }; } @@ -668,7 +668,7 @@ describe('ReadRecordStepExecutor', () => { }); it('returns user message and logs cause when agentPort.getRecord throws an infra error', async () => { - const logger = { error: jest.fn() }; + const logger = { info: jest.fn(), error: jest.fn() }; const agentPort = makeMockAgentPort(); (agentPort.getRecord as jest.Mock).mockRejectedValue(new Error('DB connection lost')); const mockModel = makeMockModel({ fieldNames: ['email'] }); diff --git a/packages/workflow-executor/test/executors/trigger-record-action-step-executor.test.ts b/packages/workflow-executor/test/executors/trigger-record-action-step-executor.test.ts index 39ee40a146..15b775dc10 100644 --- a/packages/workflow-executor/test/executors/trigger-record-action-step-executor.test.ts +++ b/packages/workflow-executor/test/executors/trigger-record-action-step-executor.test.ts @@ -128,7 +128,7 @@ function makeContext( }, schemaCache: new SchemaCache(), previousSteps: [], - logger: { error: jest.fn() }, + logger: { info: jest.fn(), error: jest.fn() }, ...overrides, }; } @@ -518,7 +518,7 @@ describe('TriggerRecordActionStepExecutor', () => { }); it('returns user message and logs cause when agentPort.executeAction throws an infra error', async () => { - const logger = { error: jest.fn() }; + const logger = { info: jest.fn(), error: jest.fn() }; const agentPort = makeMockAgentPort(); (agentPort.executeAction as jest.Mock).mockRejectedValue(new Error('DB connection lost')); const mockModel = makeMockModel({ diff --git a/packages/workflow-executor/test/executors/update-record-step-executor.test.ts b/packages/workflow-executor/test/executors/update-record-step-executor.test.ts index 14377287ef..5cc90e67e5 100644 --- a/packages/workflow-executor/test/executors/update-record-step-executor.test.ts +++ b/packages/workflow-executor/test/executors/update-record-step-executor.test.ts @@ -130,7 +130,7 @@ function makeContext( }, schemaCache: new SchemaCache(), previousSteps: [], - logger: { error: jest.fn() }, + logger: { info: jest.fn(), error: jest.fn() }, ...overrides, }; } @@ -681,7 +681,7 @@ describe('UpdateRecordStepExecutor', () => { }); it('returns user message and logs cause when agentPort.updateRecord throws an infra error', async () => { - const logger = { error: jest.fn() }; + const logger = { info: jest.fn(), error: jest.fn() }; const agentPort = makeMockAgentPort(); (agentPort.updateRecord as jest.Mock).mockRejectedValue(new Error('DB connection lost')); const mockModel = makeMockModel({ diff --git a/packages/workflow-executor/test/http/executor-http-server.test.ts b/packages/workflow-executor/test/http/executor-http-server.test.ts index 6fb5067c04..0797bbce36 100644 --- a/packages/workflow-executor/test/http/executor-http-server.test.ts +++ b/packages/workflow-executor/test/http/executor-http-server.test.ts @@ -252,7 +252,7 @@ describe('ExecutorHttpServer', () => { }); it('returns 503 when hasRunAccess throws', async () => { - const logger = { error: jest.fn() }; + const logger = { info: jest.fn(), error: jest.fn() }; const workflowPort = createMockWorkflowPort({ hasRunAccess: jest.fn().mockRejectedValue(new Error('orchestrator down')), }); diff --git a/packages/workflow-executor/test/runner.test.ts b/packages/workflow-executor/test/runner.test.ts index fadb7c6efa..9adf128e05 100644 --- a/packages/workflow-executor/test/runner.test.ts +++ b/packages/workflow-executor/test/runner.test.ts @@ -58,7 +58,7 @@ function createMockAiClient() { } function createMockLogger(): jest.Mocked> { - return { error: jest.fn(), info: jest.fn() }; + return { info: jest.fn(), error: jest.fn() }; } const VALID_ENV_SECRET = 'a'.repeat(64); @@ -708,7 +708,7 @@ describe('StepExecutorFactory.create — factory', () => { workflowPort: {} as WorkflowPort, runStore: {} as RunStore, schemaCache: new SchemaCache(), - logger: { error: jest.fn() }, + logger: { info: jest.fn(), error: jest.fn() }, }); it('dispatches Condition steps to ConditionStepExecutor', async () => { @@ -774,7 +774,7 @@ describe('StepExecutorFactory.create — factory', () => { const rootCause = new Error('root cause'); const error = new Error('wrapper'); (error as Error & { cause: Error }).cause = rootCause; - const logger = { error: jest.fn() }; + const logger = { info: jest.fn(), error: jest.fn() }; const contextConfig: StepContextConfig = { ...makeContextConfig(), aiClient: { @@ -796,7 +796,7 @@ describe('StepExecutorFactory.create — factory', () => { it('logs cause as undefined when construction error cause is not an Error instance', async () => { const error = new Error('wrapper'); (error as Error & { cause: string }).cause = 'plain string'; - const logger = { error: jest.fn() }; + const logger = { info: jest.fn(), error: jest.fn() }; const contextConfig: StepContextConfig = { ...makeContextConfig(), aiClient: { From 4ecbeb0076cf877bdb5e9a30e6298a54b7a89d9a Mon Sep 17 00:00:00 2001 From: alban bertolini Date: Thu, 26 Mar 2026 16:09:35 +0100 Subject: [PATCH 2/4] fix(workflow-executor): add info to logger mock type in HTTP server tests Co-Authored-By: Claude Opus 4.6 (1M context) --- .../workflow-executor/test/http/executor-http-server.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/workflow-executor/test/http/executor-http-server.test.ts b/packages/workflow-executor/test/http/executor-http-server.test.ts index 0797bbce36..9981b96be8 100644 --- a/packages/workflow-executor/test/http/executor-http-server.test.ts +++ b/packages/workflow-executor/test/http/executor-http-server.test.ts @@ -45,7 +45,7 @@ function createServer( overrides: { runner?: Runner; workflowPort?: WorkflowPort; - logger?: { error: jest.Mock }; + logger?: { info: jest.Mock; error: jest.Mock }; } = {}, ) { return new ExecutorHttpServer({ From 31cdb47d0df1040682f8dd9b75dee63266022242 Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 27 Mar 2026 14:42:05 +0100 Subject: [PATCH 3/4] fix: resolve rebase conflicts for info logging on top of graceful shutdown - Remove duplicate info method in Logger interface and ConsoleLogger - Update console-logger test to match console.log implementation with level field - Add missing info mock in database-store tests Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/workflow-executor/src/adapters/console-logger.ts | 4 ---- packages/workflow-executor/src/ports/logger-port.ts | 1 - .../workflow-executor/test/adapters/console-logger.test.ts | 6 +++--- .../workflow-executor/test/stores/database-store.test.ts | 4 ++-- 4 files changed, 5 insertions(+), 10 deletions(-) diff --git a/packages/workflow-executor/src/adapters/console-logger.ts b/packages/workflow-executor/src/adapters/console-logger.ts index 6c07d21f7b..d6a6457640 100644 --- a/packages/workflow-executor/src/adapters/console-logger.ts +++ b/packages/workflow-executor/src/adapters/console-logger.ts @@ -13,8 +13,4 @@ export default class ConsoleLogger implements Logger { JSON.stringify({ level: 'error', message, timestamp: new Date().toISOString(), ...context }), ); } - - info(message: string, context: Record): void { - console.info(JSON.stringify({ message, timestamp: new Date().toISOString(), ...context })); - } } diff --git a/packages/workflow-executor/src/ports/logger-port.ts b/packages/workflow-executor/src/ports/logger-port.ts index 406cf1ec8f..51d382f48d 100644 --- a/packages/workflow-executor/src/ports/logger-port.ts +++ b/packages/workflow-executor/src/ports/logger-port.ts @@ -1,5 +1,4 @@ export interface Logger { info(message: string, context: Record): void; error(message: string, context: Record): void; - info?(message: string, context: Record): void; } diff --git a/packages/workflow-executor/test/adapters/console-logger.test.ts b/packages/workflow-executor/test/adapters/console-logger.test.ts index ba354fdf2f..39aac60830 100644 --- a/packages/workflow-executor/test/adapters/console-logger.test.ts +++ b/packages/workflow-executor/test/adapters/console-logger.test.ts @@ -7,14 +7,14 @@ describe('ConsoleLogger', () => { logger = new ConsoleLogger(); }); - it('info() writes to console.info as JSON', () => { - const spy = jest.spyOn(console, 'info').mockImplementation(); + it('info() writes to console.log as JSON with level info', () => { + const spy = jest.spyOn(console, 'log').mockImplementation(); logger.info('test message', { key: 'value' }); expect(spy).toHaveBeenCalledTimes(1); const output = JSON.parse(spy.mock.calls[0][0]); - expect(output).toMatchObject({ message: 'test message', key: 'value' }); + expect(output).toMatchObject({ level: 'info', message: 'test message', key: 'value' }); expect(output.timestamp).toBeDefined(); spy.mockRestore(); diff --git a/packages/workflow-executor/test/stores/database-store.test.ts b/packages/workflow-executor/test/stores/database-store.test.ts index 4f60ea60ee..cab80c9bae 100644 --- a/packages/workflow-executor/test/stores/database-store.test.ts +++ b/packages/workflow-executor/test/stores/database-store.test.ts @@ -118,7 +118,7 @@ describe('DatabaseStore (SQLite)', () => { .spyOn(badSequelize.getQueryInterface(), 'createTable') .mockRejectedValueOnce(new Error('disk full')); - const logger = { error: jest.fn() }; + const logger = { info: jest.fn(), error: jest.fn() }; await expect(badStore.init(logger)).rejects.toThrow('disk full'); expect(logger.error).toHaveBeenCalledWith( 'Database migration failed', @@ -129,7 +129,7 @@ describe('DatabaseStore (SQLite)', () => { }); it('close() catches and logs errors instead of throwing', async () => { - const logger = { error: jest.fn() }; + const logger = { info: jest.fn(), error: jest.fn() }; jest.spyOn(sequelize, 'close').mockRejectedValueOnce(new Error('close failed')); await expect(store.close(logger)).resolves.toBeUndefined(); From 7802ae36e59f7f36a8b66b5aff93fa8c0a57e7f5 Mon Sep 17 00:00:00 2001 From: alban bertolini Date: Fri, 27 Mar 2026 17:30:06 +0100 Subject: [PATCH 4/4] refactor(workflow-executor): remove redundant level field from ConsoleLogger MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit console.log (info) vs console.error (error) already distinguishes the level via stdout/stderr. The JSON level field is redundant — users who need it can implement their own Logger. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/adapters/console-logger.ts | 13 ++++--------- packages/workflow-executor/src/ports/logger-port.ts | 2 +- .../test/adapters/console-logger.test.ts | 6 +++--- 3 files changed, 8 insertions(+), 13 deletions(-) diff --git a/packages/workflow-executor/src/adapters/console-logger.ts b/packages/workflow-executor/src/adapters/console-logger.ts index d6a6457640..cb9ca735a1 100644 --- a/packages/workflow-executor/src/adapters/console-logger.ts +++ b/packages/workflow-executor/src/adapters/console-logger.ts @@ -1,16 +1,11 @@ import type { Logger } from '../ports/logger-port'; export default class ConsoleLogger implements Logger { - info(message: string, context: Record): void { - // eslint-disable-next-line no-console - console.log( - JSON.stringify({ level: 'info', message, timestamp: new Date().toISOString(), ...context }), - ); + error(message: string, context: Record): void { + console.error(JSON.stringify({ message, timestamp: new Date().toISOString(), ...context })); } - error(message: string, context: Record): void { - console.error( - JSON.stringify({ level: 'error', message, timestamp: new Date().toISOString(), ...context }), - ); + info(message: string, context: Record): void { + console.info(JSON.stringify({ message, timestamp: new Date().toISOString(), ...context })); } } diff --git a/packages/workflow-executor/src/ports/logger-port.ts b/packages/workflow-executor/src/ports/logger-port.ts index 51d382f48d..e615eb7e4b 100644 --- a/packages/workflow-executor/src/ports/logger-port.ts +++ b/packages/workflow-executor/src/ports/logger-port.ts @@ -1,4 +1,4 @@ export interface Logger { - info(message: string, context: Record): void; error(message: string, context: Record): void; + info(message: string, context: Record): void; } diff --git a/packages/workflow-executor/test/adapters/console-logger.test.ts b/packages/workflow-executor/test/adapters/console-logger.test.ts index 39aac60830..ba354fdf2f 100644 --- a/packages/workflow-executor/test/adapters/console-logger.test.ts +++ b/packages/workflow-executor/test/adapters/console-logger.test.ts @@ -7,14 +7,14 @@ describe('ConsoleLogger', () => { logger = new ConsoleLogger(); }); - it('info() writes to console.log as JSON with level info', () => { - const spy = jest.spyOn(console, 'log').mockImplementation(); + it('info() writes to console.info as JSON', () => { + const spy = jest.spyOn(console, 'info').mockImplementation(); logger.info('test message', { key: 'value' }); expect(spy).toHaveBeenCalledTimes(1); const output = JSON.parse(spy.mock.calls[0][0]); - expect(output).toMatchObject({ level: 'info', message: 'test message', key: 'value' }); + expect(output).toMatchObject({ message: 'test message', key: 'value' }); expect(output.timestamp).toBeDefined(); spy.mockRestore();