Preserve batch cancellation outcomes
This commit is contained in:
@@ -37,6 +37,10 @@ func writeBatchStatus(stderr io.Writer, result *app.BatchResult) {
|
||||
_, _ = fmt.Fprintf(stderr, "report=%s status=failed error=%q\n", item.ReportID, item.Error)
|
||||
continue
|
||||
}
|
||||
if item.Status == "canceled" {
|
||||
_, _ = fmt.Fprintf(stderr, "report=%s status=canceled\n", item.ReportID)
|
||||
continue
|
||||
}
|
||||
_, _ = fmt.Fprintf(stderr, "report=%s status=succeeded output=%q\n", item.ReportID, item.OutputPath)
|
||||
}
|
||||
if result.Notification != nil {
|
||||
@@ -58,5 +62,5 @@ func writeBatchStatus(stderr io.Writer, result *app.BatchResult) {
|
||||
}
|
||||
_, _ = fmt.Fprintln(stderr)
|
||||
}
|
||||
_, _ = fmt.Fprintf(stderr, "batch=%s total=%d succeeded=%d failed=%d\n", result.Batch, result.Total, result.Succeeded, result.Failed)
|
||||
_, _ = fmt.Fprintf(stderr, "batch=%s total=%d succeeded=%d failed=%d canceled=%d\n", result.Batch, result.Total, result.Succeeded, result.Failed, result.Canceled)
|
||||
}
|
||||
|
||||
@@ -70,6 +70,7 @@ type batchSummary struct {
|
||||
Total int `json:"total"`
|
||||
Succeeded int `json:"succeeded"`
|
||||
Failed int `json:"failed"`
|
||||
Canceled int `json:"canceled,omitempty"`
|
||||
Notification *app.BatchNotificationResult `json:"notification,omitempty"`
|
||||
Reports []app.BatchReportResult `json:"reports"`
|
||||
Error string `json:"error,omitempty"`
|
||||
@@ -161,7 +162,7 @@ func newGenerateNotificationSummary(result *app.NotificationResult) *generateNot
|
||||
return summary
|
||||
}
|
||||
|
||||
func newBatchSummary(result *app.BatchResult) batchSummary {
|
||||
func newBatchSummary(result *app.BatchResult, err error) batchSummary {
|
||||
summary := batchSummary{Command: commandRun}
|
||||
if result == nil {
|
||||
return summary
|
||||
@@ -174,10 +175,11 @@ func newBatchSummary(result *app.BatchResult) batchSummary {
|
||||
summary.Total = result.Total
|
||||
summary.Succeeded = result.Succeeded
|
||||
summary.Failed = result.Failed
|
||||
summary.Canceled = result.Canceled
|
||||
summary.Notification = result.Notification
|
||||
summary.Reports = append([]app.BatchReportResult(nil), result.Reports...)
|
||||
if summary.Status == summaryStatusFailed {
|
||||
summary.Error = app.BatchError{Result: result}.Error()
|
||||
summary.Error = app.BatchError{Result: result, Cause: err}.Error()
|
||||
}
|
||||
return summary
|
||||
}
|
||||
@@ -267,7 +269,7 @@ func batchSummaryStatus(result *app.BatchResult) string {
|
||||
if result == nil {
|
||||
return ""
|
||||
}
|
||||
if result.Failed > 0 || (result.Notification != nil && result.Notification.Status == summaryStatusFailed) {
|
||||
if result.Failed > 0 || result.Canceled > 0 || (result.Notification != nil && result.Notification.Status == summaryStatusFailed) {
|
||||
return summaryStatusFailed
|
||||
}
|
||||
return summaryStatusSucceeded
|
||||
|
||||
@@ -111,12 +111,15 @@ func (r Runner) Run(ctx context.Context, args []string, stdout io.Writer, stderr
|
||||
}
|
||||
result, err := runBatchDetailed(ctx, req)
|
||||
if result != nil {
|
||||
summary := newBatchSummary(result)
|
||||
summary := newBatchSummary(result, err)
|
||||
if encodeErr := writeActionResult(stdout, stderr, summary, outputOptions{Quiet: opts.Quiet}, func(w io.Writer) {
|
||||
writeBatchStatus(w, result)
|
||||
}); encodeErr != nil {
|
||||
return encodeErr
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if summary.Status == summaryStatusFailed {
|
||||
return app.BatchError{Result: result}
|
||||
}
|
||||
|
||||
@@ -193,6 +193,43 @@ func TestRunActionReturnsFailureForBatchNotificationFailure(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunActionPreservesBatchCancellation(t *testing.T) {
|
||||
configPath := actionConfigPath(t)
|
||||
for _, cause := range []error{context.Canceled, context.DeadlineExceeded} {
|
||||
t.Run(cause.Error(), func(t *testing.T) {
|
||||
result := &app.BatchResult{
|
||||
Batch: app.BatchMorning, Total: 2, Succeeded: 1, Canceled: 1,
|
||||
Reports: []app.BatchReportResult{
|
||||
{ReportID: "today", Status: "succeeded", OutputPath: "/reports/today.md"},
|
||||
{ReportID: "tomorrow", Status: "canceled"},
|
||||
},
|
||||
Notification: &app.BatchNotificationResult{Status: "skipped", Reason: "batch canceled"},
|
||||
}
|
||||
var stdout, stderr bytes.Buffer
|
||||
runner := Runner{
|
||||
Clock: timeutil.FixedClock{Time: time.Date(2026, 5, 29, 8, 0, 0, 0, time.UTC)},
|
||||
ExecutorFactory: func(PromptExecutorConfig) (promptexec.Executor, error) {
|
||||
return &factoryExecutor{}, nil
|
||||
},
|
||||
runBatchDetailed: func(context.Context, app.BatchRequest) (*app.BatchResult, error) {
|
||||
return result, cause
|
||||
},
|
||||
}
|
||||
err := runner.Run(context.Background(), []string{"run", "morning", "--config", configPath}, &stdout, &stderr)
|
||||
if !errors.Is(err, cause) {
|
||||
t.Fatalf("Run() error = %v", err)
|
||||
}
|
||||
var summary batchSummary
|
||||
if decodeErr := json.Unmarshal(stdout.Bytes(), &summary); decodeErr != nil {
|
||||
t.Fatal(decodeErr)
|
||||
}
|
||||
if summary.Status != summaryStatusFailed || summary.Total != 2 || summary.Succeeded != 1 || summary.Failed != 0 || summary.Canceled != 1 || summary.Error == "" || len(summary.Reports) != 2 || summary.Reports[1].Status != "canceled" || !strings.Contains(stderr.String(), "report=tomorrow status=canceled") || !strings.Contains(stderr.String(), "canceled=1") {
|
||||
t.Fatalf("summary/stderr = %#v/%q", summary, stderr.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunCommandProjectsSuccessAndReportFailure(t *testing.T) {
|
||||
configPath := actionConfigPath(t)
|
||||
for _, tt := range []struct {
|
||||
|
||||
Reference in New Issue
Block a user