diff --git a/internal/cli/root_test.go b/internal/cli/root_test.go index d31c985..664c9ed 100644 --- a/internal/cli/root_test.go +++ b/internal/cli/root_test.go @@ -18,49 +18,40 @@ import ( ) func TestRunHelpLongFlag(t *testing.T) { - var stdout bytes.Buffer - var stderr bytes.Buffer - - err := Run(context.Background(), []string{"--help"}, &stdout, &stderr) + output, err := runRootCommand(t, "--help") if err != nil { t.Fatalf("Run() error = %v", err) } - if !strings.Contains(stdout.String(), "generate daily") { - t.Fatalf("help output missing generate command:\n%s", stdout.String()) + if !strings.Contains(output.stdout, "generate daily") { + t.Fatalf("help output missing generate command:\n%s", output.stdout) } - if !strings.Contains(stdout.String(), "weatherreporter generate hourly") { - t.Fatalf("help output missing hourly generate command:\n%s", stdout.String()) + if !strings.Contains(output.stdout, "weatherreporter generate hourly") { + t.Fatalf("help output missing hourly generate command:\n%s", output.stdout) } removedGenerateCommand := "generate " + strings.Join([]string{"near", "term"}, "-") - if strings.Contains(stdout.String(), removedGenerateCommand) { - t.Fatalf("help output includes retired generate command:\n%s", stdout.String()) + if strings.Contains(output.stdout, removedGenerateCommand) { + t.Fatalf("help output includes retired generate command:\n%s", output.stdout) } removedInspectCommand := "inspect " + "briefing" - if !strings.Contains(stdout.String(), "inspect modules") || strings.Contains(stdout.String(), removedInspectCommand) { - t.Fatalf("help output has wrong inspect commands:\n%s", stdout.String()) + if !strings.Contains(output.stdout, "inspect modules") || strings.Contains(output.stdout, removedInspectCommand) { + t.Fatalf("help output has wrong inspect commands:\n%s", output.stdout) } } func TestRunHelpShortFlag(t *testing.T) { - var stdout bytes.Buffer - var stderr bytes.Buffer - - err := Run(context.Background(), []string{"-h"}, &stdout, &stderr) + output, err := runRootCommand(t, "-h") if err != nil { t.Fatalf("Run() error = %v", err) } - if !strings.Contains(stdout.String(), "weatherreporter run evening") { - t.Fatalf("help output missing run command:\n%s", stdout.String()) + if !strings.Contains(output.stdout, "weatherreporter run evening") { + t.Fatalf("help output missing run command:\n%s", output.stdout) } } func TestRunUnknownCommand(t *testing.T) { - var stdout bytes.Buffer - var stderr bytes.Buffer - - err := Run(context.Background(), []string{"unknown"}, &stdout, &stderr) + _, err := runRootCommand(t, "unknown") if err == nil { t.Fatal("Run() error = nil, want unknown command error") } @@ -77,35 +68,22 @@ func TestRunGenerateStormWritesMarkdownReport(t *testing.T) { workspaceRoot := filepath.Join(tempDir, "workspace") configPath := writeTestConfig(t, server, scriptoriumPath, workspaceRoot) outPath := filepath.Join(tempDir, "storm.md") - var stdout bytes.Buffer - var stderr bytes.Buffer runner := Runner{Clock: fixedClock()} - err := runner.Run(context.Background(), []string{ + _, err := runTestCommand(t, runner, "generate", "storm", "--config", configPath, "--start", "2026-05-29T06:00", "--end", "2026-05-29T10:00", "--out", outPath, - }, &stdout, &stderr) + ) if err != nil { t.Fatalf("Run() error = %v", err) } - report, err := os.ReadFile(outPath) - if err != nil { - t.Fatalf("read report: %v", err) - } - if !strings.Contains(string(report), "# Daily Report") { - t.Fatalf("report output missing markdown:\n%s", string(report)) - } + assertFileContains(t, outPath, "# Daily Report") dataPackagePath := oneArtifact(t, workspaceRoot, "data-packages", "storm", "2026-05-29", "*.data_package.yaml") - data, err := os.ReadFile(dataPackagePath) - if err != nil { - t.Fatalf("read managed data package: %v", err) - } - if !strings.Contains(string(data), "id: storm") || !strings.Contains(string(data), "prompt_id: weather.storm_report") { - t.Fatalf("data package output missing storm content:\n%s", string(data)) - } + assertFileContains(t, dataPackagePath, "id: storm") + assertFileContains(t, dataPackagePath, "prompt_id: weather.storm_report") } func TestRunGenerateTomorrowWritesMarkdownReport(t *testing.T) { @@ -115,39 +93,23 @@ func TestRunGenerateTomorrowWritesMarkdownReport(t *testing.T) { workspaceRoot := filepath.Join(tempDir, "workspace") configPath := writeTestConfig(t, server, scriptoriumPath, workspaceRoot) outPath := filepath.Join(tempDir, "tomorrow.md") - var stdout bytes.Buffer - var stderr bytes.Buffer runner := Runner{Clock: fixedClock()} - err := runner.Run(context.Background(), []string{ + _, err := runTestCommand(t, runner, "generate", "tomorrow", "--config", configPath, "--out", outPath, - }, &stdout, &stderr) + ) if err != nil { t.Fatalf("Run() error = %v", err) } - report, err := os.ReadFile(outPath) - if err != nil { - t.Fatalf("read report: %v", err) - } - if !strings.Contains(string(report), "# Saturday's Weather") { - t.Fatalf("report output missing markdown:\n%s", string(report)) - } + assertFileContains(t, outPath, "# Saturday's Weather") dataPackagePath := oneArtifact(t, workspaceRoot, "data-packages", "tomorrow", "2026-05-30", "*.data_package.yaml") - data, err := os.ReadFile(dataPackagePath) - if err != nil { - t.Fatalf("read managed data package: %v", err) - } - if !strings.Contains(string(data), "id: tomorrow") || !strings.Contains(string(data), "tomorrow_planning:") { - t.Fatalf("data package output missing tomorrow content:\n%s", string(data)) - } - reportMatches, err := filepath.Glob(filepath.Join(workspaceRoot, "reports", "tomorrow", "*.md")) - if err != nil { - t.Fatalf("glob managed report: %v", err) - } - if len(reportMatches) != 1 || !strings.Contains(filepath.Base(reportMatches[0]), "tomorrow") { - t.Fatalf("managed reports = %#v, want tomorrow report", reportMatches) + assertFileContains(t, dataPackagePath, "id: tomorrow") + assertFileContains(t, dataPackagePath, "tomorrow_planning:") + reportPath := oneArtifact(t, workspaceRoot, "reports", "tomorrow", "*.md") + if !strings.Contains(filepath.Base(reportPath), "tomorrow") { + t.Fatalf("managed report = %q, want tomorrow report", reportPath) } } @@ -155,36 +117,21 @@ func TestRunEveningGeneratesTomorrowReport(t *testing.T) { server := dailyServer(t) 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 + "\n" - if err := os.WriteFile(configPath, []byte(configBody), 0o600); err != nil { - t.Fatalf("write config: %v", err) - } - var stdout bytes.Buffer - var stderr bytes.Buffer + configPath := writeTestConfig(t, server, scriptoriumPath, workspaceRoot) runner := Runner{Clock: fixedClock()} - err := runner.Run(context.Background(), []string{ + _, err := runTestCommand(t, runner, "run", "evening", "--config", configPath, - }, &stdout, &stderr) + ) if err != nil { t.Fatalf("Run() error = %v", err) } - dataPackageMatches, err := filepath.Glob(filepath.Join(workspaceRoot, "data-packages", "tomorrow", "2026-05-30", "*.data_package.yaml")) - if err != nil { - t.Fatalf("glob data package: %v", err) - } - if len(dataPackageMatches) != 1 { - t.Fatalf("data package files = %#v, want one", dataPackageMatches) - } - reportMatches, err := filepath.Glob(filepath.Join(workspaceRoot, "reports", "tomorrow", "*.md")) - if err != nil { - t.Fatalf("glob managed report: %v", err) - } - if len(reportMatches) != 1 || !strings.Contains(filepath.Base(reportMatches[0]), "tomorrow") { - t.Fatalf("managed reports = %#v, want only tomorrow report", reportMatches) + _ = oneArtifact(t, workspaceRoot, "data-packages", "tomorrow", "2026-05-30", "*.data_package.yaml") + reportPath := oneArtifact(t, workspaceRoot, "reports", "tomorrow", "*.md") + if !strings.Contains(filepath.Base(reportPath), "tomorrow") { + t.Fatalf("managed report = %q, want only tomorrow report", reportPath) } } @@ -192,142 +139,78 @@ func TestRunGenerateThreeDayWritesMarkdownReport(t *testing.T) { server := dailyServer(t) 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 + "\n" - if err := os.WriteFile(configPath, []byte(configBody), 0o600); err != nil { - t.Fatalf("write config: %v", err) - } + configPath := writeTestConfig(t, server, scriptoriumPath, workspaceRoot) outPath := filepath.Join(tempDir, "three-day.md") - var stdout bytes.Buffer - var stderr bytes.Buffer runner := Runner{Clock: fixedClock()} - err := runner.Run(context.Background(), []string{ + _, err := runTestCommand(t, runner, "generate", "three-day", "--config", configPath, "--out", outPath, - }, &stdout, &stderr) + ) if err != nil { t.Fatalf("Run() error = %v", err) } - report, err := os.ReadFile(outPath) - if err != nil { - t.Fatalf("read report: %v", err) - } - if !strings.Contains(string(report), "# Daily Report") { - t.Fatalf("report output missing markdown:\n%s", string(report)) - } - dataPackageMatches, err := filepath.Glob(filepath.Join(workspaceRoot, "data-packages", "three-day", "2026-05-29", "*.data_package.yaml")) - if err != nil { - t.Fatalf("glob data package: %v", err) - } - if len(dataPackageMatches) != 1 { - t.Fatalf("data package files = %#v, want one", dataPackageMatches) - } - data, err := os.ReadFile(dataPackageMatches[0]) - if err != nil { - t.Fatalf("read managed data package: %v", err) - } - if !strings.Contains(string(data), "id: three_day") || !strings.Contains(string(data), "derived_daypart_summaries:") { - t.Fatalf("data package output missing 3-day content:\n%s", string(data)) - } + assertFileContains(t, outPath, "# Daily Report") + dataPackagePath := oneArtifact(t, workspaceRoot, "data-packages", "three-day", "2026-05-29", "*.data_package.yaml") + assertFileContains(t, dataPackagePath, "id: three_day") + assertFileContains(t, dataPackagePath, "derived_daypart_summaries:") } func TestRunGenerateWeekendWritesMarkdownReport(t *testing.T) { server := dailyServer(t) 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 + "\n" - if err := os.WriteFile(configPath, []byte(configBody), 0o600); err != nil { - t.Fatalf("write config: %v", err) - } + configPath := writeTestConfig(t, server, scriptoriumPath, workspaceRoot) outPath := filepath.Join(tempDir, "weekend.md") - var stdout bytes.Buffer - var stderr bytes.Buffer runner := Runner{Clock: fixedClock()} - err := runner.Run(context.Background(), []string{ + _, err := runTestCommand(t, runner, "generate", "weekend", "--config", configPath, "--out", outPath, - }, &stdout, &stderr) + ) if err != nil { t.Fatalf("Run() error = %v", err) } - report, err := os.ReadFile(outPath) - if err != nil { - t.Fatalf("read report: %v", err) - } - if !strings.Contains(string(report), "# Daily Report") { - t.Fatalf("report output missing markdown:\n%s", string(report)) - } - dataPackageMatches, err := filepath.Glob(filepath.Join(workspaceRoot, "data-packages", "weekend", "2026-05-29", "*.data_package.yaml")) - if err != nil { - t.Fatalf("glob data package: %v", err) - } - if len(dataPackageMatches) != 1 { - t.Fatalf("data package files = %#v, want one", dataPackageMatches) - } - data, err := os.ReadFile(dataPackageMatches[0]) - if err != nil { - t.Fatalf("read managed data package: %v", err) - } - if !strings.Contains(string(data), "id: weekend") || !strings.Contains(string(data), "derived_daypart_summaries:") { - t.Fatalf("data package output missing weekend content:\n%s", string(data)) - } + assertFileContains(t, outPath, "# Daily Report") + dataPackagePath := oneArtifact(t, workspaceRoot, "data-packages", "weekend", "2026-05-29", "*.data_package.yaml") + assertFileContains(t, dataPackagePath, "id: weekend") + assertFileContains(t, dataPackagePath, "derived_daypart_summaries:") } func TestRunMorningIncludesWeekendExceptSunday(t *testing.T) { server := dailyServer(t) 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 + "\n" - if err := os.WriteFile(configPath, []byte(configBody), 0o600); err != nil { - t.Fatalf("write config: %v", err) - } - var stdout bytes.Buffer - var stderr bytes.Buffer + configPath := writeTestConfig(t, server, scriptoriumPath, workspaceRoot) runner := Runner{Clock: fixedClock()} - err := runner.Run(context.Background(), []string{ + _, err := runTestCommand(t, runner, "run", "morning", "--config", configPath, - }, &stdout, &stderr) + ) if err != nil { t.Fatalf("Run() error = %v", err) } - weekendPackages, err := filepath.Glob(filepath.Join(workspaceRoot, "data-packages", "weekend", "2026-05-29", "*.data_package.yaml")) - if err != nil { - t.Fatalf("glob weekend packages: %v", err) - } - if len(weekendPackages) != 1 { - t.Fatalf("weekend packages = %#v, want one", weekendPackages) - } + _ = oneArtifact(t, workspaceRoot, "data-packages", "weekend", "2026-05-29", "*.data_package.yaml") } func TestRunMorningReportsPartialFailureAndContinues(t *testing.T) { server := dailyServer(t) tempDir := t.TempDir() scriptoriumPath := writeFailingScriptorium(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 + "\n" - if err := os.WriteFile(configPath, []byte(configBody), 0o600); err != nil { - t.Fatalf("write config: %v", err) - } - var stdout bytes.Buffer - var stderr bytes.Buffer + configPath := writeTestConfig(t, server, scriptoriumPath, workspaceRoot) runner := Runner{Clock: fixedClock()} - err := runner.Run(context.Background(), []string{ + output, err := runTestCommand(t, runner, "run", "morning", "--config", configPath, - }, &stdout, &stderr) + ) if err == nil { t.Fatal("Run() error = nil, want aggregate failure") } @@ -336,26 +219,17 @@ func TestRunMorningReportsPartialFailureAndContinues(t *testing.T) { } var summary app.BatchResult - if decodeErr := json.Unmarshal(stdout.Bytes(), &summary); decodeErr != nil { - t.Fatalf("decode summary: %v\n%s", decodeErr, stdout.String()) + if decodeErr := json.Unmarshal([]byte(output.stdout), &summary); decodeErr != nil { + t.Fatalf("decode summary: %v\n%s", decodeErr, output.stdout) } if summary.Total != 3 || summary.Succeeded != 2 || summary.Failed != 1 { t.Fatalf("summary total/succeeded/failed = %d/%d/%d, want 3/2/1", summary.Total, summary.Succeeded, summary.Failed) } - if !strings.Contains(stderr.String(), "status=failed") || !strings.Contains(stderr.String(), "status=succeeded") { - t.Fatalf("stderr missing structured report logs:\n%s", stderr.String()) - } - dailyPackages, err := filepath.Glob(filepath.Join(workspaceRoot, "data-packages", "daily", "2026-05-29", "*.data_package.yaml")) - if err != nil { - t.Fatalf("glob daily packages: %v", err) - } - weekendPackages, err := filepath.Glob(filepath.Join(workspaceRoot, "data-packages", "weekend", "2026-05-29", "*.data_package.yaml")) - if err != nil { - t.Fatalf("glob weekend packages: %v", err) - } - if len(dailyPackages) != 1 || len(weekendPackages) != 1 { - t.Fatalf("daily packages = %#v, weekend packages = %#v; want successful reports to continue", dailyPackages, weekendPackages) + if !strings.Contains(output.stderr, "status=failed") || !strings.Contains(output.stderr, "status=succeeded") { + t.Fatalf("stderr missing structured report logs:\n%s", output.stderr) } + _ = oneArtifact(t, workspaceRoot, "data-packages", "daily", "2026-05-29", "*.data_package.yaml") + _ = oneArtifact(t, workspaceRoot, "data-packages", "weekend", "2026-05-29", "*.data_package.yaml") } func TestBatchOutputIncludesNotificationDetails(t *testing.T) { @@ -444,28 +318,22 @@ func TestRunEveningUsesOutputDirectoryAndSummary(t *testing.T) { server := dailyServer(t) tempDir := t.TempDir() scriptoriumPath := writeFakeScriptorium(t, tempDir) - configPath := filepath.Join(tempDir, "config.yml") workspaceRoot := filepath.Join(tempDir, "workspace") outputDir := filepath.Join(tempDir, "copies") - configBody := "weather_api:\n base_url: " + server.URL + "/\n timezone: America/Chicago\nscriptorium:\n binary: " + scriptoriumPath + "\nworkspace:\n root: " + workspaceRoot + "\n" - if err := os.WriteFile(configPath, []byte(configBody), 0o600); err != nil { - t.Fatalf("write config: %v", err) - } - var stdout bytes.Buffer - var stderr bytes.Buffer + configPath := writeTestConfig(t, server, scriptoriumPath, workspaceRoot) runner := Runner{Clock: fixedClock()} - err := runner.Run(context.Background(), []string{ + output, err := runTestCommand(t, runner, "run", "evening", "--config", configPath, "--out-dir", outputDir, - }, &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 decodeErr := json.Unmarshal([]byte(output.stdout), &summary); decodeErr != nil { + t.Fatalf("decode summary: %v\n%s", decodeErr, output.stdout) } if summary.Total != 1 || summary.Failed != 0 { t.Fatalf("summary total/failed = %d/%d, want 1/0", summary.Total, summary.Failed) @@ -1136,6 +1004,27 @@ func fixedClock() timeutil.Clock { return timeutil.FixedClock{Time: time.Date(2026, 5, 29, 12, 0, 0, 0, time.UTC)} } +type commandOutput struct { + stdout string + stderr string +} + +func runRootCommand(t *testing.T, args ...string) (commandOutput, error) { + t.Helper() + return runTestCommand(t, Runner{}, args...) +} + +func runTestCommand(t *testing.T, runner Runner, args ...string) (commandOutput, error) { + t.Helper() + var stdout bytes.Buffer + var stderr bytes.Buffer + err := runner.Run(context.Background(), args, &stdout, &stderr) + return commandOutput{ + stdout: stdout.String(), + stderr: stderr.String(), + }, err +} + func dailyServer(t *testing.T) *httptest.Server { t.Helper() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {