Report distributor notification outcomes in batches
This commit is contained in:
@@ -352,11 +352,21 @@ func writeRunLogs(stderr io.Writer, result *app.BatchResult) {
|
||||
return
|
||||
}
|
||||
for _, item := range result.Reports {
|
||||
notificationFields := ""
|
||||
if item.NotificationStatus != "" {
|
||||
notificationFields += fmt.Sprintf(" notificationStatus=%q", item.NotificationStatus)
|
||||
}
|
||||
if item.NotificationRunID != "" {
|
||||
notificationFields += fmt.Sprintf(" notificationRunId=%q", item.NotificationRunID)
|
||||
}
|
||||
if item.NotificationError != "" {
|
||||
notificationFields += fmt.Sprintf(" notificationError=%q", item.NotificationError)
|
||||
}
|
||||
if item.Status == "failed" {
|
||||
_, _ = fmt.Fprintf(stderr, "report=%s status=failed error=%q\n", item.ReportID, item.Error)
|
||||
_, _ = fmt.Fprintf(stderr, "report=%s status=failed error=%q%s\n", item.ReportID, item.Error, notificationFields)
|
||||
continue
|
||||
}
|
||||
_, _ = fmt.Fprintf(stderr, "report=%s status=succeeded output=%q\n", item.ReportID, item.OutputPath)
|
||||
_, _ = fmt.Fprintf(stderr, "report=%s status=succeeded output=%q%s\n", item.ReportID, item.OutputPath, notificationFields)
|
||||
}
|
||||
_, _ = fmt.Fprintf(stderr, "batch=%s total=%d succeeded=%d failed=%d\n", result.Batch, result.Total, result.Succeeded, result.Failed)
|
||||
}
|
||||
|
||||
@@ -346,6 +346,88 @@ func TestRunMorningReportsPartialFailureAndContinues(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchOutputIncludesNotificationDetails(t *testing.T) {
|
||||
result := &app.BatchResult{
|
||||
Batch: app.BatchMorning,
|
||||
Total: 2,
|
||||
Succeeded: 1,
|
||||
Failed: 1,
|
||||
Reports: []app.BatchReportResult{
|
||||
{
|
||||
ReportID: "daily_today",
|
||||
Status: "succeeded",
|
||||
OutputPath: "/tmp/daily.md",
|
||||
NotificationStatus: "accepted",
|
||||
NotificationRunID: "distributor-run-1",
|
||||
},
|
||||
{
|
||||
ReportID: "three_day",
|
||||
Status: "failed",
|
||||
Error: "notify report three_day: upload failed",
|
||||
NotificationStatus: "failed",
|
||||
NotificationError: "notify report three_day: upload failed",
|
||||
},
|
||||
},
|
||||
}
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
|
||||
if err := writeJSON(&stdout, result); err != nil {
|
||||
t.Fatalf("writeJSON() error = %v", err)
|
||||
}
|
||||
writeRunLogs(&stderr, result)
|
||||
|
||||
var decoded app.BatchResult
|
||||
if err := json.Unmarshal(stdout.Bytes(), &decoded); err != nil {
|
||||
t.Fatalf("decode batch JSON: %v\n%s", err, stdout.String())
|
||||
}
|
||||
if decoded.Reports[0].NotificationStatus != "accepted" || decoded.Reports[0].NotificationRunID != "distributor-run-1" {
|
||||
t.Fatalf("success notification fields = %#v", decoded.Reports[0])
|
||||
}
|
||||
if decoded.Reports[1].NotificationStatus != "failed" || !strings.Contains(decoded.Reports[1].NotificationError, "upload failed") {
|
||||
t.Fatalf("failure notification fields = %#v", decoded.Reports[1])
|
||||
}
|
||||
if !strings.Contains(stderr.String(), `notificationStatus="accepted"`) || !strings.Contains(stderr.String(), `notificationRunId="distributor-run-1"`) {
|
||||
t.Fatalf("stderr missing success notification fields:\n%s", stderr.String())
|
||||
}
|
||||
if !strings.Contains(stderr.String(), `notificationStatus="failed"`) || !strings.Contains(stderr.String(), `notificationError="notify report three_day: upload failed"`) {
|
||||
t.Fatalf("stderr missing failure notification fields:\n%s", stderr.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchOutputDoesNotExposeSecretLikeNotificationErrors(t *testing.T) {
|
||||
result := &app.BatchResult{
|
||||
Batch: app.BatchMorning,
|
||||
Total: 1,
|
||||
Failed: 1,
|
||||
Reports: []app.BatchReportResult{
|
||||
{
|
||||
ReportID: "daily_today",
|
||||
Status: "failed",
|
||||
Error: "notify report daily_today: upload failed: [redacted]",
|
||||
NotificationStatus: "failed",
|
||||
NotificationError: "notify report daily_today: upload failed: [redacted]",
|
||||
},
|
||||
},
|
||||
}
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
|
||||
if err := writeJSON(&stdout, result); err != nil {
|
||||
t.Fatalf("writeJSON() error = %v", err)
|
||||
}
|
||||
writeRunLogs(&stderr, result)
|
||||
|
||||
for _, output := range []string{stdout.String(), stderr.String()} {
|
||||
if strings.Contains(output, "DISTRIBUTOR_SECRET_TOKEN") {
|
||||
t.Fatalf("output contains token value:\n%s", output)
|
||||
}
|
||||
if !strings.Contains(output, "[redacted]") {
|
||||
t.Fatalf("output missing redacted marker:\n%s", output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunEveningUsesOutputDirectoryAndSummary(t *testing.T) {
|
||||
server := dailyServer(t)
|
||||
tempDir := t.TempDir()
|
||||
@@ -384,6 +466,103 @@ func TestRunEveningUsesOutputDirectoryAndSummary(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunEveningReportsNotificationSuccess(t *testing.T) {
|
||||
server := dailyServer(t)
|
||||
distributorServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/upload" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
_, _ = w.Write([]byte(`{"run_id":"distributor-run-1","status":"accepted"}`))
|
||||
}))
|
||||
t.Cleanup(distributorServer.Close)
|
||||
tempDir := t.TempDir()
|
||||
scriptoriumPath := writeFakeScriptorium(t, tempDir)
|
||||
configPath := filepath.Join(tempDir, "config.yml")
|
||||
workspaceRoot := filepath.Join(tempDir, "workspace")
|
||||
configBody := "weather_api:\n base_url: " + server.URL + "/\n timezone: America/Chicago\nscriptorium:\n binary: " + scriptoriumPath + "\nworkspace:\n root: " + workspaceRoot + "\nnotify:\n distributor:\n enabled: true\n endpoint: " + distributorServer.URL + "\n token_env: CLI_DISTRIBUTOR_TOKEN\n"
|
||||
if err := os.WriteFile(configPath, []byte(configBody), 0o600); err != nil {
|
||||
t.Fatalf("write config: %v", err)
|
||||
}
|
||||
t.Setenv("CLI_DISTRIBUTOR_TOKEN", "cli-secret-token")
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
runner := Runner{Clock: fixedClock()}
|
||||
|
||||
err := runner.Run(context.Background(), []string{
|
||||
"run", "evening",
|
||||
"--config", configPath,
|
||||
}, &stdout, &stderr)
|
||||
if err != nil {
|
||||
t.Fatalf("Run() error = %v", err)
|
||||
}
|
||||
|
||||
var summary app.BatchResult
|
||||
if decodeErr := json.Unmarshal(stdout.Bytes(), &summary); decodeErr != nil {
|
||||
t.Fatalf("decode summary: %v\n%s", decodeErr, stdout.String())
|
||||
}
|
||||
if len(summary.Reports) != 1 {
|
||||
t.Fatalf("reports = %#v, want one report", summary.Reports)
|
||||
}
|
||||
if summary.Reports[0].NotificationStatus != "accepted" || summary.Reports[0].NotificationRunID != "distributor-run-1" {
|
||||
t.Fatalf("notification fields = %#v", summary.Reports[0])
|
||||
}
|
||||
if !strings.Contains(stderr.String(), `notificationStatus="accepted"`) || !strings.Contains(stderr.String(), `notificationRunId="distributor-run-1"`) {
|
||||
t.Fatalf("stderr missing notification fields:\n%s", stderr.String())
|
||||
}
|
||||
if strings.Contains(stdout.String(), "cli-secret-token") || strings.Contains(stderr.String(), "cli-secret-token") {
|
||||
t.Fatalf("output contains token value\nstdout=%s\nstderr=%s", stdout.String(), stderr.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunEveningReportsNotificationFailureWithoutToken(t *testing.T) {
|
||||
server := dailyServer(t)
|
||||
distributorServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
_, _ = w.Write([]byte(`{"error":"rejected cli-secret-token","retryable":false}`))
|
||||
}))
|
||||
t.Cleanup(distributorServer.Close)
|
||||
tempDir := t.TempDir()
|
||||
scriptoriumPath := writeFakeScriptorium(t, tempDir)
|
||||
configPath := filepath.Join(tempDir, "config.yml")
|
||||
workspaceRoot := filepath.Join(tempDir, "workspace")
|
||||
configBody := "weather_api:\n base_url: " + server.URL + "/\n timezone: America/Chicago\nscriptorium:\n binary: " + scriptoriumPath + "\nworkspace:\n root: " + workspaceRoot + "\nnotify:\n distributor:\n enabled: true\n endpoint: " + distributorServer.URL + "\n token_env: CLI_DISTRIBUTOR_TOKEN\n"
|
||||
if err := os.WriteFile(configPath, []byte(configBody), 0o600); err != nil {
|
||||
t.Fatalf("write config: %v", err)
|
||||
}
|
||||
t.Setenv("CLI_DISTRIBUTOR_TOKEN", "cli-secret-token")
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
runner := Runner{Clock: fixedClock()}
|
||||
|
||||
err := runner.Run(context.Background(), []string{
|
||||
"run", "evening",
|
||||
"--config", configPath,
|
||||
}, &stdout, &stderr)
|
||||
if err == nil {
|
||||
t.Fatal("Run() error = nil, want notification failure")
|
||||
}
|
||||
|
||||
var summary app.BatchResult
|
||||
if decodeErr := json.Unmarshal(stdout.Bytes(), &summary); decodeErr != nil {
|
||||
t.Fatalf("decode summary: %v\n%s", decodeErr, stdout.String())
|
||||
}
|
||||
if len(summary.Reports) != 1 || summary.Reports[0].NotificationStatus != "failed" {
|
||||
t.Fatalf("summary reports = %#v, want failed notification", summary.Reports)
|
||||
}
|
||||
for _, output := range []string{stdout.String(), stderr.String(), err.Error()} {
|
||||
if strings.Contains(output, "cli-secret-token") {
|
||||
t.Fatalf("output contains token value:\n%s", output)
|
||||
}
|
||||
}
|
||||
for _, output := range []string{stdout.String(), stderr.String()} {
|
||||
if !strings.Contains(output, "[redacted]") {
|
||||
t.Fatalf("output missing redaction marker:\n%s", output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunMorningGeneratesDailyAndThreeDayOnSunday(t *testing.T) {
|
||||
server := dailyServer(t)
|
||||
tempDir := t.TempDir()
|
||||
|
||||
Reference in New Issue
Block a user