|
|
|
|
@@ -41,6 +41,7 @@ describe('Queues', () => {
|
|
|
|
|
if (data.token) {
|
|
|
|
|
token = data.token
|
|
|
|
|
}
|
|
|
|
|
payload.config.jobs.deleteJobOnComplete = true
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('will run access control on jobs runner', async () => {
|
|
|
|
|
@@ -182,7 +183,6 @@ describe('Queues', () => {
|
|
|
|
|
|
|
|
|
|
// @ts-expect-error amountRetried is new arbitrary data and not in the type
|
|
|
|
|
expect(jobAfterRun.input.amountRetried).toBe(3)
|
|
|
|
|
payload.config.jobs.deleteJobOnComplete = true
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('ensure workflow-level retries are respected', async () => {
|
|
|
|
|
@@ -218,8 +218,6 @@ describe('Queues', () => {
|
|
|
|
|
|
|
|
|
|
// @ts-expect-error amountRetried is new arbitrary data and not in the type
|
|
|
|
|
expect(jobAfterRun.input.amountRetried).toBe(2)
|
|
|
|
|
|
|
|
|
|
payload.config.jobs.deleteJobOnComplete = true
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('ensure workflows dont limit retries if no retries property is sett', async () => {
|
|
|
|
|
@@ -255,8 +253,6 @@ describe('Queues', () => {
|
|
|
|
|
|
|
|
|
|
// @ts-expect-error amountRetried is new arbitrary data and not in the type
|
|
|
|
|
expect(jobAfterRun.input.amountRetried).toBe(3)
|
|
|
|
|
|
|
|
|
|
payload.config.jobs.deleteJobOnComplete = true
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('ensure workflows dont retry if retries set to 0, even if individual tasks have retries > 0 set', async () => {
|
|
|
|
|
@@ -292,8 +288,6 @@ describe('Queues', () => {
|
|
|
|
|
|
|
|
|
|
// @ts-expect-error amountRetried is new arbitrary data and not in the type
|
|
|
|
|
expect(jobAfterRun.input.amountRetried).toBe(0)
|
|
|
|
|
|
|
|
|
|
payload.config.jobs.deleteJobOnComplete = true
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('ensure workflows dont retry if neither workflows nor tasks have retries set', async () => {
|
|
|
|
|
@@ -329,8 +323,6 @@ describe('Queues', () => {
|
|
|
|
|
|
|
|
|
|
// @ts-expect-error amountRetried is new arbitrary data and not in the type
|
|
|
|
|
expect(jobAfterRun.input.amountRetried).toBe(0)
|
|
|
|
|
|
|
|
|
|
payload.config.jobs.deleteJobOnComplete = true
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('ensure workflows retry if workflows have retries set and tasks do not have retries set, due to tasks inheriting workflow retries', async () => {
|
|
|
|
|
@@ -366,8 +358,6 @@ describe('Queues', () => {
|
|
|
|
|
|
|
|
|
|
// @ts-expect-error amountRetried is new arbitrary data and not in the type
|
|
|
|
|
expect(jobAfterRun.input.amountRetried).toBe(2)
|
|
|
|
|
|
|
|
|
|
payload.config.jobs.deleteJobOnComplete = true
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('ensure workflows do not retry if workflows have retries set and tasks have retries set to 0', async () => {
|
|
|
|
|
@@ -403,8 +393,6 @@ describe('Queues', () => {
|
|
|
|
|
|
|
|
|
|
// @ts-expect-error amountRetried is new arbitrary data and not in the type
|
|
|
|
|
expect(jobAfterRun.input.amountRetried).toBe(0)
|
|
|
|
|
|
|
|
|
|
payload.config.jobs.deleteJobOnComplete = true
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
@@ -492,7 +480,7 @@ describe('Queues', () => {
|
|
|
|
|
id: job.id,
|
|
|
|
|
})
|
|
|
|
|
expect(jobAfterRun.totalTried).toBe(5)
|
|
|
|
|
expect((jobAfterRun.taskStatus as JobTaskStatus).inline['1'].totalTried).toBe(5)
|
|
|
|
|
expect((jobAfterRun.taskStatus as JobTaskStatus).inline?.['1']?.totalTried).toBe(5)
|
|
|
|
|
|
|
|
|
|
// @ts-expect-error amountRetried is new arbitrary data and not in the type
|
|
|
|
|
expect(jobAfterRun.input.amountRetried).toBe(4)
|
|
|
|
|
@@ -518,7 +506,7 @@ describe('Queues', () => {
|
|
|
|
|
if (index === arr.length - 1) {
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
return new Date(arr[index + 1]).getTime() - new Date(time).getTime()
|
|
|
|
|
return new Date(arr[index + 1] as string).getTime() - new Date(time).getTime()
|
|
|
|
|
})
|
|
|
|
|
.filter((p) => p !== null)
|
|
|
|
|
|
|
|
|
|
@@ -527,8 +515,6 @@ describe('Queues', () => {
|
|
|
|
|
expect(durations[1]).toBeGreaterThan(600)
|
|
|
|
|
expect(durations[2]).toBeGreaterThan(1200)
|
|
|
|
|
expect(durations[3]).toBeGreaterThan(2400)
|
|
|
|
|
|
|
|
|
|
payload.config.jobs.deleteJobOnComplete = true
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('ensure jobs run in FIFO order by default', async () => {
|
|
|
|
|
@@ -647,7 +633,7 @@ describe('Queues', () => {
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(allSimples.totalDocs).toBe(1)
|
|
|
|
|
expect(allSimples.docs[0].title).toBe('hello!')
|
|
|
|
|
expect(allSimples.docs[0]?.title).toBe('hello!')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('can create and autorun jobs', async () => {
|
|
|
|
|
@@ -677,12 +663,12 @@ describe('Queues', () => {
|
|
|
|
|
const { id } = await payload.jobs.queue({
|
|
|
|
|
workflow: 'inlineTaskTest',
|
|
|
|
|
input: {
|
|
|
|
|
message: 'hello!',
|
|
|
|
|
message: 'deleteJobOnComplete test',
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const before = await payload.findByID({ collection: 'payload-jobs', id, disableErrors: true })
|
|
|
|
|
expect(before.id).toBe(id)
|
|
|
|
|
expect(before?.id).toBe(id)
|
|
|
|
|
|
|
|
|
|
await payload.jobs.run()
|
|
|
|
|
|
|
|
|
|
@@ -690,6 +676,21 @@ describe('Queues', () => {
|
|
|
|
|
expect(after).toBeNull()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('should not delete failed jobs if deleteJobOnComplete is true', async () => {
|
|
|
|
|
const { id } = await payload.jobs.queue({
|
|
|
|
|
workflow: 'failsImmediately',
|
|
|
|
|
input: {},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const before = await payload.findByID({ collection: 'payload-jobs', id, disableErrors: true })
|
|
|
|
|
expect(before?.id).toBe(id)
|
|
|
|
|
|
|
|
|
|
await payload.jobs.run()
|
|
|
|
|
|
|
|
|
|
const after = await payload.findByID({ collection: 'payload-jobs', id, disableErrors: true })
|
|
|
|
|
expect(after?.id).toBe(id)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('should respect deleteJobOnComplete false configuration', async () => {
|
|
|
|
|
payload.config.jobs.deleteJobOnComplete = false
|
|
|
|
|
const { id } = await payload.jobs.queue({
|
|
|
|
|
@@ -700,14 +701,12 @@ describe('Queues', () => {
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const before = await payload.findByID({ collection: 'payload-jobs', id, disableErrors: true })
|
|
|
|
|
expect(before.id).toBe(id)
|
|
|
|
|
expect(before?.id).toBe(id)
|
|
|
|
|
|
|
|
|
|
await payload.jobs.run()
|
|
|
|
|
|
|
|
|
|
const after = await payload.findByID({ collection: 'payload-jobs', id, disableErrors: true })
|
|
|
|
|
expect(after.id).toBe(id)
|
|
|
|
|
|
|
|
|
|
payload.config.jobs.deleteJobOnComplete = true
|
|
|
|
|
expect(after?.id).toBe(id)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('can queue single tasks', async () => {
|
|
|
|
|
@@ -726,7 +725,7 @@ describe('Queues', () => {
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(allSimples.totalDocs).toBe(1)
|
|
|
|
|
expect(allSimples.docs[0].title).toBe('from single task')
|
|
|
|
|
expect(allSimples.docs[0]?.title).toBe('from single task')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('can queue and run via the endpoint single tasks without workflows', async () => {
|
|
|
|
|
@@ -751,7 +750,7 @@ describe('Queues', () => {
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(allSimples.totalDocs).toBe(1)
|
|
|
|
|
expect(allSimples.docs[0].title).toBe('from single task')
|
|
|
|
|
expect(allSimples.docs[0]?.title).toBe('from single task')
|
|
|
|
|
payload.config.jobs.workflows = workflowsRef
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
@@ -885,8 +884,8 @@ describe('Queues', () => {
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(allSimples.totalDocs).toBe(8)
|
|
|
|
|
expect(allSimples.docs[0].title).toBe('from single task')
|
|
|
|
|
expect(allSimples.docs[7].title).toBe('from single task')
|
|
|
|
|
expect(allSimples.docs[0]?.title).toBe('from single task')
|
|
|
|
|
expect(allSimples.docs[7]?.title).toBe('from single task')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('can queue single tasks hundreds of times', async () => {
|
|
|
|
|
@@ -912,9 +911,8 @@ describe('Queues', () => {
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(allSimples.totalDocs).toBe(numberOfTasks) // Default limit: 10
|
|
|
|
|
expect(allSimples.docs[0].title).toBe('from single task')
|
|
|
|
|
expect(allSimples.docs[numberOfTasks - 1].title).toBe('from single task')
|
|
|
|
|
payload.config.jobs.deleteJobOnComplete = true
|
|
|
|
|
expect(allSimples.docs[0]?.title).toBe('from single task')
|
|
|
|
|
expect(allSimples.docs[numberOfTasks - 1]?.title).toBe('from single task')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('ensure default jobs run limit of 10 works', async () => {
|
|
|
|
|
@@ -935,8 +933,8 @@ describe('Queues', () => {
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(allSimples.totalDocs).toBe(10) // Default limit: 10
|
|
|
|
|
expect(allSimples.docs[0].title).toBe('from single task')
|
|
|
|
|
expect(allSimples.docs[9].title).toBe('from single task')
|
|
|
|
|
expect(allSimples.docs[0]?.title).toBe('from single task')
|
|
|
|
|
expect(allSimples.docs[9]?.title).toBe('from single task')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('ensure jobs run limit can be customized', async () => {
|
|
|
|
|
@@ -959,9 +957,9 @@ describe('Queues', () => {
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(allSimples.totalDocs).toBe(42) // Default limit: 10
|
|
|
|
|
expect(allSimples.docs[0].title).toBe('from single task')
|
|
|
|
|
expect(allSimples.docs[30].title).toBe('from single task')
|
|
|
|
|
expect(allSimples.docs[41].title).toBe('from single task')
|
|
|
|
|
expect(allSimples.docs[0]?.title).toBe('from single task')
|
|
|
|
|
expect(allSimples.docs[30]?.title).toBe('from single task')
|
|
|
|
|
expect(allSimples.docs[41]?.title).toBe('from single task')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('can queue different kinds of single tasks multiple times', async () => {
|
|
|
|
|
@@ -1026,7 +1024,7 @@ describe('Queues', () => {
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(allSimples.totalDocs).toBe(1)
|
|
|
|
|
expect(allSimples.docs[0].title).toBe('external')
|
|
|
|
|
expect(allSimples.docs[0]?.title).toBe('external')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('can queue external workflow that is running external task', async () => {
|
|
|
|
|
@@ -1045,13 +1043,13 @@ describe('Queues', () => {
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(allSimples.totalDocs).toBe(1)
|
|
|
|
|
expect(allSimples.docs[0].title).toBe('externalWorkflow')
|
|
|
|
|
expect(allSimples.docs[0]?.title).toBe('externalWorkflow')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('ensure payload.jobs.runByID works and only runs the specified job', async () => {
|
|
|
|
|
payload.config.jobs.deleteJobOnComplete = false
|
|
|
|
|
|
|
|
|
|
let lastJobID: string = null
|
|
|
|
|
let lastJobID: null | string = null
|
|
|
|
|
for (let i = 0; i < 3; i++) {
|
|
|
|
|
const job = await payload.jobs.queue({
|
|
|
|
|
task: 'CreateSimple',
|
|
|
|
|
@@ -1061,6 +1059,9 @@ describe('Queues', () => {
|
|
|
|
|
})
|
|
|
|
|
lastJobID = job.id
|
|
|
|
|
}
|
|
|
|
|
if (!lastJobID) {
|
|
|
|
|
throw new Error('No job ID found after queuing jobs')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await payload.jobs.runByID({
|
|
|
|
|
id: lastJobID,
|
|
|
|
|
@@ -1072,7 +1073,7 @@ describe('Queues', () => {
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(allSimples.totalDocs).toBe(1)
|
|
|
|
|
expect(allSimples.docs[0].title).toBe('from single task')
|
|
|
|
|
expect(allSimples.docs[0]?.title).toBe('from single task')
|
|
|
|
|
|
|
|
|
|
const allCompletedJobs = await payload.find({
|
|
|
|
|
collection: 'payload-jobs',
|
|
|
|
|
@@ -1085,13 +1086,13 @@ describe('Queues', () => {
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(allCompletedJobs.totalDocs).toBe(1)
|
|
|
|
|
expect(allCompletedJobs.docs[0].id).toBe(lastJobID)
|
|
|
|
|
expect(allCompletedJobs.docs[0]?.id).toBe(lastJobID)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('ensure where query for id in payload.jobs.run works and only runs the specified job', async () => {
|
|
|
|
|
payload.config.jobs.deleteJobOnComplete = false
|
|
|
|
|
|
|
|
|
|
let lastJobID: string = null
|
|
|
|
|
let lastJobID: null | string = null
|
|
|
|
|
for (let i = 0; i < 3; i++) {
|
|
|
|
|
const job = await payload.jobs.queue({
|
|
|
|
|
task: 'CreateSimple',
|
|
|
|
|
@@ -1101,6 +1102,9 @@ describe('Queues', () => {
|
|
|
|
|
})
|
|
|
|
|
lastJobID = job.id
|
|
|
|
|
}
|
|
|
|
|
if (!lastJobID) {
|
|
|
|
|
throw new Error('No job ID found after queuing jobs')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await payload.jobs.run({
|
|
|
|
|
where: {
|
|
|
|
|
@@ -1116,7 +1120,7 @@ describe('Queues', () => {
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(allSimples.totalDocs).toBe(1)
|
|
|
|
|
expect(allSimples.docs[0].title).toBe('from single task')
|
|
|
|
|
expect(allSimples.docs[0]?.title).toBe('from single task')
|
|
|
|
|
|
|
|
|
|
const allCompletedJobs = await payload.find({
|
|
|
|
|
collection: 'payload-jobs',
|
|
|
|
|
@@ -1129,7 +1133,7 @@ describe('Queues', () => {
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(allCompletedJobs.totalDocs).toBe(1)
|
|
|
|
|
expect(allCompletedJobs.docs[0].id).toBe(lastJobID)
|
|
|
|
|
expect(allCompletedJobs.docs[0]?.id).toBe(lastJobID)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('ensure where query for input data in payload.jobs.run works and only runs the specified job', async () => {
|
|
|
|
|
@@ -1158,7 +1162,7 @@ describe('Queues', () => {
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(allSimples.totalDocs).toBe(1)
|
|
|
|
|
expect(allSimples.docs[0].title).toBe('from single task 2')
|
|
|
|
|
expect(allSimples.docs[0]?.title).toBe('from single task 2')
|
|
|
|
|
|
|
|
|
|
const allCompletedJobs = await payload.find({
|
|
|
|
|
collection: 'payload-jobs',
|
|
|
|
|
@@ -1171,7 +1175,7 @@ describe('Queues', () => {
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(allCompletedJobs.totalDocs).toBe(1)
|
|
|
|
|
expect((allCompletedJobs.docs[0].input as any).message).toBe('from single task 2')
|
|
|
|
|
expect((allCompletedJobs.docs[0]?.input as any).message).toBe('from single task 2')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('can run sub-tasks', async () => {
|
|
|
|
|
@@ -1191,24 +1195,24 @@ describe('Queues', () => {
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(allSimples.totalDocs).toBe(2)
|
|
|
|
|
expect(allSimples.docs[0].title).toBe('hello!')
|
|
|
|
|
expect(allSimples.docs[1].title).toBe('hello!')
|
|
|
|
|
expect(allSimples.docs[0]?.title).toBe('hello!')
|
|
|
|
|
expect(allSimples.docs[1]?.title).toBe('hello!')
|
|
|
|
|
|
|
|
|
|
const jobAfterRun = await payload.findByID({
|
|
|
|
|
collection: 'payload-jobs',
|
|
|
|
|
id: job.id,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(jobAfterRun.log[0].taskID).toBe('create doc 1')
|
|
|
|
|
expect(jobAfterRun?.log?.[0]?.taskID).toBe('create doc 1')
|
|
|
|
|
//expect(jobAfterRun.log[0].parent.taskID).toBe('create two docs')
|
|
|
|
|
// jobAfterRun.log[0].parent should not exist
|
|
|
|
|
expect(jobAfterRun.log[0].parent).toBeUndefined()
|
|
|
|
|
expect(jobAfterRun?.log?.[0]?.parent).toBeUndefined()
|
|
|
|
|
|
|
|
|
|
expect(jobAfterRun.log[1].taskID).toBe('create doc 2')
|
|
|
|
|
expect(jobAfterRun?.log?.[1]?.taskID).toBe('create doc 2')
|
|
|
|
|
//expect(jobAfterRun.log[1].parent.taskID).toBe('create two docs')
|
|
|
|
|
expect(jobAfterRun.log[1].parent).toBeUndefined()
|
|
|
|
|
expect(jobAfterRun?.log?.[1]?.parent).toBeUndefined()
|
|
|
|
|
|
|
|
|
|
expect(jobAfterRun.log[2].taskID).toBe('create two docs')
|
|
|
|
|
expect(jobAfterRun?.log?.[2]?.taskID).toBe('create two docs')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('ensure successful sub-tasks are not retried', async () => {
|
|
|
|
|
@@ -1237,7 +1241,7 @@ describe('Queues', () => {
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(allSimples.totalDocs).toBe(1)
|
|
|
|
|
expect(allSimples.docs[0].title).toBe('hello!')
|
|
|
|
|
expect(allSimples?.docs?.[0]?.title).toBe('hello!')
|
|
|
|
|
|
|
|
|
|
const jobAfterRun = await payload.findByID({
|
|
|
|
|
collection: 'payload-jobs',
|
|
|
|
|
@@ -1339,8 +1343,8 @@ describe('Queues', () => {
|
|
|
|
|
|
|
|
|
|
expect(jobAfterRun.hasError).toBe(true)
|
|
|
|
|
expect(jobAfterRun.log?.length).toBe(1)
|
|
|
|
|
expect(jobAfterRun.log[0].error.message).toBe('failed')
|
|
|
|
|
expect(jobAfterRun.log[0].state).toBe('failed')
|
|
|
|
|
expect(jobAfterRun?.log?.[0]?.error?.message).toBe('failed')
|
|
|
|
|
expect(jobAfterRun?.log?.[0]?.state).toBe('failed')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('can tasks return error', async () => {
|
|
|
|
|
@@ -1360,8 +1364,8 @@ describe('Queues', () => {
|
|
|
|
|
|
|
|
|
|
expect(jobAfterRun.hasError).toBe(true)
|
|
|
|
|
expect(jobAfterRun.log?.length).toBe(1)
|
|
|
|
|
expect(jobAfterRun.log[0].error.message).toBe('failed')
|
|
|
|
|
expect(jobAfterRun.log[0].state).toBe('failed')
|
|
|
|
|
expect(jobAfterRun?.log?.[0]?.error?.message).toBe('Task handler returned a failed state')
|
|
|
|
|
expect(jobAfterRun?.log?.[0]?.state).toBe('failed')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('can tasks return error with custom error message', async () => {
|
|
|
|
|
@@ -1383,8 +1387,8 @@ describe('Queues', () => {
|
|
|
|
|
|
|
|
|
|
expect(jobAfterRun.hasError).toBe(true)
|
|
|
|
|
expect(jobAfterRun.log?.length).toBe(1)
|
|
|
|
|
expect(jobAfterRun.log[0].error.message).toBe('custom error message')
|
|
|
|
|
expect(jobAfterRun.log[0].state).toBe('failed')
|
|
|
|
|
expect(jobAfterRun?.log?.[0]?.error?.message).toBe('custom error message')
|
|
|
|
|
expect(jobAfterRun?.log?.[0]?.state).toBe('failed')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('can reliably run workflows with parallel tasks', async () => {
|
|
|
|
|
|