@outputai/core 0.6.0 → 0.6.1-dev.aab2335.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/package.json +2 -2
  2. package/src/activity_integration/context.d.ts +5 -9
  3. package/src/activity_integration/context.js +5 -4
  4. package/src/activity_integration/context.spec.js +10 -15
  5. package/src/activity_integration/events.d.ts +2 -4
  6. package/src/activity_integration/events.js +8 -3
  7. package/src/activity_integration/events.spec.js +58 -29
  8. package/src/bus.js +18 -9
  9. package/src/bus.spec.js +30 -0
  10. package/src/hooks/index.d.ts +112 -58
  11. package/src/hooks/index.js +15 -12
  12. package/src/hooks/index.spec.js +60 -32
  13. package/src/interface/workflow.js +19 -35
  14. package/src/interface/workflow.spec.js +104 -15
  15. package/src/internal_activities/index.js +3 -3
  16. package/src/internal_activities/index.spec.js +31 -1
  17. package/src/internal_utils/temporal_context.js +12 -0
  18. package/src/internal_utils/temporal_context.spec.ts +83 -0
  19. package/src/internal_utils/trace_info.js +21 -0
  20. package/src/internal_utils/trace_info.spec.js +47 -0
  21. package/src/internal_utils/workflow_context.js +29 -0
  22. package/src/internal_utils/workflow_context.spec.js +46 -0
  23. package/src/logger/development.js +61 -0
  24. package/src/logger/development.spec.js +70 -0
  25. package/src/logger/index.js +14 -0
  26. package/src/logger/index.spec.js +27 -0
  27. package/src/logger/production.js +15 -0
  28. package/src/logger/production.spec.js +52 -0
  29. package/src/tracing/internal_interface.js +4 -4
  30. package/src/tracing/processors/local/index.js +21 -26
  31. package/src/tracing/processors/local/index.spec.js +39 -45
  32. package/src/tracing/processors/s3/index.js +13 -23
  33. package/src/tracing/processors/s3/index.spec.js +33 -26
  34. package/src/tracing/trace_attribute.js +0 -1
  35. package/src/tracing/trace_engine.js +8 -12
  36. package/src/tracing/trace_engine.spec.js +31 -27
  37. package/src/worker/configs.js +2 -0
  38. package/src/worker/configs.spec.js +25 -0
  39. package/src/worker/index.js +4 -1
  40. package/src/worker/index.spec.js +4 -0
  41. package/src/worker/interceptors/activity.js +31 -29
  42. package/src/worker/interceptors/activity.spec.js +58 -26
  43. package/src/worker/interceptors/workflow.js +7 -2
  44. package/src/worker/interceptors/workflow.spec.js +42 -6
  45. package/src/worker/log_hooks.js +35 -46
  46. package/src/worker/log_hooks.spec.js +43 -46
  47. package/src/worker/setup_telemetry.js +19 -0
  48. package/src/worker/setup_telemetry.spec.js +80 -0
  49. package/src/worker/sinks.js +24 -24
  50. package/src/interface/workflow_context.js +0 -33
  51. package/src/logger.js +0 -73
@@ -0,0 +1,47 @@
1
+ import { describe, expect, it, vi, beforeEach } from 'vitest';
2
+ import { TraceInfo } from './trace_info.js';
3
+
4
+ const inWorkflowContextMock = vi.hoisted( () => vi.fn() );
5
+ const workflowInfoMock = vi.hoisted( () => vi.fn() );
6
+
7
+ vi.mock( '@temporalio/workflow', () => ( {
8
+ inWorkflowContext: inWorkflowContextMock,
9
+ workflowInfo: workflowInfoMock
10
+ } ) );
11
+
12
+ describe( 'TraceInfo', () => {
13
+ beforeEach( () => {
14
+ vi.clearAllMocks();
15
+ } );
16
+
17
+ it( 'builds trace info from Temporal workflow info in workflow context', () => {
18
+ inWorkflowContextMock.mockReturnValue( true );
19
+ workflowInfoMock.mockReturnValue( {
20
+ workflowId: 'workflow-id',
21
+ workflowType: 'workflow-type',
22
+ runId: 'run-id',
23
+ startTime: new Date( '2026-06-02T09:00:00.000Z' )
24
+ } );
25
+
26
+ expect( TraceInfo.build( { disableTrace: false } ) ).toEqual( {
27
+ workflowId: 'workflow-id',
28
+ workflowType: 'workflow-type',
29
+ runId: 'run-id',
30
+ startTime: Date.parse( '2026-06-02T09:00:00.000Z' ),
31
+ disableTrace: false
32
+ } );
33
+ } );
34
+
35
+ it( 'builds trace info without Temporal fields outside workflow context', () => {
36
+ inWorkflowContextMock.mockReturnValue( false );
37
+
38
+ expect( TraceInfo.build( { disableTrace: true } ) ).toEqual( {
39
+ workflowId: undefined,
40
+ workflowType: undefined,
41
+ runId: undefined,
42
+ startTime: undefined,
43
+ disableTrace: true
44
+ } );
45
+ expect( workflowInfoMock ).not.toHaveBeenCalled();
46
+ } );
47
+ } );
@@ -0,0 +1,29 @@
1
+ import { workflowInfo, continueAsNew, inWorkflowContext } from '@temporalio/workflow';
2
+
3
+ export class WorkflowContext {
4
+
5
+ /**
6
+ * Builds a new context instance
7
+ * @returns {object} context
8
+ */
9
+ static build() {
10
+ if ( !inWorkflowContext() ) {
11
+ return {
12
+ control: {
13
+ continueAsNew: async () => {},
14
+ isContinueAsNewSuggested: () => false
15
+ },
16
+ info: { workflowId: 'test-workflow', runId: 'test-run' }
17
+ };
18
+ }
19
+
20
+ const { workflowId, runId } = workflowInfo();
21
+ return {
22
+ control: {
23
+ continueAsNew,
24
+ isContinueAsNewSuggested: () => workflowInfo().continueAsNewSuggested
25
+ },
26
+ info: { workflowId, runId }
27
+ };
28
+ }
29
+ };
@@ -0,0 +1,46 @@
1
+ import { describe, expect, it, vi, beforeEach } from 'vitest';
2
+ import { WorkflowContext } from './workflow_context.js';
3
+
4
+ const inWorkflowContextMock = vi.hoisted( () => vi.fn() );
5
+ const workflowInfoMock = vi.hoisted( () => vi.fn() );
6
+ const continueAsNewMock = vi.hoisted( () => vi.fn() );
7
+
8
+ vi.mock( '@temporalio/workflow', () => ( {
9
+ continueAsNew: continueAsNewMock,
10
+ inWorkflowContext: inWorkflowContextMock,
11
+ workflowInfo: workflowInfoMock
12
+ } ) );
13
+
14
+ describe( 'WorkflowContext', () => {
15
+ beforeEach( () => {
16
+ vi.clearAllMocks();
17
+ } );
18
+
19
+ it( 'builds a test context outside Temporal workflow context', async () => {
20
+ inWorkflowContextMock.mockReturnValue( false );
21
+
22
+ const context = WorkflowContext.build();
23
+
24
+ expect( context.info ).toEqual( { workflowId: 'test-workflow', runId: 'test-run' } );
25
+ expect( context.control.isContinueAsNewSuggested() ).toBe( false );
26
+ await expect( context.control.continueAsNew() ).resolves.toBeUndefined();
27
+ expect( workflowInfoMock ).not.toHaveBeenCalled();
28
+ expect( continueAsNewMock ).not.toHaveBeenCalled();
29
+ } );
30
+
31
+ it( 'builds a workflow context from Temporal workflow info', () => {
32
+ inWorkflowContextMock.mockReturnValue( true );
33
+ workflowInfoMock.mockReturnValue( {
34
+ workflowId: 'workflow-id',
35
+ runId: 'run-id',
36
+ continueAsNewSuggested: true
37
+ } );
38
+
39
+ const context = WorkflowContext.build();
40
+
41
+ expect( context.info ).toEqual( { workflowId: 'workflow-id', runId: 'run-id' } );
42
+ expect( context.control.continueAsNew ).toBe( continueAsNewMock );
43
+ expect( context.control.isContinueAsNewSuggested() ).toBe( true );
44
+ expect( workflowInfoMock ).toHaveBeenCalledTimes( 2 );
45
+ } );
46
+ } );
@@ -0,0 +1,61 @@
1
+ import { format, transports } from 'winston';
2
+ import { isPlainObject, shuffleArray } from '#utils';
3
+
4
+ /** Available colors enum */
5
+ const Color = {
6
+ Blue: '033',
7
+ Green: '030',
8
+ Orange: '208',
9
+ Turquoise: '045',
10
+ Purple: '129',
11
+ Yellow: '184'
12
+ };
13
+
14
+ /**
15
+ * Recursively format object as friendly JSON: { name: "foo", count: 5 }
16
+ *
17
+ * @param {any} v - The input value
18
+ * @returns {string} Formatted result
19
+ */
20
+ export const formatJson = v => {
21
+ if ( isPlainObject( v ) ) {
22
+ const entries = Object.entries( v );
23
+ return entries.length === 0 ? '{}' :
24
+ `{ ${entries.map( ( [ k, v ] ) => `${k}: ${formatJson( v )}` ).join( ', ' )} }`;
25
+ }
26
+ if ( Array.isArray( v ) ) {
27
+ return v.length === 0 ? '[]' : `[ ${v.map( p => formatJson( p ) ).join( ', ' )} ]`;
28
+ }
29
+ if ( typeof v === 'string' ) {
30
+ return JSON.stringify( v );
31
+ }
32
+ return v;
33
+ };
34
+
35
+ /** This instance color schema */
36
+ const COLORS = shuffleArray( Object.values( Color ) );
37
+
38
+ /** Stores all assigned colors per namespace. */
39
+ const assignedColors = new Map();
40
+
41
+ /**
42
+ * Get the previous assigned color for this value or if not present, assign a new one and store it
43
+ * @param {string} v - A text value
44
+ * @returns {string} The color
45
+ * */
46
+ const getColor = v =>
47
+ assignedColors.get( v ) ?? assignedColors.set( v, COLORS[assignedColors.size % COLORS.length] ).get( v );
48
+
49
+ export const options = {
50
+ level: 'debug',
51
+ transports: [ new transports.Console() ],
52
+ format: format.combine(
53
+ format.colorize(),
54
+ format.metadata(),
55
+ format.printf( ( { level, message, metadata } ) => {
56
+ const { namespace, ...fields } = metadata;
57
+ const jsonText = Object.keys( fields ).length > 0 ? formatJson( fields ) : null;
58
+ return `[${level}] \x1b[38;5;${getColor( namespace )}m${namespace}: ${message}\x1b[0m${jsonText ? ' ' + jsonText : ''}`;
59
+ } )
60
+ )
61
+ };
@@ -0,0 +1,70 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+
3
+ const LEVEL = Symbol.for( 'level' );
4
+ const MESSAGE = Symbol.for( 'message' );
5
+
6
+ vi.mock( '#utils', () => ( {
7
+ isPlainObject: v => Object.prototype.toString.call( v ) === '[object Object]',
8
+ shuffleArray: v => v
9
+ } ) );
10
+
11
+ const loadDevelopmentLogger = async () => {
12
+ vi.resetModules();
13
+ return import( './development.js' );
14
+ };
15
+
16
+ describe( 'logger/development', () => {
17
+ describe( 'formatJson', () => {
18
+ it( 'formats nested plain objects and arrays', async () => {
19
+ const { formatJson } = await loadDevelopmentLogger();
20
+
21
+ expect( formatJson( {
22
+ name: 'foo',
23
+ count: 5,
24
+ nested: { ok: true },
25
+ list: [ 1, 'two', { three: 3 } ]
26
+ } ) ).toBe( '{ name: "foo", count: 5, nested: { ok: true }, list: [ 1, "two", { three: 3 } ] }' );
27
+ } );
28
+
29
+ it( 'formats empty objects and arrays', async () => {
30
+ const { formatJson } = await loadDevelopmentLogger();
31
+
32
+ expect( formatJson( { emptyObject: {}, emptyArray: [] } ) ).toBe( '{ emptyObject: {}, emptyArray: [] }' );
33
+ } );
34
+
35
+ it( 'escapes string values', async () => {
36
+ const { formatJson } = await loadDevelopmentLogger();
37
+
38
+ expect( formatJson( { quote: 'hello "world"' } ) ).toBe( '{ quote: "hello \\"world\\"" }' );
39
+ } );
40
+ } );
41
+
42
+ describe( 'options.format', () => {
43
+ it( 'formats level, namespace, message, and metadata fields', async () => {
44
+ const { options } = await loadDevelopmentLogger();
45
+ const info = options.format.transform( {
46
+ [LEVEL]: 'info',
47
+ level: 'info',
48
+ message: 'Worker',
49
+ namespace: 'Telemetry',
50
+ status: { runState: 'RUNNING' },
51
+ memory: { heapUsed: 123 }
52
+ } );
53
+
54
+ expect( info[MESSAGE] ).toMatch( /^\[\x1b\[[\d;]+minfo\x1b\[[\d;]+m\] \x1b\[38;5;033mTelemetry: Worker\x1b\[0m /u );
55
+ expect( info[MESSAGE] ).toContain( '{ status: { runState: "RUNNING" }, memory: { heapUsed: 123 } }' );
56
+ } );
57
+
58
+ it( 'does not append metadata text when only namespace is present', async () => {
59
+ const { options } = await loadDevelopmentLogger();
60
+ const info = options.format.transform( {
61
+ [LEVEL]: 'debug',
62
+ level: 'debug',
63
+ message: 'Loading config...',
64
+ namespace: 'Worker'
65
+ } );
66
+
67
+ expect( info[MESSAGE] ).toMatch( /^\[\x1b\[[\d;]+mdebug\x1b\[[\d;]+m\] \x1b\[38;5;033mWorker: Loading config\.\.\.\x1b\[0m$/u );
68
+ } );
69
+ } );
70
+ } );
@@ -0,0 +1,14 @@
1
+ import winston from 'winston';
2
+
3
+ const { options } = await import( process.env.NODE_ENV === 'production' ? './production.js' : './development.js' );
4
+
5
+ // creates the root winston logger
6
+ const logger = winston.createLogger( options );
7
+
8
+ /**
9
+ * Creates a child logger with a specific namespace
10
+ *
11
+ * @param {string} namespace - The namespace for this logger (e.g., 'Scanner', 'Tracing')
12
+ * @returns {winston.Logger} Child logger instance with namespace metadata
13
+ */
14
+ export const createChildLogger = namespace => logger.child( { namespace } );
@@ -0,0 +1,27 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ const loadLogger = async nodeEnv => {
4
+ vi.resetModules();
5
+ vi.stubEnv( 'NODE_ENV', nodeEnv );
6
+ return import( './index.js' );
7
+ };
8
+
9
+ describe( 'logger/index', () => {
10
+ afterEach( () => {
11
+ vi.unstubAllEnvs();
12
+ } );
13
+
14
+ it( 'loads the development logger options', async () => {
15
+ const { createChildLogger } = await loadLogger( 'development' );
16
+ const log = createChildLogger( 'Test' );
17
+
18
+ expect( typeof log.info ).toBe( 'function' );
19
+ } );
20
+
21
+ it( 'loads the production logger options', async () => {
22
+ const { createChildLogger } = await loadLogger( 'production' );
23
+ const log = createChildLogger( 'Test' );
24
+
25
+ expect( typeof log.info ).toBe( 'function' );
26
+ } );
27
+ } );
@@ -0,0 +1,15 @@
1
+ import { transports, format } from 'winston';
2
+
3
+ export const options = {
4
+ level: 'info',
5
+ transports: [ new transports.Console() ],
6
+ defaultMeta: {
7
+ service: 'output-worker',
8
+ environment: 'production'
9
+ },
10
+ format: format.combine(
11
+ format.timestamp( { format: 'YYYY-MM-DDTHH:mm:ss.SSSZ' } ),
12
+ format.errors( { stack: true } ),
13
+ format.json()
14
+ )
15
+ };
@@ -0,0 +1,52 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { options } from './production.js';
3
+
4
+ const LEVEL = Symbol.for( 'level' );
5
+ const MESSAGE = Symbol.for( 'message' );
6
+
7
+ describe( 'logger/production', () => {
8
+ it( 'uses info level and default production metadata', () => {
9
+ expect( options.level ).toBe( 'info' );
10
+ expect( options.defaultMeta ).toEqual( {
11
+ service: 'output-worker',
12
+ environment: 'production'
13
+ } );
14
+ } );
15
+
16
+ it( 'formats logs as JSON with timestamp and metadata fields', () => {
17
+ const info = options.format.transform( {
18
+ [LEVEL]: 'info',
19
+ level: 'info',
20
+ message: 'Worker',
21
+ namespace: 'Telemetry',
22
+ status: { runState: 'RUNNING' }
23
+ } );
24
+
25
+ const output = JSON.parse( info[MESSAGE] );
26
+
27
+ expect( output ).toMatchObject( {
28
+ level: 'info',
29
+ message: 'Worker',
30
+ namespace: 'Telemetry',
31
+ status: { runState: 'RUNNING' }
32
+ } );
33
+ expect( output.timestamp ).toEqual( expect.stringMatching( /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}[+-]\d{2}:\d{2}$/u ) );
34
+ } );
35
+
36
+ it( 'includes error stack when formatting Error messages', () => {
37
+ const error = new Error( 'boom' );
38
+ const info = options.format.transform( {
39
+ [LEVEL]: 'error',
40
+ level: 'error',
41
+ message: error
42
+ } );
43
+
44
+ const output = JSON.parse( info[MESSAGE] );
45
+
46
+ expect( output ).toMatchObject( {
47
+ level: 'error',
48
+ message: 'boom'
49
+ } );
50
+ expect( output.stack ).toContain( 'Error: boom' );
51
+ } );
52
+ } );
@@ -32,7 +32,7 @@ export { EventAction };
32
32
  * @param {string} args.name - The human-friendly name of the Event: query, request, create.
33
33
  * @param {any} args.details - Arbitrary data to add to this event, it will be used as the "input" field.
34
34
  * @param {string} args.parentId - The parent Event, used to build a tree.
35
- * @param {object} args.executionContext - The original execution context from the workflow
35
+ * @param {object} args.traceInfo - The trace information object, propagated from the root-most workflow.
36
36
  * @returns {void}
37
37
  */
38
38
  export const addEventStart = options => addEventAction( EventAction.START, options );
@@ -45,7 +45,7 @@ export const addEventStart = options => addEventAction( EventAction.START, optio
45
45
  * @param {object} args
46
46
  * @param {string} args.id - The id of the event to conclude.
47
47
  * @param {any} args.details - Arbitrary data to add to the event; it is used as the "output" field.
48
- * @param {object} args.executionContext - The original execution context from the workflow
48
+ * @param {object} args.traceInfo - The trace information object, propagated from the root-most workflow.
49
49
  * @returns {void}
50
50
  */
51
51
  export const addEventEnd = options => addEventAction( EventAction.END, options );
@@ -58,7 +58,7 @@ export const addEventEnd = options => addEventAction( EventAction.END, options )
58
58
  * @param {object} args
59
59
  * @param {string} args.id - The id of the event to conclude.
60
60
  * @param {any} args.details - Arbitrary data to add to the event; it is used as the "error" field.
61
- * @param {object} args.executionContext - The original execution context from the workflow
61
+ * @param {object} args.traceInfo - The trace information object, propagated from the root-most workflow.
62
62
  * @returns {void}
63
63
  */
64
64
  export const addEventError = options => addEventAction( EventAction.ERROR, options );
@@ -71,7 +71,7 @@ export const addEventError = options => addEventAction( EventAction.ERROR, optio
71
71
  * @param {object} args
72
72
  * @param {string} args.id - The id of the event to attach the attribute to.
73
73
  * @param {object} args.details - The attribute to add to this event, must be in `{ name: string, value: any }` format.
74
- * @param {object} args.executionContext - The original execution context from the workflow
74
+ * @param {object} args.traceInfo - The trace information object, propagated from the root-most workflow.
75
75
  * @returns {void}
76
76
  */
77
77
  export const addEventAttribute = options => addEventAction( EventAction.ADD_ATTR, options );
@@ -19,10 +19,10 @@ const tempTraceFilesDir = join( __dirname, 'temp', 'traces' );
19
19
  /**
20
20
  * Builds the temp file path to accumulate trace entries
21
21
  *
22
- * @param {object} executionContext - The execution context around a given trace entry
22
+ * @param {object} traceInfo - Trace information object
23
23
  * @returns {string}
24
24
  */
25
- const createTempFilePath = ( { workflowId, startTime } ) => join( tempTraceFilesDir, `${startTime}_${workflowId}.trace` );
25
+ const createTempFilePath = ( { startTime, runId } ) => join( tempTraceFilesDir, `${startTime}_${runId}.trace` );
26
26
 
27
27
  /**
28
28
  * Adds a trace entry to the accumulation file.
@@ -49,18 +49,18 @@ const cleanupOldTempFiles = ( threshold = Date.now() - tempFilesTTL ) =>
49
49
 
50
50
  /**
51
51
  * Resolves the deep folder structure that stores a workflow trace.
52
- * @param {string} workflowName - Name of the workflow
52
+ * @param {string} workflowType
53
53
  * @returns {string}
54
54
  */
55
- const resolveTraceFolder = workflowName => join( 'runs', workflowName );
55
+ const resolveTraceFolder = workflowType => join( 'runs', workflowType );
56
56
 
57
57
  /**
58
58
  * Resolves the local file system path for ALL file I/O operations (read/write)
59
59
  * Uses the project root path
60
- * @param {string} workflowName - The name of the workflow
60
+ * @param {string} workflowType
61
61
  * @returns {string} The local filesystem path for file operations
62
62
  */
63
- const resolveIOPath = workflowName => join( callerDir, 'logs', resolveTraceFolder( workflowName ) );
63
+ const resolveIOPath = workflowType => join( callerDir, 'logs', resolveTraceFolder( workflowType ) );
64
64
 
65
65
  /**
66
66
  * Resolves the file path to be reported as the trace destination.
@@ -71,19 +71,17 @@ const resolveIOPath = workflowName => join( callerDir, 'logs', resolveTraceFolde
71
71
  *
72
72
  * If the env variable is not present, it falls back to the same value used to write files locally.
73
73
  *
74
- * @param {string} workflowName - The name of the workflow
74
+ * @param {string} workflowType - The name of the workflow
75
75
  * @returns {string} The path to report, reflecting the actual filesystem
76
76
  */
77
- const resolveReportPath = workflowName => process.env.OUTPUT_TRACE_HOST_PATH ?
78
- join( process.env.OUTPUT_TRACE_HOST_PATH, resolveTraceFolder( workflowName ) ) :
79
- resolveIOPath( workflowName );
77
+ const resolveReportPath = workflowType => process.env.OUTPUT_TRACE_HOST_PATH ?
78
+ join( process.env.OUTPUT_TRACE_HOST_PATH, resolveTraceFolder( workflowType ) ) :
79
+ resolveIOPath( workflowType );
80
80
 
81
81
  /**
82
82
  * Builds the actual trace filename
83
83
  *
84
- * @param {object} options
85
- * @param {number} options.startTime
86
- * @param {string} options.workflowId
84
+ * @param {object} traceInfo - The trace information object
87
85
  * @returns {string}
88
86
  */
89
87
  const buildTraceFilename = ( { startTime, workflowId } ) => {
@@ -108,15 +106,15 @@ export const init = () => {
108
106
  *
109
107
  * @param {object} args
110
108
  * @param {object} args.entry - The trace entry to append.
111
- * @param {object} args.executionContext - Execution info: workflowId, workflowName, startTime
109
+ * @param {object} args.traceInfo - Trace information object
112
110
  * @returns {void}
113
111
  */
114
- export const exec = async ( { entry, executionContext } ) => {
115
- const { workflowId, workflowName, startTime } = executionContext;
116
- const tempFilePath = createTempFilePath( executionContext );
112
+ export const exec = async ( { entry, traceInfo } ) => {
113
+ const { runId, workflowType } = traceInfo;
114
+ const tempFilePath = createTempFilePath( traceInfo );
117
115
  addEntry( entry, tempFilePath );
118
116
 
119
- const isRootWorkflowEnd = entry.id === workflowId && entry.action !== 'start';
117
+ const isRootWorkflowEnd = entry.id === runId && entry.action !== 'start';
120
118
  const isError = entry.action === 'error';
121
119
 
122
120
  if ( !isRootWorkflowEnd && !isError ) {
@@ -124,8 +122,8 @@ export const exec = async ( { entry, executionContext } ) => {
124
122
  }
125
123
 
126
124
  const content = buildTraceTree( getEntries( tempFilePath ) );
127
- const dir = resolveIOPath( workflowName );
128
- const path = join( dir, buildTraceFilename( { startTime, workflowId } ) );
125
+ const dir = resolveIOPath( workflowType );
126
+ const path = join( dir, buildTraceFilename( traceInfo ) );
129
127
 
130
128
  mkdirSync( dir, { recursive: true } );
131
129
 
@@ -140,11 +138,8 @@ export const exec = async ( { entry, executionContext } ) => {
140
138
  *
141
139
  * This uses the optional OUTPUT_TRACE_HOST_PATH to return values relative to the host OS, not the container, if applicable.
142
140
  *
143
- * @param {object} executionContext
144
- * @param {string} executionContext.startTime - The start time of the workflow
145
- * @param {string} executionContext.workflowId - The id of the workflow execution
146
- * @param {string} executionContext.workflowName - The name of the workflow
141
+ * @param {object} info
147
142
  * @returns {string} The absolute path where the trace will be saved
148
143
  */
149
- export const getDestination = ( { startTime, workflowId, workflowName } ) =>
150
- join( resolveReportPath( workflowName ), buildTraceFilename( { workflowId, startTime } ) );
144
+ export const getDestination = traceInfo =>
145
+ join( resolveReportPath( traceInfo.workflowType ), buildTraceFilename( traceInfo ) );
@@ -52,12 +52,20 @@ vi.mock( 'json-stream-stringify', async () => {
52
52
  const buildTraceTreeMock = vi.fn( entries => ( { count: entries.length } ) );
53
53
  vi.mock( '../../tools/build_trace_tree.js', () => ( { default: buildTraceTreeMock } ) );
54
54
 
55
- /** Flush happens when the root id matches workflowId and action is not 'start', or when action is 'error'. */
56
- const rootStart = ( workflowId, ts ) => ( { id: workflowId, action: 'start', timestamp: ts } );
57
- const rootEnd = ( workflowId, ts ) => ( { id: workflowId, action: 'end', timestamp: ts } );
55
+ /** Flush happens when the root id matches runId and action is not 'start', or when action is 'error'. */
56
+ const rootStart = ( runId, ts ) => ( { id: runId, action: 'start', timestamp: ts } );
57
+ const rootEnd = ( runId, ts ) => ( { id: runId, action: 'end', timestamp: ts } );
58
58
  const childTick = ( id, ts ) => ( { id, action: 'tick', timestamp: ts } );
59
59
 
60
60
  describe( 'tracing/processors/local', () => {
61
+ const startTime = Date.parse( '2020-01-02T03:04:05.678Z' );
62
+ const traceInfo = {
63
+ workflowId: 'id1',
64
+ runId: 'run-1',
65
+ workflowType: 'WF',
66
+ startTime
67
+ };
68
+
61
69
  beforeEach( () => {
62
70
  vi.clearAllMocks();
63
71
  store.files.clear();
@@ -81,13 +89,9 @@ describe( 'tracing/processors/local', () => {
81
89
  const { exec, init } = await import( './index.js' );
82
90
  init();
83
91
 
84
- const startTime = Date.parse( '2020-01-02T03:04:05.678Z' );
85
- const workflowId = 'id1';
86
- const ctx = { executionContext: { workflowId, workflowName: 'WF', startTime } };
87
-
88
- await exec( { ...ctx, entry: rootStart( workflowId, startTime ) } );
89
- await exec( { ...ctx, entry: childTick( 'child-1', startTime + 1 ) } );
90
- await exec( { ...ctx, entry: rootEnd( workflowId, startTime + 2 ) } );
92
+ await exec( { traceInfo, entry: rootStart( traceInfo.runId, startTime ) } );
93
+ await exec( { traceInfo, entry: childTick( 'child-1', startTime + 1 ) } );
94
+ await exec( { traceInfo, entry: rootEnd( traceInfo.runId, startTime + 2 ) } );
91
95
 
92
96
  expect( buildTraceTreeMock ).toHaveBeenCalledTimes( 1 );
93
97
  expect( buildTraceTreeMock.mock.calls[0][0] ).toHaveLength( 3 );
@@ -103,12 +107,8 @@ describe( 'tracing/processors/local', () => {
103
107
  const { exec, init } = await import( './index.js' );
104
108
  init();
105
109
 
106
- const startTime = Date.parse( '2020-01-02T03:04:05.678Z' );
107
- const workflowId = 'id1';
108
- const ctx = { executionContext: { workflowId, workflowName: 'WF', startTime } };
109
-
110
- await exec( { ...ctx, entry: rootStart( workflowId, startTime ) } );
111
- await exec( { ...ctx, entry: childTick( 'child-1', startTime + 1 ) } );
110
+ await exec( { traceInfo, entry: rootStart( traceInfo.runId, startTime ) } );
111
+ await exec( { traceInfo, entry: childTick( 'child-1', startTime + 1 ) } );
112
112
 
113
113
  expect( buildTraceTreeMock ).not.toHaveBeenCalled();
114
114
  expect( writeFileSyncMock ).not.toHaveBeenCalled();
@@ -120,12 +120,8 @@ describe( 'tracing/processors/local', () => {
120
120
  const { exec, init } = await import( './index.js' );
121
121
  init();
122
122
 
123
- const startTime = Date.parse( '2020-01-02T03:04:05.678Z' );
124
- const workflowId = 'id1';
125
- const ctx = { executionContext: { workflowId, workflowName: 'WF', startTime } };
126
-
127
- await exec( { ...ctx, entry: rootStart( workflowId, startTime ) } );
128
- await exec( { ...ctx, entry: { id: 'step-1', action: 'error', timestamp: startTime + 1 } } );
123
+ await exec( { traceInfo, entry: rootStart( traceInfo.runId, startTime ) } );
124
+ await exec( { traceInfo, entry: { id: 'step-1', action: 'error', timestamp: startTime + 1 } } );
129
125
 
130
126
  expect( buildTraceTreeMock ).toHaveBeenCalledTimes( 1 );
131
127
  expect( buildTraceTreeMock.mock.calls[0][0] ).toHaveLength( 2 );
@@ -136,11 +132,11 @@ describe( 'tracing/processors/local', () => {
136
132
  it( 'getDestination(): returns absolute path under callerDir logs', async () => {
137
133
  const { getDestination } = await import( './index.js' );
138
134
 
139
- const startTime = Date.parse( '2020-01-02T03:04:05.678Z' );
140
- const workflowId = 'workflow-id-123';
141
- const workflowName = 'test-workflow';
142
-
143
- const destination = getDestination( { startTime, workflowId, workflowName } );
135
+ const destination = getDestination( {
136
+ ...traceInfo,
137
+ workflowId: 'workflow-id-123',
138
+ workflowType: 'test-workflow'
139
+ } );
144
140
 
145
141
  expect( destination ).toMatch( /^\/|^[A-Z]:\\/i );
146
142
  expect( destination ).toBe(
@@ -155,12 +151,8 @@ describe( 'tracing/processors/local', () => {
155
151
 
156
152
  init();
157
153
 
158
- const startTime = Date.parse( '2020-01-02T03:04:05.678Z' );
159
- const workflowId = 'id1';
160
- const ctx = { executionContext: { workflowId, workflowName: 'WF', startTime } };
161
-
162
- await exec( { ...ctx, entry: rootStart( workflowId, startTime ) } );
163
- await exec( { ...ctx, entry: rootEnd( workflowId, startTime + 1 ) } );
154
+ await exec( { traceInfo, entry: rootStart( traceInfo.runId, startTime ) } );
155
+ await exec( { traceInfo, entry: rootEnd( traceInfo.runId, startTime + 1 ) } );
164
156
 
165
157
  expect( createWriteStreamMock ).toHaveBeenCalledTimes( 1 );
166
158
  const [ writtenPath ] = createWriteStreamMock.mock.calls[0];
@@ -174,11 +166,11 @@ describe( 'tracing/processors/local', () => {
174
166
 
175
167
  process.env.OUTPUT_TRACE_HOST_PATH = '/host/path/logs';
176
168
 
177
- const startTime = Date.parse( '2020-01-02T03:04:05.678Z' );
178
- const workflowId = 'workflow-id-123';
179
- const workflowName = 'test-workflow';
180
-
181
- const destination = getDestination( { startTime, workflowId, workflowName } );
169
+ const destination = getDestination( {
170
+ ...traceInfo,
171
+ workflowId: 'workflow-id-123',
172
+ workflowType: 'test-workflow'
173
+ } );
182
174
 
183
175
  expect( destination ).toBe( '/host/path/logs/runs/test-workflow/2020-01-02-03-04-05-678Z_workflow-id-123.json' );
184
176
  } );
@@ -190,15 +182,17 @@ describe( 'tracing/processors/local', () => {
190
182
 
191
183
  init();
192
184
 
193
- const startTime = Date.parse( '2020-01-02T03:04:05.678Z' );
194
- const workflowId = 'workflow-id-123';
195
- const workflowName = 'test-workflow';
196
- const ctx = { executionContext: { workflowId, workflowName, startTime } };
185
+ const testTraceInfo = {
186
+ ...traceInfo,
187
+ workflowId: 'workflow-id-123',
188
+ runId: 'run-123',
189
+ workflowType: 'test-workflow'
190
+ };
197
191
 
198
- await exec( { ...ctx, entry: rootStart( workflowId, startTime ) } );
199
- await exec( { ...ctx, entry: rootEnd( workflowId, startTime + 1 ) } );
192
+ await exec( { traceInfo: testTraceInfo, entry: rootStart( testTraceInfo.runId, startTime ) } );
193
+ await exec( { traceInfo: testTraceInfo, entry: rootEnd( testTraceInfo.runId, startTime + 1 ) } );
200
194
 
201
- const destination = getDestination( { startTime, workflowId, workflowName } );
195
+ const destination = getDestination( testTraceInfo );
202
196
 
203
197
  const [ writtenPath ] = createWriteStreamMock.mock.calls[0];
204
198
  expect( writtenPath ).not.toContain( '/Users/ben/project' );