@calmo/task-runner 1.2.2 → 2.0.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 (44) hide show
  1. package/.gemini/commands/openspec/apply.toml +21 -0
  2. package/.gemini/commands/openspec/archive.toml +25 -0
  3. package/.gemini/commands/openspec/proposal.toml +26 -0
  4. package/.jules/sentinel.md +4 -0
  5. package/AGENTS.md +21 -0
  6. package/CHANGELOG.md +21 -0
  7. package/GEMINI.md +10 -0
  8. package/coverage/coverage-final.json +4 -4
  9. package/coverage/index.html +9 -9
  10. package/coverage/lcov-report/index.html +9 -9
  11. package/coverage/lcov-report/src/EventBus.ts.html +4 -4
  12. package/coverage/lcov-report/src/TaskGraphValidator.ts.html +165 -54
  13. package/coverage/lcov-report/src/TaskRunner.ts.html +7 -109
  14. package/coverage/lcov-report/src/WorkflowExecutor.ts.html +169 -106
  15. package/coverage/lcov-report/src/contracts/RunnerEvents.ts.html +1 -1
  16. package/coverage/lcov-report/src/contracts/index.html +1 -1
  17. package/coverage/lcov-report/src/index.html +13 -13
  18. package/coverage/lcov.info +165 -167
  19. package/coverage/src/EventBus.ts.html +4 -4
  20. package/coverage/src/TaskGraphValidator.ts.html +165 -54
  21. package/coverage/src/TaskRunner.ts.html +7 -109
  22. package/coverage/src/WorkflowExecutor.ts.html +169 -106
  23. package/coverage/src/contracts/RunnerEvents.ts.html +1 -1
  24. package/coverage/src/contracts/index.html +1 -1
  25. package/coverage/src/index.html +13 -13
  26. package/dist/TaskGraphValidator.d.ts +6 -0
  27. package/dist/TaskGraphValidator.js +48 -16
  28. package/dist/TaskGraphValidator.js.map +1 -1
  29. package/dist/TaskRunner.js +1 -32
  30. package/dist/TaskRunner.js.map +1 -1
  31. package/dist/WorkflowExecutor.d.ts +16 -0
  32. package/dist/WorkflowExecutor.js +67 -54
  33. package/dist/WorkflowExecutor.js.map +1 -1
  34. package/dist/contracts/ITaskGraphValidator.d.ts +6 -0
  35. package/openspec/AGENTS.md +456 -0
  36. package/openspec/changes/add-external-task-cancellation/proposal.md +14 -0
  37. package/openspec/changes/add-external-task-cancellation/tasks.md +10 -0
  38. package/openspec/project.md +31 -0
  39. package/package.json +1 -1
  40. package/src/TaskGraphValidator.ts +56 -19
  41. package/src/TaskRunner.ts +1 -35
  42. package/src/WorkflowExecutor.ts +84 -63
  43. package/src/contracts/ITaskGraphValidator.ts +7 -0
  44. package/test-report.xml +51 -39
@@ -0,0 +1,31 @@
1
+ # Project: Task Runner
2
+
3
+ ## Overview
4
+ The 'task-runner' project is a TypeScript-based utility designed to manage and execute tasks. It incorporates features such as task cancellation, pre-execution validation, and concurrency control, providing a robust framework for workflow automation.
5
+
6
+ ## Tech Stack
7
+ - **Languages:** TypeScript 5.9.3
8
+ - **Testing:** Vitest 4.0.17
9
+ - **Core APIs:** AbortSignal/AbortController (Standard Web APIs for cancellation)
10
+ - **Package Manager:** pnpm
11
+
12
+ ## Architecture
13
+ The project follows a modular architecture with distinct components for managing different aspects of task execution:
14
+ - `EventBus.ts`: Handles event propagation within the system.
15
+ - `TaskGraph.ts`: Represents the structure and dependencies of tasks.
16
+ - `TaskGraphValidator.ts`: Ensures the validity of task graphs before execution.
17
+ - `TaskRunner.ts`: Orchestrates the execution of tasks.
18
+ - `WorkflowExecutor.ts`: Manages the overall workflow.
19
+ - `contracts/`: Defines interfaces and types for various components, promoting loose coupling and clear API boundaries.
20
+
21
+ ## Conventions
22
+ - **Coding Style:** Adheres to standard TypeScript conventions, enforced by ESLint and Prettier.
23
+ - **Commit Messages:** Follows conventional commits enforced by Commitlint.
24
+ - **Git Hooks:** Utilizes Husky for pre-commit and commit-msg hooks.
25
+ - **Testing:** Uses Vitest for unit and integration testing.
26
+
27
+ ## Build/Test/Run Commands
28
+ - **Install Dependencies:** `pnpm install`
29
+ - **Build Project:** `pnpm build`
30
+ - **Run Tests:** `pnpm test`
31
+ - **Lint Code:** `pnpm lint`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@calmo/task-runner",
3
- "version": "1.2.2",
3
+ "version": "2.0.0",
4
4
  "description": "",
5
5
  "repository": {
6
6
  "type": "git",
@@ -93,34 +93,71 @@ export class TaskGraphValidator implements ITaskGraphValidator {
93
93
  };
94
94
  }
95
95
 
96
+ /**
97
+ * Creates a human-readable error message from a validation result.
98
+ * @param result The validation result containing errors.
99
+ * @returns A formatted error string.
100
+ */
101
+ createErrorMessage(result: ValidationResult): string {
102
+ const errorDetails = result.errors.map(e => e.message);
103
+ return `Task graph validation failed: ${errorDetails.join("; ")}`;
104
+ }
105
+
96
106
  private detectCycle(
97
- taskId: string,
107
+ startTaskId: string,
98
108
  path: string[],
99
109
  visited: Set<string>,
100
110
  recursionStack: Set<string>,
101
111
  adjacencyList: Map<string, string[]>
102
112
  ): boolean {
103
- visited.add(taskId);
104
- recursionStack.add(taskId);
105
- path.push(taskId);
106
-
107
- const dependencies = adjacencyList.get(taskId)!;
108
- for (const dependenceId of dependencies) {
109
- if (
110
- !visited.has(dependenceId) &&
111
- this.detectCycle(dependenceId, path, visited, recursionStack, adjacencyList)
112
- ) {
113
- return true;
114
- } else if (recursionStack.has(dependenceId)) {
115
- // Cycle detected
116
- // Add the dependency to complete the visual cycle
117
- path.push(dependenceId);
118
- return true;
113
+ // Use an explicit stack to avoid maximum call stack size exceeded errors
114
+ const stack: { taskId: string; index: number; dependencies: string[] }[] = [];
115
+
116
+ visited.add(startTaskId);
117
+ recursionStack.add(startTaskId);
118
+ path.push(startTaskId);
119
+
120
+ stack.push({
121
+ taskId: startTaskId,
122
+ index: 0,
123
+ /* v8 ignore next */
124
+ dependencies: adjacencyList.get(startTaskId) ?? []
125
+ });
126
+
127
+ while (stack.length > 0) {
128
+ const frame = stack[stack.length - 1];
129
+ const { taskId, dependencies } = frame;
130
+
131
+ if (frame.index < dependencies.length) {
132
+ const dependenceId = dependencies[frame.index];
133
+ frame.index++;
134
+
135
+ if (recursionStack.has(dependenceId)) {
136
+ // Cycle detected
137
+ path.push(dependenceId);
138
+ return true;
139
+ }
140
+
141
+ if (!visited.has(dependenceId)) {
142
+ visited.add(dependenceId);
143
+ recursionStack.add(dependenceId);
144
+ path.push(dependenceId);
145
+
146
+ stack.push({
147
+ taskId: dependenceId,
148
+ index: 0,
149
+ /* v8 ignore next */
150
+ dependencies: adjacencyList.get(dependenceId) ?? []
151
+ });
152
+ }
153
+ } else {
154
+ // Finished all dependencies for this node
155
+ recursionStack.delete(taskId);
156
+ path.pop();
157
+ stack.pop();
119
158
  }
120
159
  }
121
160
 
122
- recursionStack.delete(taskId);
123
- path.pop();
124
161
  return false;
125
162
  }
126
163
  }
package/src/TaskRunner.ts CHANGED
@@ -65,41 +65,7 @@ export class TaskRunner<TContext> {
65
65
 
66
66
  const validationResult = this.validator.validate(taskGraph);
67
67
  if (!validationResult.isValid) {
68
- // Construct error message compatible with legacy tests
69
- const affectedTasks = new Set<string>();
70
- const errorDetails: string[] = [];
71
-
72
- for (const error of validationResult.errors) {
73
- errorDetails.push(error.message);
74
- switch (error.type) {
75
- case "cycle": {
76
- // details is { cyclePath: string[] }
77
- const path = (error.details as { cyclePath: string[] }).cyclePath;
78
- // The last element duplicates the first in the path representation, so valid unique tasks are slice(0, -1) or just all as Set handles uniq
79
- path.forEach((t) => affectedTasks.add(t));
80
- break;
81
- }
82
- case "missing_dependency": {
83
- // details is { taskId: string, missingDependencyId: string }
84
- const d = error.details as { taskId: string };
85
- affectedTasks.add(d.taskId);
86
- break;
87
- }
88
- case "duplicate_task": {
89
- const d = error.details as { taskId: string };
90
- affectedTasks.add(d.taskId);
91
- break;
92
- }
93
- }
94
- }
95
-
96
- // Legacy error format: "Circular dependency or missing dependency detected. Unable to run tasks: A, B"
97
- const taskList = Array.from(affectedTasks).join(", ");
98
- const legacyMessage = `Circular dependency or missing dependency detected. Unable to run tasks: ${taskList}`;
99
- const detailedMessage = `Task graph validation failed: ${errorDetails.join("; ")}`;
100
-
101
- // Combine them to satisfy both legacy tests (checking for legacy message) and new requirements (clear details)
102
- throw new Error(`${legacyMessage} | ${detailedMessage}`);
68
+ throw new Error(this.validator.createErrorMessage(validationResult));
103
69
  }
104
70
 
105
71
  const executor = new WorkflowExecutor(this.context, this.eventBus);
@@ -29,77 +29,98 @@ export class WorkflowExecutor<TContext> {
29
29
  const results = new Map<string, TaskResult>();
30
30
  const executingPromises = new Set<Promise<void>>();
31
31
 
32
- // Helper to process pending steps and launch ready ones
33
- const processPendingSteps = () => {
34
- const pendingSteps = steps.filter(
35
- (step) => !results.has(step.name) && !this.running.has(step.name)
36
- );
37
-
38
- // 1. Identify and mark skipped tasks
39
- for (const step of pendingSteps) {
40
- const deps = step.dependencies ?? [];
41
- const failedDep = deps.find(
42
- (dep) => results.has(dep) && results.get(dep)?.status !== "success"
43
- );
44
- if (failedDep) {
45
- const result: TaskResult = {
46
- status: "skipped",
47
- message: `Skipped due to failed dependency: ${failedDep}`,
48
- };
49
- results.set(step.name, result);
50
- this.eventBus.emit("taskSkipped", { step, result });
51
- }
52
- }
53
-
54
- // Re-filter pending steps as some might have been skipped above
55
- const readySteps = steps.filter((step) => {
56
- if (results.has(step.name) || this.running.has(step.name)) return false;
57
- const deps = step.dependencies ?? [];
58
- return deps.every(
59
- (dep) => results.has(dep) && results.get(dep)?.status === "success"
60
- );
61
- });
62
-
63
- // 2. Launch ready tasks
64
- for (const step of readySteps) {
65
- this.running.add(step.name);
66
- this.eventBus.emit("taskStart", { step });
67
-
68
- const taskPromise = (async () => {
69
- try {
70
- const result = await step.run(this.context);
71
- results.set(step.name, result);
72
- } catch (e) {
73
- results.set(step.name, {
74
- status: "failure",
75
- error: e instanceof Error ? e.message : String(e),
76
- });
77
- } finally {
78
- this.running.delete(step.name);
79
- const result = results.get(step.name)!;
80
- this.eventBus.emit("taskEnd", { step, result });
81
- }
82
- })();
83
-
84
- // Wrap the task promise to ensure we can track it in the Set
85
- const trackedPromise = taskPromise.then(() => {
86
- executingPromises.delete(trackedPromise);
87
- });
88
- executingPromises.add(trackedPromise);
89
- }
90
- };
91
-
92
- // Initial check to start independent tasks
93
- processPendingSteps();
32
+ // Initial pass
33
+ this.processQueue(steps, results, executingPromises);
94
34
 
95
35
  while (results.size < steps.length && executingPromises.size > 0) {
96
36
  // Wait for the next task to finish
97
37
  await Promise.race(executingPromises);
98
38
  // After a task finishes, check for new work
99
- processPendingSteps();
39
+ this.processQueue(steps, results, executingPromises);
100
40
  }
101
41
 
102
42
  this.eventBus.emit("workflowEnd", { context: this.context, results });
103
43
  return results;
104
44
  }
45
+
46
+ /**
47
+ * Logic to identify tasks that can be started or must be skipped.
48
+ */
49
+ private processQueue(
50
+ steps: TaskStep<TContext>[],
51
+ results: Map<string, TaskResult>,
52
+ executingPromises: Set<Promise<void>>
53
+ ): void {
54
+ this.handleSkippedTasks(steps, results);
55
+
56
+ const readySteps = this.getReadySteps(steps, results);
57
+
58
+ for (const step of readySteps) {
59
+ const taskPromise = this.runStep(step, results).then(() => {
60
+ executingPromises.delete(taskPromise);
61
+ });
62
+ executingPromises.add(taskPromise);
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Identifies steps that cannot run because a dependency failed.
68
+ */
69
+ private handleSkippedTasks(steps: TaskStep<TContext>[], results: Map<string, TaskResult>): void {
70
+ const pendingSteps = steps.filter(
71
+ (step) => !results.has(step.name) && !this.running.has(step.name)
72
+ );
73
+
74
+ for (const step of pendingSteps) {
75
+ const deps = step.dependencies ?? [];
76
+ const failedDep = deps.find(
77
+ (dep) => results.has(dep) && results.get(dep)?.status !== "success"
78
+ );
79
+
80
+ if (failedDep) {
81
+ const result: TaskResult = {
82
+ status: "skipped",
83
+ message: `Skipped due to failed dependency: ${failedDep}`,
84
+ };
85
+ results.set(step.name, result);
86
+ this.eventBus.emit("taskSkipped", { step, result });
87
+ }
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Returns steps where all dependencies have finished successfully.
93
+ */
94
+ private getReadySteps(steps: TaskStep<TContext>[], results: Map<string, TaskResult>): TaskStep<TContext>[] {
95
+ return steps.filter((step) => {
96
+ if (results.has(step.name) || this.running.has(step.name)) return false;
97
+
98
+ const deps = step.dependencies ?? [];
99
+ return deps.every(
100
+ (dep) => results.has(dep) && results.get(dep)?.status === "success"
101
+ );
102
+ });
103
+ }
104
+
105
+ /**
106
+ * Handles the lifecycle of a single task execution.
107
+ */
108
+ private async runStep(step: TaskStep<TContext>, results: Map<string, TaskResult>): Promise<void> {
109
+ this.running.add(step.name);
110
+ this.eventBus.emit("taskStart", { step });
111
+
112
+ try {
113
+ const result = await step.run(this.context);
114
+ results.set(step.name, result);
115
+ } catch (e) {
116
+ results.set(step.name, {
117
+ status: "failure",
118
+ error: e instanceof Error ? e.message : String(e),
119
+ });
120
+ } finally {
121
+ this.running.delete(step.name);
122
+ const result = results.get(step.name)!;
123
+ this.eventBus.emit("taskEnd", { step, result });
124
+ }
125
+ }
105
126
  }
@@ -11,4 +11,11 @@ export interface ITaskGraphValidator {
11
11
  * @returns A ValidationResult object indicating the outcome of the validation.
12
12
  */
13
13
  validate(taskGraph: TaskGraph): ValidationResult;
14
+
15
+ /**
16
+ * Creates a human-readable error message from a validation result.
17
+ * @param result The validation result containing errors.
18
+ * @returns A formatted error string.
19
+ */
20
+ createErrorMessage(result: ValidationResult): string;
14
21
  }
package/test-report.xml CHANGED
@@ -1,7 +1,7 @@
1
1
  <?xml version="1.0" encoding="UTF-8" ?>
2
- <testsuites name="vitest tests" tests="32" failures="0" errors="0" time="0.171985314">
3
- <testsuite name="tests/ComplexScenario.test.ts" timestamp="2026-01-18T05:44:30.743Z" hostname="runnervmmtnos" tests="2" failures="0" errors="0" skipped="0" time="0.008824338">
4
- <testcase classname="tests/ComplexScenario.test.ts" name="Complex Scenario Integration Tests &gt; 1. All steps execute when all succeed and context is hydrated" time="0.005524092">
2
+ <testsuites name="vitest tests" tests="36" failures="0" errors="0" time="0.286169223">
3
+ <testsuite name="tests/ComplexScenario.test.ts" timestamp="2026-01-18T15:13:46.722Z" hostname="runnervmmtnos" tests="2" failures="0" errors="0" skipped="0" time="0.009123917">
4
+ <testcase classname="tests/ComplexScenario.test.ts" name="Complex Scenario Integration Tests &gt; 1. All steps execute when all succeed and context is hydrated" time="0.005950364">
5
5
  <system-out>
6
6
  Running StepA
7
7
  Running StepB
@@ -15,77 +15,89 @@ Running StepG
15
15
 
16
16
  </system-out>
17
17
  </testcase>
18
- <testcase classname="tests/ComplexScenario.test.ts" name="Complex Scenario Integration Tests &gt; 2. The &apos;skip&apos; propagation works as intended if something breaks up in the tree" time="0.001386436">
18
+ <testcase classname="tests/ComplexScenario.test.ts" name="Complex Scenario Integration Tests &gt; 2. The &apos;skip&apos; propagation works as intended if something breaks up in the tree" time="0.001176806">
19
19
  </testcase>
20
20
  </testsuite>
21
- <testsuite name="tests/EventBus.test.ts" timestamp="2026-01-18T05:44:30.744Z" hostname="runnervmmtnos" tests="1" failures="0" errors="0" skipped="0" time="0.019386386">
22
- <testcase classname="tests/EventBus.test.ts" name="EventBus &gt; should handle async listeners throwing errors without crashing" time="0.017035792">
21
+ <testsuite name="tests/EventBus.test.ts" timestamp="2026-01-18T15:13:46.724Z" hostname="runnervmmtnos" tests="1" failures="0" errors="0" skipped="0" time="0.019297461">
22
+ <testcase classname="tests/EventBus.test.ts" name="EventBus &gt; should handle async listeners throwing errors without crashing" time="0.017335491">
23
23
  </testcase>
24
24
  </testsuite>
25
- <testsuite name="tests/TaskGraphValidator.test.ts" timestamp="2026-01-18T05:44:30.745Z" hostname="runnervmmtnos" tests="9" failures="0" errors="0" skipped="0" time="0.010354972">
26
- <testcase classname="tests/TaskGraphValidator.test.ts" name="TaskGraphValidator &gt; should be instantiated" time="0.001917275">
25
+ <testsuite name="tests/TaskGraphValidator.test.ts" timestamp="2026-01-18T15:13:46.724Z" hostname="runnervmmtnos" tests="9" failures="0" errors="0" skipped="0" time="0.008829958">
26
+ <testcase classname="tests/TaskGraphValidator.test.ts" name="TaskGraphValidator &gt; should be instantiated" time="0.001727303">
27
27
  </testcase>
28
- <testcase classname="tests/TaskGraphValidator.test.ts" name="TaskGraphValidator &gt; should return valid result for empty graph" time="0.00165162">
28
+ <testcase classname="tests/TaskGraphValidator.test.ts" name="TaskGraphValidator &gt; should return valid result for empty graph" time="0.001258177">
29
29
  </testcase>
30
- <testcase classname="tests/TaskGraphValidator.test.ts" name="TaskGraphValidator &gt; should detect duplicate tasks" time="0.001990723">
30
+ <testcase classname="tests/TaskGraphValidator.test.ts" name="TaskGraphValidator &gt; should detect duplicate tasks" time="0.001873847">
31
31
  </testcase>
32
- <testcase classname="tests/TaskGraphValidator.test.ts" name="TaskGraphValidator &gt; should detect missing dependencies" time="0.00050915">
32
+ <testcase classname="tests/TaskGraphValidator.test.ts" name="TaskGraphValidator &gt; should detect missing dependencies" time="0.000404865">
33
33
  </testcase>
34
- <testcase classname="tests/TaskGraphValidator.test.ts" name="TaskGraphValidator &gt; should detect cycles" time="0.000507055">
34
+ <testcase classname="tests/TaskGraphValidator.test.ts" name="TaskGraphValidator &gt; should detect cycles" time="0.000448717">
35
35
  </testcase>
36
- <testcase classname="tests/TaskGraphValidator.test.ts" name="TaskGraphValidator &gt; should return valid for a correct graph" time="0.00041269">
36
+ <testcase classname="tests/TaskGraphValidator.test.ts" name="TaskGraphValidator &gt; should return valid for a correct graph" time="0.000404856">
37
37
  </testcase>
38
- <testcase classname="tests/TaskGraphValidator.test.ts" name="TaskGraphValidator &gt; should not detect cycles if missing dependencies are present" time="0.00036427">
38
+ <testcase classname="tests/TaskGraphValidator.test.ts" name="TaskGraphValidator &gt; should not detect cycles if missing dependencies are present" time="0.000360703">
39
39
  </testcase>
40
- <testcase classname="tests/TaskGraphValidator.test.ts" name="TaskGraphValidator &gt; should detect more complex cycles" time="0.000319536">
40
+ <testcase classname="tests/TaskGraphValidator.test.ts" name="TaskGraphValidator &gt; should detect more complex cycles" time="0.000276827">
41
41
  </testcase>
42
- <testcase classname="tests/TaskGraphValidator.test.ts" name="TaskGraphValidator &gt; should handle cycle detection with no cycles but shared dependencies" time="0.000239367">
42
+ <testcase classname="tests/TaskGraphValidator.test.ts" name="TaskGraphValidator &gt; should handle cycle detection with no cycles but shared dependencies" time="0.000241571">
43
43
  </testcase>
44
44
  </testsuite>
45
- <testsuite name="tests/TaskRunner.test.ts" timestamp="2026-01-18T05:44:30.747Z" hostname="runnervmmtnos" tests="10" failures="0" errors="0" skipped="0" time="0.112430041">
46
- <testcase classname="tests/TaskRunner.test.ts" name="TaskRunner &gt; should run tasks in the correct sequential order" time="0.004173253">
45
+ <testsuite name="tests/TaskGraphValidatorDoS.test.ts" timestamp="2026-01-18T15:13:46.726Z" hostname="runnervmmtnos" tests="1" failures="0" errors="0" skipped="0" time="0.049329936">
46
+ <testcase classname="tests/TaskGraphValidatorDoS.test.ts" name="TaskGraphValidator - Deep Recursion &gt; should handle deep graphs without stack overflow" time="0.047337509">
47
47
  </testcase>
48
- <testcase classname="tests/TaskRunner.test.ts" name="TaskRunner &gt; should handle an empty list of tasks gracefully" time="0.00045054">
48
+ </testsuite>
49
+ <testsuite name="tests/TaskRunner.test.ts" timestamp="2026-01-18T15:13:46.727Z" hostname="runnervmmtnos" tests="10" failures="0" errors="0" skipped="0" time="0.112220851">
50
+ <testcase classname="tests/TaskRunner.test.ts" name="TaskRunner &gt; should run tasks in the correct sequential order" time="0.0038045">
51
+ </testcase>
52
+ <testcase classname="tests/TaskRunner.test.ts" name="TaskRunner &gt; should handle an empty list of tasks gracefully" time="0.000635346">
49
53
  </testcase>
50
- <testcase classname="tests/TaskRunner.test.ts" name="TaskRunner &gt; should run independent tasks in parallel" time="0.099953731">
54
+ <testcase classname="tests/TaskRunner.test.ts" name="TaskRunner &gt; should run independent tasks in parallel" time="0.100728925">
51
55
  </testcase>
52
- <testcase classname="tests/TaskRunner.test.ts" name="TaskRunner &gt; should skip dependent tasks if a root task fails" time="0.000542632">
56
+ <testcase classname="tests/TaskRunner.test.ts" name="TaskRunner &gt; should skip dependent tasks if a root task fails" time="0.000416347">
53
57
  </testcase>
54
- <testcase classname="tests/TaskRunner.test.ts" name="TaskRunner &gt; should throw an error for &apos;circular dependency&apos;" time="0.002602664">
58
+ <testcase classname="tests/TaskRunner.test.ts" name="TaskRunner &gt; should throw an error for &apos;circular dependency&apos;" time="0.002357119">
55
59
  </testcase>
56
- <testcase classname="tests/TaskRunner.test.ts" name="TaskRunner &gt; should throw an error for &apos;missing dependency&apos;" time="0.000402671">
60
+ <testcase classname="tests/TaskRunner.test.ts" name="TaskRunner &gt; should throw an error for &apos;missing dependency&apos;" time="0.000314708">
57
61
  </testcase>
58
- <testcase classname="tests/TaskRunner.test.ts" name="TaskRunner &gt; should handle tasks that throw an error during execution" time="0.000365011">
62
+ <testcase classname="tests/TaskRunner.test.ts" name="TaskRunner &gt; should handle tasks that throw an error during execution" time="0.000313525">
59
63
  </testcase>
60
- <testcase classname="tests/TaskRunner.test.ts" name="TaskRunner &gt; should skip tasks whose dependencies are skipped" time="0.000483552">
64
+ <testcase classname="tests/TaskRunner.test.ts" name="TaskRunner &gt; should skip tasks whose dependencies are skipped" time="0.00036969">
61
65
  </testcase>
62
- <testcase classname="tests/TaskRunner.test.ts" name="TaskRunner &gt; should handle tasks that throw a non-Error object during execution" time="0.000424773">
66
+ <testcase classname="tests/TaskRunner.test.ts" name="TaskRunner &gt; should handle tasks that throw a non-Error object during execution" time="0.000305942">
63
67
  </testcase>
64
- <testcase classname="tests/TaskRunner.test.ts" name="TaskRunner &gt; should handle duplicate steps where one gets skipped due to failed dependency" time="0.000467091">
68
+ <testcase classname="tests/TaskRunner.test.ts" name="TaskRunner &gt; should handle duplicate steps where one gets skipped due to failed dependency" time="0.000409565">
65
69
  </testcase>
66
70
  </testsuite>
67
- <testsuite name="tests/TaskRunnerEvents.test.ts" timestamp="2026-01-18T05:44:30.748Z" hostname="runnervmmtnos" tests="7" failures="0" errors="0" skipped="0" time="0.015344879">
68
- <testcase classname="tests/TaskRunnerEvents.test.ts" name="TaskRunner Events &gt; should fire all lifecycle events in a successful run" time="0.005483039">
71
+ <testsuite name="tests/TaskRunnerEvents.test.ts" timestamp="2026-01-18T15:13:46.729Z" hostname="runnervmmtnos" tests="7" failures="0" errors="0" skipped="0" time="0.020279996">
72
+ <testcase classname="tests/TaskRunnerEvents.test.ts" name="TaskRunner Events &gt; should fire all lifecycle events in a successful run" time="0.007839309">
73
+ </testcase>
74
+ <testcase classname="tests/TaskRunnerEvents.test.ts" name="TaskRunner Events &gt; should fire taskSkipped event when dependency fails" time="0.005329486">
69
75
  </testcase>
70
- <testcase classname="tests/TaskRunnerEvents.test.ts" name="TaskRunner Events &gt; should fire taskSkipped event when dependency fails" time="0.004914606">
76
+ <testcase classname="tests/TaskRunnerEvents.test.ts" name="TaskRunner Events &gt; should not crash if a listener throws an error" time="0.001806131">
71
77
  </testcase>
72
- <testcase classname="tests/TaskRunnerEvents.test.ts" name="TaskRunner Events &gt; should not crash if a listener throws an error" time="0.001456386">
78
+ <testcase classname="tests/TaskRunnerEvents.test.ts" name="TaskRunner Events &gt; should fire workflow events even for empty step list" time="0.000492118">
73
79
  </testcase>
74
- <testcase classname="tests/TaskRunnerEvents.test.ts" name="TaskRunner Events &gt; should fire workflow events even for empty step list" time="0.000272478">
80
+ <testcase classname="tests/TaskRunnerEvents.test.ts" name="TaskRunner Events &gt; should handle unsubscribe correctly" time="0.000713071">
75
81
  </testcase>
76
- <testcase classname="tests/TaskRunnerEvents.test.ts" name="TaskRunner Events &gt; should handle unsubscribe correctly" time="0.000554882">
82
+ <testcase classname="tests/TaskRunnerEvents.test.ts" name="TaskRunner Events &gt; should safely handle off() when no listeners exist" time="0.00075527">
83
+ </testcase>
84
+ <testcase classname="tests/TaskRunnerEvents.test.ts" name="TaskRunner Events &gt; should support multiple listeners for the same event" time="0.000722328">
85
+ </testcase>
86
+ </testsuite>
87
+ <testsuite name="tests/WorkflowExecutor.test.ts" timestamp="2026-01-18T15:13:46.730Z" hostname="runnervmmtnos" tests="3" failures="0" errors="0" skipped="0" time="0.058601689">
88
+ <testcase classname="tests/WorkflowExecutor.test.ts" name="WorkflowExecutor &gt; should execute steps sequentially when dependencies exist" time="0.004596878">
77
89
  </testcase>
78
- <testcase classname="tests/TaskRunnerEvents.test.ts" name="TaskRunner Events &gt; should safely handle off() when no listeners exist" time="0.000162473">
90
+ <testcase classname="tests/WorkflowExecutor.test.ts" name="WorkflowExecutor &gt; should skip dependent steps if dependency fails" time="0.000424162">
79
91
  </testcase>
80
- <testcase classname="tests/TaskRunnerEvents.test.ts" name="TaskRunner Events &gt; should support multiple listeners for the same event" time="0.000345023">
92
+ <testcase classname="tests/WorkflowExecutor.test.ts" name="WorkflowExecutor &gt; should run independent steps in parallel" time="0.051090362">
81
93
  </testcase>
82
94
  </testsuite>
83
- <testsuite name="tests/integration.test.ts" timestamp="2026-01-18T05:44:30.750Z" hostname="runnervmmtnos" tests="3" failures="0" errors="0" skipped="0" time="0.005644698">
84
- <testcase classname="tests/integration.test.ts" name="TaskRunner Validation Integration &gt; should throw validation error with clear message for duplicate tasks" time="0.003288223">
95
+ <testsuite name="tests/integration.test.ts" timestamp="2026-01-18T15:13:46.731Z" hostname="runnervmmtnos" tests="3" failures="0" errors="0" skipped="0" time="0.008485415">
96
+ <testcase classname="tests/integration.test.ts" name="TaskRunner Validation Integration &gt; should throw validation error with clear message for duplicate tasks" time="0.004859699">
85
97
  </testcase>
86
- <testcase classname="tests/integration.test.ts" name="TaskRunner Validation Integration &gt; should throw validation error with clear message for missing dependencies" time="0.000378435">
98
+ <testcase classname="tests/integration.test.ts" name="TaskRunner Validation Integration &gt; should throw validation error with clear message for missing dependencies" time="0.000630747">
87
99
  </testcase>
88
- <testcase classname="tests/integration.test.ts" name="TaskRunner Validation Integration &gt; should throw validation error with clear message for cycles" time="0.000325437">
100
+ <testcase classname="tests/integration.test.ts" name="TaskRunner Validation Integration &gt; should throw validation error with clear message for cycles" time="0.000504291">
89
101
  </testcase>
90
102
  </testsuite>
91
103
  </testsuites>