Cover hourly CLI generation workflow
This commit is contained in:
@@ -89,3 +89,18 @@ reports:
|
|||||||
- weather_story
|
- weather_story
|
||||||
- outdoor_windows
|
- outdoor_windows
|
||||||
- hourly_forecast
|
- hourly_forecast
|
||||||
|
hourly:
|
||||||
|
deterministic_modules:
|
||||||
|
- metadata
|
||||||
|
- current_conditions
|
||||||
|
- hourly_forecast
|
||||||
|
- precip_timing
|
||||||
|
- alert_digest
|
||||||
|
- spc_convective_outlooks
|
||||||
|
- id: area_forecast_discussion
|
||||||
|
options:
|
||||||
|
sections:
|
||||||
|
- key_messages
|
||||||
|
- short_term
|
||||||
|
- spc_convective_discussion
|
||||||
|
- weather_story
|
||||||
|
|||||||
@@ -685,6 +685,59 @@ func TestRunGenerateDailyWritesMarkdownReport(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRunGenerateHourlyWritesGeneratedTextReport(t *testing.T) {
|
||||||
|
server := dailyServer(t)
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
scriptoriumPath := writeStructuredOutputScriptorium(t, tempDir)
|
||||||
|
workspaceRoot := filepath.Join(tempDir, "workspace")
|
||||||
|
configPath := writeTestConfig(t, server, scriptoriumPath, workspaceRoot)
|
||||||
|
outPath := filepath.Join(tempDir, "hourly.md")
|
||||||
|
var stdout bytes.Buffer
|
||||||
|
var stderr bytes.Buffer
|
||||||
|
runner := Runner{Clock: timeutil.FixedClock{Time: time.Date(2026, 5, 29, 11, 0, 0, 0, time.UTC)}}
|
||||||
|
|
||||||
|
err := runner.Run(context.Background(), []string{
|
||||||
|
"generate", "hourly",
|
||||||
|
"--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)
|
||||||
|
}
|
||||||
|
for _, want := range []string{
|
||||||
|
"# Hourly Report",
|
||||||
|
"Storm chances increase through late morning.",
|
||||||
|
"The main window is 10 AM to noon.",
|
||||||
|
"Brief downpours may slow travel.",
|
||||||
|
} {
|
||||||
|
if !strings.Contains(string(report), want) {
|
||||||
|
t.Fatalf("report output missing %q:\n%s", want, string(report))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dataPackagePath := oneArtifact(t, workspaceRoot, "data-packages", "hourly", "2026-05-29", "*.data_package.yaml")
|
||||||
|
dataPackage, err := os.ReadFile(dataPackagePath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("read managed data package: %v", err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(string(dataPackage), "id: hourly") ||
|
||||||
|
!strings.Contains(string(dataPackage), "prompt_id: weather.hourly_generated_text") ||
|
||||||
|
!strings.Contains(string(dataPackage), "hourly_forecast:") {
|
||||||
|
t.Fatalf("data package output missing hourly content:\n%s", string(dataPackage))
|
||||||
|
}
|
||||||
|
rawGeneratedTextPath := oneArtifact(t, workspaceRoot, "snapshots", "hourly", "2026-05-29", "*.generated_text.raw.json")
|
||||||
|
validatedGeneratedTextPath := oneArtifact(t, workspaceRoot, "snapshots", "hourly", "2026-05-29", "*.generated_text.json")
|
||||||
|
renderContextPath := oneArtifact(t, workspaceRoot, "snapshots", "hourly", "2026-05-29", "*.render_context.json")
|
||||||
|
managedReportPath := oneArtifact(t, workspaceRoot, "reports", "hourly", "*.md")
|
||||||
|
assertFileContains(t, rawGeneratedTextPath, `"summary": " Storm chances increase through late morning. "`)
|
||||||
|
assertFileContains(t, validatedGeneratedTextPath, `"summary":"Storm chances increase through late morning."`)
|
||||||
|
assertFileContains(t, renderContextPath, `"ReportTitle": "Hourly Report"`)
|
||||||
|
assertFileContains(t, managedReportPath, "# Hourly Report")
|
||||||
|
}
|
||||||
|
|
||||||
func TestRunInspectGeneratedArtifacts(t *testing.T) {
|
func TestRunInspectGeneratedArtifacts(t *testing.T) {
|
||||||
server := dailyServer(t)
|
server := dailyServer(t)
|
||||||
tempDir := t.TempDir()
|
tempDir := t.TempDir()
|
||||||
@@ -867,6 +920,8 @@ func TestResolveGenerateHourlyRejectsDateAndStormBounds(t *testing.T) {
|
|||||||
{"hourly", "--date", "2026-05-29"},
|
{"hourly", "--date", "2026-05-29"},
|
||||||
{"hourly", "--start", "2026-05-29T18:00"},
|
{"hourly", "--start", "2026-05-29T18:00"},
|
||||||
{"hourly", "--end", "2026-05-29T20:00"},
|
{"hourly", "--end", "2026-05-29T20:00"},
|
||||||
|
{"hourly", "--hours", "6"},
|
||||||
|
{"hourly", "--duration", "6h"},
|
||||||
} {
|
} {
|
||||||
_, err := runner.resolveGenerate(args)
|
_, err := runner.resolveGenerate(args)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@@ -1092,6 +1147,17 @@ func oneArtifact(t *testing.T, root string, parts ...string) string {
|
|||||||
return matches[0]
|
return matches[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func assertFileContains(t *testing.T, path string, want string) {
|
||||||
|
t.Helper()
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("read %s: %v", path, err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(string(data), want) {
|
||||||
|
t.Fatalf("%s missing %q:\n%s", path, want, string(data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func writeFakeScriptorium(t *testing.T, dir string) string {
|
func writeFakeScriptorium(t *testing.T, dir string) string {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
path := filepath.Join(dir, "scriptorium")
|
path := filepath.Join(dir, "scriptorium")
|
||||||
@@ -1122,6 +1188,52 @@ exit 1
|
|||||||
return path
|
return path
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func writeStructuredOutputScriptorium(t *testing.T, dir string) string {
|
||||||
|
t.Helper()
|
||||||
|
path := filepath.Join(dir, "scriptorium")
|
||||||
|
body := `#!/bin/sh
|
||||||
|
if [ "$1" = "render" ]; then
|
||||||
|
printf '{"ok":true,"argv":"%s"}' "$*"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
if [ "$1" = "run" ]; then
|
||||||
|
out=""
|
||||||
|
prompt=""
|
||||||
|
while [ "$#" -gt 0 ]; do
|
||||||
|
if [ "$1" = "--out" ]; then
|
||||||
|
shift
|
||||||
|
out="$1"
|
||||||
|
elif [ "$1" = "--prompt" ]; then
|
||||||
|
shift
|
||||||
|
prompt="$1"
|
||||||
|
fi
|
||||||
|
shift
|
||||||
|
done
|
||||||
|
if [ "$prompt" = "weather.hourly_generated_text" ]; then
|
||||||
|
cat > "$out" <<'JSON'
|
||||||
|
{
|
||||||
|
"summary": " Storm chances increase through late morning. ",
|
||||||
|
"timing": "The main window is 10 AM to noon.",
|
||||||
|
"impacts": "Brief downpours may slow travel.",
|
||||||
|
"confidence": "Medium"
|
||||||
|
}
|
||||||
|
JSON
|
||||||
|
printf 'wrote generated text\n' >&2
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
printf '# Daily Report\n\nGenerated by fake scriptorium.\n' > "$out"
|
||||||
|
printf 'wrote report\n' >&2
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
printf 'unexpected command\n' >&2
|
||||||
|
exit 1
|
||||||
|
`
|
||||||
|
if err := os.WriteFile(path, []byte(body), 0o700); err != nil {
|
||||||
|
t.Fatalf("write fake scriptorium: %v", err)
|
||||||
|
}
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
func writeFailingScriptorium(t *testing.T, dir string) string {
|
func writeFailingScriptorium(t *testing.T, dir string) string {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
path := filepath.Join(dir, "scriptorium")
|
path := filepath.Join(dir, "scriptorium")
|
||||||
|
|||||||
@@ -91,6 +91,22 @@ func TestLoadExampleConfig(t *testing.T) {
|
|||||||
if len(cfg.Notify.Distributor.ReportPathTemplates) != 1 {
|
if len(cfg.Notify.Distributor.ReportPathTemplates) != 1 {
|
||||||
t.Fatalf("ReportPathTemplates = %#v, want example archive path", cfg.Notify.Distributor.ReportPathTemplates)
|
t.Fatalf("ReportPathTemplates = %#v, want example archive path", cfg.Notify.Distributor.ReportPathTemplates)
|
||||||
}
|
}
|
||||||
|
overrides := cfg.ReportModuleOverrides()
|
||||||
|
hourly := overrides[report.Hourly]
|
||||||
|
if len(hourly) != 9 {
|
||||||
|
t.Fatalf("hourly example override length = %d, want 9", len(hourly))
|
||||||
|
}
|
||||||
|
if hourly[0].ID != module.Metadata ||
|
||||||
|
hourly[1].ID != module.CurrentConditions ||
|
||||||
|
hourly[2].ID != module.HourlyForecast ||
|
||||||
|
hourly[3].ID != module.PrecipTiming ||
|
||||||
|
hourly[4].ID != module.AlertDigest ||
|
||||||
|
hourly[5].ID != module.SPCConvectiveOutlooks ||
|
||||||
|
hourly[6].ID != module.AreaForecastDiscussion ||
|
||||||
|
hourly[7].ID != module.SPCConvectiveDiscussion ||
|
||||||
|
hourly[8].ID != module.WeatherStory {
|
||||||
|
t.Fatalf("hourly example override = %#v, want configured module order", hourly)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLoadMinimalExampleConfig(t *testing.T) {
|
func TestLoadMinimalExampleConfig(t *testing.T) {
|
||||||
|
|||||||
Reference in New Issue
Block a user