Conversation
Add core types for the collection runner feature: - RunSession: manages execution state with session-scoped environment - RunConfig: configurable execution parameters (delay, timeout, stop-on-failure) - RequestResult: captures individual request execution results - RunReport: comprehensive summary with statistics and timing - Request collector for traversing collection folders Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add unit tests covering: - RunStatus string conversion - RunConfig validation (delay limits, timeout validation) - RequestResult state transitions - RunSession lifecycle (start, stop, cancel, complete) - Progress tracking and percentage calculation - RunReport generation with statistics - Export functionality with directory creation Coverage: ~72% for runner package Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add UI components for collection runner: - RunnerModal: interactive modal with progress bar and results list - Bubble Tea messages for runner lifecycle events - Animated loader with spinner states - Keyboard controls (q/Escape to cancel, e to export) - Real-time progress tracking with visual feedback Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add Bubble Tea commands for runner execution: - ExecuteCollectionRunCmd: initializes and validates run session - ExecuteNextRequestCmd: executes requests with script support - DelayCmd: handles inter-request delays - CancelRunCmd: graceful cancellation with cleanup - ExportRunReportCmd: exports results to JSON Key features: - Pre-request script execution before variable substitution - Post-response script with environment updates - Stop-on-failure support with remaining requests marked as skipped - Session-scoped environment isolation Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Integrate runner with main model: - Add Ctrl+R keybinding to launch runner from collections panel - Add GetSelectedCollection/GetSelectedFolderPath helpers - Wire all runner message handlers in Update() - Add runner modal overlay in View() - Handle input priority for modal focus Supports running entire collections or specific folders with real-time progress display and cancellation. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
|
Note Other AI code review bot(s) detectedCodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review. 📝 WalkthroughWalkthroughAdds a collection runner: core runner model and types, session lifecycle and reporting (including export), collection traversal, extensive tests, new UI modal and command/message plumbing, keybinding, and integration of pre/post script execution into per-request run flow. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant UI_Model
participant Runner_Cmd
participant API_Runner
participant HTTP_Client
participant Modal
User->>UI_Model: trigger run (ctrl+r)
UI_Model->>Runner_Cmd: ExecuteCollectionRunCmd(collection, folderPath, env, cfg)
Runner_Cmd->>API_Runner: CollectRequests(...)
API_Runner-->>Runner_Cmd: requests[]
Runner_Cmd->>API_Runner: NewRunSession(...)
API_Runner-->>Runner_Cmd: session
Runner_Cmd->>UI_Model: RunnerStartedMsg(session, requests)
UI_Model->>Modal: Show(session, requests)
loop per request
Runner_Cmd->>Runner_Cmd: run pre-script -> apply env changes
Runner_Cmd->>HTTP_Client: send request
HTTP_Client-->>Runner_Cmd: response
Runner_Cmd->>Runner_Cmd: run post-script -> apply env changes
Runner_Cmd->>API_Runner: AddResult(...)
Runner_Cmd->>UI_Model: RunnerRequestCompleteMsg(result, session)
Runner_Cmd->>Runner_Cmd: maybe delay / check stop-on-failure
end
Runner_Cmd->>API_Runner: GenerateReport(session)
API_Runner-->>Runner_Cmd: report
Runner_Cmd->>UI_Model: RunnerCompleteMsg(session, report)
UI_Model->>Modal: UpdateSession() / SetExported(...)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested labels
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Pull request overview
This pull request implements a comprehensive Collection Runner feature that enables batch execution of HTTP requests from collections or folders. The runner supports sequential execution with configurable delays, stop-on-failure behavior, pre/post-request script execution, and detailed reporting with JSON export capabilities.
Changes:
- Added core runner API with session management, request result tracking, and comprehensive reporting
- Implemented interactive UI modal with real-time progress tracking, animated loader, and scrollable results
- Integrated runner into the main application with Ctrl+R keybinding and full message flow handling
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| internal/api/runner.go | Core runner logic with session management, request result types, report generation, and JSON export |
| internal/api/runner_test.go | Comprehensive test suite with 26 tests covering configuration, execution flow, and report generation |
| internal/ui/runner_modal.go | Interactive modal component with progress bar, status indicators, scrolling, and keyboard controls |
| internal/ui/runner_messages.go | Message type definitions for runner lifecycle events and communication |
| internal/ui/runner_commands.go | Command functions for request execution, script handling, and runner control flow |
| internal/ui/model.go | Integration of runner into main model with message handlers and state management |
| internal/ui/collections_view.go | Helper methods to retrieve selected collection and folder path for runner initialization |
| internal/config/config.go | Added run_collection keybinding configuration (Ctrl+R) |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| switch msg.String() { | ||
| case "esc": | ||
| if m.session.IsTerminal() { | ||
| // Close modal when run is finished | ||
| m.Hide() | ||
| return m, func() tea.Msg { | ||
| return RunnerHideModalMsg{} | ||
| } | ||
| } | ||
| // Cancel running execution | ||
| return m, func() tea.Msg { | ||
| return RunnerCancelMsg{} | ||
| } |
There was a problem hiding this comment.
The PR description mentions "q/Escape to cancel" as a keyboard control, but the code only handles "esc" key, not "q". The key handler in the Update method does not include a case for "q". This creates a discrepancy between documentation and implementation.
| m.statusBar.Success("Run complete", | ||
| fmt.Sprintf("%d/%d passed, %d failed, %d errors", | ||
| report.Summary.PassedAssertions, | ||
| report.Summary.TotalAssertions, | ||
| report.Summary.FailedAssertions, | ||
| report.Summary.Errors)) |
There was a problem hiding this comment.
The status bar message format may be confusing. It displays "passed/total passed, failed, errors" where "passed/total" uses TotalAssertions as the denominator, but TotalAssertions represents the total number of assertions run (passed + failed), not the total number of requests. This could be misleading. Consider reformatting to clarify whether these numbers represent assertions or requests, e.g., "Assertions: X/Y passed" or "Requests: X passed, Y failed, Z errors".
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Fix all issues with AI agents
In `@internal/api/runner_test.go`:
- Around line 823-853: The test TestExportRunReport_DirectoryPermissions calls
os.Getuid(), which is not available on Windows; update the test to guard that
root-skip logic with a platform check (import runtime) so Windows builds don't
call os.Getuid(): change the root check to something like "if runtime.GOOS !=
\"windows\" && os.Getuid() == 0 { t.Skip(...) }" (ensure runtime is imported) so
the test compiles and still skips when running as root on Unix; keep the rest of
the TestExportRunReport_DirectoryPermissions logic unchanged.
In `@internal/api/runner.go`:
- Around line 446-468: ExportRunReport uses second-precision timestamps so
concurrent calls can produce filename collisions; update ExportRunReport to
generate higher-resolution unique filenames (e.g., include nanoseconds or a UUID
suffix) by changing the timestamp format in the filename generation (function
ExportRunReport and the filename variable) and ensure the filepath.Join logic
remains the same; additionally add a unit test that concurrently calls
ExportRunReport (multiple goroutines) and asserts all returned paths are unique
and files exist to verify collision prevention.
In `@internal/ui/runner_commands.go`:
- Around line 155-181: SetCompleted currently records the HTTP-only timing but
later the code overwrites result.Duration and result.DurationMs with
time.Since(startTime) after running the post-response script
(ExecutePostResponse / applyEnvChanges), causing the HTTP duration to be lost;
preserve the HTTP-only duration by removing the second overwrite or by storing
total duration in a separate field (e.g., TotalDuration/TotalDurationMs) and
keeping result.Duration/result.DurationMs as the HTTP-only values set by
SetCompleted, and when updating script results ensure you call
applyEnvChanges(session, postScriptResult) without resetting
result.Duration/result.DurationMs.
In `@internal/ui/runner_modal.go`:
- Around line 188-196: The visibleResultRows calculation currently subtracts a
fixed offset from m.height which can overestimate space when the modal is
capped; change visibleResultRows in RunnerModal to compute available rows from
the actual modal height (the effective/capped modal size) rather than the raw
terminal height. Use the modal's effective height (e.g., an existing field or
helper like modalHeight, effectiveHeight, or min(m.height, m.maxModalHeight)) to
subtract the header/progress/summary/footer/borders (the same 16 offset) and
keep the minimum floor (3) as before; update visibleResultRows to reference that
effective modal height so scrolling is accurate when the modal is capped.
- Around line 276-314: renderProgress can divide by zero because
m.session.TotalRequests may be 0 before Start(); update renderProgress to guard
the percent calculation the same way renderResults does: check if
m.session.TotalRequests == 0 and set percent to 0.0 (or otherwise avoid
division) before computing filled/empty from barWidth, using
m.session.CurrentIndex and m.session.TotalRequests to locate the values; ensure
the computed filled value is clamped between 0 and barWidth to avoid negative or
overflow values when constructing the bar.
🧹 Nitpick comments (3)
internal/api/runner.go (1)
475-480: Run IDs can collide within the same second.
Using second precision can duplicate IDs for rapid successive runs; consider higher resolution.🔧 Proposed tweak
- return fmt.Sprintf("run_%d_%s", time.Now().Unix(), sanitized) + return fmt.Sprintf("run_%d_%s", time.Now().UnixNano(), sanitized)internal/api/runner_test.go (1)
113-216: Convert single‑case tests to table‑driven style.
Several tests here are single-case (e.g., DefaultRunConfig, NewRequestResult, SetRunning/SetCompleted/SetError). Refactor to struct‑slice tables for consistency. As per coding guidelines, please use table‑driven tests.internal/ui/runner_commands.go (1)
350-357: String-based timeout detection is fragile but acceptable.The
isTimeoutErrorfunction relies on string matching in error messages. This works for standard Go HTTP timeouts but could miss wrapped errors or fail with localized error messages.Consider using
errors.Aswithnet.Errorinterface for more robust timeout detection if this becomes problematic:var netErr net.Error if errors.As(err, &netErr) && netErr.Timeout() { return true }
| // ExportRunReport saves the report to .lazycurl/reports/ | ||
| func ExportRunReport(report *RunReport) (string, error) { | ||
| // Ensure directory exists | ||
| dir := ".lazycurl/reports" | ||
| if err := os.MkdirAll(dir, 0755); err != nil { | ||
| return "", fmt.Errorf("failed to create reports directory: %w", err) | ||
| } | ||
|
|
||
| // Generate filename with timestamp | ||
| filename := fmt.Sprintf("run_%s.json", time.Now().Format("20060102_150405")) | ||
| path := filepath.Join(dir, filename) | ||
|
|
||
| // Marshal and write | ||
| data, err := json.MarshalIndent(report, "", " ") | ||
| if err != nil { | ||
| return "", fmt.Errorf("failed to marshal report: %w", err) | ||
| } | ||
|
|
||
| if err := os.WriteFile(path, data, 0644); err != nil { | ||
| return "", fmt.Errorf("failed to write report file: %w", err) | ||
| } | ||
|
|
||
| return path, nil |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, check if the file follows snake_case naming convention
ls -la internal/api/ | grep -E "\.go$"Repository: kbrdn1/LazyCurl
Length of output: 3539
🏁 Script executed:
# Search for ExportRunReport usage to understand if concurrent calls are possible
rg "ExportRunReport" --type go -A 3 -B 3Repository: kbrdn1/LazyCurl
Length of output: 3067
🏁 Script executed:
# Check for any existing tests related to ExportRunReport
fd "test" internal/api/ --type f -name "*.go" | xargs rg "ExportRunReport" 2>/dev/null || echo "No tests found for ExportRunReport"Repository: kbrdn1/LazyCurl
Length of output: 1219
🏁 Script executed:
# Check the full context of the ExportRunReport function
sed -n '446,468p' internal/api/runner.goRepository: kbrdn1/LazyCurl
Length of output: 804
🏁 Script executed:
# Check if there are multiple calls to ExportRunReport or concurrent patterns
rg -B 5 -A 5 "ExportRunReport" --type go | head -50Repository: kbrdn1/LazyCurl
Length of output: 2681
🏁 Script executed:
# Check the full test file for ExportRunReport tests
grep -A 30 "func TestExportRunReport" internal/api/runner_test.goRepository: kbrdn1/LazyCurl
Length of output: 1886
🏁 Script executed:
# Check if there are any tests for concurrent/collision scenarios
rg "TestExportRunReport|concurrent|collision|parallel" internal/api/runner_test.goRepository: kbrdn1/LazyCurl
Length of output: 161
Add higher-resolution filename suffix to prevent collision on concurrent exports.
The current timestamp format uses second precision, so multiple concurrent exports within the same second will overwrite each other. The Tea framework executes commands asynchronously, making concurrent calls possible.
🔧 Proposed fix
- // Generate filename with timestamp
- filename := fmt.Sprintf("run_%s.json", time.Now().Format("20060102_150405"))
+ // Generate filename with high-resolution timestamp
+ ts := time.Now()
+ filename := fmt.Sprintf("run_%s_%09d.json", ts.Format("20060102_150405"), ts.Nanosecond())Additionally, add a test case for concurrent exports to verify collision prevention, as this is a public API function.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // ExportRunReport saves the report to .lazycurl/reports/ | |
| func ExportRunReport(report *RunReport) (string, error) { | |
| // Ensure directory exists | |
| dir := ".lazycurl/reports" | |
| if err := os.MkdirAll(dir, 0755); err != nil { | |
| return "", fmt.Errorf("failed to create reports directory: %w", err) | |
| } | |
| // Generate filename with timestamp | |
| filename := fmt.Sprintf("run_%s.json", time.Now().Format("20060102_150405")) | |
| path := filepath.Join(dir, filename) | |
| // Marshal and write | |
| data, err := json.MarshalIndent(report, "", " ") | |
| if err != nil { | |
| return "", fmt.Errorf("failed to marshal report: %w", err) | |
| } | |
| if err := os.WriteFile(path, data, 0644); err != nil { | |
| return "", fmt.Errorf("failed to write report file: %w", err) | |
| } | |
| return path, nil | |
| // ExportRunReport saves the report to .lazycurl/reports/ | |
| func ExportRunReport(report *RunReport) (string, error) { | |
| // Ensure directory exists | |
| dir := ".lazycurl/reports" | |
| if err := os.MkdirAll(dir, 0755); err != nil { | |
| return "", fmt.Errorf("failed to create reports directory: %w", err) | |
| } | |
| // Generate filename with high-resolution timestamp | |
| ts := time.Now() | |
| filename := fmt.Sprintf("run_%s_%09d.json", ts.Format("20060102_150405"), ts.Nanosecond()) | |
| path := filepath.Join(dir, filename) | |
| // Marshal and write | |
| data, err := json.MarshalIndent(report, "", " ") | |
| if err != nil { | |
| return "", fmt.Errorf("failed to marshal report: %w", err) | |
| } | |
| if err := os.WriteFile(path, data, 0644); err != nil { | |
| return "", fmt.Errorf("failed to write report file: %w", err) | |
| } | |
| return path, nil |
🤖 Prompt for AI Agents
In `@internal/api/runner.go` around lines 446 - 468, ExportRunReport uses
second-precision timestamps so concurrent calls can produce filename collisions;
update ExportRunReport to generate higher-resolution unique filenames (e.g.,
include nanoseconds or a UUID suffix) by changing the timestamp format in the
filename generation (function ExportRunReport and the filename variable) and
ensure the filepath.Join logic remains the same; additionally add a unit test
that concurrently calls ExportRunReport (multiple goroutines) and asserts all
returned paths are unique and files exist to verify collision prevention.
| // Set completed with response | ||
| result.SetCompleted(resp, time.Since(startTime)) | ||
|
|
||
| // Execute post-response script if present | ||
| var postScriptResult *api.ScriptResult | ||
| postScript := getPostResponseScript(collReq) | ||
| if postScript != "" { | ||
| scriptReq := api.NewScriptRequestFromHTTP(httpReq) | ||
| scriptResp := createScriptResponseFromHTTP(resp) | ||
| env := sessionEnvToEnvironment(session.SessionEnv) | ||
|
|
||
| var err error | ||
| postScriptResult, err = executor.ExecutePostResponse(postScript, scriptReq, scriptResp, env) | ||
| if err != nil { | ||
| result.SetError("script", "Post-response script error", err.Error()) | ||
| } else { | ||
| // Apply environment changes from post-response script | ||
| applyEnvChanges(session, postScriptResult) | ||
| } | ||
| } | ||
|
|
||
| // Set script results | ||
| result.SetScriptResults(preScriptResult, postScriptResult) | ||
|
|
||
| // Finalize timing | ||
| result.Duration = time.Since(startTime) | ||
| result.DurationMs = result.Duration.Milliseconds() |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
ast-grep --pattern $'func ($_) SetCompleted($_, $_) {
$$$
}'Repository: kbrdn1/LazyCurl
Length of output: 805
Duration fields are overwritten after post-response script execution.
SetCompleted sets result.Duration and result.DurationMs on line 156 with the elapsed time to the HTTP response. However, lines 180-181 overwrite these values with time.Since(startTime) executed after the post-response script (lines 158-174) completes.
This means the final duration includes both HTTP request time and post-response script execution time. Verify if this is intentional—you may want separate metrics for HTTP-only duration (performance analysis) and total duration (including scripts).
🤖 Prompt for AI Agents
In `@internal/ui/runner_commands.go` around lines 155 - 181, SetCompleted
currently records the HTTP-only timing but later the code overwrites
result.Duration and result.DurationMs with time.Since(startTime) after running
the post-response script (ExecutePostResponse / applyEnvChanges), causing the
HTTP duration to be lost; preserve the HTTP-only duration by removing the second
overwrite or by storing total duration in a separate field (e.g.,
TotalDuration/TotalDurationMs) and keeping result.Duration/result.DurationMs as
the HTTP-only values set by SetCompleted, and when updating script results
ensure you call applyEnvChanges(session, postScriptResult) without resetting
result.Duration/result.DurationMs.
- Add Windows platform guard for os.Getuid() in tests - Add nanosecond precision to report filenames to prevent collision - Fix divide by zero guard in renderProgress - Fix visibleResultRows calculation using effective modal height - Add 'q' key handler to close modal when run is finished Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Description
Add a Collection Runner feature that enables batch execution of HTTP requests from collections or folders. This feature allows users to run multiple requests sequentially with configurable delays, stop-on-failure behavior, and comprehensive reporting.
Related Issue
Closes #44
Type of Change
Changes Made
Core API (
internal/api/runner.go)RunSession: Manages execution state with session-scoped environment cloningRunConfig: Configurable parameters (delay, timeout, stop-on-failure, script timeout)RequestResult: Captures individual request results with timing and script outputsRunReport: Comprehensive summary with pass/fail/skip statisticsCollectFromCollection: Depth-first request collection from foldersUI Components (
internal/ui/)RunnerModal: Interactive modal with animated progress bar and results listrunner_messages.go: Bubble Tea messages for runner lifecycle eventsrunner_commands.go: Execution commands with pre/post-script supportIntegration
Ctrl+Rkeybinding to launch runner from Collections panelKey Features
.lazycurl/reports/Testing
make test)make build)Test Coverage
Screenshots (if applicable)
Runner modal shows progress bar, request list with status icons, and real-time updates
Checklist
Additional Notes
Critical Bug Fix
During multi-agent verification, a timing bug was identified and fixed: variable substitution was happening BEFORE pre-request script execution, which meant variables set by scripts weren't being substituted in the request URL. The fix reorders the execution flow:
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Tests
✏️ Tip: You can customize this high-level summary in your review settings.