Write reports to operator-selected outputs
This commit is contained in:
@@ -5,6 +5,8 @@ import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/app"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/buildinfo"
|
||||
@@ -37,9 +39,9 @@ Options:
|
||||
--config PATH Load configuration from PATH instead of /usr/local/etc/weatherreporter/config.yml.
|
||||
--units VALUE Override weather API units.
|
||||
--tz NAME Override weather API timezone.
|
||||
--out PATH Write an extra Markdown report copy where supported by the generate command.
|
||||
--out PATH Write the generated Markdown report to PATH.
|
||||
--llm-debug-dir PATH Write sensitive prompt debug artifacts outside the managed workspace.
|
||||
--out-dir PATH Write extra Markdown report copies for run commands.
|
||||
--out-dir PATH Write generated Markdown reports beneath PATH for run commands.
|
||||
--quiet Suppress successful generate and run output.
|
||||
`
|
||||
|
||||
@@ -47,6 +49,7 @@ type Runner struct {
|
||||
Clock timeutil.Clock
|
||||
ExecutorFactory ExecutorFactory
|
||||
Version string
|
||||
WorkingDir string
|
||||
}
|
||||
|
||||
func Run(ctx context.Context, args []string, stdout io.Writer, stderr io.Writer) error {
|
||||
@@ -240,10 +243,20 @@ func (r Runner) resolveGenerateAction(args []string) (app.GenerateRequest, commo
|
||||
return app.GenerateRequest{}, commonOptions{}, err
|
||||
}
|
||||
|
||||
workingDir, err := r.workingDir()
|
||||
if err != nil {
|
||||
return app.GenerateRequest{}, commonOptions{}, err
|
||||
}
|
||||
outputPath, err := resolveOutputOverride(workingDir, opts.Output)
|
||||
if err != nil {
|
||||
return app.GenerateRequest{}, commonOptions{}, err
|
||||
}
|
||||
|
||||
req := app.GenerateRequest{
|
||||
Config: cfg,
|
||||
Report: reportKind,
|
||||
OutputPath: opts.Output,
|
||||
WorkingDir: workingDir,
|
||||
OutputPath: outputPath,
|
||||
LLMDebugDir: opts.LLMDebugDir,
|
||||
Now: r.Clock.Now(),
|
||||
Executor: executor,
|
||||
@@ -304,7 +317,15 @@ func (r Runner) resolveRunAction(args []string) (app.BatchRequest, commonOptions
|
||||
if err != nil {
|
||||
return app.BatchRequest{}, commonOptions{}, err
|
||||
}
|
||||
return app.BatchRequest{Config: cfg, Batch: batch, Now: r.Clock.Now(), OutputDir: opts.OutputDir, LLMDebugDir: opts.LLMDebugDir, Executor: executor}, opts, nil
|
||||
workingDir, err := r.workingDir()
|
||||
if err != nil {
|
||||
return app.BatchRequest{}, commonOptions{}, err
|
||||
}
|
||||
outputDir, err := resolveOutputOverride(workingDir, opts.OutputDir)
|
||||
if err != nil {
|
||||
return app.BatchRequest{}, commonOptions{}, err
|
||||
}
|
||||
return app.BatchRequest{Config: cfg, Batch: batch, Now: r.Clock.Now(), WorkingDir: workingDir, OutputDir: outputDir, LLMDebugDir: opts.LLMDebugDir, Executor: executor}, opts, nil
|
||||
}
|
||||
|
||||
func resolveRun(args []string) (app.BatchRequest, error) {
|
||||
@@ -334,7 +355,7 @@ func parseRunFlags(args []string) (commonOptions, error) {
|
||||
fs.SetOutput(io.Discard)
|
||||
opts := commonOptions{}
|
||||
addCommonFlags(fs, &opts, false)
|
||||
fs.StringVar(&opts.OutputDir, "out-dir", "", "extra Markdown report copy directory")
|
||||
fs.StringVar(&opts.OutputDir, "out-dir", "", "generated Markdown report directory")
|
||||
fs.BoolVar(&opts.Quiet, "quiet", false, "suppress successful action output")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return commonOptions{}, err
|
||||
@@ -384,6 +405,31 @@ func addCommonFlags(fs *flag.FlagSet, opts *commonOptions, includeOutput bool) {
|
||||
fs.StringVar(&opts.Timezone, "tz", "", "weather API timezone")
|
||||
fs.StringVar(&opts.LLMDebugDir, "llm-debug-dir", "", "write sensitive prompt debug artifacts under PATH")
|
||||
if includeOutput {
|
||||
fs.StringVar(&opts.Output, "out", "", "extra Markdown report copy path")
|
||||
fs.StringVar(&opts.Output, "out", "", "generated Markdown report path")
|
||||
}
|
||||
}
|
||||
|
||||
func (r Runner) workingDir() (string, error) {
|
||||
workingDir := r.WorkingDir
|
||||
if workingDir == "" {
|
||||
var err error
|
||||
workingDir, err = os.Getwd()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("get working directory: %w", err)
|
||||
}
|
||||
}
|
||||
if !filepath.IsAbs(workingDir) {
|
||||
return "", fmt.Errorf("working directory %q must be absolute", workingDir)
|
||||
}
|
||||
return filepath.Clean(workingDir), nil
|
||||
}
|
||||
|
||||
func resolveOutputOverride(workingDir, value string) (string, error) {
|
||||
if value == "" {
|
||||
return "", nil
|
||||
}
|
||||
if !filepath.IsAbs(value) {
|
||||
value = filepath.Join(workingDir, value)
|
||||
}
|
||||
return filepath.Clean(value), nil
|
||||
}
|
||||
|
||||
@@ -101,6 +101,11 @@ func TestRunnerHelpListsOnlySupportedCommands(t *testing.T) {
|
||||
t.Fatalf("help contains retired command %q:\n%s", retired, output.stdout)
|
||||
}
|
||||
}
|
||||
for _, description := range []string{"Write the generated Markdown report to PATH.", "Write generated Markdown reports beneath PATH"} {
|
||||
if !strings.Contains(output.stdout, description) {
|
||||
t.Fatalf("help missing output description %q:\n%s", description, output.stdout)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunnerVersion(t *testing.T) {
|
||||
@@ -185,6 +190,7 @@ func TestResolveSupportedCommandsAndFlags(t *testing.T) {
|
||||
func TestResolveGenerateAndRunApplySharedActionFlags(t *testing.T) {
|
||||
configPath := writeCLIConfig(t, t.TempDir(), "")
|
||||
runner, _ := countingRunner(cliExecutor{})
|
||||
runner.WorkingDir = t.TempDir()
|
||||
|
||||
generate, generateOpts, err := runner.resolveGenerateAction([]string{
|
||||
"daily", "--config", configPath, "--date", "2026-05-30", "--units", "metric", "--tz", "UTC",
|
||||
@@ -193,7 +199,7 @@ func TestResolveGenerateAndRunApplySharedActionFlags(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("resolveGenerateAction() error = %v", err)
|
||||
}
|
||||
if generate.Config.WeatherAPI.Units != "metric" || generate.Config.WeatherAPI.Timezone != "UTC" || generate.OutputPath != "daily.md" || generate.LLMDebugDir != "/safe/debug" || !generateOpts.Quiet {
|
||||
if generate.Config.WeatherAPI.Units != "metric" || generate.Config.WeatherAPI.Timezone != "UTC" || generate.OutputPath != filepath.Join(runner.WorkingDir, "daily.md") || generate.WorkingDir != runner.WorkingDir || generate.LLMDebugDir != "/safe/debug" || !generateOpts.Quiet {
|
||||
t.Fatalf("generate request/options = %#v/%#v", generate, generateOpts)
|
||||
}
|
||||
if got := generate.Date.Format(timeutil.DateLayout); got != "2026-05-30" {
|
||||
@@ -207,7 +213,7 @@ func TestResolveGenerateAndRunApplySharedActionFlags(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("resolveRunAction() error = %v", err)
|
||||
}
|
||||
if batch.Config.WeatherAPI.Units != "metric" || batch.Config.WeatherAPI.Timezone != "UTC" || batch.OutputDir != "reports" || batch.LLMDebugDir != "/safe/debug" || !batchOpts.Quiet {
|
||||
if batch.Config.WeatherAPI.Units != "metric" || batch.Config.WeatherAPI.Timezone != "UTC" || batch.OutputDir != filepath.Join(runner.WorkingDir, "reports") || batch.WorkingDir != runner.WorkingDir || batch.LLMDebugDir != "/safe/debug" || !batchOpts.Quiet {
|
||||
t.Fatalf("batch request/options = %#v/%#v", batch, batchOpts)
|
||||
}
|
||||
}
|
||||
@@ -280,6 +286,7 @@ func TestRunnerSuccessfulSingleAndBatchActions(t *testing.T) {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
fixture := newCLIFixture(t)
|
||||
runner, constructions := countingRunner(cliExecutor{})
|
||||
runner.WorkingDir = t.TempDir()
|
||||
output, err := runCLICommand(runner, tt.args(fixture.configPath, fixture.path("copies"))...)
|
||||
if err != nil {
|
||||
t.Fatalf("Run() error = %v", err)
|
||||
@@ -314,6 +321,7 @@ func TestRunnerSuccessfulSingleAndBatchActions(t *testing.T) {
|
||||
|
||||
func TestRunnerPreRunFailureAndQuietMode(t *testing.T) {
|
||||
runner, constructions := countingRunner(cliExecutor{})
|
||||
runner.WorkingDir = t.TempDir()
|
||||
output, err := runCLICommand(runner, "generate", "daily")
|
||||
if err == nil || output.stdout != "" || output.stderr != "" {
|
||||
t.Fatalf("pre-run output/error = %#v/%v, want error without summary", output, err)
|
||||
@@ -324,6 +332,7 @@ func TestRunnerPreRunFailureAndQuietMode(t *testing.T) {
|
||||
|
||||
fixture := newCLIFixture(t)
|
||||
runner, _ = countingRunner(cliExecutor{})
|
||||
runner.WorkingDir = t.TempDir()
|
||||
output, err = runCLICommand(runner, "generate", "today", "--config", fixture.configPath, "--quiet")
|
||||
if err != nil || output.stdout != "" || output.stderr != "" {
|
||||
t.Fatalf("quiet output/error = %#v/%v", output, err)
|
||||
@@ -333,6 +342,7 @@ func TestRunnerPreRunFailureAndQuietMode(t *testing.T) {
|
||||
func TestRunnerFailedActionReportsSafePartialSummary(t *testing.T) {
|
||||
fixture := newCLIFixture(t)
|
||||
runner, constructions := countingRunner(cliExecutor{fail: true})
|
||||
runner.WorkingDir = t.TempDir()
|
||||
output, err := runCLICommand(runner, "generate", "today", "--config", fixture.configPath)
|
||||
if err == nil {
|
||||
t.Fatal("Run() error = nil, want execution failure")
|
||||
@@ -364,6 +374,7 @@ func TestRunnerFailedActionReportsSafePartialSummary(t *testing.T) {
|
||||
func TestRunnerMixedBatchReportsSafePartialFailure(t *testing.T) {
|
||||
fixture := newCLIFixture(t)
|
||||
runner, constructions := countingRunner(cliExecutor{failPrompt: "weather.tomorrow_generated_text"})
|
||||
runner.WorkingDir = t.TempDir()
|
||||
output, err := runCLICommand(runner, "run", "morning", "--config", fixture.configPath)
|
||||
if err == nil {
|
||||
t.Fatal("Run() error = nil, want aggregate batch failure")
|
||||
@@ -399,6 +410,7 @@ func TestRunnerMixedBatchReportsSafePartialFailure(t *testing.T) {
|
||||
func TestRunnerInspectsReportsAndCurrentArtifacts(t *testing.T) {
|
||||
fixture := newCLIFixture(t)
|
||||
runner, _ := countingRunner(cliExecutor{})
|
||||
runner.WorkingDir = t.TempDir()
|
||||
first := runSuccessfulGenerate(t, runner, fixture.configPath, time.Date(2026, 5, 29, 11, 0, 0, 0, time.UTC))
|
||||
second := runSuccessfulGenerate(t, runner, fixture.configPath, time.Date(2026, 5, 29, 12, 0, 0, 0, time.UTC))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user