@outputai/core 0.6.1-next.fc6a93e.0 → 0.7.1-dev.144d64f.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.
- package/package.json +2 -2
- package/src/hooks/index.d.ts +10 -0
- package/src/interface/webhook.js +1 -1
- package/src/interface/webhook.spec.js +9 -3
- package/src/worker/index.js +1 -1
- package/src/worker/index.spec.js +1 -1
- package/src/worker/interceptors/activity.js +18 -25
- package/src/worker/interceptors/activity.spec.js +1 -1
- package/src/worker/interceptors/headers.spec.js +73 -0
- package/src/worker/{interceptors.js → interceptors/index.js} +2 -2
- package/src/worker/interceptors/workflow.js +3 -7
- package/src/worker/interceptors/workflow.spec.js +1 -1
- package/src/worker/loader_tools.js +1 -1
- package/src/worker/loader_tools.spec.js +36 -0
- /package/src/worker/{sandboxed_utils.js → interceptors/headers.js} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@outputai/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.1-dev.144d64f.0",
|
|
4
4
|
"description": "The core module of the output framework",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"winston": "3.19.0"
|
|
53
53
|
},
|
|
54
54
|
"peerDependencies": {
|
|
55
|
-
"zod": "
|
|
55
|
+
"zod": ">=4.3 <5"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
58
|
"zod": "4.3.6"
|
package/src/hooks/index.d.ts
CHANGED
|
@@ -85,6 +85,8 @@ export interface WorkflowDetails {
|
|
|
85
85
|
export interface ErrorHookPayload {
|
|
86
86
|
/** UUID v4 stamped per emit. Stable per-emit idempotency key. */
|
|
87
87
|
eventId: string;
|
|
88
|
+
/** Timestamp of the event */
|
|
89
|
+
eventDate: number;
|
|
88
90
|
/** Origin of the error: workflow execution, activity execution, or runtime. */
|
|
89
91
|
source: 'workflow' | 'activity' | 'runtime';
|
|
90
92
|
/** Information about the current workflow execution */
|
|
@@ -103,6 +105,8 @@ export interface ErrorHookPayload {
|
|
|
103
105
|
export interface WorkflowStartHookPayload {
|
|
104
106
|
/** UUID v4 stamped per emit. Stable per-emit idempotency key. */
|
|
105
107
|
eventId: string;
|
|
108
|
+
/** Timestamp of the event */
|
|
109
|
+
eventDate: number;
|
|
106
110
|
/** Information about the current workflow execution */
|
|
107
111
|
workflowDetails: WorkflowDetails;
|
|
108
112
|
}
|
|
@@ -113,6 +117,8 @@ export interface WorkflowStartHookPayload {
|
|
|
113
117
|
export interface WorkflowEndHookPayload {
|
|
114
118
|
/** UUID v4 stamped per emit. Stable per-emit idempotency key. */
|
|
115
119
|
eventId: string;
|
|
120
|
+
/** Timestamp of the event */
|
|
121
|
+
eventDate: number;
|
|
116
122
|
/** Information about the current workflow execution */
|
|
117
123
|
workflowDetails: WorkflowDetails;
|
|
118
124
|
}
|
|
@@ -123,6 +129,8 @@ export interface WorkflowEndHookPayload {
|
|
|
123
129
|
export interface WorkflowErrorHookPayload {
|
|
124
130
|
/** UUID v4 stamped per emit. Stable per-emit idempotency key. */
|
|
125
131
|
eventId: string;
|
|
132
|
+
/** Timestamp of the event */
|
|
133
|
+
eventDate: number;
|
|
126
134
|
/** Information about the current workflow execution */
|
|
127
135
|
workflowDetails: WorkflowDetails;
|
|
128
136
|
/** The error thrown. */
|
|
@@ -177,6 +185,8 @@ export declare function onWorkflowError( handler: ( payload: WorkflowErrorHookPa
|
|
|
177
185
|
export interface OnHookEnvelope {
|
|
178
186
|
/** UUID v4 stamped per emit. Stable per-emit idempotency key. */
|
|
179
187
|
eventId: string;
|
|
188
|
+
/** Timestamp of the event */
|
|
189
|
+
eventDate: number;
|
|
180
190
|
/** Information about the current workflow execution */
|
|
181
191
|
workflowDetails: WorkflowDetails;
|
|
182
192
|
/** Temporal's activityInfo(). */
|
package/src/interface/webhook.js
CHANGED
|
@@ -10,6 +10,12 @@ vi.mock( './validations/static.js', () => ( {
|
|
|
10
10
|
validateRequestPayload: validateRequestPayloadMock
|
|
11
11
|
} ) );
|
|
12
12
|
|
|
13
|
+
const activityEnvelope = output => ( {
|
|
14
|
+
__output_activity_wrapper_version: 1,
|
|
15
|
+
output,
|
|
16
|
+
aggregations: null
|
|
17
|
+
} );
|
|
18
|
+
|
|
13
19
|
// Minimal, legible mock of @temporalio/workflow APIs used by webhook.js
|
|
14
20
|
const activityFnMock = vi.fn();
|
|
15
21
|
const proxyActivitiesMock = vi.fn( () => ( { ['__internal#sendHttpRequest']: activityFnMock } ) );
|
|
@@ -70,7 +76,7 @@ describe( 'interface/webhook', () => {
|
|
|
70
76
|
headers: { 'content-type': 'application/json' },
|
|
71
77
|
body: { ok: true }
|
|
72
78
|
};
|
|
73
|
-
activityFnMock.mockResolvedValueOnce( fakeSerializedResponse );
|
|
79
|
+
activityFnMock.mockResolvedValueOnce( activityEnvelope( fakeSerializedResponse ) );
|
|
74
80
|
|
|
75
81
|
const args = { url: 'https://example.com/api', method: 'GET' };
|
|
76
82
|
const res = await sendHttpRequest( args );
|
|
@@ -97,14 +103,14 @@ describe( 'interface/webhook', () => {
|
|
|
97
103
|
const { sendPostRequestAndAwaitWebhook } = await import( './webhook.js' );
|
|
98
104
|
|
|
99
105
|
// Make the inner activity resolve (through sendHttpRequest)
|
|
100
|
-
activityFnMock.mockResolvedValueOnce( {
|
|
106
|
+
activityFnMock.mockResolvedValueOnce( activityEnvelope( {
|
|
101
107
|
url: 'https://webhook.site',
|
|
102
108
|
status: 200,
|
|
103
109
|
statusText: 'OK',
|
|
104
110
|
ok: true,
|
|
105
111
|
headers: {},
|
|
106
112
|
body: null
|
|
107
|
-
} );
|
|
113
|
+
} ) );
|
|
108
114
|
|
|
109
115
|
const url = 'https://webhook.site/ingest';
|
|
110
116
|
const promise = sendPostRequestAndAwaitWebhook( { url, payload: { x: 1 }, headers: { a: 'b' } } );
|
package/src/worker/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import { sinks } from './sinks.js';
|
|
|
5
5
|
import { createCatalog } from './catalog_workflow/index.js';
|
|
6
6
|
import { init as initTracing } from '#tracing';
|
|
7
7
|
import { webpackConfigHook } from './bundler_options.js';
|
|
8
|
-
import { initInterceptors } from './interceptors.js';
|
|
8
|
+
import { initInterceptors } from './interceptors/index.js';
|
|
9
9
|
import { createChildLogger } from '#logger';
|
|
10
10
|
import { registerShutdown } from './shutdown.js';
|
|
11
11
|
import { startCatalog } from './start_catalog.js';
|
package/src/worker/index.spec.js
CHANGED
|
@@ -51,7 +51,7 @@ vi.mock( './catalog_workflow/index.js', () => ( { createCatalog: createCatalogMo
|
|
|
51
51
|
vi.mock( './bundler_options.js', () => ( { webpackConfigHook: vi.fn() } ) );
|
|
52
52
|
|
|
53
53
|
const initInterceptorsMock = vi.fn().mockReturnValue( [] );
|
|
54
|
-
vi.mock( './interceptors.js', () => ( { initInterceptors: initInterceptorsMock } ) );
|
|
54
|
+
vi.mock( './interceptors/index.js', () => ( { initInterceptors: initInterceptorsMock } ) );
|
|
55
55
|
|
|
56
56
|
const startCatalogMock = vi.fn().mockResolvedValue( undefined );
|
|
57
57
|
vi.mock( './start_catalog.js', () => ( { startCatalog: startCatalogMock } ) );
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Context, activityInfo as activityInfoFn } from '@temporalio/activity';
|
|
2
2
|
import { Storage } from '#async_storage';
|
|
3
3
|
import * as Tracing from '#tracing';
|
|
4
|
-
import { headersToObject } from '
|
|
4
|
+
import { headersToObject } from './headers.js';
|
|
5
5
|
import { ACTIVITY_WRAPPER_VERSION_FIELD, BusEventType, METADATA_ACCESS_SYMBOL, Signal } from '#consts';
|
|
6
6
|
import { activityHeartbeatEnabled, activityHeartbeatIntervalMs, namespace } from '../configs.js';
|
|
7
7
|
import { messageBus } from '#bus';
|
|
@@ -29,37 +29,30 @@ const log = createChildLogger( 'ActivityInterceptor' );
|
|
|
29
29
|
*/
|
|
30
30
|
export class ActivityExecutionInterceptor {
|
|
31
31
|
constructor( { activities, workflows, connection } ) {
|
|
32
|
-
|
|
33
|
-
this.
|
|
34
|
-
map
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
32
|
+
// convert activities{} object to a map: activityType:kind
|
|
33
|
+
this.activityKindMap = new Map( Object.entries( activities )
|
|
34
|
+
.map( ( [ type, fn ] ) => ( [ type, fn[METADATA_ACCESS_SYMBOL].type ] ) ) );
|
|
35
|
+
|
|
36
|
+
// convert workflows[] array to a map: workflowType/alias.n:path
|
|
37
|
+
this.workflowsPathMap = new Map( workflows.flatMap( ( { name, aliases, path } ) =>
|
|
38
|
+
[ name, ...aliases ?? [] ].map( a => ( [ a, path ] ) )
|
|
39
|
+
) );
|
|
40
40
|
this.connection = connection;
|
|
41
41
|
};
|
|
42
42
|
|
|
43
|
-
/**
|
|
44
|
-
* Returns a workflow entry by its name or throws error
|
|
45
|
-
* @param {string} workflowType
|
|
46
|
-
* @returns {object} Workflow entry
|
|
47
|
-
* @throws {Error}
|
|
48
|
-
*/
|
|
49
|
-
getWorkflowEntry( workflowType ) {
|
|
50
|
-
const workflowEntry = this.workflowsMap.get( workflowType );
|
|
51
|
-
if ( !workflowEntry ) {
|
|
52
|
-
throw new Error( `Activity interceptor: workflow "${workflowType}" not found in workflowsMap.` );
|
|
53
|
-
}
|
|
54
|
-
return workflowEntry;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
43
|
async execute( input, next ) {
|
|
58
44
|
const activityInfo = activityInfoFn();
|
|
59
45
|
const { workflowExecution: { workflowId, runId }, activityId, activityType, workflowType } = activityInfo;
|
|
60
46
|
const { traceInfo, workflowDetails } = headersToObject( input.headers );
|
|
61
|
-
const
|
|
62
|
-
const
|
|
47
|
+
const outputActivityKind = this.activityKindMap.get( activityType );
|
|
48
|
+
const workflowFilename = this.workflowsPathMap.get( workflowType );
|
|
49
|
+
|
|
50
|
+
if ( !outputActivityKind ) {
|
|
51
|
+
throw new Error( `Activity interceptor: activity "${activityType}" was not registered.` );
|
|
52
|
+
}
|
|
53
|
+
if ( !workflowFilename ) {
|
|
54
|
+
throw new Error( `Activity interceptor: workflow "${workflowType}" was not registered.` );
|
|
55
|
+
}
|
|
63
56
|
|
|
64
57
|
const state = {
|
|
65
58
|
heartbeat: null,
|
|
@@ -73,7 +73,7 @@ vi.mock( '#tracing', () => ( {
|
|
|
73
73
|
addEventError: addEventErrorMock
|
|
74
74
|
} ) );
|
|
75
75
|
|
|
76
|
-
vi.mock( '
|
|
76
|
+
vi.mock( './headers.js', () => ( {
|
|
77
77
|
headersToObject: () => ( { traceInfo: traceInfoMock, workflowDetails: workflowDetailsMock } )
|
|
78
78
|
} ) );
|
|
79
79
|
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
const toPayloadMock = vi.hoisted( () => vi.fn( value => ( { encoded: value } ) ) );
|
|
4
|
+
const fromPayloadMock = vi.hoisted( () => vi.fn( payload => payload.encoded ) );
|
|
5
|
+
|
|
6
|
+
vi.mock( '@temporalio/common', () => ( {
|
|
7
|
+
defaultPayloadConverter: {
|
|
8
|
+
toPayload: toPayloadMock,
|
|
9
|
+
fromPayload: fromPayloadMock
|
|
10
|
+
}
|
|
11
|
+
} ) );
|
|
12
|
+
|
|
13
|
+
describe( 'headers utils', () => {
|
|
14
|
+
beforeEach( () => {
|
|
15
|
+
vi.clearAllMocks();
|
|
16
|
+
} );
|
|
17
|
+
|
|
18
|
+
describe( 'memoToHeaders', () => {
|
|
19
|
+
it( 'converts memo entries into Temporal payload headers', async () => {
|
|
20
|
+
const { memoToHeaders } = await import( './headers.js' );
|
|
21
|
+
const memo = {
|
|
22
|
+
traceInfo: { runId: 'run-1' },
|
|
23
|
+
workflowDetails: { workflowId: 'workflow-1' }
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const headers = memoToHeaders( memo );
|
|
27
|
+
|
|
28
|
+
expect( headers ).toEqual( {
|
|
29
|
+
traceInfo: { encoded: memo.traceInfo },
|
|
30
|
+
workflowDetails: { encoded: memo.workflowDetails }
|
|
31
|
+
} );
|
|
32
|
+
expect( toPayloadMock ).toHaveBeenCalledTimes( 2 );
|
|
33
|
+
expect( toPayloadMock ).toHaveBeenCalledWith( memo.traceInfo );
|
|
34
|
+
expect( toPayloadMock ).toHaveBeenCalledWith( memo.workflowDetails );
|
|
35
|
+
} );
|
|
36
|
+
|
|
37
|
+
it( 'returns an empty object for nullish memo', async () => {
|
|
38
|
+
const { memoToHeaders } = await import( './headers.js' );
|
|
39
|
+
|
|
40
|
+
expect( memoToHeaders() ).toEqual( {} );
|
|
41
|
+
expect( memoToHeaders( null ) ).toEqual( {} );
|
|
42
|
+
expect( toPayloadMock ).not.toHaveBeenCalled();
|
|
43
|
+
} );
|
|
44
|
+
} );
|
|
45
|
+
|
|
46
|
+
describe( 'headersToObject', () => {
|
|
47
|
+
it( 'converts Temporal payload headers into plain object values', async () => {
|
|
48
|
+
const { headersToObject } = await import( './headers.js' );
|
|
49
|
+
const headers = {
|
|
50
|
+
traceInfo: { encoded: { runId: 'run-1' } },
|
|
51
|
+
workflowDetails: { encoded: { workflowId: 'workflow-1' } }
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const object = headersToObject( headers );
|
|
55
|
+
|
|
56
|
+
expect( object ).toEqual( {
|
|
57
|
+
traceInfo: headers.traceInfo.encoded,
|
|
58
|
+
workflowDetails: headers.workflowDetails.encoded
|
|
59
|
+
} );
|
|
60
|
+
expect( fromPayloadMock ).toHaveBeenCalledTimes( 2 );
|
|
61
|
+
expect( fromPayloadMock ).toHaveBeenCalledWith( headers.traceInfo );
|
|
62
|
+
expect( fromPayloadMock ).toHaveBeenCalledWith( headers.workflowDetails );
|
|
63
|
+
} );
|
|
64
|
+
|
|
65
|
+
it( 'returns an empty object for nullish headers', async () => {
|
|
66
|
+
const { headersToObject } = await import( './headers.js' );
|
|
67
|
+
|
|
68
|
+
expect( headersToObject() ).toEqual( {} );
|
|
69
|
+
expect( headersToObject( null ) ).toEqual( {} );
|
|
70
|
+
expect( fromPayloadMock ).not.toHaveBeenCalled();
|
|
71
|
+
} );
|
|
72
|
+
} );
|
|
73
|
+
} );
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { dirname, join } from 'path';
|
|
2
2
|
import { fileURLToPath } from 'node:url';
|
|
3
|
-
import { ActivityExecutionInterceptor } from './
|
|
3
|
+
import { ActivityExecutionInterceptor } from './activity.js';
|
|
4
4
|
|
|
5
5
|
const __dirname = dirname( fileURLToPath( import.meta.url ) );
|
|
6
6
|
|
|
7
7
|
export const initInterceptors = ( { activities, workflows, connection } ) => ( {
|
|
8
|
-
workflowModules: [ join( __dirname, './
|
|
8
|
+
workflowModules: [ join( __dirname, './workflow.js' ) ],
|
|
9
9
|
activity: [
|
|
10
10
|
() => ( {
|
|
11
11
|
inbound: new ActivityExecutionInterceptor( { activities, workflows, connection } )
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// THIS RUNS IN THE TEMPORAL'S SANDBOX ENVIRONMENT
|
|
2
2
|
import { workflowInfo, proxySinks, ApplicationFailure, ContinueAsNew, isCancellation } from '@temporalio/workflow';
|
|
3
|
-
import { memoToHeaders } from '
|
|
3
|
+
import { memoToHeaders } from './headers.js';
|
|
4
4
|
import { deepMerge } from '#utils';
|
|
5
5
|
import { METADATA_ACCESS_SYMBOL, WorkflowSpecialOutput } from '#consts';
|
|
6
6
|
// this is a dynamic generated file with activity configs overwrites
|
|
@@ -8,13 +8,9 @@ import stepOptions from '../temp/__activity_options.js';
|
|
|
8
8
|
import { createWorkflowDetails } from '#internal_utils/temporal_context';
|
|
9
9
|
|
|
10
10
|
/*
|
|
11
|
-
This
|
|
12
|
-
|
|
13
|
-
This interceptor adds information value from workflowInfo().memo as Activity invocation headers.
|
|
14
|
-
|
|
11
|
+
This interceptor adds Memo and serialized workflowInfo() to the Activity invocation headers.
|
|
15
12
|
This is a strategy to share values between the workflow context and activity context.
|
|
16
|
-
|
|
17
|
-
We also want to preserve existing headers that might have been inject somewhere else and
|
|
13
|
+
We also want to preserve existing headers that might have been inject somewhere else.
|
|
18
14
|
*/
|
|
19
15
|
class HeadersInjectionInterceptor {
|
|
20
16
|
async scheduleActivity( input, next ) {
|
|
@@ -61,7 +61,7 @@ vi.mock( '@temporalio/workflow', () => ( {
|
|
|
61
61
|
} ) );
|
|
62
62
|
|
|
63
63
|
const memoToHeadersMock = vi.fn( memo => ( memo ? { ...memo, __asHeaders: true } : {} ) );
|
|
64
|
-
vi.mock( '
|
|
64
|
+
vi.mock( './headers.js', () => ( { memoToHeaders: ( ...args ) => memoToHeadersMock( ...args ) } ) );
|
|
65
65
|
|
|
66
66
|
const deepMergeMock = vi.fn( ( a, b ) => ( { ...( a || {} ), ...( b || {} ) } ) );
|
|
67
67
|
vi.mock( '#utils', () => ( { deepMerge: ( ...args ) => deepMergeMock( ...args ) } ) );
|
|
@@ -314,7 +314,7 @@ export const hashSourceCode = async rootDir => {
|
|
|
314
314
|
try {
|
|
315
315
|
const { hash } = await hashElement( rootDir, {
|
|
316
316
|
folders: {
|
|
317
|
-
exclude: [ '.*', 'node_modules', 'test_coverage', 'vendor', 'test' ],
|
|
317
|
+
exclude: [ '.*', 'node_modules', 'test_coverage', 'vendor', 'test', 'logs', 'dist' ],
|
|
318
318
|
ignoreRootName: true
|
|
319
319
|
},
|
|
320
320
|
files: {
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
findSharedActivitiesFromWorkflows,
|
|
12
12
|
importComponents,
|
|
13
13
|
findPackageRoot,
|
|
14
|
+
hashSourceCode,
|
|
14
15
|
isPackageRoot,
|
|
15
16
|
isPathDescendentFromNodeModules,
|
|
16
17
|
resolveNodeModulesPath,
|
|
@@ -568,6 +569,41 @@ describe( 'findSharedActivitiesFromWorkflows', () => {
|
|
|
568
569
|
} );
|
|
569
570
|
} );
|
|
570
571
|
|
|
572
|
+
describe( 'hashSourceCode', () => {
|
|
573
|
+
const writeSource = root => {
|
|
574
|
+
mkdirSync( join( root, 'src' ), { recursive: true } );
|
|
575
|
+
writeFileSync( join( root, 'package.json' ), JSON.stringify( { name: 'proj' } ) );
|
|
576
|
+
writeFileSync( join( root, 'src', 'workflow.js' ), 'export default {};\n' );
|
|
577
|
+
};
|
|
578
|
+
|
|
579
|
+
it( 'ignores excluded folders so accumulated run logs do not change the hash', async () => {
|
|
580
|
+
const baseline = join( TEMP_BASE, `hash-baseline-${Date.now()}` );
|
|
581
|
+
const withCruft = join( TEMP_BASE, `hash-cruft-${Date.now()}` );
|
|
582
|
+
writeSource( baseline );
|
|
583
|
+
writeSource( withCruft );
|
|
584
|
+
|
|
585
|
+
// The cruft tree is identical source plus large excluded artifacts that
|
|
586
|
+
// boot must not walk: local trace dumps under logs/ and build output under dist/.
|
|
587
|
+
for ( const excluded of [ 'logs', 'logs/runs', 'dist', 'node_modules' ] ) {
|
|
588
|
+
const dir = join( withCruft, excluded );
|
|
589
|
+
mkdirSync( dir, { recursive: true } );
|
|
590
|
+
writeFileSync( join( dir, 'dump.json' ), JSON.stringify( { blob: 'x'.repeat( 50_000 ) } ) );
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
expect( await hashSourceCode( withCruft ) ).toBe( await hashSourceCode( baseline ) );
|
|
594
|
+
} );
|
|
595
|
+
|
|
596
|
+
it( 'changes the hash when actual source changes', async () => {
|
|
597
|
+
const before = join( TEMP_BASE, `hash-src-before-${Date.now()}` );
|
|
598
|
+
const after = join( TEMP_BASE, `hash-src-after-${Date.now()}` );
|
|
599
|
+
writeSource( before );
|
|
600
|
+
writeSource( after );
|
|
601
|
+
writeFileSync( join( after, 'src', 'workflow.js' ), 'export default { changed: true };\n' );
|
|
602
|
+
|
|
603
|
+
expect( await hashSourceCode( after ) ).not.toBe( await hashSourceCode( before ) );
|
|
604
|
+
} );
|
|
605
|
+
} );
|
|
606
|
+
|
|
571
607
|
describe( 'staticMatchers', () => {
|
|
572
608
|
describe( 'workflowFile', () => {
|
|
573
609
|
it( 'matches paths ending with path separator and workflow.js', () => {
|
|
File without changes
|