2026-05-29 16:52:02 +00:00
package cli
import (
"bytes"
"context"
2026-05-29 18:20:39 +00:00
"encoding/json"
2026-05-29 17:23:49 +00:00
"net/http"
"net/http/httptest"
"os"
"path/filepath"
2026-05-29 16:52:02 +00:00
"strings"
"testing"
2026-05-29 16:58:18 +00:00
"time"
"gitea.maximumdirect.net/eric/weatherreporter/internal/app"
2026-06-15 12:33:55 +00:00
"gitea.maximumdirect.net/eric/weatherreporter/internal/report"
2026-05-29 16:58:18 +00:00
"gitea.maximumdirect.net/eric/weatherreporter/internal/timeutil"
2026-05-29 16:52:02 +00:00
)
func TestRunHelpLongFlag ( t * testing . T ) {
2026-06-15 12:48:36 +00:00
output , err := runRootCommand ( t , "--help" )
2026-05-29 16:52:02 +00:00
if err != nil {
t . Fatalf ( "Run() error = %v" , err )
}
2026-06-15 16:46:10 +00:00
if ! strings . Contains ( output . stdout , "weatherreporter generate daily --date YYYY-MM-DD" ) {
2026-06-15 12:48:36 +00:00
t . Fatalf ( "help output missing generate command:\n%s" , output . stdout )
2026-05-29 16:52:02 +00:00
}
2026-06-15 14:58:36 +00:00
if ! strings . Contains ( output . stdout , "generate today" ) {
t . Fatalf ( "help output missing today generate command:\n%s" , output . stdout )
}
2026-06-15 12:48:36 +00:00
if ! strings . Contains ( output . stdout , "weatherreporter generate hourly" ) {
t . Fatalf ( "help output missing hourly generate command:\n%s" , output . stdout )
2026-06-12 17:36:58 +00:00
}
2026-06-14 05:24:25 +00:00
removedGenerateCommand := "generate " + strings . Join ( [ ] string { "near" , "term" } , "-" )
2026-06-15 12:48:36 +00:00
if strings . Contains ( output . stdout , removedGenerateCommand ) {
t . Fatalf ( "help output includes retired generate command:\n%s" , output . stdout )
2026-06-14 05:24:25 +00:00
}
2026-06-09 20:58:35 +00:00
removedInspectCommand := "inspect " + "briefing"
2026-06-15 12:48:36 +00:00
if ! strings . Contains ( output . stdout , "inspect modules" ) || strings . Contains ( output . stdout , removedInspectCommand ) {
t . Fatalf ( "help output has wrong inspect commands:\n%s" , output . stdout )
2026-06-09 20:58:35 +00:00
}
2026-05-29 16:52:02 +00:00
}
func TestRunHelpShortFlag ( t * testing . T ) {
2026-06-15 12:48:36 +00:00
output , err := runRootCommand ( t , "-h" )
2026-05-29 16:52:02 +00:00
if err != nil {
t . Fatalf ( "Run() error = %v" , err )
}
2026-06-15 12:48:36 +00:00
if ! strings . Contains ( output . stdout , "weatherreporter run evening" ) {
t . Fatalf ( "help output missing run command:\n%s" , output . stdout )
2026-05-29 16:58:18 +00:00
}
}
func TestRunUnknownCommand ( t * testing . T ) {
2026-06-15 12:48:36 +00:00
_ , err := runRootCommand ( t , "unknown" )
2026-05-29 16:58:18 +00:00
if err == nil {
t . Fatal ( "Run() error = nil, want unknown command error" )
}
2026-05-29 18:33:24 +00:00
if ! strings . Contains ( err . Error ( ) , ` unknown command "unknown" ` ) {
2026-05-29 16:58:18 +00:00
t . Fatalf ( "Run() error = %q, want unknown command message" , err . Error ( ) )
2026-05-29 16:52:02 +00:00
}
}
2026-05-29 18:28:25 +00:00
func TestRunGenerateStormWritesMarkdownReport ( t * testing . T ) {
server := dailyServer ( t )
tempDir := t . TempDir ( )
scriptoriumPath := writeFakeScriptorium ( t , tempDir )
workspaceRoot := filepath . Join ( tempDir , "workspace" )
2026-05-29 20:44:25 +00:00
configPath := writeTestConfig ( t , server , scriptoriumPath , workspaceRoot )
2026-05-29 18:28:25 +00:00
outPath := filepath . Join ( tempDir , "storm.md" )
2026-05-29 16:58:18 +00:00
runner := Runner { Clock : fixedClock ( ) }
2026-06-15 12:48:36 +00:00
_ , err := runTestCommand ( t , runner ,
2026-05-29 18:28:25 +00:00
"generate" , "storm" ,
"--config" , configPath ,
"--start" , "2026-05-29T06:00" ,
"--end" , "2026-05-29T10:00" ,
"--out" , outPath ,
2026-06-15 12:48:36 +00:00
)
2026-05-29 18:28:25 +00:00
if err != nil {
t . Fatalf ( "Run() error = %v" , err )
}
2026-06-15 12:48:36 +00:00
assertFileContains ( t , outPath , "# Daily Report" )
2026-06-09 21:08:17 +00:00
dataPackagePath := oneArtifact ( t , workspaceRoot , "data-packages" , "storm" , "2026-05-29" , "*.data_package.yaml" )
2026-06-15 12:48:36 +00:00
assertFileContains ( t , dataPackagePath , "id: storm" )
assertFileContains ( t , dataPackagePath , "prompt_id: weather.storm_report" )
2026-05-29 16:58:18 +00:00
}
2026-05-29 18:00:27 +00:00
func TestRunGenerateTomorrowWritesMarkdownReport ( t * testing . T ) {
server := dailyServer ( t )
tempDir := t . TempDir ( )
scriptoriumPath := writeFakeScriptorium ( t , tempDir )
workspaceRoot := filepath . Join ( tempDir , "workspace" )
2026-05-29 20:44:25 +00:00
configPath := writeTestConfig ( t , server , scriptoriumPath , workspaceRoot )
2026-05-29 18:00:27 +00:00
outPath := filepath . Join ( tempDir , "tomorrow.md" )
runner := Runner { Clock : fixedClock ( ) }
2026-06-15 12:48:36 +00:00
_ , err := runTestCommand ( t , runner ,
2026-05-29 18:00:27 +00:00
"generate" , "tomorrow" ,
"--config" , configPath ,
"--out" , outPath ,
2026-06-15 12:48:36 +00:00
)
2026-05-29 18:00:27 +00:00
if err != nil {
t . Fatalf ( "Run() error = %v" , err )
}
2026-06-15 12:48:36 +00:00
assertFileContains ( t , outPath , "# Saturday's Weather" )
2026-06-14 23:22:31 +00:00
dataPackagePath := oneArtifact ( t , workspaceRoot , "data-packages" , "tomorrow" , "2026-05-30" , "*.data_package.yaml" )
2026-06-15 12:48:36 +00:00
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 )
2026-05-29 18:00:27 +00:00
}
}
func TestRunEveningGeneratesTomorrowReport ( t * testing . T ) {
server := dailyServer ( t )
tempDir := t . TempDir ( )
scriptoriumPath := writeFakeScriptorium ( t , tempDir )
workspaceRoot := filepath . Join ( tempDir , "workspace" )
2026-06-15 12:48:36 +00:00
configPath := writeTestConfig ( t , server , scriptoriumPath , workspaceRoot )
2026-05-29 18:00:27 +00:00
runner := Runner { Clock : fixedClock ( ) }
2026-06-15 12:48:36 +00:00
_ , err := runTestCommand ( t , runner ,
2026-05-29 18:00:27 +00:00
"run" , "evening" ,
"--config" , configPath ,
2026-06-15 12:48:36 +00:00
)
2026-05-29 18:00:27 +00:00
if err != nil {
t . Fatalf ( "Run() error = %v" , err )
}
2026-06-15 12:48:36 +00:00
_ = 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 )
2026-05-29 18:00:27 +00:00
}
}
2026-05-29 18:08:50 +00:00
func TestRunGenerateThreeDayWritesMarkdownReport ( t * testing . T ) {
server := dailyServer ( t )
tempDir := t . TempDir ( )
scriptoriumPath := writeFakeScriptorium ( t , tempDir )
workspaceRoot := filepath . Join ( tempDir , "workspace" )
2026-06-15 12:48:36 +00:00
configPath := writeTestConfig ( t , server , scriptoriumPath , workspaceRoot )
2026-05-29 18:08:50 +00:00
outPath := filepath . Join ( tempDir , "three-day.md" )
runner := Runner { Clock : fixedClock ( ) }
2026-06-15 12:48:36 +00:00
_ , err := runTestCommand ( t , runner ,
2026-05-29 18:08:50 +00:00
"generate" , "three-day" ,
"--config" , configPath ,
"--out" , outPath ,
2026-06-15 12:48:36 +00:00
)
2026-05-29 18:08:50 +00:00
if err != nil {
t . Fatalf ( "Run() error = %v" , err )
}
2026-06-15 12:48:36 +00:00
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:" )
2026-05-29 18:08:50 +00:00
}
2026-05-29 18:16:10 +00:00
func TestRunGenerateWeekendWritesMarkdownReport ( t * testing . T ) {
server := dailyServer ( t )
tempDir := t . TempDir ( )
scriptoriumPath := writeFakeScriptorium ( t , tempDir )
workspaceRoot := filepath . Join ( tempDir , "workspace" )
2026-06-15 12:48:36 +00:00
configPath := writeTestConfig ( t , server , scriptoriumPath , workspaceRoot )
2026-05-29 18:16:10 +00:00
outPath := filepath . Join ( tempDir , "weekend.md" )
runner := Runner { Clock : fixedClock ( ) }
2026-06-15 12:48:36 +00:00
_ , err := runTestCommand ( t , runner ,
2026-05-29 18:16:10 +00:00
"generate" , "weekend" ,
"--config" , configPath ,
"--out" , outPath ,
2026-06-15 12:48:36 +00:00
)
2026-05-29 18:16:10 +00:00
if err != nil {
t . Fatalf ( "Run() error = %v" , err )
}
2026-06-15 12:48:36 +00:00
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:" )
2026-05-29 18:16:10 +00:00
}
func TestRunMorningIncludesWeekendExceptSunday ( t * testing . T ) {
server := dailyServer ( t )
tempDir := t . TempDir ( )
scriptoriumPath := writeFakeScriptorium ( t , tempDir )
workspaceRoot := filepath . Join ( tempDir , "workspace" )
2026-06-15 12:48:36 +00:00
configPath := writeTestConfig ( t , server , scriptoriumPath , workspaceRoot )
2026-05-29 18:16:10 +00:00
runner := Runner { Clock : fixedClock ( ) }
2026-06-15 12:48:36 +00:00
_ , err := runTestCommand ( t , runner ,
2026-05-29 18:16:10 +00:00
"run" , "morning" ,
"--config" , configPath ,
2026-06-15 12:48:36 +00:00
)
2026-05-29 18:16:10 +00:00
if err != nil {
t . Fatalf ( "Run() error = %v" , err )
}
2026-06-15 14:54:37 +00:00
_ = oneArtifact ( t , workspaceRoot , "data-packages" , "today" , "2026-05-29" , "*.data_package.yaml" )
2026-06-15 12:48:36 +00:00
_ = oneArtifact ( t , workspaceRoot , "data-packages" , "weekend" , "2026-05-29" , "*.data_package.yaml" )
2026-06-15 14:54:37 +00:00
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 )
}
if len ( dailyPackages ) != 0 {
t . Fatalf ( "daily packages = %#v, want morning batch to use Today only" , dailyPackages )
}
2026-05-29 18:16:10 +00:00
}
2026-05-29 18:20:39 +00:00
func TestRunMorningReportsPartialFailureAndContinues ( t * testing . T ) {
server := dailyServer ( t )
tempDir := t . TempDir ( )
scriptoriumPath := writeFailingScriptorium ( t , tempDir )
workspaceRoot := filepath . Join ( tempDir , "workspace" )
2026-06-15 12:48:36 +00:00
configPath := writeTestConfig ( t , server , scriptoriumPath , workspaceRoot )
2026-05-29 18:20:39 +00:00
runner := Runner { Clock : fixedClock ( ) }
2026-06-15 12:48:36 +00:00
output , err := runTestCommand ( t , runner ,
2026-05-29 18:20:39 +00:00
"run" , "morning" ,
"--config" , configPath ,
2026-06-15 12:48:36 +00:00
)
2026-05-29 18:20:39 +00:00
if err == nil {
t . Fatal ( "Run() error = nil, want aggregate failure" )
}
if ! strings . Contains ( err . Error ( ) , "1 of 3 reports failed" ) {
t . Fatalf ( "Run() error = %q, want aggregate failure" , err . Error ( ) )
}
var summary app . BatchResult
2026-06-15 12:48:36 +00:00
if decodeErr := json . Unmarshal ( [ ] byte ( output . stdout ) , & summary ) ; decodeErr != nil {
t . Fatalf ( "decode summary: %v\n%s" , decodeErr , output . stdout )
2026-05-29 18:20:39 +00:00
}
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 )
}
2026-06-15 12:48:36 +00:00
if ! strings . Contains ( output . stderr , "status=failed" ) || ! strings . Contains ( output . stderr , "status=succeeded" ) {
t . Fatalf ( "stderr missing structured report logs:\n%s" , output . stderr )
2026-05-29 18:20:39 +00:00
}
2026-06-15 14:54:37 +00:00
_ = oneArtifact ( t , workspaceRoot , "data-packages" , "today" , "2026-05-29" , "*.data_package.yaml" )
2026-06-15 12:48:36 +00:00
_ = oneArtifact ( t , workspaceRoot , "data-packages" , "weekend" , "2026-05-29" , "*.data_package.yaml" )
2026-05-29 18:20:39 +00:00
}
2026-06-07 23:42:27 +00:00
func TestBatchOutputIncludesNotificationDetails ( t * testing . T ) {
result := & app . BatchResult {
Batch : app . BatchMorning ,
Total : 2 ,
Succeeded : 1 ,
Failed : 1 ,
Reports : [ ] app . BatchReportResult {
{
2026-06-15 16:43:06 +00:00
ReportID : "daily" ,
2026-06-07 23:42:27 +00:00
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 {
{
2026-06-15 16:43:06 +00:00
ReportID : "daily" ,
2026-06-07 23:42:27 +00:00
Status : "failed" ,
2026-06-15 16:43:06 +00:00
Error : "notify report daily: upload failed: [redacted]" ,
2026-06-07 23:42:27 +00:00
NotificationStatus : "failed" ,
2026-06-15 16:43:06 +00:00
NotificationError : "notify report daily: upload failed: [redacted]" ,
2026-06-07 23:42:27 +00:00
} ,
} ,
}
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 )
}
}
}
2026-05-29 18:20:39 +00:00
func TestRunEveningUsesOutputDirectoryAndSummary ( t * testing . T ) {
server := dailyServer ( t )
tempDir := t . TempDir ( )
scriptoriumPath := writeFakeScriptorium ( t , tempDir )
workspaceRoot := filepath . Join ( tempDir , "workspace" )
outputDir := filepath . Join ( tempDir , "copies" )
2026-06-15 12:48:36 +00:00
configPath := writeTestConfig ( t , server , scriptoriumPath , workspaceRoot )
2026-05-29 18:20:39 +00:00
runner := Runner { Clock : fixedClock ( ) }
2026-06-15 12:48:36 +00:00
output , err := runTestCommand ( t , runner ,
2026-05-29 18:20:39 +00:00
"run" , "evening" ,
"--config" , configPath ,
"--out-dir" , outputDir ,
2026-06-15 12:48:36 +00:00
)
2026-05-29 18:20:39 +00:00
if err != nil {
t . Fatalf ( "Run() error = %v" , err )
}
var summary app . BatchResult
2026-06-15 12:48:36 +00:00
if decodeErr := json . Unmarshal ( [ ] byte ( output . stdout ) , & summary ) ; decodeErr != nil {
t . Fatalf ( "decode summary: %v\n%s" , decodeErr , output . stdout )
2026-05-29 18:20:39 +00:00
}
if summary . Total != 1 || summary . Failed != 0 {
t . Fatalf ( "summary total/failed = %d/%d, want 1/0" , summary . Total , summary . Failed )
}
if _ , err := os . Stat ( filepath . Join ( outputDir , "tomorrow.md" ) ) ; err != nil {
t . Fatalf ( "expected copied report: %v" , err )
}
if len ( summary . Reports ) != 1 || summary . Reports [ 0 ] . OutputPath != filepath . Join ( outputDir , "tomorrow.md" ) {
t . Fatalf ( "summary reports = %#v, want output path" , summary . Reports )
}
}
2026-06-07 23:42:27 +00:00
func TestRunEveningReportsNotificationSuccess ( t * testing . T ) {
server := dailyServer ( t )
distributorServer := httptest . NewServer ( http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
2026-06-08 07:04:31 -05:00
if r . URL . Path == "/runs/distributor-run-1" {
w . Header ( ) . Set ( "Content-Type" , "application/json" )
2026-06-14 23:22:31 +00:00
_ , _ = w . Write ( [ ] byte ( ` { "run_id":"distributor-run-1","pipeline_id":"weatherreporter.tomorrow","status":"succeeded","report": { "actions":[ { "action":"replace_older"}]}} ` ) )
2026-06-08 07:04:31 -05:00
return
}
2026-06-14 23:22:31 +00:00
if r . URL . Path != "/v1/pipelines/weatherreporter.tomorrow/upload" {
2026-06-07 23:42:27 +00:00
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" )
2026-06-08 07:04:31 -05:00
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 pipeline_id_template: weatherreporter.{artifact_group}\n"
2026-06-07 23:42:27 +00:00
if err := os . WriteFile ( configPath , [ ] byte ( configBody ) , 0 o600 ) ; 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 )
}
2026-06-14 23:22:31 +00:00
if summary . Reports [ 0 ] . NotificationStatus != "succeeded" || summary . Reports [ 0 ] . NotificationRunID != "distributor-run-1" || summary . Reports [ 0 ] . NotificationPipelineID != "weatherreporter.tomorrow" {
2026-06-07 23:42:27 +00:00
t . Fatalf ( "notification fields = %#v" , summary . Reports [ 0 ] )
}
2026-06-08 07:04:31 -05:00
if ! strings . Contains ( stderr . String ( ) , ` notificationStatus="succeeded" ` ) || ! strings . Contains ( stderr . String ( ) , ` notificationRunId="distributor-run-1" ` ) {
2026-06-07 23:42:27 +00:00
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" )
2026-06-08 07:04:31 -05:00
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 pipeline_id_template: weatherreporter.{artifact_group}\n"
2026-06-07 23:42:27 +00:00
if err := os . WriteFile ( configPath , [ ] byte ( configBody ) , 0 o600 ) ; 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 )
}
}
}
2026-06-15 14:54:37 +00:00
func TestRunMorningGeneratesTodayAndThreeDayOnSunday ( t * testing . T ) {
2026-05-29 18:08:50 +00:00
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 ) , 0 o600 ) ; err != nil {
t . Fatalf ( "write config: %v" , err )
}
var stdout bytes . Buffer
var stderr bytes . Buffer
runner := Runner { Clock : timeutil . FixedClock { Time : time . Date ( 2026 , 5 , 31 , 12 , 0 , 0 , 0 , time . UTC ) } }
err := runner . Run ( context . Background ( ) , [ ] string {
"run" , "morning" ,
"--config" , configPath ,
} , & stdout , & stderr )
if err != nil {
t . Fatalf ( "Run() error = %v" , err )
}
2026-06-15 14:54:37 +00:00
todayPackages , err := filepath . Glob ( filepath . Join ( workspaceRoot , "data-packages" , "today" , "2026-05-31" , "*.data_package.yaml" ) )
2026-05-29 18:08:50 +00:00
if err != nil {
2026-06-15 14:54:37 +00:00
t . Fatalf ( "glob today packages: %v" , err )
2026-05-29 18:08:50 +00:00
}
2026-06-09 21:08:17 +00:00
threeDayPackages , err := filepath . Glob ( filepath . Join ( workspaceRoot , "data-packages" , "three-day" , "2026-05-31" , "*.data_package.yaml" ) )
2026-05-29 18:08:50 +00:00
if err != nil {
t . Fatalf ( "glob 3-day packages: %v" , err )
}
2026-06-15 14:54:37 +00:00
dailyPackages , err := filepath . Glob ( filepath . Join ( workspaceRoot , "data-packages" , "daily" , "2026-05-31" , "*.data_package.yaml" ) )
if err != nil {
t . Fatalf ( "glob daily packages: %v" , err )
}
if len ( todayPackages ) != 1 || len ( threeDayPackages ) != 1 || len ( dailyPackages ) != 0 {
t . Fatalf ( "today packages = %#v, 3-day packages = %#v, daily packages = %#v; want Today and 3-day only" , todayPackages , threeDayPackages , dailyPackages )
2026-05-29 18:08:50 +00:00
}
}
2026-05-29 17:47:42 +00:00
func TestRunGenerateDailyWritesMarkdownReport ( t * testing . T ) {
2026-05-29 17:23:49 +00:00
server := dailyServer ( t )
2026-05-29 17:32:57 +00:00
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 ) , 0 o600 ) ; err != nil {
2026-05-29 17:23:49 +00:00
t . Fatalf ( "write config: %v" , err )
}
2026-05-29 17:47:42 +00:00
outPath := filepath . Join ( tempDir , "daily.md" )
2026-05-29 17:23:49 +00:00
var stdout bytes . Buffer
var stderr bytes . Buffer
runner := Runner { Clock : fixedClock ( ) }
err := runner . Run ( context . Background ( ) , [ ] string {
"generate" , "daily" ,
"--config" , configPath ,
"--date" , "2026-05-29" ,
2026-05-29 19:57:08 -05:00
"--tz" , "UTC" ,
2026-05-29 17:23:49 +00:00
"--out" , outPath ,
} , & stdout , & stderr )
if err != nil {
t . Fatalf ( "Run() error = %v" , err )
}
2026-05-29 17:47:42 +00:00
report , err := os . ReadFile ( outPath )
2026-05-29 17:23:49 +00:00
if err != nil {
2026-05-29 17:47:42 +00:00
t . Fatalf ( "read report: %v" , err )
}
2026-06-15 16:43:06 +00:00
if ! strings . Contains ( string ( report ) , "# Friday's Weather" ) {
2026-05-29 17:47:42 +00:00
t . Fatalf ( "report output missing markdown:\n%s" , string ( report ) )
}
2026-06-09 21:08:17 +00:00
dataPackageMatches , err := filepath . Glob ( filepath . Join ( workspaceRoot , "data-packages" , "daily" , "2026-05-29" , "*.data_package.yaml" ) )
2026-05-29 17:47:42 +00:00
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 )
2026-05-29 17:23:49 +00:00
}
2026-06-15 20:27:28 +00:00
if ! strings . Contains ( string ( data ) , "schema_version: weatherreporter.data_package.v3" ) || ! strings . Contains ( string ( data ) , "id: daily" ) {
2026-05-29 17:32:57 +00:00
t . Fatalf ( "data package output missing expected content:\n%s" , string ( data ) )
}
2026-06-09 21:08:17 +00:00
if ! strings . Contains ( string ( data ) , "location:" ) ||
! strings . Contains ( string ( data ) , "id: home" ) ||
! strings . Contains ( string ( data ) , "name: Brentwood" ) ||
! strings . Contains ( string ( data ) , "region: St. Louis Metro" ) ||
! strings . Contains ( string ( data ) , "timezone: UTC" ) {
t . Fatalf ( "data package missing configured location with overridden timezone:\n%s" , string ( data ) )
2026-05-29 19:57:08 -05:00
}
2026-05-29 17:32:57 +00:00
preflightMatches , err := filepath . Glob ( filepath . Join ( workspaceRoot , "preflight" , "daily" , "2026-05-29" , "*.render.json" ) )
if err != nil {
t . Fatalf ( "glob preflight: %v" , err )
}
if len ( preflightMatches ) != 1 {
t . Fatalf ( "preflight files = %#v, want one render output" , preflightMatches )
}
preflight , err := os . ReadFile ( preflightMatches [ 0 ] )
if err != nil {
t . Fatalf ( "read preflight: %v" , err )
}
if ! strings . Contains ( string ( preflight ) , ` ok ` ) {
t . Fatalf ( "preflight missing fake render output:\n%s" , string ( preflight ) )
2026-05-29 17:23:49 +00:00
}
2026-05-29 17:47:42 +00:00
reportMatches , err := filepath . Glob ( filepath . Join ( workspaceRoot , "reports" , "daily" , "*.md" ) )
if err != nil {
t . Fatalf ( "glob managed report: %v" , err )
}
if len ( reportMatches ) != 1 {
t . Fatalf ( "managed reports = %#v, want one" , reportMatches )
}
2026-06-15 16:46:10 +00:00
rawGeneratedTextPath := oneArtifact ( t , workspaceRoot , "snapshots" , "daily" , "2026-05-29" , "*.generated_text.raw.json" )
validatedGeneratedTextPath := oneArtifact ( t , workspaceRoot , "snapshots" , "daily" , "2026-05-29" , "*.generated_text.json" )
renderContextPath := oneArtifact ( t , workspaceRoot , "snapshots" , "daily" , "2026-05-29" , "*.render_context.json" )
metadataPath := oneArtifact ( t , workspaceRoot , "snapshots" , "daily" , "2026-05-29" , "*.metadata.json" )
assertFileContains ( t , rawGeneratedTextPath , ` "summary": "Showers are possible during the selected day." ` )
assertFileContains ( t , validatedGeneratedTextPath , ` "summary":"Showers are possible during the selected day." ` )
assertFileContains ( t , renderContextPath , ` "Title": "Friday's Weather" ` )
assertFileContains ( t , metadataPath , ` "generatedTextSchemaId": "daily" ` )
2026-05-29 17:23:49 +00:00
}
2026-06-15 14:58:36 +00:00
func TestRunGenerateTodayWritesGeneratedTextReport ( 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 , "today.md" )
var stdout bytes . Buffer
var stderr bytes . Buffer
runner := Runner { Clock : fixedClock ( ) }
err := runner . Run ( context . Background ( ) , [ ] string {
"generate" , "today" ,
"--config" , configPath ,
"--date" , "2026-05-29" ,
"--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 {
"# Today's Weather" ,
"Today starts with showers before improving." ,
"Morning showers should taper as drier air arrives." ,
} {
if ! strings . Contains ( string ( report ) , want ) {
t . Fatalf ( "today report output missing %q:\n%s" , want , string ( report ) )
}
}
dataPackagePath := oneArtifact ( t , workspaceRoot , "data-packages" , "today" , "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: today" ) ||
! strings . Contains ( string ( dataPackage ) , "prompt_id: weather.today_generated_text" ) ||
! strings . Contains ( string ( dataPackage ) , "today_planning:" ) {
t . Fatalf ( "data package output missing Today content:\n%s" , string ( dataPackage ) )
}
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 )
}
if len ( dailyPackages ) != 0 {
t . Fatalf ( "daily packages = %#v, want generate today to stay separate" , dailyPackages )
}
rawGeneratedTextPath := oneArtifact ( t , workspaceRoot , "snapshots" , "today" , "2026-05-29" , "*.generated_text.raw.json" )
validatedGeneratedTextPath := oneArtifact ( t , workspaceRoot , "snapshots" , "today" , "2026-05-29" , "*.generated_text.json" )
renderContextPath := oneArtifact ( t , workspaceRoot , "snapshots" , "today" , "2026-05-29" , "*.render_context.json" )
managedReportPath := oneArtifact ( t , workspaceRoot , "reports" , "today" , "*.md" )
assertFileContains ( t , rawGeneratedTextPath , ` "summary": "Today starts with showers before improving." ` )
assertFileContains ( t , validatedGeneratedTextPath , ` "summary":"Today starts with showers before improving." ` )
assertFileContains ( t , renderContextPath , ` "Title": "Today's Weather" ` )
assertFileContains ( t , managedReportPath , "# Today's Weather" )
}
2026-06-14 05:17:05 +00:00
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." ,
2026-06-14 12:54:32 -05:00
"A cold front is moving into the region." ,
"A front will keep the region unsettled." ,
2026-06-14 05:17:05 +00:00
} {
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." ` )
2026-06-14 08:57:53 -05:00
assertFileContains ( t , renderContextPath , ` "Report": { ` )
assertFileContains ( t , renderContextPath , ` "Title": "Hourly Report" ` )
assertFileContains ( t , renderContextPath , ` "Modules": { ` )
assertFileContains ( t , renderContextPath , ` "Collected": { ` )
assertFileContains ( t , renderContextPath , ` "Derived": { ` )
2026-06-14 05:17:05 +00:00
assertFileContains ( t , managedReportPath , "# Hourly Report" )
}
2026-06-15 14:58:36 +00:00
func TestRunInspectTodayArtifacts ( 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 ) , 0 o600 ) ; err != nil {
t . Fatalf ( "write config: %v" , err )
}
runner := Runner { Clock : fixedClock ( ) }
var stdout bytes . Buffer
var stderr bytes . Buffer
err := runner . Run ( context . Background ( ) , [ ] string {
"generate" , "today" ,
"--config" , configPath ,
"--date" , "2026-05-29" ,
} , & stdout , & stderr )
if err != nil {
t . Fatalf ( "Run(generate) error = %v" , err )
}
dataPackageMatches , err := filepath . Glob ( filepath . Join ( workspaceRoot , "data-packages" , "today" , "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 )
}
runID := strings . TrimSuffix ( filepath . Base ( dataPackageMatches [ 0 ] ) , ".data_package.yaml" )
stdout . Reset ( )
err = runner . Run ( context . Background ( ) , [ ] string { "inspect" , "reports" , "--config" , configPath , "--limit" , "1" } , & stdout , & stderr )
if err != nil {
t . Fatalf ( "Run(inspect reports) error = %v" , err )
}
if ! strings . Contains ( stdout . String ( ) , runID ) || ! strings . Contains ( stdout . String ( ) , ` "reportId": "today" ` ) {
t . Fatalf ( "inspect reports output missing Today run:\n%s" , stdout . String ( ) )
}
var sourcesOutput string
for _ , command := range [ ] string { "metadata" , "modules" , "data-package" , "sources" } {
stdout . Reset ( )
err = runner . Run ( context . Background ( ) , [ ] string { "inspect" , command , "--config" , configPath , runID } , & stdout , & stderr )
if err != nil {
t . Fatalf ( "Run(inspect %s) error = %v" , command , err )
}
if ! strings . Contains ( stdout . String ( ) , runID ) || ! strings . Contains ( stdout . String ( ) , "today" ) {
t . Fatalf ( "inspect %s output missing Today run id:\n%s" , command , stdout . String ( ) )
}
if command == "sources" {
sourcesOutput = stdout . String ( )
}
}
if ! strings . Contains ( sourcesOutput , ` "name": "narrative" ` ) || strings . Contains ( sourcesOutput , ` "name": "daily" ` ) {
t . Fatalf ( "inspect sources output missing narrative source or has unexpected daily source:\n%s" , sourcesOutput )
}
}
2026-05-29 18:33:24 +00:00
func TestRunInspectGeneratedArtifacts ( 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 ) , 0 o600 ) ; err != nil {
t . Fatalf ( "write config: %v" , err )
}
runner := Runner { Clock : fixedClock ( ) }
var stdout bytes . Buffer
var stderr bytes . Buffer
err := runner . Run ( context . Background ( ) , [ ] string {
"generate" , "daily" ,
"--config" , configPath ,
"--date" , "2026-05-29" ,
} , & stdout , & stderr )
if err != nil {
t . Fatalf ( "Run(generate) error = %v" , err )
}
2026-06-09 21:08:17 +00:00
dataPackageMatches , err := filepath . Glob ( filepath . Join ( workspaceRoot , "data-packages" , "daily" , "2026-05-29" , "*.data_package.yaml" ) )
2026-05-29 18:33:24 +00:00
if err != nil {
t . Fatalf ( "glob data package: %v" , err )
}
if len ( dataPackageMatches ) != 1 {
t . Fatalf ( "data package files = %#v, want one" , dataPackageMatches )
}
2026-06-09 21:08:17 +00:00
runID := strings . TrimSuffix ( filepath . Base ( dataPackageMatches [ 0 ] ) , ".data_package.yaml" )
2026-05-29 18:33:24 +00:00
stdout . Reset ( )
err = runner . Run ( context . Background ( ) , [ ] string { "inspect" , "reports" , "--config" , configPath , "--limit" , "1" } , & stdout , & stderr )
if err != nil {
t . Fatalf ( "Run(inspect reports) error = %v" , err )
}
if ! strings . Contains ( stdout . String ( ) , runID ) || ! strings . Contains ( stdout . String ( ) , ` "metadataPath" ` ) {
t . Fatalf ( "inspect reports output missing run:\n%s" , stdout . String ( ) )
}
2026-06-09 20:58:35 +00:00
var sourcesOutput string
for _ , command := range [ ] string { "metadata" , "modules" , "data-package" , "sources" } {
2026-05-29 18:33:24 +00:00
stdout . Reset ( )
err = runner . Run ( context . Background ( ) , [ ] string { "inspect" , command , "--config" , configPath , runID } , & stdout , & stderr )
if err != nil {
t . Fatalf ( "Run(inspect %s) error = %v" , command , err )
}
if ! strings . Contains ( stdout . String ( ) , runID ) {
t . Fatalf ( "inspect %s output missing run id:\n%s" , command , stdout . String ( ) )
}
2026-06-09 20:58:35 +00:00
if command == "sources" {
sourcesOutput = stdout . String ( )
}
2026-05-29 18:33:24 +00:00
}
2026-06-10 08:24:50 -05:00
if ! strings . Contains ( sourcesOutput , ` "name": "narrative" ` ) || strings . Contains ( sourcesOutput , ` "name": "daily" ` ) {
t . Fatalf ( "inspect sources output missing narrative source or has unexpected daily source:\n%s" , sourcesOutput )
2026-05-29 18:33:24 +00:00
}
}
func TestRunInspectMissingMetadata ( t * testing . T ) {
tempDir := t . TempDir ( )
configPath := filepath . Join ( tempDir , "config.yml" )
configBody := "workspace:\n root: " + filepath . Join ( tempDir , "workspace" ) + "\n"
if err := os . WriteFile ( configPath , [ ] byte ( configBody ) , 0 o600 ) ; err != nil {
t . Fatalf ( "write config: %v" , err )
}
var stdout bytes . Buffer
var stderr bytes . Buffer
runner := Runner { Clock : fixedClock ( ) }
err := runner . Run ( context . Background ( ) , [ ] string { "inspect" , "metadata" , "--config" , configPath , "missing" } , & stdout , & stderr )
if err == nil {
t . Fatal ( "Run(inspect metadata) error = nil, want missing metadata error" )
}
if ! strings . Contains ( err . Error ( ) , "metadata for run id" ) {
t . Fatalf ( "error = %q, want missing run id context" , err . Error ( ) )
}
}
2026-05-29 20:39:17 +00:00
func TestRunInspectRunCommandsParseRunIDAndConfig ( t * testing . T ) {
tempDir := t . TempDir ( )
configPath := filepath . Join ( tempDir , "config.yml" )
configBody := "workspace:\n root: " + filepath . Join ( tempDir , "workspace" ) + "\n"
if err := os . WriteFile ( configPath , [ ] byte ( configBody ) , 0 o600 ) ; err != nil {
t . Fatalf ( "write config: %v" , err )
}
runner := Runner { Clock : fixedClock ( ) }
2026-06-09 20:58:35 +00:00
commands := [ ] string { "metadata" , "modules" , "data-package" , "prior" , "sources" }
2026-05-29 20:39:17 +00:00
for _ , command := range commands {
t . Run ( command + " requires run id" , func ( t * testing . T ) {
var stdout bytes . Buffer
var stderr bytes . Buffer
err := runner . Run ( context . Background ( ) , [ ] string { "inspect" , command , "--config" , configPath } , & stdout , & stderr )
if err == nil {
t . Fatal ( "Run() error = nil, want missing run id error" )
}
if ! strings . Contains ( err . Error ( ) , "requires a run id" ) {
t . Fatalf ( "error = %q, want missing run id context" , err . Error ( ) )
}
} )
t . Run ( command + " accepts config" , func ( t * testing . T ) {
var stdout bytes . Buffer
var stderr bytes . Buffer
err := runner . Run ( context . Background ( ) , [ ] string { "inspect" , command , "--config" , configPath , "missing" } , & stdout , & stderr )
if err == nil {
t . Fatal ( "Run() error = nil, want missing metadata error" )
}
if ! strings . Contains ( err . Error ( ) , "metadata for run id" ) {
t . Fatalf ( "error = %q, want missing metadata context" , err . Error ( ) )
}
} )
}
}
2026-05-29 16:58:18 +00:00
func TestResolveGenerateCommands ( t * testing . T ) {
runner := Runner { Clock : fixedClock ( ) }
tests := [ ] struct {
name string
args [ ] string
want app . ReportKind
} {
{ name : "daily" , args : [ ] string { "daily" , "--date" , "2026-05-29" } , want : app . ReportDaily } ,
2026-06-15 14:58:36 +00:00
{ name : "today" , args : [ ] string { "today" , "--date" , "2026-05-29" } , want : app . ReportToday } ,
2026-05-29 16:58:18 +00:00
{ name : "tomorrow" , args : [ ] string { "tomorrow" } , want : app . ReportTomorrow } ,
2026-06-14 04:26:05 +00:00
{ name : "hourly" , args : [ ] string { "hourly" } , want : app . ReportHourly } ,
2026-05-29 16:58:18 +00:00
{ name : "three-day" , args : [ ] string { "three-day" } , want : app . ReportThreeDay } ,
{ name : "weekend" , args : [ ] string { "weekend" } , want : app . ReportWeekend } ,
{ name : "storm" , args : [ ] string { "storm" , "--start" , "2026-05-29T18:00" , "--end" , "2026-05-30T06:00" } , want : app . ReportStorm } ,
}
for _ , tt := range tests {
t . Run ( tt . name , func ( t * testing . T ) {
req , err := runner . resolveGenerate ( tt . args )
if err != nil {
t . Fatalf ( "resolveGenerate() error = %v" , err )
}
if req . Report != tt . want {
t . Fatalf ( "Report = %q, want %q" , req . Report , tt . want )
}
} )
}
}
2026-06-15 12:33:55 +00:00
func TestResolveGenerateSupportsEveryReportCommandName ( t * testing . T ) {
runner := Runner { Clock : fixedClock ( ) }
for _ , name := range report . CommandNames ( ) {
t . Run ( name , func ( t * testing . T ) {
args := [ ] string { name }
2026-06-15 14:58:36 +00:00
if name == report . CommandNameDaily || name == report . CommandNameToday {
2026-06-15 12:33:55 +00:00
args = append ( args , "--date" , "2026-05-29" )
}
if name == report . CommandNameStorm {
args = append ( args , "--start" , "2026-05-29T18:00" , "--end" , "2026-05-30T06:00" )
}
req , err := runner . resolveGenerate ( args )
if err != nil {
t . Fatalf ( "resolveGenerate() error = %v" , err )
}
want , err := report . IDForCommandName ( name )
if err != nil {
t . Fatalf ( "IDForCommandName() error = %v" , err )
}
resolved , err := app . ResolveGenerate ( req , req . Now )
if err != nil {
t . Fatalf ( "ResolveGenerate() error = %v" , err )
}
if resolved . Definition . ID != want {
t . Fatalf ( "resolved ID = %q, want %q" , resolved . Definition . ID , want )
}
} )
}
}
2026-06-14 04:26:05 +00:00
func TestResolveGenerateHourlyAppliesSharedFlags ( t * testing . T ) {
2026-06-12 17:36:58 +00:00
runner := Runner { Clock : fixedClock ( ) }
configPath := filepath . Join ( t . TempDir ( ) , "config.yml" )
if err := os . WriteFile ( configPath , [ ] byte ( "weather_api:\n units: metric\n timezone: UTC\n" ) , 0 o600 ) ; err != nil {
t . Fatalf ( "write config: %v" , err )
}
2026-06-14 04:26:05 +00:00
req , err := runner . resolveGenerate ( [ ] string { "hourly" , "--config" , configPath , "--units" , "us" , "--tz" , "America/Chicago" , "--out" , "./hourly.md" } )
2026-06-12 17:36:58 +00:00
if err != nil {
t . Fatalf ( "resolveGenerate() error = %v" , err )
}
2026-06-14 04:26:05 +00:00
if req . Report != app . ReportHourly {
t . Fatalf ( "Report = %q, want hourly" , req . Report )
2026-06-12 17:36:58 +00:00
}
if req . Config . WeatherAPI . Units != "us" {
t . Fatalf ( "Units = %q, want us" , req . Config . WeatherAPI . Units )
}
if req . Config . WeatherAPI . Timezone != "America/Chicago" {
t . Fatalf ( "Timezone = %q, want America/Chicago" , req . Config . WeatherAPI . Timezone )
}
2026-06-14 04:26:05 +00:00
if req . OutputPath != "./hourly.md" {
t . Fatalf ( "OutputPath = %q, want ./hourly.md" , req . OutputPath )
2026-06-12 17:36:58 +00:00
}
if ! req . Date . IsZero ( ) || ! req . StormStart . IsZero ( ) || ! req . StormEnd . IsZero ( ) {
2026-06-14 04:26:05 +00:00
t . Fatalf ( "date/storm bounds = %s/%s/%s, want unset for hourly" , req . Date , req . StormStart , req . StormEnd )
2026-06-12 17:36:58 +00:00
}
}
2026-06-14 04:26:05 +00:00
func TestResolveGenerateHourlyRejectsDateAndStormBounds ( t * testing . T ) {
2026-06-12 17:36:58 +00:00
runner := Runner { Clock : fixedClock ( ) }
for _ , args := range [ ] [ ] string {
2026-06-14 04:26:05 +00:00
{ "hourly" , "--date" , "2026-05-29" } ,
{ "hourly" , "--start" , "2026-05-29T18:00" } ,
{ "hourly" , "--end" , "2026-05-29T20:00" } ,
2026-06-14 05:17:05 +00:00
{ "hourly" , "--hours" , "6" } ,
{ "hourly" , "--duration" , "6h" } ,
2026-06-12 17:36:58 +00:00
} {
_ , err := runner . resolveGenerate ( args )
if err == nil {
t . Fatalf ( "resolveGenerate(%v) error = nil, want flag error" , args )
}
if ! strings . Contains ( err . Error ( ) , "flag provided but not defined" ) {
t . Fatalf ( "resolveGenerate(%v) error = %q, want undefined flag error" , args , err . Error ( ) )
}
}
}
2026-06-15 16:46:10 +00:00
func TestResolveGenerateDailyRequiresDate ( t * testing . T ) {
2026-05-29 16:58:18 +00:00
runner := Runner { Clock : fixedClock ( ) }
req , err := runner . resolveGenerate ( [ ] string { "daily" } )
2026-06-15 16:46:10 +00:00
if err == nil {
t . Fatal ( "resolveGenerate() error = nil, want required date error" )
}
if ! strings . Contains ( err . Error ( ) , "generate daily requires --date YYYY-MM-DD" ) {
t . Fatalf ( "resolveGenerate() error = %q, want required date context" , err . Error ( ) )
}
if ! req . Date . IsZero ( ) {
t . Fatalf ( "Date = %s, want unset on error" , req . Date )
}
}
func TestResolveGenerateDailyRejectsMalformedDate ( t * testing . T ) {
runner := Runner { Clock : fixedClock ( ) }
_ , err := runner . resolveGenerate ( [ ] string { "daily" , "--date" , "bad-date" } )
if err == nil {
t . Fatal ( "resolveGenerate() error = nil, want date parse error" )
}
if ! strings . Contains ( err . Error ( ) , ` parse date "bad-date" as YYYY-MM-DD ` ) {
t . Fatalf ( "resolveGenerate() error = %q, want date parse context" , err . Error ( ) )
}
}
func TestResolveGenerateDailyParsesDate ( t * testing . T ) {
runner := Runner { Clock : fixedClock ( ) }
req , err := runner . resolveGenerate ( [ ] string { "daily" , "--date" , "2026-05-29" } )
2026-05-29 16:58:18 +00:00
if err != nil {
t . Fatalf ( "resolveGenerate() error = %v" , err )
}
2026-06-15 16:46:10 +00:00
if req . Report != app . ReportDaily {
t . Fatalf ( "Report = %q, want daily" , req . Report )
}
2026-05-29 16:58:18 +00:00
if got := req . Date . Format ( timeutil . DateLayout ) ; got != "2026-05-29" {
t . Fatalf ( "Date = %s, want 2026-05-29" , got )
}
}
2026-06-15 14:58:36 +00:00
func TestResolveGenerateTodayDate ( t * testing . T ) {
runner := Runner { Clock : fixedClock ( ) }
defaultReq , err := runner . resolveGenerate ( [ ] string { "today" } )
if err != nil {
t . Fatalf ( "resolveGenerate(default) error = %v" , err )
}
if defaultReq . Report != app . ReportToday {
t . Fatalf ( "Report = %q, want today" , defaultReq . Report )
}
if got := defaultReq . Date . Format ( timeutil . DateLayout ) ; got != "2026-05-29" {
t . Fatalf ( "default Date = %s, want 2026-05-29" , got )
}
explicitReq , err := runner . resolveGenerate ( [ ] string { "today" , "--date" , "2026-05-30" , "--tz" , "UTC" } )
if err != nil {
t . Fatalf ( "resolveGenerate(explicit) error = %v" , err )
}
if got := explicitReq . Date . Format ( timeutil . DateLayout ) ; got != "2026-05-30" {
t . Fatalf ( "explicit Date = %s, want 2026-05-30" , got )
}
resolved , err := app . ResolveGenerate ( explicitReq , explicitReq . Now )
if err != nil {
t . Fatalf ( "ResolveGenerate() error = %v" , err )
}
if resolved . Definition . ID != report . Today {
t . Fatalf ( "resolved ID = %q, want today" , resolved . Definition . ID )
}
if got := resolved . ValidPeriod . Start . Format ( time . RFC3339 ) ; got != "2026-05-30T00:00:00Z" {
t . Fatalf ( "valid period start = %s, want explicit UTC date" , got )
}
}
2026-05-29 16:52:02 +00:00
2026-05-29 16:58:18 +00:00
func TestResolveGenerateAppliesSharedFlags ( t * testing . T ) {
runner := Runner { Clock : fixedClock ( ) }
2026-06-15 16:46:10 +00:00
req , err := runner . resolveGenerate ( [ ] string { "daily" , "--date" , "2026-05-29" , "--units" , "metric" , "--tz" , "UTC" , "--out" , "./daily.md" } )
2026-05-29 16:58:18 +00:00
if err != nil {
t . Fatalf ( "resolveGenerate() error = %v" , err )
}
if req . Config . WeatherAPI . Units != "metric" {
t . Fatalf ( "Units = %q, want metric" , req . Config . WeatherAPI . Units )
}
if req . Config . WeatherAPI . Timezone != "UTC" {
t . Fatalf ( "Timezone = %q, want UTC" , req . Config . WeatherAPI . Timezone )
}
if req . OutputPath != "./daily.md" {
t . Fatalf ( "OutputPath = %q, want ./daily.md" , req . OutputPath )
}
}
2026-06-14 04:35:52 +00:00
func TestResolveGenerateRejectsRetiredHourlyCommand ( t * testing . T ) {
runner := Runner { Clock : fixedClock ( ) }
retired := strings . Join ( [ ] string { "near" , "term" } , "-" )
_ , err := runner . resolveGenerate ( [ ] string { retired } )
if err == nil {
t . Fatal ( "resolveGenerate() error = nil, want unknown report" )
}
if ! strings . Contains ( err . Error ( ) , "unknown generate report" ) {
t . Fatalf ( "error = %q, want unknown generate report" , err . Error ( ) )
}
}
2026-05-29 16:58:18 +00:00
func TestResolveGenerateStormRequiresStartAndEnd ( t * testing . T ) {
runner := Runner { Clock : fixedClock ( ) }
2026-05-29 20:41:47 +00:00
_ , err := runner . resolveGenerate ( [ ] string { "storm" , "--end" , "2026-05-29T18:00" } )
if err == nil {
t . Fatal ( "resolveGenerate() error = nil, want missing start error" )
}
if ! strings . Contains ( err . Error ( ) , "requires --start" ) {
t . Fatalf ( "error = %q, want missing start" , err . Error ( ) )
}
_ , err = runner . resolveGenerate ( [ ] string { "storm" , "--start" , "2026-05-29T18:00" } )
2026-05-29 16:52:02 +00:00
if err == nil {
2026-05-29 16:58:18 +00:00
t . Fatal ( "resolveGenerate() error = nil, want missing end error" )
}
if ! strings . Contains ( err . Error ( ) , "requires --end" ) {
t . Fatalf ( "error = %q, want missing end" , err . Error ( ) )
2026-05-29 16:52:02 +00:00
}
2026-05-29 16:58:18 +00:00
}
2026-05-29 20:41:47 +00:00
func TestResolveGenerateStormParsesLocalTimestamps ( t * testing . T ) {
runner := Runner { Clock : fixedClock ( ) }
req , err := runner . resolveGenerate ( [ ] string {
"storm" ,
"--tz" , "America/Chicago" ,
"--start" , "2026-05-29T18:00" ,
"--end" , "2026-05-30T06:00" ,
} )
if err != nil {
t . Fatalf ( "resolveGenerate() error = %v" , err )
}
if got := req . StormStart . Format ( time . RFC3339 ) ; got != "2026-05-29T18:00:00-05:00" {
t . Fatalf ( "StormStart = %q, want local Chicago time" , got )
}
if got := req . StormEnd . Format ( time . RFC3339 ) ; got != "2026-05-30T06:00:00-05:00" {
t . Fatalf ( "StormEnd = %q, want local Chicago time" , got )
}
}
2026-05-29 16:58:18 +00:00
func TestResolveGenerateStormParsesRFC3339 ( t * testing . T ) {
runner := Runner { Clock : fixedClock ( ) }
2026-05-29 16:52:02 +00:00
2026-05-29 16:58:18 +00:00
req , err := runner . resolveGenerate ( [ ] string {
"storm" ,
"--start" , "2026-05-29T18:00:00-05:00" ,
"--end" , "2026-05-30T06:00:00-05:00" ,
} )
if err != nil {
t . Fatalf ( "resolveGenerate() error = %v" , err )
2026-05-29 16:52:02 +00:00
}
2026-05-29 16:58:18 +00:00
if ! req . StormEnd . After ( req . StormStart ) {
t . Fatalf ( "StormEnd = %s, want after %s" , req . StormEnd , req . StormStart )
}
}
2026-05-29 20:41:47 +00:00
func TestResolveGenerateStormRejectsInvalidBounds ( t * testing . T ) {
runner := Runner { Clock : fixedClock ( ) }
_ , err := runner . resolveGenerate ( [ ] string {
"storm" ,
"--start" , "2026-05-30T06:00" ,
"--end" , "2026-05-29T18:00" ,
} )
if err == nil {
t . Fatal ( "resolveGenerate() error = nil, want invalid bounds error" )
}
if ! strings . Contains ( err . Error ( ) , "end time after start time" ) {
t . Fatalf ( "error = %q, want invalid bounds context" , err . Error ( ) )
}
}
2026-05-29 16:58:18 +00:00
func TestResolveRunCommands ( t * testing . T ) {
tests := [ ] struct {
name string
args [ ] string
want app . BatchKind
} {
{ name : "morning" , args : [ ] string { "morning" } , want : app . BatchMorning } ,
{ name : "evening" , args : [ ] string { "evening" , "--tz" , "UTC" } , want : app . BatchEvening } ,
}
for _ , tt := range tests {
t . Run ( tt . name , func ( t * testing . T ) {
req , err := resolveRun ( tt . args )
if err != nil {
t . Fatalf ( "resolveRun() error = %v" , err )
}
if req . Batch != tt . want {
t . Fatalf ( "Batch = %q, want %q" , req . Batch , tt . want )
}
} )
}
}
func TestResolveRunRejectsOutputFlag ( t * testing . T ) {
_ , err := resolveRun ( [ ] string { "morning" , "--out" , "./report.md" } )
if err == nil {
t . Fatal ( "resolveRun() error = nil, want flag error" )
}
if ! strings . Contains ( err . Error ( ) , "flag provided but not defined" ) {
t . Fatalf ( "error = %q, want undefined flag error" , err . Error ( ) )
}
}
2026-05-29 18:20:39 +00:00
func TestResolveRunAppliesOutputDirectory ( t * testing . T ) {
req , err := resolveRun ( [ ] string { "evening" , "--out-dir" , "./reports" } )
if err != nil {
t . Fatalf ( "resolveRun() error = %v" , err )
}
if req . OutputDir != "./reports" {
t . Fatalf ( "OutputDir = %q, want ./reports" , req . OutputDir )
}
}
2026-05-29 16:58:18 +00:00
func fixedClock ( ) timeutil . Clock {
return timeutil . FixedClock { Time : time . Date ( 2026 , 5 , 29 , 12 , 0 , 0 , 0 , time . UTC ) }
2026-05-29 16:52:02 +00:00
}
2026-05-29 17:23:49 +00:00
2026-06-15 12:48:36 +00:00
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
}
2026-05-29 17:23:49 +00:00
func dailyServer ( t * testing . T ) * httptest . Server {
t . Helper ( )
server := httptest . NewServer ( http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
switch r . URL . Path {
case "/observations" :
_ , _ = w . Write ( [ ] byte ( ` { "data": { "timestamp":"2026-05-29T14:00:00Z","conditionCode":3}} ` ) )
case "/conditions/current" :
_ , _ = w . Write ( [ ] byte ( ` { "data": { "conditionText":"Clear"}} ` ) )
case "/forecast/hourly" :
2026-05-29 18:00:27 +00:00
_ , _ = w . Write ( [ ] byte ( ` { "data": { "locationId":"test-grid","locationName":"Testville","issuedAt":"2026-05-29T10:30:00-05:00","product":"hourly","periods":[ { "startTime":"2026-05-29T06:00:00-05:00","endTime":"2026-05-29T07:00:00-05:00","textDescription":"Showers and thunderstorms","temperatureF":66,"probabilityOfPrecipitationPercent":80,"windGustMph":32}, { "startTime":"2026-05-30T06:00:00-05:00","endTime":"2026-05-30T07:00:00-05:00","textDescription":"Showers and thunderstorms","temperatureF":66,"probabilityOfPrecipitationPercent":80,"windGustMph":32}]}} ` ) )
2026-05-29 17:23:49 +00:00
case "/forecast/narrative" :
2026-05-29 18:00:27 +00:00
_ , _ = w . Write ( [ ] byte ( ` { "data": { "issuedAt":"2026-05-29T10:30:00-05:00","product":"narrative","periods":[ { "startTime":"2026-05-29T06:00:00-05:00","endTime":"2026-05-29T18:00:00-05:00","textDescription":"Morning storms, then partly sunny."}, { "startTime":"2026-05-30T06:00:00-05:00","endTime":"2026-05-30T18:00:00-05:00","textDescription":"Tomorrow starts stormy."}]}} ` ) )
2026-05-29 17:23:49 +00:00
case "/alerts/active" :
_ , _ = w . Write ( [ ] byte ( ` { "data": { "alerts":[]}} ` ) )
case "/discussion" :
_ , _ = w . Write ( [ ] byte ( ` { "data": { "product":"discussion","issuedAt":"2026-05-29T09:25:00-05:00","keyMessages":["Storms are most likely during the morning."]}} ` ) )
2026-05-30 07:47:49 -05:00
case "/weatherstories/latest" :
_ , _ = w . Write ( [ ] byte ( ` { "data": { "officeId":"LSX","startTime":"2026-05-30T08:46:00Z","endTime":"2026-05-31T11:00:00Z","updatedAt":"2026-05-30T09:00:34Z","title":"Several Chances for Rain Through Monday","description":"Scattered showers and thunderstorms remain possible.","altText":"Forecast weather story graphic.","priority":false,"order":1,"downloadUrl":"https://api.weather.gov/offices/LSX/weatherstories/download/test"}} ` ) )
2026-06-12 14:51:54 +00:00
case "/outlooks/convective" :
_ , _ = w . Write ( [ ] byte ( ` { "data": { "asOf":"2026-05-29T16:00:00Z","outlooks":[],"discussions":[]}} ` ) )
2026-05-29 17:23:49 +00:00
default :
http . NotFound ( w , r )
}
} ) )
t . Cleanup ( server . Close )
return server
}
2026-05-29 17:32:57 +00:00
2026-05-29 20:44:25 +00:00
func writeTestConfig ( t * testing . T , server * httptest . Server , scriptoriumPath string , workspaceRoot string ) string {
t . Helper ( )
configPath := filepath . Join ( t . TempDir ( ) , "config.yml" )
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 ) , 0 o600 ) ; err != nil {
t . Fatalf ( "write config: %v" , err )
}
return configPath
}
func oneArtifact ( t * testing . T , root string , parts ... string ) string {
t . Helper ( )
matches , err := filepath . Glob ( filepath . Join ( append ( [ ] string { root } , parts ... ) ... ) )
if err != nil {
t . Fatalf ( "glob artifact: %v" , err )
}
if len ( matches ) != 1 {
t . Fatalf ( "artifact matches = %#v, want one" , matches )
}
return matches [ 0 ]
}
2026-06-14 05:17:05 +00:00
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 ) )
}
}
2026-05-29 17:32:57 +00:00
func writeFakeScriptorium ( t * testing . T , dir string ) string {
t . Helper ( )
path := filepath . Join ( dir , "scriptorium" )
2026-05-29 17:47:42 +00:00
body := ` # ! / bin / sh
if [ "$1" = "render" ] ; then
printf ' { "ok" : true , "argv" : "%s" } ' "$*"
exit 0
fi
if [ "$1" = "run" ] ; then
out = ""
2026-06-14 23:44:31 +00:00
prompt = ""
2026-05-29 17:47:42 +00:00
while [ "$#" - gt 0 ] ; do
if [ "$1" = "--out" ] ; then
shift
out = "$1"
2026-06-14 23:44:31 +00:00
elif [ "$1" = "--prompt" ] ; then
shift
prompt = "$1"
2026-05-29 17:47:42 +00:00
fi
shift
done
2026-06-15 14:54:37 +00:00
if [ "$prompt" = "weather.today_generated_text" ] ; then
cat > "$out" << ' JSON '
{
"summary" : "Today starts with showers before improving." ,
"forecast_discussion" : [
"Morning showers should taper as drier air arrives." ,
"Afternoon conditions trend quieter."
] ,
"precipitation_timing" : "The best rain chance is during the morning."
}
JSON
printf ' wrote generated text \ n ' > & 2
exit 0
fi
2026-06-14 23:44:31 +00:00
if [ "$prompt" = "weather.tomorrow_generated_text" ] ; then
cat > "$out" << ' JSON '
{
"summary" : "Tomorrow starts with showers before improving." ,
"forecast_discussion" : [
"Morning showers should taper as drier air arrives." ,
"Afternoon conditions trend quieter."
] ,
"precipitation_timing" : "The best rain chance is during the morning."
}
2026-06-15 16:43:06 +00:00
JSON
printf ' wrote generated text \ n ' > & 2
exit 0
fi
if [ "$prompt" = "weather.daily_generated_text" ] ; then
cat > "$out" << ' JSON '
{
"summary" : "Showers are possible during the selected day." ,
"forecast_discussion" : [
"A front will keep rain chances in the forecast." ,
"Temperatures stay seasonable by afternoon."
] ,
"precipitation_timing" : "Rain is most likely during the afternoon." ,
"confidence" : "Medium"
}
2026-06-14 23:44:31 +00:00
JSON
printf ' wrote generated text \ n ' > & 2
exit 0
fi
2026-05-29 17:47:42 +00:00
printf ' # Daily Report \ n \ nGenerated by fake scriptorium . \ n ' > "$out"
printf ' wrote report \ n ' > & 2
exit 0
fi
printf ' unexpected command \ n ' > & 2
2026-06-14 05:17:05 +00:00
exit 1
`
if err := os . WriteFile ( path , [ ] byte ( body ) , 0 o700 ) ; err != nil {
t . Fatalf ( "write fake scriptorium: %v" , err )
}
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. " ,
2026-06-14 12:54:32 -05:00
"forecast_discussion" : "A front will keep the region unsettled." ,
"precipitation_timing" : "A cold front is moving into the region." ,
2026-06-14 05:17:05 +00:00
"confidence" : "Medium"
}
2026-06-15 16:43:06 +00:00
JSON
printf ' wrote generated text \ n ' > & 2
exit 0
fi
if [ "$prompt" = "weather.daily_generated_text" ] ; then
cat > "$out" << ' JSON '
{
"summary" : "Showers are possible during the selected day." ,
"forecast_discussion" : [
"A front will keep rain chances in the forecast." ,
"Temperatures stay seasonable by afternoon."
] ,
"precipitation_timing" : "Rain is most likely during the afternoon." ,
"confidence" : "Medium"
}
2026-06-15 14:54:37 +00:00
JSON
printf ' wrote generated text \ n ' > & 2
exit 0
fi
if [ "$prompt" = "weather.today_generated_text" ] ; then
cat > "$out" << ' JSON '
{
"summary" : "Today starts with showers before improving." ,
"forecast_discussion" : [
"Morning showers should taper as drier air arrives." ,
"Afternoon conditions trend quieter."
] ,
"precipitation_timing" : "The best rain chance is during the morning."
}
2026-06-14 23:44:31 +00:00
JSON
printf ' wrote generated text \ n ' > & 2
exit 0
fi
if [ "$prompt" = "weather.tomorrow_generated_text" ] ; then
cat > "$out" << ' JSON '
{
"summary" : "Tomorrow starts with showers before improving." ,
"forecast_discussion" : [
"Morning showers should taper as drier air arrives." ,
"Afternoon conditions trend quieter."
] ,
"precipitation_timing" : "The best rain chance is during the morning."
}
2026-06-14 05:17:05 +00:00
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
2026-05-29 17:47:42 +00:00
exit 1
`
2026-05-29 17:32:57 +00:00
if err := os . WriteFile ( path , [ ] byte ( body ) , 0 o700 ) ; err != nil {
t . Fatalf ( "write fake scriptorium: %v" , err )
}
return path
}
2026-05-29 18:20:39 +00:00
func writeFailingScriptorium ( t * testing . T , dir string ) string {
t . Helper ( )
path := filepath . Join ( dir , "scriptorium" )
body := ` # ! / bin / sh
if [ "$1" = "render" ] ; then
prompt = ""
while [ "$#" - gt 0 ] ; do
if [ "$1" = "--prompt" ] ; then
shift
prompt = "$1"
fi
shift
done
if [ "$prompt" = "weather.three_day_outlook" ] ; then
printf ' render failed \ n ' > & 2
exit 1
fi
printf ' { "ok" : true , "prompt" : "%s" } ' "$prompt"
exit 0
fi
if [ "$1" = "run" ] ; then
out = ""
2026-06-15 14:54:37 +00:00
prompt = ""
2026-05-29 18:20:39 +00:00
while [ "$#" - gt 0 ] ; do
if [ "$1" = "--out" ] ; then
shift
out = "$1"
2026-06-15 14:54:37 +00:00
elif [ "$1" = "--prompt" ] ; then
shift
prompt = "$1"
2026-05-29 18:20:39 +00:00
fi
shift
done
2026-06-15 14:54:37 +00:00
if [ "$prompt" = "weather.today_generated_text" ] ; then
cat > "$out" << ' JSON '
{
"summary" : "Today starts with showers before improving." ,
"forecast_discussion" : [
"Morning showers should taper as drier air arrives." ,
"Afternoon conditions trend quieter."
] ,
"precipitation_timing" : "The best rain chance is during the morning."
}
JSON
exit 0
fi
2026-05-29 18:20:39 +00:00
printf ' # Batch Report \ n \ nGenerated by fake scriptorium . \ n ' > "$out"
exit 0
fi
printf ' unexpected command \ n ' > & 2
exit 1
`
if err := os . WriteFile ( path , [ ] byte ( body ) , 0 o700 ) ; err != nil {
t . Fatalf ( "write fake scriptorium: %v" , err )
}
return path
}