-
Notifications
You must be signed in to change notification settings - Fork 1
feat: add CLI reporter plugin #241
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| ## 1. Implementation | ||
|
|
||
| - [x] 1.1 Create `src/plugins/CLIReporterPlugin.ts` implementing `Plugin<TContext>`. | ||
| - [x] 1.2 Implement event listeners for `taskStart`, `taskEnd`, and `taskSkipped`. | ||
| - [x] 1.3 Implement real-time console rendering logic (e.g., using ANSI escape sequences or a minimal progress library) to display running tasks. | ||
| - [x] 1.4 Implement a summary output function to display total execution time, successful tasks, failed tasks, and skipped tasks at the end of the workflow. | ||
| - [x] 1.5 Write unit and integration tests in `tests/plugins/CLIReporterPlugin.test.ts`. | ||
| - [x] 1.6 Update `README.md` to document the new `CLIReporterPlugin` usage. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| import { Plugin, PluginContext } from "../contracts/Plugin.js"; | ||
|
|
||
| /** | ||
| * A plugin that reports task execution progress and summarizes results in the CLI. | ||
| */ | ||
| export class CLIReporterPlugin<TContext> implements Plugin<TContext> { | ||
| public readonly name = "cli-reporter"; | ||
| public readonly version = "1.0.0"; | ||
|
|
||
| private startTime = 0; | ||
| private successCount = 0; | ||
| private failureCount = 0; | ||
| private skippedCount = 0; | ||
|
|
||
| public install(context: PluginContext<TContext>): void { | ||
| context.events.on("workflowStart", () => { | ||
| this.startTime = performance.now(); | ||
| this.successCount = 0; | ||
| this.failureCount = 0; | ||
| this.skippedCount = 0; | ||
| console.log("\n🚀 Starting TaskRunner workflow..."); | ||
| }); | ||
|
|
||
| context.events.on("taskStart", ({ step }) => { | ||
| console.log(`⏳ Starting: ${step.name}`); | ||
| }); | ||
|
|
||
| context.events.on("taskEnd", ({ step, result }) => { | ||
| if (result.status === "success") { | ||
| this.successCount++; | ||
| console.log(`✅ Success: ${step.name}`); | ||
| } else { | ||
| this.failureCount++; | ||
| console.log(`❌ Failed: ${step.name}${result.error ? ` - ${result.error}` : ""}`); | ||
|
Check warning on line 34 in src/plugins/CLIReporterPlugin.ts
|
||
| } | ||
| }); | ||
|
|
||
| context.events.on("taskSkipped", ({ step, result }) => { | ||
| this.skippedCount++; | ||
| const reason = result.error ? ` - ${result.error}` : ""; | ||
| console.log(`⏭️ Skipped: ${step.name}${reason}`); | ||
| }); | ||
|
|
||
| context.events.on("workflowEnd", () => { | ||
| const duration = performance.now() - this.startTime; | ||
| console.log(`\n🏁 Workflow Completed in ${duration.toFixed(2)}ms`); | ||
| console.log("📊 Summary:"); | ||
| console.log(` ✅ Success: ${this.successCount}`); | ||
| console.log(` ❌ Failed: ${this.failureCount}`); | ||
| console.log(` ⏭️ Skipped: ${this.skippedCount}\n`); | ||
| }); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,106 @@ | ||||||
| import { describe, it, expect, vi, beforeEach } from "vitest"; | ||||||
| import { CLIReporterPlugin } from "../../src/plugins/CLIReporterPlugin.js"; | ||||||
| import { EventBus } from "../../src/EventBus.js"; | ||||||
| import { PluginContext } from "../../src/contracts/Plugin.js"; | ||||||
| import { TaskStep } from "../../src/TaskStep.js"; | ||||||
| import { TaskResult } from "../../src/TaskResult.js"; | ||||||
|
|
||||||
| describe("CLIReporterPlugin", () => { | ||||||
| let plugin: CLIReporterPlugin<unknown>; | ||||||
| let events: EventBus<unknown>; | ||||||
| let context: PluginContext<unknown>; | ||||||
| let consoleLogSpy: ReturnType<typeof vi.spyOn>; | ||||||
|
|
||||||
| beforeEach(() => { | ||||||
| plugin = new CLIReporterPlugin(); | ||||||
| events = new EventBus(); | ||||||
| context = { events }; | ||||||
| consoleLogSpy = vi.spyOn(console, "log").mockImplementation(() => {}); | ||||||
| }); | ||||||
|
|
||||||
| it("should have correct name and version", () => { | ||||||
| expect(plugin.name).toBe("cli-reporter"); | ||||||
| expect(plugin.version).toBe("1.0.0"); | ||||||
| }); | ||||||
|
|
||||||
| it("should output workflow start", async () => { | ||||||
| plugin.install(context); | ||||||
| events.emit("workflowStart", { context: {}, steps: [] }); | ||||||
| // events are processed asynchronously | ||||||
| await new Promise(process.nextTick); | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using
Suggested change
|
||||||
|
|
||||||
| expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining("Starting TaskRunner workflow")); | ||||||
| }); | ||||||
|
|
||||||
| it("should output task start", async () => { | ||||||
| plugin.install(context); | ||||||
| const step: TaskStep<unknown> = { name: "Step1", run: async () => ({ status: "success" }) }; | ||||||
| events.emit("taskStart", { step }); | ||||||
| await new Promise(process.nextTick); | ||||||
|
|
||||||
| expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining("Starting: Step1")); | ||||||
| }); | ||||||
|
|
||||||
| it("should output task success", async () => { | ||||||
| plugin.install(context); | ||||||
| const step: TaskStep<unknown> = { name: "Step1", run: async () => ({ status: "success" }) }; | ||||||
| const result: TaskResult = { status: "success" }; | ||||||
| events.emit("taskEnd", { step, result }); | ||||||
| await new Promise(process.nextTick); | ||||||
|
|
||||||
| expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining("Success: Step1")); | ||||||
| }); | ||||||
|
|
||||||
| it("should output task failure", async () => { | ||||||
| plugin.install(context); | ||||||
| const step: TaskStep<unknown> = { name: "Step1", run: async () => ({ status: "failure" }) }; | ||||||
| const result: TaskResult = { status: "failure", error: "Oops" }; | ||||||
| events.emit("taskEnd", { step, result }); | ||||||
| await new Promise(process.nextTick); | ||||||
|
|
||||||
| expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining("Failed: Step1 - Oops")); | ||||||
| }); | ||||||
|
|
||||||
| it("should output task skip", async () => { | ||||||
| plugin.install(context); | ||||||
| const step: TaskStep<unknown> = { name: "Step1", run: async () => ({ status: "success" }) }; | ||||||
| const result: TaskResult = { status: "skipped" }; | ||||||
| events.emit("taskSkipped", { step, result }); | ||||||
| await new Promise(process.nextTick); | ||||||
|
|
||||||
| expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining("Skipped: Step1")); | ||||||
| }); | ||||||
|
|
||||||
| it("should output task skip with error", async () => { | ||||||
| plugin.install(context); | ||||||
| const step: TaskStep<unknown> = { name: "Step1", run: async () => ({ status: "success" }) }; | ||||||
| const result: TaskResult = { status: "skipped", error: "Condition failed" }; | ||||||
| events.emit("taskSkipped", { step, result }); | ||||||
| await new Promise(process.nextTick); | ||||||
|
|
||||||
| expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining("Skipped: Step1 - Condition failed")); | ||||||
| }); | ||||||
|
|
||||||
| it("should output summary at workflow end", async () => { | ||||||
| plugin.install(context); | ||||||
| events.emit("workflowStart", { context: {}, steps: [] }); | ||||||
|
|
||||||
| const step1: TaskStep<unknown> = { name: "Step1", run: async () => ({ status: "success" }) }; | ||||||
| events.emit("taskEnd", { step: step1, result: { status: "success" } }); | ||||||
|
|
||||||
| const step2: TaskStep<unknown> = { name: "Step2", run: async () => ({ status: "failure" }) }; | ||||||
| events.emit("taskEnd", { step: step2, result: { status: "failure" } }); | ||||||
|
|
||||||
| const step3: TaskStep<unknown> = { name: "Step3", run: async () => ({ status: "success" }) }; | ||||||
| events.emit("taskSkipped", { step: step3, result: { status: "skipped" } }); | ||||||
|
|
||||||
| events.emit("workflowEnd", { context: {}, results: new Map() }); | ||||||
|
|
||||||
| await new Promise(process.nextTick); | ||||||
|
|
||||||
| expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining("Summary:")); | ||||||
| expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining("Success: 1")); | ||||||
| expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining("Failed: 1")); | ||||||
| expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining("Skipped: 1")); | ||||||
| }); | ||||||
| }); | ||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The plugin currently stores execution state as class members. To prevent stale data across multiple initializations or concurrent executions, ensure all state-holding members are cleared or move state into the method scope. This aligns with the repository rule regarding stateful class initialization.
References